검색창 자동완성 키보드 컨트롤 (with:react)

이지원·2022년 4월 9일
13

react

목록 보기
2/2

해당 포스팅은 이전 포스팅과 이어집니다.
링크
오늘 포스팅 내용은 제목 그대로 검색어 자동완성된 데이터를 키보드로 접근해서 선택하는 기능이다.

-> 지난 포스팅 결과물에서 시작을 하겠다.

useRef

우선 첫번째로 useRef hooks를 이용해야한다.
useRef에 대하여 간단히 소개하자면 일반 자바스크립트에서 getElementById, querySelector 같은 DOM Selector 함수를 사용해서 DOM 을 선택하는데 react에서는 useRef hook으로 dom을 접근할 수있다.
useRef에 관한 추가적인 설명은 링크
그렇다면 키보드 컨트롤을 하는데 왜 Dom을 접근해야 하는가? 사진을 확인해보자

해당 사진은 우리의 자동완성 검색어 html 형태이다.
div -> ul -> li 로 접근해야만.우리의 자동완성 키워드에 도달할 수 있다.

생략 코드는 이전 포스팅 최종 코드에서 확인하실 수 있습니다.

useRef 적용

function Header() {
...생략
const autoRef = useRef<HTMLUListElement>(null);
}
console.log(autoRef);
return (
...생략
	<AutoSearchWrap ref={autoRef}>
    {keyItems.map((search, idx) => (
     <AutoSearchData
       isFocus={index === idx ? true : false}
       key={search.city}
        onClick={() => {
        setKeyword(search.city);
       }}
)

styled-component로 선언한 ulTag에 ref값을 props로 넘겨주었다.
또한 콘솔을 확인해 보면

사진과 같은 속성을 확인할 수 있다.
우리가 봐야할 속성은 표시한 children이다.
children 속성에는 아까위에서 설명한 li tag리스트들이 존재한다.

즉. ref로 children에 접근하여 우리는 div tag를 클릭하지 않고도 li tag에 있는 값에 접근하면 끝난다.

키보드로 index 선택

function Header() {

const AutoSearchData = styled.li<{isFocus?: boolean}>`
  padding: 10px 8px;
  width: 100%;
  font-size: 14px;
  font-weight: bold;
  z-index: 4;
  letter-spacing: 2px;
  &:hover {
    background-color: #edf5f5;
    cursor: pointer;
  }
  background-color: ${props => props.isFocus? "#edf5f5" : "#fff"};
  position: relative;
  img {
    position: absolute;
    right: 5px;
    width: 18px;
    top: 50%;
    transform: translateY(-50%);
  }
`;

const [index,setIndex] = useState<number>(-1);
return (
<AutoSearchWrap ref={autoRef}>
    {keyItems.map((search, idx) => (
     <AutoSearchData
       isFocus={index === idx ? true : false}
       key={search.city}
        onClick={() => {
        setKeyword(search.city);
       }}
 )}

해당 코드를 살펴보자 유의깊게 봐야할 부분은 index 변수의 초기값이 -1인 점인데
우리가 자동완성 데이터를 받아오면 해당 배열에 첫번째 인덱스값이 0으로 시작하기때문에 -1값으로 지정해줬다.
그리고 AutoSearchData 컴포넌트에 isFocus라는 props를 전달하여 키보드로도 자동완성 키워드 hover 스타일처럼 만들고자 index값과 ES6 map 인자에 인덱스를 받아와 true,false를 반환해줄 수 있게되었다.

keydown 함수 생성

const ArrowDown = "ArrowDown";
const ArrowUp = "ArrowUp";
const Escape = "Escape";
const handleKeyArrow = (e:React.KeyboardEvent) => {
    if (keyItems.length > 0) {
      switch (e.key) {
        case ArrowDown: //키보드 아래 키
          setIndex(index + 1);
          if (autoRef.current?.childElementCount === index + 1) setIndex(0);
          break;
        case ArrowUp: //키보드 위에 키
          setIndex(index - 1);
          if (index <= 0) {
            setKeyItems([]);
            setIndex(-1);
          }
          break;
        case Escape: // esc key를 눌렀을때,
          setKeyItems([]);
          setIndex(-1);
          break;
      }  
    } 
  }
  return (
  	<Search
     value={keyword}
     onChange={onChangeData}
     onKeyDown={handleKeyArrow}
    />
    ...생략
  )

우선 key가 눌리기때문에 event속성을 사용해야한다.
위 방향키 "ArrowDown" 아래 방향키 "ArrowUp"를 의미한다
즉 e.key === "ArrowDown" 케이스는 아래방향키를 눌렀을때 실행된다.
e.key === '문자열'은 오타와 실수방지를 위하여 상수처리해줬다.

if (autoRef.current?.childElementCount === index + 1) setIndex(0); 이부분은 만약 마지막 인덱스 키워드에서 또 아래 방향키를 누르면 맨처음 인덱스 키워드로 돌아가라는 의미이다.
Ex: childElementCount-> li tag의 개수를 의미한다.

결과화면

최종코드

import React, { useRef } from 'react';
import { useEffect } from 'react';
import { useState } from 'react';
import styled from 'styled-components';

const SearchContainer = styled.div`
  width: 400px;
  height: 45px;
  position: relative;
  border: 0;
  img {
    position: absolute;
    right: 10px;
    top: 10px;
  }
`;

const Search = styled.input`
  border: 0;
  padding-left: 10px;
  background-color: #eaeaea;
  width: 100%;
  height: 100%;
  outline: none;
`;


const AutoSearchContainer = styled.div`
  z-index: 3;
  height: 50vh;
  width: 400px;
  background-color: #fff;
  position: absolute;
  top: 45px;
  border: 2px solid;
  padding: 15px;
`;

const AutoSearchWrap = styled.ul`

`;

const AutoSearchData = styled.li<{isFocus?: boolean}>`
  padding: 10px 8px;
  width: 100%;
  font-size: 14px;
  font-weight: bold;
  z-index: 4;
  letter-spacing: 2px;
  &:hover {
    background-color: #edf5f5;
    cursor: pointer;
  }
  background-color: ${props => props.isFocus? "#edf5f5" : "#fff"};
  position: relative;
  img {
    position: absolute;
    right: 5px;
    width: 18px;
    top: 50%;
    transform: translateY(-50%);
  }
`;
interface autoDatas {
  city: string;
  growth_from_2000_to_2013: string;
  latitude:number;
  longitude:number;
  population:string;
  rank:string;
  state:string;
}
function Header() {
	const [keyword, setKeyword] = useState<string>("");
    const [index,setIndex] = useState<number>(-1);
    const [keyItems, setKeyItems] = useState<autoDatas[]>([]);
    const autoRef = useRef<HTMLUListElement>(null);
    const onChangeData = (e:React.FormEvent<HTMLInputElement>) => {
    setKeyword(e.currentTarget.value);
  };
  const handleKeyArrow = (e:React.KeyboardEvent) => {
    if (keyItems.length > 0) {
      switch (e.key) {
        case ArrowDown:
          setIndex(index + 1);
          if (autoRef.current?.childElementCount === index + 1) setIndex(0);
          break;
        case ArrowUp:
          setIndex(index - 1);
          if (index <= 0) {
            setKeyItems([]);
            setIndex(-1);
          }
          break;
        case Escape:
          setKeyItems([]);
          setIndex(-1);
          break;
      }  
    } 
  }
  const fetchData = ()  =>{
    return fetch(
      `https://gist.githubusercontent.com/Miserlou/c5cd8364bf9b2420bb29/raw/2bf258763cdddd704f8ffd3ea9a3e81d25e2c6f6/cities.json`
    )
      .then((res) => res.json())
      .then((data) => data.slice(0,100))
  }
  interface ICity {
    includes(data:string): boolean;
    city?: any;
  }
  const updateData = async() => {
    const res = await fetchData();
    let b = res.filter((list: ICity) => list.city.includes(keyword) === true)
                .slice(0,10);
    // console.log(b);
    setKeyItems(b);
  }
  useEffect(() => {
    updateData();
    },[keyword])
    return (
    <SearchContainer>
     <Search value={keyword} onChange={onChangeData} onKeyDown={handleKeyArrow}/>
      <img src="assets/imgs/search.svg" alt="searchIcon" />
       {keyItems.length > 0 && keyword && (
        <AutoSearchContainer>
         <AutoSearchWrap ref={autoRef}>
          {keyItems.map((search, idx) => (
           <AutoSearchData
           	isFocus={index === idx ? true : false}
            key={search.city}
            onClick={() => {
            setKeyword(search.city);
           }}
            >
            <a href="#">{search.city}</a>
            <img src="assets/imgs/north_west.svg" alt="arrowIcon" />
           </AutoSearchData>
          ))}
         </AutoSearchWrap>
        </AutoSearchContainer>
       )}
      </SearchContainer>
     );
}
export default Header;

감사합니다

profile
안녕하세요 피드백은 언제나환영입니다.

2개의 댓글

comment-user-thumbnail
2022년 5월 15일

낄낄

답글 달기