검색창 자동완성 만들기 (with:react)

이지원·2022년 4월 2일
33

react

목록 보기
1/2
post-thumbnail

개인프로젝트 검색창을 구현하던 도중 문득 자동완성 기능을 구현하고 싶었다

포털사이트에서 키워드를 검색하면 해당하는 키워드가 포함된 검색어들이 자동완성되는것을 구현해보고자 한다.

해당 파일은 react typescript 버전으로 작성되어있습니다.

검색창 스타일링

css 스타일링은 styled-components를 이용했다.
styled-components의 설명과 사용법은 링크를 참조
링크

Header.tsx

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;
`;

function Header() {
	<SearchContainer>
      <Search/>
      <img src="assets/imgs/search.svg" alt="searchIcon" />
    </SearchContainer>
}
export default Header;


위 사진처럼 스타일이 적용되었을것이다.

검색창 ts구현

구현 순서
1.우선 검색할 keyword변수를 useState Hooks를 사용하여 만들어준다.
2.검색창에 이벤트 조작을 이용하여 우리가 검색할 keyword변수를 이용해서 검색한다.

const [keyword, setKeyword] = useState<string>("");
const onChangeData = (e:React.FormEvent<HTMLInputElement>) => {
    setKeyword(e.currentTarget.value);
};

자동완성창 스타일링

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`
  padding: 10px 8px;
  width: 100%;
  font-size: 14px;
  font-weight: bold;
  z-index: 4;
  letter-spacing: 2px;
  &:hover {
    background-color: #edf5f5;
    cursor: pointer;
  }
  position: relative;
  img {
    position: absolute;
    right: 5px;
    width: 18px;
    top: 50%;
    transform: translateY(-50%);
  }
`;
function Header() {
	<SearchContainer>
      <Search value={keyword} onChange={onChangeData}/> //keyword변수와 이벤트조작
      <img src="assets/imgs/search.svg" alt="searchIcon" />
      <AutoSearchContainer>
       <AutoSearchWrap>
        <AutoSearchData>
          <a href="#"></a>
          <img src="assets/imgs/north_west.svg" alt="arrowIcon" />
         </AutoSearchData>
        </AutoSearchWrap>
      </AutoSearchContainer>
    </SearchContainer>
}
export default Header;

자동완성 데이터 fetch

구현 순서
1.api를 통해 받아온 데이터를 keyItems 변수에 담는다.
2.받아온 데이터중에 우리의 키워드가 포함된 데이터만 저장한다.
3.useEffect Hooks를 통해 우리의 api를 호출한다.
오픈 api는 리서치하는 중에
https://gist.githubusercontent.com/Miserlou/c5cd8364bf9b2420bb29/raw/2bf258763cdddd704f8ffd3ea9a3e81d25e2c6f6/cities.json
해당 데이터를 사용했다.

interface autoDatas { //api를 통해 받아온 데이터 interface
  city: string;
  growth_from_2000_to_2013: string;
  latitude:number;
  longitude:number;
  population:string;
  rank:string;
  state:string;
}
function Header() {
	const [keyItems, setKeyItems] = useState<autoDatas[]>([]);
    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(() => {
		const debounce = setTimeout(() => {
      		if(keyword) updateData();
    	},200)
        return () => {
          clearTimeout(debounce)
        }
    },[keyword]) //키워드가 변경되면 api를 호출
    
    ...return tsx파일 생략
}
export default Header;

해당 부분에서는 useEffect부분이 중요한데
useEffect(() => {
const debounce = setTimeout(() => {
if(keyword) updateData();
},200)
return () => {
clearTimeout(debounce)
}
},[keyword])
setTimeOut 함수를 사용한 이유는 debounce기법 때문이다.만약 setTimeout없이 계속 keyword가 있을때마다 updateData()함수를 호출한다면 예를들어 카카오를 검색한다면
ㅋ ㅏ ㅋ ㅏ ㅇ ㅗ 총6번을 호출하게될것이기 때문에 딜레이를 두어 입력이 다끝나면 호출하는 개념이다
setTimeout의 시간초는 적당히 설정하면된다 . 나는 0.2초정도로 계산했기 때문에 200을 입력했다

받아온 자동완성 데이터를 우리의 페이지에 바인딩

function Header() {
...생략
	<SearchContainer>
      <Search value={keyword} onChange={onChangeData}/>
      <img src="assets/imgs/search.svg" alt="searchIcon" />
      {keyItems.length > 0 && keyword && ( //키워드가 존재하고,해당키워드에 맞는 이름이 있을때만 보여주기 
        <AutoSearchContainer>
         <AutoSearchWrap>
           {keyItems.map((search, idx) => (
            <AutoSearchData
              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>
}

결과화면

전체코드

import React 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`
  padding: 10px 8px;
  width: 100%;
  /* height: 30px; */
  font-size: 14px;
  font-weight: bold;
  z-index: 4;
  letter-spacing: 2px;
  &:hover {
    background-color: #edf5f5;
    cursor: pointer;
  }
  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 [keyItems, setKeyItems] = useState<autoDatas[]>([]);
    const onChangeData = (e:React.FormEvent<HTMLInputElement>) => {
    setKeyword(e.currentTarget.value);
  };
  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(() => {
		const debounce = setTimeout(() => {
      		if(keyword) updateData();
    	},200)
        return () => {
          clearTimeout(debounce)
        }
    },[keyword]) //키워드가 변경되면 api를 호출
    return (
    <SearchContainer>
     <Search value={keyword} onChange={onChangeData} />
      <img src="assets/imgs/search.svg" alt="searchIcon" />
       {keyItems.length > 0 && keyword && (
        <AutoSearchContainer>
         <AutoSearchWrap ref={autoRef}>
          {keyItems.map((search, idx) => (
           <AutoSearchData
            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;

마무리

이번 포스팅을 진행하면서, 이전 포스팅에서 설명하지못한 부분들이 있습니다.(useState,useEffect,interface)등등..
기회가 된다면 정리해서 올리도록 하겠습니다.
+ 다음 포스팅 예고
자동완성 검색창이 나타났을때, 키보드 방향키를 통해 접근할 수 있도록 하는 포스팅을 업로드하겠습니다.

감사합니다

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

4개의 댓글

comment-user-thumbnail
2022년 4월 7일

머시써요

1개의 답글
comment-user-thumbnail
2022년 7월 1일

좋은 정보 알아갑니다 감사해요!

1개의 답글