React 자동완성 구현 - react jadong-wanseong guhyeon

태그 기능과 마찬가지로 자동완성기능을 구현하기 위해서는 어떻게 해야할지 알아보기 위해 google창을 먼저 살펴봤습니다.

React 자동완성 구현 - react jadong-wanseong guhyeon

검색창에 input값을 입력하면 밑에 input값과 유사한 전체 단어을 dropdown 형식으로 보여주는 자동 완성 기능을 구현했습니다.

필요한 state 정리

1. inputValue

input에 들어간 값

  • google에서는 해당 input값으로 시작하는 단어만 보여주지만, 과제사항에서는 해당 값을 포함하는 모든 단어를 보여주기 때문에 input에 입력한 값을 포함한 단어를 dropdown에 보여줍니다
  • string.includes() 메서드 활용
  • 초기 state: 빈문자열 ''

2.isHaveInputValue

입력된 input값이 있는지의 여부

  • 입력된 input값이 있다면 dropdown을 보여주고, 입력된 input값이 없다면 dropdown을 보여주지 않습니다
  • Boolean형태
  • 초기 state: false

3. dropDownList

dropdown에 보여줄 자동완성된 단어목록(리스트)

  • dropdown을 살펴보면 동일한 형태의 컴포넌트가 반복적으로 보여줍니다
  • array.map() 메서드 활용
  • 초기 state: []

4. dropDownItemIndex

내가 선택한 자동완성된 단어 item의 index

  • dropdown에 나타나는 추천단어 리스트 array에서 내가 선택한 단어(요소)를 가리키기 위해 필요합니다.
  • 만약 dropDownList.map() 를 통해 dropdown이 나왔을 때, 해당하는 indexdropDownItemIndex이 동일하면 배경색을 lightgrey로 변경해서 선택되었음을 알려줍니다.

InputBox 영역

  • Input값이 변경될 때마다 inputValue, isHaveInputValue 값 업데이트했습니다.
  • 키보드가 아래,위,enter 눌릴 때 dropdown의 요소를 선택하기 위해 키보드에 버튼을 눌릴 때마다, 아래의 조건에 따라 dropDownItemIndex 값 업데이트했습니다.

조건

  1. isHaveInputValue 이 없다면 dropdown의 영역도 없을 것이기 때문에 이러한 키보드 이벤트를 고려하지 않습니다.

  2. dropDownItemIndex

    • 아래버튼을 눌렀을 때: dropDownList.length - 1 > dropDownItemIndex ~~
    • 위버튼을 눌렀을 때: dropDownItemIndex >= 0
    • Enter버튼을 눌렀을 때(=해당 dropDownItemIndex선택): dropDownItemIndex >= 0
// 조건을 위와 같이 설정한 이유
dropDownList = ['apple', 'banana', 'javascript']
dropDownList.length // 3
beasts.indexOf('apple') // 0
beasts.indexOf('banana') // 1
beasts.indexOf('javascript') // 2
  1. inputValue이 존재할 때만 보여져야 하기 때문에 isHaveInputValue이 true일 때 렌더링되도록 했습니다.
  2. dropDown에 들어갈 데이터는 array형태의 dropDownList state인데, 만약 하나도 없을 경우 빈 배열만 나오기 때문에
  • dropDownList.length === 0 일 때에는 해당하는 단어가 없다고 렌더링했습니다.
  • 하나 이상 있을 때에는 map()메서드를 통해 <DropDownItem> 컴포넌트를 반복적으로 만들었습니다.
  1. <DropDownItem> 을 눌렀을 때 (=클릭했을 때)
  • 해당하는 값으로 inputValue state를 업데이트 하고,
  • 자동완성 단어를 선택되었기 때문에 dropDown 영역을 보여주지 않기 위해 isHaveInputValue state를 false로 변경했습니다.
  1. <DropDownItem> 을 마우스오버했을 때
  • 해당하는 index를 dropDownItemIndex state로 업데이트합니다.

코드

import React, { useEffect, useState } from 'react'
import styled from 'styled-components'
import Title from '../components/Title'

const wholeTextArray = [
  'apple',
  'banana',
  'coding',
  'javascript',
  '원티드',
  '프리온보딩',
  '프론트엔드',
]

const AutoComplete = () => {
  const [inputValue, setInputValue] = useState('')
  const [isHaveInputValue, setIsHaveInputValue] = useState(false)
  const [dropDownList, setDropDownList] = useState(wholeTextArray)
  const [dropDownItemIndex, setDropDownItemIndex] = useState(-1)

  const showDropDownList = () => {
    if (inputValue === '') {
      setIsHaveInputValue(false)
      setDropDownList([])
    } else {
      const choosenTextList = wholeTextArray.filter(textItem =>
        textItem.includes(inputValue)
      )
      setDropDownList(choosenTextList)
    }
  }

  const changeInputValue = event => {
    setInputValue(event.target.value)
    setIsHaveInputValue(true)
  }

  const clickDropDownItem = clickedItem => {
    setInputValue(clickedItem)
    setIsHaveInputValue(false)
  }

  const handleDropDownKey = event => {
    //input에 값이 있을때만 작동
    if (isHaveInputValue) {
      if (
        event.key === 'ArrowDown' &&
        dropDownList.length - 1 > dropDownItemIndex
      ) {
        setDropDownItemIndex(dropDownItemIndex + 1)
      }

      if (event.key === 'ArrowUp' && dropDownItemIndex >= 0)
        setDropDownItemIndex(dropDownItemIndex - 1)
      if (event.key === 'Enter' && dropDownItemIndex >= 0) {
        clickDropDownItem(dropDownList[dropDownItemIndex])
        setDropDownItemIndex(-1)
      }
    }
  }

  useEffect(showDropDownList, [inputValue])

  return (
    <WholeBox>
      <Title text='AutoComplete' />
      <InputBox isHaveInputValue={isHaveInputValue}>
        <Input
          type='text'
          value={inputValue}
          onChange={changeInputValue}
          onKeyUp={handleDropDownKey}
        />
        <DeleteButton onClick={() => setInputValue('')}>&times;</DeleteButton>
      </InputBox>
      {isHaveInputValue && (
        <DropDownBox>
          {dropDownList.length === 0 && (
            <DropDownItem>해당하는 단어가 없습니다</DropDownItem>
          )}
          {dropDownList.map((dropDownItem, dropDownIndex) => {
            return (
              <DropDownItem
                key={dropDownIndex}
                onClick={() => clickDropDownItem(dropDownItem)}
                onMouseOver={() => setDropDownItemIndex(dropDownIndex)}
                className={
                  dropDownItemIndex === dropDownIndex ? 'selected' : ''
                }
              >
                {dropDownItem}
              </DropDownItem>
            )
          })}
        </DropDownBox>
      )}
    </WholeBox>
  )
}

const activeBorderRadius = '16px 16px 0 0'
const inactiveBorderRadius = '16px 16px 16px 16px'

const WholeBox = styled.div`
  padding: 10px;
`

const InputBox = styled.div`
  display: flex;
  flex-direction: row;
  padding: 16px;
  border: 1px solid rgba(0, 0, 0, 0.3);
  border-radius: ${props =>
    props.isHaveInputValue ? activeBorderRadius : inactiveBorderRadius};
  z-index: 3;

  &:focus-within {
    box-shadow: 0 10px 10px rgb(0, 0, 0, 0.3);
  }
`

const Input = styled.input`
  flex: 1 0 0;
  margin: 0;
  padding: 0;
  background-color: transparent;
  border: none;
  outline: none;
  font-size: 16px;
`

const DeleteButton = styled.div`
  cursor: pointer;
`

const DropDownBox = styled.ul`
  display: block;
  margin: 0 auto;
  padding: 8px 0;
  background-color: white;
  border: 1px solid rgba(0, 0, 0, 0.3);
  border-top: none;
  border-radius: 0 0 16px 16px;
  box-shadow: 0 10px 10px rgb(0, 0, 0, 0.3);
  list-style-type: none;
  z-index: 3;
`

const DropDownItem = styled.li`
  padding: 0 16px;

  &.selected {
    background-color: lightgray;
  }
`

export default AutoComplete