[React] Debounce(디바운스) 적용한 검색어 자동완성 구현기 (1)
전체코드 (관련 코드는 SearchPage와 Search 폴더에 있음) 검색어 자동완성 기능은 의외로 자주 사용되는 UI이기 때문에 디바운스 연습할 겸 미국의 도시명을 검색할 수 있는 검색창 및 자동완성 기능
bluepebble25.tistory.com
전체코드 (관련 코드는 SearchPage와 Search 폴더에 있음)
GitHub - bluepebble25/react-pieces: react로 자주 구현되는 솔루션 조각들을 모아놓는 저장소
react로 자주 구현되는 솔루션 조각들을 모아놓는 저장소. Contribute to bluepebble25/react-pieces development by creating an account on GitHub.
github.com
앞에서는 마크업 및 데이터 fetch, 검색어 필터링, 디바운스까지 만들어봤다.
이번에 구현할 내용은
1. 검색어 자동완성을 화살표 방향키로 선택하는 기능
2. 키보드로 위로 올라가거나 내려가면 목록도 따라서 스크롤되는 기능
자동완성 키보드로 선택하는 기능 구현하기
키보드로 현재 몇 번째 아이템을 선택했는지 저장하고 있는 인덱스와, 리스트 아이템 중 key index가 일치하는 요소에게 focus 효과를 주는 원리이다.
1. 현재 몇 번째 리스트 아이템을 선택하고 있는지 저장하는 currIndex 상태를 만든다.
그리고 검색어 자동완성 목록(SearchPreview) 컴포넌트에게 currIndex 값을 내려준다.
function SearchPage() {
// ...
const [currIndex, setCurrIndex] = useState(-1);
const selectRef = useRef<HTMLLIElement>(null);
// ...
return (
<Container>
<Title>Search for Cities</Title>
<SearchForm
hasQuery={query ? true : false}
onChangeInput={onChangeInput}
onKeyDown={handleArrowKey}
/>
{query && (
<SearchPreview
previewList={previewList}
currIndex={currIndex}
selectRef={selectRef}
/>
)}
</Container>
);
}
2. 아래의 키보드 핸들 함수를 만들어 input에 onKeyDown 이벤트로 부착한다.
- input 창에서 화살표를 안누른 기본 상태가 currIndex가 -1이고, 첫번째 리스트 아이템을 선택했을 때가 1 이런 식으로 카운트된다. 맨 마지막에서 아래 키를 한번 더 누르면 0이 되어 리스트의 첫 번째로 돌아간다.
const handleArrowKey = (e: React.KeyboardEvent) => {
const DOWN = 'ArrowDown';
const UP = 'ArrowUp';
const ESCAPE = 'Escape';
const ENTER = 'Enter';
if (previewList.length > 0) {
switch (e.key) {
case DOWN:
setCurrIndex((prev) => ++prev);
if (previewList.length === currIndex + 1) {
// 자동완성 리스트 맨 아래에서 아래 화살표 눌렀을 때 맨 위로
setCurrIndex(0);
}
break;
case UP:
setCurrIndex((prev) => --prev);
if (currIndex <= 0) setCurrIndex(-1);
break;
case ESCAPE:
setCurrIndex(-1);
break;
case ENTER:
if (currIndex >= 0) {
selectRef.current &&
(selectRef.current.children[0] as HTMLElement).click();
}
break;
}
}
};
3. 리스트의 index 값과 currIndex가 일치한 요소에게 isFocused=ture 속성을 부여한다.
styled-component에서 isFocused가 true인 경우에 색을 진하게 주도록 하면 포커스 된 효과가 나타난다.
interface SearchPreviewProps {
previewList: string[];
currIndex: number;
selectRef: React.RefObject<HTMLLIElement>;
}
interface SearchPreviewItemProps {
isFocused?: boolean;
selectRef?: React.RefObject<HTMLLIElement>;
}
function SearchPreview({
previewList,
currIndex,
selectRef,
}: SearchPreviewProps) {
return (
<SearchPreviewList>
// ...
previewList.map((item, index) => {
const location = item.split('>');
const isFocused = index === currIndex;
return (
<SearchPreviewItem
key={index}
isFocused={isFocused}
ref={isFocused ? selectRef : null}
>
<Link
to={`https://www.google.com/maps/place/${location[0]}+${location[1]}`}
target="_blank"
>
<p>{item}</p>
</Link>
</SearchPreviewItem>
);
})
</SearchPreviewList>
);
}
const SearchPreviewItem = styled.li<SearchPreviewItemProps>`
// ...
background-color: ${(props) => props.isFocused && '#efefef'};
&:hover {
background-color: #efefef;
}
`;
+) 키보드 핸들러 함수에서 엔터를 눌렀을 때 아이템을 클릭한 효과를 줬다. 그러면 컴포넌트를 누를 때와 같이 검색 결과 페이지로 이동한다. 나는 구글 map에 해당 도시의 지도를 검색하도록 연결했다.
리스트 스크롤 기능 구현하기
element.scrollIntoView() 메소드를 사용해 구현할 것이다.
이 메소드는 해당 요소가 호출될 때 사용자에게 잘 보이도록 상위 요소를 스크롤하는 특성을 가졌다.
그래서 선택된 리스트 아이템.scrollIntoView()를 실행하게 되면 상위의 <ul>을 스크롤하게 된다.
- 현재 어떤 요소가 focus 되어 있는지 상위 요소가 알기 위해서는, 앞에서 css 스타일링을 위해 index === currIndex로 구한 isFocus는 불충분하다.
상위요소가 현재 어떤 아이템이 선택되었는지 알기 위해 isFocused된 요소에 조건부로 ref를 부착하자. SearchPreview(검색 자동완성 리스트)에게 ref를 내려준다. - currIndex가 바뀔 때마다(=화살표로 리스트 아이템을 내려가며 선택할 때마다) handleScroll 함수를 실행하도록 한다.
function SearchPage() {
// ...
const selectRef = useRef<HTMLLIElement>(null);
useEffect(() => {
const handleScroll = () => {
const selectedList = selectRef.current;
selectedList?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
};
handleScroll();
}, [currIndex]);
return (
<Container>
<Title>Search for Cities</Title>
<SearchForm
hasQuery={query ? true : false}
onChangeInput={onChangeInput}
onKeyDown={handleArrowKey}
/>
{query && (
<SearchPreview
previewList={previewList}
currIndex={currIndex}
selectRef={selectRef}
/>
)}
</Container>
);
}
SearchPrevirwProps는 받아온 ref를 조건부로 리스트에게 달아주기.
아까 어떤 요소가 선택되었는지 알기 위해 currIndex와 index가 일치하는지 검사한 것처럼 여기서도 일치하는 요소에게 ref를 달아주면 된다.
interface SearchPreviewProps {
previewList: string[];
currIndex: number;
selectRef: React.RefObject<HTMLLIElement>;
}
interface SearchPreviewItemProps {
isFocused?: boolean;
selectRef?: React.RefObject<HTMLLIElement>;
}
function SearchPreview({
previewList,
currIndex,
selectRef,
}: SearchPreviewProps) {
return (
<SearchPreviewList>
{previewList.length === 0 ? (
<SearchPreviewItem>
<p>No Matching Results</p>
</SearchPreviewItem>
) : (
previewList.map((item, index) => {
const location = item.split('>');
const isFocused = index === currIndex;
return (
<SearchPreviewItem
key={index}
isFocused={isFocused}
ref={isFocused ? selectRef : null}
>
<Link
to={`https://www.google.com/maps/place/${location[0]}+${location[1]}`}
target="_blank"
>
<p>{item}</p>
</Link>
</SearchPreviewItem>
);
})
)}
</SearchPreviewList>
);
}
마주한 문제와 해결방법 (Typescript 타입 부여)
1. styled-components props 타입 에러 문제
styled-components에서 props를 사용해 스타일링 하려면 아래처럼 styled.li<타입> 형태로 props의 타입을 지정해줘야 한다.
interface SearchPreviewItemProps {
isFocused?: boolean;
selectRef?: React.RefObject<HTMLLIElement>;
}
const SearchPreviewItem = styled.li<SearchPreviewItemProps>`
// ...
background-color: ${(props) => props.isFocused && '#efefef'};
&:hover {
background-color: #efefef;
}
`;
2. 'Element' 형식에 'click' 속성이 없습니다. (ts2339) 문제
keyboard 핸들러에서 엔터 이벤트를 만들 때 <li> 아이템 하위의 <a>태그 (<Link> 컴포넌트)를 선택하려고
selectRef.current.,children[0].click()을 했는데 에러가 발생했다.
'Element' 형식에 'click' 속성이 없습니다. (ts2339)
case ENTER:
if (currIndex >= 0) {
selectRef.current &&
selectRef.current.children[0].click();
}
break;
그래서 다음과 같이 영어로 검색해봤더니 원인을 찾을 수 있었다.
Property 'click' does not exist on type 'Element'
current.children으로 선택한 엘리먼트의 타입이 HTMLElement가 아니라 자동으로 Element로 인식되었기에 타입을 HTMLElement로 명확히 지정해주면 되는 문제였다.
case ENTER:
if (currIndex >= 0) {
selectRef.current &&
(selectRef.current.children[0] as HTMLElement).click();
}
break;
'UI 구현 연구일지' 카테고리의 다른 글
[React] useInfiniteQuery와 Intersection Observer 이용한 무한스크롤 구현 👀 unsplash 이미지 무한으로 불러오기 (0) | 2023.07.26 |
---|---|
[React] Debounce(디바운스) 적용한 검색어 자동완성 구현기 (1) (0) | 2023.06.14 |
[React 컴포넌트] 댓글 기능 구현하기 (0) | 2022.07.12 |
[React 컴포넌트] Like, Dislike (좋아요, 싫어요) 기능 만드는 법 (0) | 2022.07.11 |
[React 컴포넌트] Favorite(찜, 좋아요) 버튼 만드는 법 (0) | 2022.07.11 |