-
[React] 검색어 자동완성 키보드 이벤트 기능 구현React 2023. 1. 5. 16:40
영화 검색 기능 구현
인풋에 텍스트를 작성하면 인풋 아래 자동 완성 목록이 나타나게 되는데
위와 같이 검색키보드 이벤트를 통하여 목록들 하나씩 선택되도록 구현해보려고 한다.- 키보드 아래 방향키를 누르면 아래의 자동완성 목록들이 하나씩 선택되고 인풋에 선택된 영화의 제목이 나타난다.
- 자동 완성 목록 끝에서 아래 방향키를 누르면 다시 목록의 첫번째 아이템으로 이동한다.
- 첫번째 아이템에서 방향키 위를 누르면 기존의 내가 검색한 텍스트가 다시 나타난다.
- 선택된 검색 아이템을 기준으로 스크롤이 자동으로 된다.
- 엔터를 누르면 인풋에 나타난 텍스트를 쿼리로 받아 검색 결과 페이지로 라우팅된다.
* 기능 구현 과정에 초점을 두어 코드를 간소화하여 작성하였다.
사용될 state 및 refconst [isAutoSearch, setIsAutoSearch] = useState(false); const [searchKeyword, setSearchKeyword] = useState(""); const [autoSearchKeyword, setAutoSearchKeyword] = useState(""); const [focusIndex, setFocusIndex] = useState(-1); const focusRef = useRef(null); const scrollRef = useRef(null);
isAutoSearch : 현재 자동완성목록을 탐색하고 있는 지에 대한 불린값
searchKeyword : 내가 직접 인풋에 타이핑한 검색값
autoSearchKeyword : 현재 선택된 자동완성 검색값
focusIndex : 현재 선택된 자동완성 목록의 인덱스
listRef : 자동완성목록 컨테이너 요소
focusRef : 현재 선택된 아이템 요소
input<input type="text" placeholder="영화를 검색해보세요" title="검색" name="검색" value={isAutoSearch ? autoSearchKeyword : searchKeyword} onChange={handleInputChange} onKeyUp={handleKeyUp} />
value 값에 isAutoSearch를 기준으로 true이면 autoSearchKeyword, false이면 searchKeyword로 관리를 하였다.
isAutoSearch는 유저가 인풋에 검색어를 작성 후 아래에 자동완성 목록이 있어 키보드 아래방향키를 누르게 되면 true가 된다. 그리고 첫번째 아이템에서 위방향키를 누르거나 추가로 검색어를 작성시 false로 바뀐다.
change 이벤트const handleInputChange = (e) => { if (isAutoSearch) { const enteredValue = e.nativeEvent.inputType === "deleteContentBackward" ? "" : e.nativeEvent.data; focusIndex >= 0 && setSearchKeyword(autoSearchKeyword + enteredValue) setIsAutoSearch(false) setFocusIndex(-1) return } setSearchKeyword(e.target.value) }
onChange 이벤트 핸들러 함수에 이벤트 객체를 받아 searchKeyword에 담았다.
if(isAutoSearch) 구문은 밑에 다시 언급할 예정
keyup 이벤트const handleKeyUp = (e) => { if(KeyEvent[e.key]) KeyEvent[e.key]() }
KeyEvent 객체를 만들어 e.key와 동일한 이름의 메소드를 지어 동일하다면 그 메소드를 실행시키는 코드이다.만약 KeyEvent[e.key]가 있다면 그 메소드를 실행시켜 주었다.
const KeyEvent = { Enter: () => { goToSearchPage() }, ArrowDown: () => { if (autoSearchList.length === 0) { return } if (listRef.current.childElementCount === focusIndex + 1) { setFocusIndex(() => 0) return } if (focusIndex === -1) { setIsAutoSearch(true) } setFocusIndex((index) => index + 1) setAutoSearchKeyword(autoSearchList.results[focusIndex + 1].title) }, ArrowUp: () => { if (focusIndex === -1) { return } if (focusIndex === 0) { setAutoSearchKeyword("") setFocusIndex((index) => index - 1) setIsAutoSearch(false) return } setFocusIndex((index) => index - 1) setAutoSearchKeyword(autoSearchList.results[focusIndex - 1].title) }, Escape: () => { setAutoSearchKeyword("") setFocusIndex(-1) setIsAutoSearch(false) }, }
KeyEvent 객체이다. 한 메소드 씩 살펴보겠다.const KeyEvent = { Enter: () => { goToSearchPage() } } const goToSearchPage = () => { if (isNull()) return navigate( `/search?query=${isAutoSearch ? autoSearchKeyword : searchKeyword}` ) }
엔터키를 누르면 goToSearchPage 함수가 실행된다.
isAutoSearch state를 통해 분기처리 했다.
(isNull은 인풋 공백을 체크하는 용도로 작성하였다)
const KeyEvent = { ArrowDown: () => { if (autoSearchList.length === 0) { return } if (listRef.current.childElementCount === focusIndex + 1) { setFocusIndex(() => 0) return } if (focusIndex === -1) { setIsAutoSearch(true) } setFocusIndex((index) => index + 1) setAutoSearchKeyword(autoSearchList[focusIndex + 1].title) } }
아래 방향키를 누르면 실행된다.
아래 방향키를 누르면 focusIndex가 1씩 증가하고,
AutoSearchKeyword에 선택된 아이템의 title을 가져와 인풋에 나타낸다.- 자동검색목록이 없으면 리턴
- focusIndex가 자동완성목록 컨테이너의 아이템의 개수를 초과하면 focusIndex를 다시 0으로 만들어 첫번째 아이템을 선택하게 한다.
- focusIndex가 -1이라면 유저가 자동검색목록을 이제 막 탐색하려고 하는 상태이기 때문에 isAutoSearch를 ture로 준다.
const KeyEvent = { ArrowUp: () => { if (focusIndex === -1) { return } if (focusIndex === 0) { setAutoSearchKeyword("") setFocusIndex((index) => index - 1) setIsAutoSearch(false) return } setFocusIndex((index) => index - 1) setAutoSearchKeyword(autoSearchList.results[focusIndex - 1].title) } }
위 방향키를 선택하면 실행된다.
위 방향키를 누르면 focusIndex가 1씩 감소하고,
AutoSearchKeyword에 선택된 아이템의 title을 가져와 인풋에 나타낸다.- focusIndex가 -1이면 탐색 상태가 아니기 때문에 리턴
- focusIndex가 0이면 isAutoSearch가 false가 된다. (키워드 공백, 인덱스 -1)
- 인풋의 value를 isAutoSearch로 분기처리 했기 때문에 내가 기존에 검색했던 searchKeyword가 다시 나타나게 된다.
const KeyEvent = { Escape: () => { setAutoSearchKeyword("") setFocusIndex(-1) setIsAutoSearch(false) } }
esc키를 누르면 자동검색 목록 탐색 종료를 하게 된다.const handleInputChange = (e) => { if (isAutoSearch) { const enteredValue = e.nativeEvent.inputType === "deleteContentBackward" ? "" : e.nativeEvent.data focusIndex >= 0 && setSearchKeyword(autoSearchKeyword + enteredValue) setIsAutoSearch(false) setFocusIndex(-1) return } setSearchKeyword(e.target.value) }
다시 돌아와 인풋 체인지 핸들러를 보면,
if 구문은 자동검색 목록을 탐색 중일 때 내가 타이핑을 추가하게 될 경우의 코드이다.
autoSearchKeyword에 내가 방금 타이핑한 값인 enteredValue가 더해져 searchKeyword에 담겨지고 탐색은 종료된다.
하지만 타이핑말고 backspace를 치면 오류가 나게 된다.
그래서 뒤로가기를 누른 걸 감지하는 e.nativeEvent.inputType === "deleteContentBackward"를 통해
뒤로가기를 누르면 ""이 추가, 아니면 방금 내가 타이핑한 값을 추출하는 e.nativeEvent.data를 추가하였다.useEffect(() => { if (isAutoSearch) { return } getSearchQuery({ query: searchKeyword }) }, [getSearchQuery, searchKeyword, isAutoSearch])
useEffect로 searchKeyword가 변경될 때마다 api 호출을 하여 자동검색 목록을 받아온다.
자동검색 목록 탐색을 중일 땐 가져오지 않게 리턴처리 하였다.
현재 선택된 아이템에 따른 스크롤useEffect(() => { scrollRef.current?.scrollIntoView({ behavior: "smooth", block: "center" }) }, [focusIndex]) <SearchResultList ref={listRef}> {autoSearchList.map((movie, listIndex) => ( <Link to={`/detail/${movie.id}`} key={movie.id}> <SearchResultItem ref={listIndex === focusIndex ? focusRef : undefined} > // 생략 </SearchResultItem> </Link> ))} </SearchResultList>
전에 따로 작성해둔 글을 참고하면 된다.[JavaScript] 특정 요소에 따른 스크롤 자동 조정 : scrollIntoView()
element.scrollIntoView scrollIntoView()를 호출하면 호출된 요소가 사용자에게 보여지도록 상위 요소의 스크롤이 이동된다. element.scrollIntoView(); element.scrollIntoView(alignToTop); // Boolean 파라미터 element.scrollInto
ddd120.tistory.com
전체 코드
const Search = () => { const [isAutoSearch, setIsAutoSearch] = useState(false); const [searchKeyword, setSearchKeyword] = useState(""); const [autoSearchKeyword, setAutoSearchKeyword] = useState(""); const [focusIndex, setFocusIndex] = useState(-1); const listRef = useRef(null); const focusRef = useRef(null); const handleInputChange = (e) => { const enteredValue = e.nativeEvent.inputType === "deleteContentBackward" ? "" : e.nativeEvent.data; if (isAutoSearch) { focusIndex >= 0 && setSearchKeyword(autoSearchKeyword + enteredValue); setIsAutoSearch(false); setFocusIndex(-1); return; } setSearchKeyword(e.target.value); }; const goToSearchPage = () => { if (isNull()) return; navigate( `/search?query=${isAutoSearch ? autoSearchKeyword : searchKeyword}` ); }; const KeyEvent = { Enter: () => { goToSearchPage(); }, ArrowDown: () => { if (autoSearchList.length === 0) { return; } if (listRef.current.childElementCount === focusIndex + 1) { setFocusIndex(() => 0); return; } if (focusIndex === -1) { setIsAutoSearch(true); } setFocusIndex((index) => index + 1); setAutoSearchKeyword(autoSearchList.results[focusIndex + 1].title); }, ArrowUp: () => { if (focusIndex === -1) { return; } if (focusIndex === 0) { setAutoSearchKeyword(""); setFocusIndex((index) => index - 1); setIsAutoSearch(false); return; } setFocusIndex((index) => index - 1); setAutoSearchKeyword(autoSearchList.results[focusIndex - 1].title); }, Escape: () => { setAutoSearchKeyword(""); setFocusIndex(-1); setIsAutoSearch(false); }, }; const handleKeyUp = (e) => { if(KeyEvent[e.key]) KeyEvent[e.key](); }; useEffect(() => { if (isAutoSearch) { return; } getSearchQuery({ query: searchKeyword }); }, [getSearchQuery, searchKeyword, isAutoSearch]); useEffect(() => { scrollRef.current?.scrollIntoView({ behavior: "smooth", block: "center" }); }, [focusIndex]); return ( <Base> <Input type="text" placeholder="영화를 검색해보세요" title="검색" name="검색" value={isAutoSearch ? autoSearchKeyword : searchKeyword} onChange={handleInputChange} onKeyUp={handleKeyUp} /> <SearchResultList ref={listRef}> {autoSearchList.map((movie, listIndex) => ( <Link to={`/detail/${movie.id}`} key={movie.id}> <SearchResultItem ref={listIndex === focusIndex ? focusRef : undefined} > // 생략 </SearchResultItem> </Link> ))} </SearchResultList> </Base> ); };
반응형'React' 카테고리의 다른 글
[React] vite + ts에서 svg component 사용하기 (0) 2023.01.30 [React] useCallback으로 useEffect 무한루프 방지 (0) 2023.01.12 [React] 별점 기능 구현 (0) 2022.12.14 [React] 함수형 컴포넌트와 함수 컴포넌트 (0) 2022.11.09 [React] useEffect의 dependency array에 object (0) 2022.11.01