Input에서 값을 입력받아, 검색할 수 있도록 하기 위해 Form과 Input을 만들어 줍니다.
Input에 검색할 내용을 State값으로 관리해줍니다.
ref를 써서 form에 접근해도 괜찮지 않을가 합니다.
// isFocus 상태 관리
const [isFocus, setIsFocus] = useState(false)
// 클릭시 DropDown을 나타내고(isFocus:true), 다른곳 클릭시 없애주도록(isFocus:false)
const handleClickSearchBox = (e: MouseEvent) => {
const isFocus = (e.target as HTMLElement).closest('form')?.dataset.id;
isFocus ? setIsFocus(true) : setIsFocus(false);
};
// form을 제외한 다른곳 클릭시 없애기 위해 window에 click Event를 걸어줍니다.
useEffect(() => {
window.addEventListener('click', handleClickSearchBox);
return () => {
window.removeEventListener('click', handleClickSearchBox);
};
}, []);
// 임시 data arr
const PREVIEW_LIST = [
'굽네치킨',
'지코바 치킨',
'버거킹',
'bhc',
'bbq',
'pizza',
'pizza hut',
'hamburger',
'도미노피자',
'반올림피자',
'돈까스',
'돈치킨',
'치킨까스',
'햄버거',
'햄토스트',
];
// 검색어 입력시 검색어와 연관된 Data를 담을 List, default값은 PREVIEW_LIST
const [dropDownList, setDropDownList] = useState(PREVIEW_LIST);
// dropDownList들 중에 마우스나 키보드를 사용해서 index번째 값에 접근하기 위한 index
// -1로 시작한 이유는 data가 0번째부터 있기 때문에 -1부터 시작해서 내려가도록 합니다.
const [dropDownItemIndex, setDropDownItemIndex] = useState(-1);
// 키워드가 없다면 빈배열[]로 만들어주고 종료합니다.
const updatedDropDownList = () => {
if (inputValue === '') {
setDropDownList([]);
return;
}
// 키워드가 있다면 filter와 includes를 통해 해당 키워드의 대.소문자 구별없이 filtering 해줍니다.
const getRelatedKeywordArr = PREVIEW_LIST.filter(
textItem => textItem.includes(inputValue.toLocaleLowerCase()) || textItem.includes(inputValue.toUpperCase())
);
// dropDownList 상태값을 filtering한 배열로 바꿔주고, -1로 초기화해줍니다. 초기화 하는 이유는 검색어가 바뀔때 이전 index를 기억하지 않도록 하기 위함입니다.
setDropDownList(getRelatedKeywordArr);
setDropDownItemIndex(-1);
};
// useEffect의 dep[]를 통해 inputValue가 바뀔때마다 updatedDropDownList를 update 해줍니다.
useEffect(() => {
updatedDropDownList();
}, [inputValue]);
const navigate = useNavigate();
// 마우스로 클릭시 navigate를 통해 이동
const handleClickDropDownList = (dropDownItem: string) => {
navigate(`/search?keyword=${dropDownItem}`);
};
// 키보드로 입력시
const handleDropDownKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
// 빈값이거나 isComposing이 true일 경우 종료해줍니다. isComposing관련해서 아래에서 다시 말씀드리겟습니다.
if (inputValue.trim() === '' || event.nativeEvent.isComposing) return;
// 아래방향 키보드 누를경우
if (event.code === 'ArrowDown') {
dropDownItemIndex === dropDownList.length - 1
? setDropDownItemIndex(-1)
: setDropDownItemIndex(dropDownItemIndex + 1);
}
// 위방향 키보드 누를경우
if (event.code === 'ArrowUp') {
dropDownItemIndex === -1
? setDropDownItemIndex(dropDownList.length - 1)
: setDropDownItemIndex(dropDownItemIndex - 1);
}
// Enter 키보드 누를경우
if (event.code === 'Enter') {
let keyword = dropDownList[dropDownItemIndex] ? dropDownList[dropDownItemIndex] : inputValue;
navigate(`/search?keyword=${keyword}`);
}
};
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
const PREVIEW_LIST = [
'굽네치킨',
'지코바 치킨',
'버거킹',
'bhc',
'bbq',
'pizza',
'pizza hut',
'hamburger',
'도미노피자',
'반올림피자',
'돈까스',
'돈치킨',
'치킨까스',
'햄버거',
'햄토스트',
];
const useDropdown = (inputValue: string) => {
const navigate = useNavigate();
const [dropDownList, setDropDownList] = useState(PREVIEW_LIST);
const [dropDownItemIndex, setDropDownItemIndex] = useState(-1);
const [isFocus, setIsFocus] = useState(false);
const updatedDropDownList = () => {
if (inputValue === '') {
setDropDownList([]);
return;
}
const getRelatedKeywordArr = PREVIEW_LIST.filter(
textItem => textItem.includes(inputValue.toLocaleLowerCase()) || textItem.includes(inputValue.toUpperCase())
);
setDropDownList(getRelatedKeywordArr);
setDropDownItemIndex(-1);
};
const handleDropDownKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (inputValue.trim() === '' || event.nativeEvent.isComposing) return;
if (event.code === 'ArrowDown') {
dropDownItemIndex === dropDownList.length - 1
? setDropDownItemIndex(-1)
: setDropDownItemIndex(dropDownItemIndex + 1);
}
if (event.code === 'ArrowUp') {
dropDownItemIndex === -1
? setDropDownItemIndex(dropDownList.length - 1)
: setDropDownItemIndex(dropDownItemIndex - 1);
}
if (event.code === 'Enter') {
let keyword = dropDownList[dropDownItemIndex] ? dropDownList[dropDownItemIndex] : inputValue;
navigate(`/search?keyword=${keyword}`);
}
};
const handleClickSearchBox = (e: MouseEvent) => {
const isFocus = (e.target as HTMLElement).closest('form')?.dataset.id;
isFocus ? setIsFocus(true) : setIsFocus(false);
};
const handleClickDropDownList = (dropDownItem: string) => {
navigate(`/search?keyword=${dropDownItem}`);
};
useEffect(() => {
updatedDropDownList();
}, [inputValue]);
useEffect(() => {
window.addEventListener('click', handleClickSearchBox);
return () => {
window.removeEventListener('click', handleClickSearchBox);
};
}, []);
return {
handleClickDropDownList,
handleDropDownKeyDown,
isFocus,
dropDownList,
setDropDownItemIndex,
dropDownItemIndex,
};
};
export default useDropdown;
import { css } from '@emotion/react';
import theme from '../../../styles/theme';
const DropDownBox = css`
margin: -10px auto;
padding-top: 10px;
background-color: white;
height: 100px;
`;
const DropDownItem = css`
padding: 0 16px;
&.selected {
cursor: pointer;
background-color: ${theme.grey200};
}
`;
interface Props {
handleClickDropDownList: (dropDownItem: string) => void;
dropDownList: string[];
setDropDownItemIndex: React.Dispatch<React.SetStateAction<number>>;
dropDownItemIndex: number;
}
const Dropdown = ({ handleClickDropDownList, dropDownList, setDropDownItemIndex, dropDownItemIndex }: Props) => {
return (
<div css={DropDownBox}>
// dropDownList가 없으면 없다는 메세지를 나타내줍니다.
{dropDownList.length === 0 && <div css={DropDownItem}>해당하는 단어가 없습니다</div>}
// dropDownList가 존재한다면 dropDownList를 map으로 돌면서 각각의 Item에 onClick handler, mouseOver handler를 달아줍니다.
{dropDownList.map((dropDownItem, dropDownIndex) => {
return (
<ul
css={DropDownItem}
key={`${dropDownItem}-${dropDownIndex}`}
onClick={() => handleClickDropDownList(dropDownItem)}
onMouseOver={() => setDropDownItemIndex(dropDownIndex)}
className={dropDownItemIndex === dropDownIndex ? 'selected' : 'null'}
>
<li>{dropDownItem}<li>
</ul>
);
})}
</div>
);
};
export default Dropdown;
import { useState } from 'react';
import { Input, Wrapper } from './SearchBar.style';
import useDropdown from '../../../hooks/useDropdown';
import Dropdown from '../../common/Dropdown/Dropdown';
const SearchBar = () => {
const [inputValue, setInputValue] = useState('');
const {
handleDropDownKeyDown,
isFocus,
handleClickDropDownList,
dropDownList,
setDropDownItemIndex,
dropDownItemIndex,
} = useDropdown(inputValue);
const changeInputValue = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (inputValue.trim() === '') return;
};
return (
<form css={Wrapper} data-id={'isInputFocus'} onSubmit={handleSubmit}>
<input
placeholder="테스트를 위해 피자를 입력해주세요"
css={Input}
autoComplete="off"
type="text"
name="keyword"
value={inputValue}
onChange={changeInputValue}
onKeyDown={handleDropDownKeyDown}
/>
{isFocus && (
<Dropdown
handleClickDropDownList={handleClickDropDownList}
dropDownList={dropDownList}
setDropDownItemIndex={setDropDownItemIndex}
dropDownItemIndex={dropDownItemIndex}
/>
)}
</form>
);
};
export default SearchBar;
한글의 경우 2번의 Event가 발생하는 문제
영어의 경우 문제가 없으나, 한글의 경우 event가 2번 발생하는 문제가 생깁니다.
즉 아래 방향키를 한번만 눌러도 영상에서의 첫번째 값인 도미노 피자가 아닌, 반올림피자에 커서가 가게됩니다.
바로 영상 보고 가시죠
한글의 경우 조합문자이기 때문에 현재의 단어가 완성된 단어인지, 조합중인 단어인지 판단하기 위해 isComposing이라는 속성을 통해 판단한다고 합니다.
따라서 키보드를 누를땐 isComposing이 true, 키보드를 땔때 isComposing이 false가 되서 2번 이벤트가 발생합니다.
아래의 사진에서 한글의 경우 _
표시가 나타납니다. 이경우는 2번의 Event가 발생합니다.
따라서 isComposing이 true인 경우 Return 시켜서 문제를 해결할 수 있었습니다.
if (inputValue.trim() === '' || event.nativeEvent.isComposing) return;
일반적으로 React 이벤트 핸들러에서 event 객체는 React가 제공하는 합성 이벤트(Synthetic Event)입니다. 합성 이벤트는 브라우저 간의 이벤트 호환성을 보장하고 이벤트 풀링(Event Pooling)을 통해 성능을 최적화하기 위해 사용됩니다
따라서 키보드 event에 접근하기 위해 event.nativeEvent.isComposing로 접근해야 합니다. 리액트 공식문서 참고