Name | ํฉ์ํ | ์ด์คํธ | ๋ฐ์ํ | ์ด์๋ฏผ | ์ ๋ํ |
---|---|---|---|---|---|
Profile | |||||
GitHub | @rjsej12 | @wujuno | @pySoo | @sangminlee98 | @robin14dev |
Name | ๊ฐ๋ช ์ฃผ | ๋ฐ๊ฒธ์ | ์ ์ ์ | ๊ณ ์์ฑ | ์ถํ์ฌ |
---|---|---|---|---|---|
Profile | |||||
GitHub | @myungju030 | @seoltang | @wjdwjdtn92 | @free-ko | @Chuhj |
๊ฒ์์ฐฝ ๊ตฌํ + ๊ฒ์์ด ์ถ์ฒ ๊ธฐ๋ฅ ๊ตฌํ + ์บ์ฑ ๊ธฐ๋ฅ ๊ตฌํ
์งํ ๊ธฐ๊ฐ: 2023-05-02 ~ 2023-05-05
- ๐ ๋ฐฐํฌ ๋งํฌ
- ๐ฅ๏ธ ๋์ ํ๋ฉด
- ๐ ์ฌ์ฉํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- ๐ ํ๋ก์ ํธ ๊ตฌ์กฐ
- ๐น๏ธ ํ๋ก์ ํธ ์คํ ๋ฐฉ๋ฒ
- ๐ฏ ๊ณผ์ ์ํ ๋ด์ฉ
- ๐ญ ๊ณ ๋ฏผํ๋ ์ฌํญ๋ค
- ๋น๋ ํด ์ ์ ์ ๊ดํ ๋ ผ์
- CSS ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ์ ์ ๊ดํ ๋ ผ์
- Redux ์ฌ์ฉ ์ฌ๋ถ์ ๊ดํ ๋ ผ์
- ๋ก์ปฌ ์บ์ฑ ๋ฐฉ๋ฒ์ ๊ดํ ๋ ผ์
- Expire time ๊ฒฐ์ ์ ๊ดํ ๋ ผ์
- ๋๋ฐ์ด์ฑ(Debouncing) vs ์ฐ๋กํ๋ง(Throttling)
- API ํธ์ถ์ CORS ์ค๋ฅ
- ํ๊ธ ์ ๋ ฅ ์ keyDown ์ด๋ฒคํธ ์ค๋ณต ๋ฐ์ ์ด์
- ํฌ์ปค์ค ์คํ์ผ๋ง์ด ์ค๋ณต๋๋ ์ด์
- ์์ด ๋๋ฌธ์๋ก ๊ฒ์ ์ ์ถ์ฒ ๊ฒ์์ด๊ฐ ์๋ ์ด์
- ๊ฒ์์ด๋ฅผ ๋นจ๋ฆฌ ์ ๋ ฅํ์ ๋ ์ถ์ฒ ๊ฒ์์ด ๋ฆฌ์คํธ๊ฐ inputText๋ก ๋ํ๋๋ ์ด์
- ์์ด๋ก ๊ฒ์ ์ ์ถ์ฒ ๊ฒ์์ด์ inputText์ ๊ฐ์ ๋ถ๋ถ์ ๋ชจ๋ ์๋ฌธ์๋ก ๋ณด์ด๋ ์ด์
https://pre-onboarding-10th-2-3.netlify.app/
๐ฆsrc
โโโ ๐components
โ โโโ ๐SearchSection
โ โโโ ๐SearchBar
โ โ โโโ ๐DeleteButton
โ โโโ ๐SearchIcon
โ โโโ ๐SearchWordBox
โ โโโ ๐SearchWord
โโโ ๐constants
โโโ ๐hooks
โโโ ๐services
โโโ ๐types
โโโ ๐utils
๋ ํ์งํ ๋ฆฌ ํด๋ก
$ git clone https://github.com/WANTED-TEAM03/pre-onboarding-10th-2-3.git
ํจํค์ง ์ค์น
$ yarn
์ ํ๋ฆฌ์ผ์ด์ ์คํ
$ yarn dev
์ฝ๋์ ๊ฐ๋ ์ฑ ๋ฐ ์ฌ์ฌ์ฉ์ฑ
- ํน์ ํค์๋๋ค์ ์์ํ ํ์ฌ ์ ์ง๋ณด์๊ฐ ์ฉ์ดํ๋๋ก ๊ตฌํํ์์ต๋๋ค.
- ์ถ์ฒ๊ฒ์์ด๋ฅผ ์ ์ฅํ๋ ์บ์์คํ ๋ฆฌ์ง์ ๋ง๋ฃ ์๊ฐ๊ณผ key
- API ์ฃผ์์ ์๋ฌ๋ฉ์์ง
- ์ต์ ๊ฒ์์ด๋ฅผ ์ ์ฅํ๋ ์ธ์ ์คํ ๋ฆฌ์ง์ key
- ์ฌ์ฌ์ฉ์ด ๊ฐ๋ฅํ ์ปค์คํ
ํ
์ ๊ตฌํํ์์ต๋๋ค.
- ๋๋ฐ์ด์ค ๊ธฐ๋ฅ์ด ๋ด๊ฒจ์๋
useDebounce
- ์
๋ ฅ๋ ํ
์คํธ๋ฅผ ์ธ์
์คํ ๋ฆฌ์ง์ ์ ์ฅํ๋
useRecentSearchWords
- ๋ฐฉํฅํค ์ด๋ฒคํธ์ ๋ฐ๋ผ ์ธ๋ฑ์ค๋ฅผ ๋ณ๊ฒฝํด์ฃผ๋
useKeyFocus
- ๋๋ฐ์ด์ค ๊ธฐ๋ฅ์ด ๋ด๊ฒจ์๋
- ํน์ ํค์๋๋ค์ ์์ํ ํ์ฌ ์ ์ง๋ณด์๊ฐ ์ฉ์ดํ๋๋ก ๊ตฌํํ์์ต๋๋ค.
์ฑ๋ฅ ์ต์ ํ
- ๋ก์ปฌ ์บ์ฑ์ ๊ตฌํํ์ฌ, ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ๋ ์๊ฐ์ ์ค์์ต๋๋ค.
- ๋๋ฐ์ด์ฑ์ ํตํด API์ ํธ์ถ์ ์ค์์ต๋๋ค.
์ฌ์ฉ์ ๊ฒฝํ
- ์
๋ ฅ์ ๋นจ๊ฐ์ค์ ์์ ์ฃผ๋
spellCheck
- ํด๋น ์ปดํฌ๋ํธ๊ฐ ๋ ๋๋ง๋์์ ๋ ์๋์ผ๋ก ์ปค์๊ฐ focus๋๋
autoFocus
- ์
๋ ฅ์ ๋นจ๊ฐ์ค์ ์์ ์ฃผ๋
-
input
์ ์ ๋ ฅ๋๋ ํ ์คํธ์ ๋ฐ๋ผ API๋ฅผ ํธ์ถํ๋๋ก ๊ตฌํํ์์ต๋๋ค. -
API ํธ์ถ์ ํตํด ๋ฐ์์จ ๋ฐ์ดํฐ๋ ์ํ ์ ๋ฐ์ดํธ ํ, ์ถ์ฒ๊ฒ์์ด๋ฅผ ๋ณด์ฌ์ฃผ๋ UI์
props
๋ก ๋๊ฒจ์ฃผ๋๋ก ๊ตฌํํ์์ต๋๋ค.// src/components/SearchSection/index.tsx const [autocompleteWords, setAutocompleteWords] = useState<SearchWordType[]>([]); const [inputText, setInputText] = useState(''); ~~ useEffect(() => { ~~ const words = await searchAPI(inputText.trim()); setAutocompleteWords(words.slice(0, MAX_DISPLAYED)); ~~ }, [inputText]); ~~ return ( <SearchWordBox ~~ recentSearchWords={recentSearchWords} ~~ /> )
-
API ํธ์ถ์ฌ๋ถ๋ฅผ ์๋ ค์ฃผ๋
console.info('calling api')
๋ฅผ ์ถ๊ฐํ์์ต๋๋ค.// src/services.search.ts export const searchAPI = async (name: string) => { if (name === '') return []; ~~ try { const { data } = await apiClient.get<SearchWordType[]>( API_URLS.search, config ); console.info('calling api'); // api ํธ์ถ ์ฌ๋ถ๋ฅผ ์๋ ค์ฃผ๋ ์ฝ์ ๊ธฐ๋ก ~~ return data; } catch (error) { ~ alert(axiosError.response?.data.message || DEFAULT_ERROR_MESSAGE); return []; } };
๋ก์ปฌ ์บ์ฑ์ ๊ตฌํํ๊ธฐ ์ํด Cache API๋ฅผ ์ฌ์ฉํ์์ต๋๋ค.
์บ์ Expire time์ ๊ตฌํํ์์ต๋๋ค.
src/constants/cache.ts
์์ EXPIRE_TIME
์ ์กฐ์ ํ์ฌ ๋ง๋ฃ ์๊ฐ์ ์กฐ์ ํ ์ ์์ต๋๋ค.
export const EXPIRE_TIME = 1000 * 60 * 10; // ํ
์คํธ ํธ์์ฑ์ ์ํด ๋ง๋ฃ์๊ฐ์ 10๋ถ์ผ๋ก ์ค์ ํ์ต๋๋ค.
์๋ ์ฝ๋๋ค์ src/utils/cacheStorage.ts
์ ์์นํ๊ณ ์์ต๋๋ค.
-
setCacheStorage
export const setCacheStorage = async ( url: string, queryStr: string, data: SearchWordType[], ) => { const cacheStorage = await caches.open(url); const response = new Response(JSON.stringify(data)); // ์บ์์ ์ ์ฅํ ๋ฐ์ดํฐ๋ฅผ Response ๊ฐ์ฒด๋ก ์์ฑ // ์บ์ Response์ Header๋ก ํ์ฌ ์๊ฐ์ ์ ์ฅํฉ const clonedResponse = response.clone(); const newBody = await clonedResponse.blob(); const newHeaders = new Headers(clonedResponse.headers); newHeaders.append(HEADER_FETCH_DATE, new Date().toISOString()); // ์บ์ ์ ์ฅ๋ ์ง๊ฐ ๋ด๊ธด Header๋ฅผ ํฌํจํ Response๊ฐ์ฒด๋ฅผ ์์ฑํ์ฌ ์บ์ ์คํ ๋ฆฌ์ง์ ์ ์ฅ const newResponse = new Response(newBody, { status: clonedResponse.status, statusText: clonedResponse.statusText, headers: newHeaders, }); cacheStorage.put(queryStr, newResponse); };
๋ฐ์ดํฐ๋ฅผ ์บ์ ์คํ ๋ฆฌ์ง์ ์ ์ฅํ๋ ํจ์์ ๋๋ค. ์บ์ ์คํ ๋ฆฌ์ง์ ์ ์ฅํ ๋ฐ์ดํฐ๋ฅผ Response ๊ฐ์ฒด๋ก ์์ฑํ๊ณ , ํด๋น Response Header์
ํ์ฌ ์๊ฐ
์ ๋ด์ต๋๋ค. ์ด๋ ๊ฒ ๋ง๋ค์ด์ง Response๋ฅผ ์บ์ ์คํ ๋ฆฌ์ง์ ์ ์ฅํฉ๋๋ค.
-
getCachedResponse
export const getCachedResponse = async (url: string, queryStr: string) => { // ์บ์ ์คํ ๋ฆฌ์ง์์ ํ์ฌ ๊ฒ์ํ ๋ฐ์ดํฐ๊ฐ ์๋์ง ํ์ธ const cacheStorage = await caches.open(url); const cachedResponse = await cacheStorage.match(queryStr); if (cachedResponse) { if (!getIsCacheExpired(cachedResponse)) return cachedResponse; // ์บ์ ๋ฐ์ดํฐ๊ฐ ์๋ค๋ฉด ์บ์ ๋ฐ์ดํฐ ๋ฐํ // ์บ์ ๋ฐ์ดํฐ๊ฐ ๋ง๋ฃ๋์๋ค๋ฉด ์บ์ ๋ฐ์ดํฐ ์ญ์ ํ null ๋ฐํ await cacheStorage.delete(queryStr); return null; } // ์บ์ ๋ฐ์ดํฐ๊ฐ ์๋ ๊ฒฝ์ฐ null ๋ฐํ return null; };
์บ์ ๋ฐ์ดํฐ๋ฅผ ์บ์ ์คํ ๋ฆฌ์ง์์ ๊บผ๋ด์ค๋ ํจ์์ ๋๋ค. ์ฌ์ฉ์๊ฐ ๊ฒ์ํ ๋ฐ์ดํฐ์ธ
queryStr
๋ฅผ ์บ์ ์คํ ๋ฆฌ์ง์์ ์ฐพ์ต๋๋ค. ์บ์ ๋ฐ์ดํฐ๊ฐ ์กด์ฌํ๋ค๋ฉด ํด๋น ์บ์ Response๋ฅผ ๋ฐํํฉ๋๋ค. ์บ์ ๋ฐ์ดํฐ๊ฐ ์กด์ฌํ์ง๋ง ๋ง๋ฃ๋ ์บ์๋ผ๋ฉด ์บ์ ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํ๊ณnull
์ ๋ฐํํฉ๋๋ค. ์บ์ ๋ฐ์ดํฐ๊ฐ ์กด์ฌํ์ง ์๋ค๋ฉดnull
์ ๋ฐํํฉ๋๋ค.
-
getIsCacheExpired
export const getIsCacheExpired = (cacheResponse: Response) => { // ์บ์ Response ํค๋์ ์๋ ์บ์ ์ ์ฅ ๋ ์ง ํ์ธ const cachedDate = cacheResponse.headers.get(HEADER_FETCH_DATE); if (!cachedDate) return; const fetchDate = new Date(cachedDate).getTime(); const today = new Date().getTime(); // (ํ์ฌ๋ ์ง - ์บ์ ์ ์ฅ ๋ ์ง > ๋ง๋ฃ๋ ์ง)๋ฅผ ๋น๊ตํ์ฌ boolean๊ฐ ๋ฐํ return today - fetchDate > EXPIRE_TIME; };
์บ์ ๋ฐ์ดํฐ๊ฐ ๋ง๋ฃ๋์๋์ง ํ๋จํ์ฌ boolean ๊ฐ์ ๋ฐํํ๋ ํจ์์ ๋๋ค. ์ธ์๋ก Response๋ฅผ ๋ฐ์ ํด๋น Response์ Header์ ๋ด์์๋
์บ์ ์ ์ฅ ๋ ์ง
๋ฅผ ํ์ธํฉ๋๋ค.ํ์ฌ ๋ ์ง
์์บ์ ์ ์ฅ ๋ ์ง
์ ์ฐจ์ด๊ฐ๋ง๋ฃ ์๊ฐ
๋ณด๋ค ํด ๊ฒฝ์ฐ ํด๋น ์บ์๋ ๋ง๋ฃ๋ ์บ์์ด๋ฏ๋กfalse
๋ฅผ ๋ฐํํฉ๋๋ค.
-
์ถ์ฒ ๊ฒ์์ด API ํธ์ถ
// src/services/search.ts ... const queryStr = new URLSearchParams(config.params).toString(); // ์ฌ์ฉ์๊ฐ ๊ฒ์ํ ๋จ์ด ์ถ์ถ const responsedCache = await getCachedResponse(API_URLS.search, queryStr); if (responsedCache) return await responsedCache.json(); try { const { data } = await apiClient.get<SearchWordType[]>( API_URLS.search, config, ); console.info('calling api'); setCacheStorage(API_URLS.search, queryStr, data); return data; ...
์ค์ ๊ฒ์์ด API๋ฅผ ํธ์ถํ๋ ๋ถ๋ถ์ ๋๋ค.
getCachedResponse
ํจ์๋ฅผ ํธ์ถํด ํด๋น ๊ฒ์์ด์ ์บ์ ๋ฐ์ดํฐ๊ฐ ์๋์ง ํ์ธํ๊ณ ์บ์ ๋ฐ์ดํฐ๊ฐ ์๋ค๋ฉด ์บ์ ๋ฐ์ดํฐ๋ฅผ ๋ฐํํฉ๋๋ค. ์บ์ ๋ฐ์ดํฐ๊ฐ ์๋ค๋ฉด API ํธ์ถ์ ํตํด ๋ฐ์ดํฐ๋ฅผ ํธ์ถํ๊ณsetCacheStorage
ํจ์๋ฅผ ํธ์ถํ์ฌ ์บ์ ์คํ ๋ฆฌ์ง์ ์บ์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํฉ๋๋ค.
-
์ด๋ฒคํธ๋ฅผ ๊ทธ๋ฃนํํ์ฌ ํน์ ์๊ฐ์ด ์ง๋ ํ ํ๋์ ์ด๋ฒคํธ๋ง ๋ฐ์ํ๋๋ก ํ๋ Debounceย ๊ธฐ์ ์ ์ฌ์ฉํ์ต๋๋ค.
-
input
์ฐฝ์ ๊ฒ์์ด๋ฅผ ์ ๋ ฅํ ๋๋ง๋ค ๊ฒ์์ด๋ฅผ ๋ชจ์์ฃผ๋ ๊ธฐ๋ฅ์ ๋ด์ ํจ์useDebounce
์ ์ ๋ฌํฉ๋๋ค.// src/components/SearchSection/index.tsx const [inputText, setInputText] = useState(''); ... const debouncedInputText = useDebounce(inputText); useEffect(() => { ... const words = await searchAPI(debouncedInputText.trim()); setAutocompleteWords(words.slice(0, MAX_DISPLAYED)); ... }, [debouncedInputText]); // ์ถ์ ๋ ํ ์คํธ์ ๋ณ๊ฒฝ ์ ๋ฌด์ ๋ฐ๋ผ useEffect๊ฐ ํธ์ถ๋ฉ๋๋ค.
-
ํน์ ์๊ฐ์ ์ ํด๋๊ณ , ํด๋น ์๊ฐ ๋ด์ ์ ๋ ฅํ๋ ๋ชจ๋ ํ ์คํธ๋ฅผ ํ๋๋ก ๋ชจ์ ๋ค์์ ํด๋น ์๊ฐ์ด ์ง๋๋ฉด ์ถ์ ๋ ํ ์คํธ์ ๋ณ๊ฒฝ ์ ๋ฌด์ ๋ฐ๋ผ API๋ฅผ ํธ์ถํ์ฌ ๋คํธ์ํฌ ๋น์ฉ์ ์ค์ด๋๋ก ๊ตฌํํ์์ต๋๋ค.
// src/components/SearchSection/index.tsx useEffect(() => { const fetchAutocompleteWords = async () => { setIsLoading(true); const words = await searchAPI(debouncedInputText.trim()); setIsLoading(false); setAutocompleteWords(words.slice(0, MAX_DISPLAYED)); }; fetchAutocompleteWords(); }, [debouncedInputText]);
-
-
๋ํ ํด๋น ๋๋ฐ์ด์ค ์์ ์ ์ ์ง, ๋ณด์๊ฐ ์ฉ์ดํ๋๋ก ์ปค์คํ ํ ์ผ๋ก ๋ถ๋ฆฌํ์์ต๋๋ค.
-
ํ์ฌ๋ ์ธ์๋ก ๊ฒ์์ด๋ฅผ ๋ฐ๋
string
์ด์ง๋ง ์ถํ ์ฌ์ฌ์ฉ์ ๊ณ ๋ คํด value์ ํ์ ์ generic<T>
๋ก ์ ์ธํ์์ต๋๋ค. (๊ทธ๋ผ delay๋ ์ซ์๋ก ์ธํ ํ๋ ๊ฒ๋ ์ผ๊ด์ฑ์ด ์์ง ์์๊น??)// src/hooks/useDebounce.ts export default function useDebounce<T>(value: T, delay = 300) { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const timerId = setTimeout(() => { setDebouncedValue(value); }, delay); return () => { clearTimeout(timerId); }; }, [value, delay]); return debouncedValue; }
- ์, ์๋ ๋ฐฉํฅํค ๋ฐ
tab
ํค๋ก ์ด๋ํ์ฌ ์ถ์ฒ ๊ฒ์์ด๋ค๋ก ์ด๋์ด ๊ฐ๋ฅํ๋๋ก ํค๋ณด๋ ๋ค๋น๊ฒ์ด์ ์ ๊ตฌํํ์์ต๋๋ค. - ์ด๋ ํ Enter ์ ๋ ฅ ์, focus๋ ์ถ์ฒ ๊ฒ์์ด๋ฅผ ๊ฒ์ํฉ๋๋ค.
์๋งจํฑ ๋งํฌ์
์ ์ํด ์ถ์ฒ ๊ฒ์์ด๋ฅผ <li>
ํ๊ทธ๋ก ์ค์ ํ์์ผ๋, <li>
ํ๊ทธ๋ ๋น๋ํํ ์์์ด๊ธฐ ๋๋ฌธ์ ์ ๊ทผ์ฑ ํธ๋ฆฌ์ ๋ํ๋์ง ์์ ํค๋ณด๋ tab
ํค๋ก ์ ๊ทผํ ์ ์์์ต๋๋ค.
์ ๊ทผ์ฑ ํธ๋ฆฌ์ ํฌํจ๋ ์ ์๋๋ก ๋น๋ํํ ์์์ธ <li>
ํ๊ทธ๋ฅผ ๋ํํ ์์์ธ <button>
ํ๊ทธ๋ก ๋ฐ๊พธ์ด ํค๋ณด๋ tab
ํค๋ฅผ ํตํด ์ ๊ทผ ๊ฐ๋ฅํ๋๋ก ํ์ต๋๋ค.
๋น๋ํํ ์์๋ฅผ ์ฌ์ฉํด ๋ง๋ ๋ํํ ์ปดํฌ๋ํธ๋ ์ ๊ทผ์ฑ ํธ๋ฆฌ์ ๋ํ๋์ง ์์ผ๋ฏ๋ก, ๋ณด์กฐ ๊ธฐ์ ์ด ํด๋น ์ปดํฌ๋ํธ๋ก ํ์ํ๊ฑฐ๋ ์กฐ์ํ๋ ๊ฒ์ ๋ฐฉ์งํฉ๋๋ค. ์ํธ์์ฉ ๊ฐ๋ฅํ ํญ๋ชฉ์ ๋ํํ ์์๋ฅผ ์ฌ์ฉํด ์ ์ ํ ์๋ฏธ์ ํจ๊ป ๋ํ๋ด์ผ ํฉ๋๋ค.
์ฐธ๊ณ : ์ ๊ทผ์ฑ ๊ณ ๋ ค์ฌํญ - MDN
- UI ๋จ์์์ ๋ก์ง ์ฝ๋๋ฅผ ์ค์ด๊ธฐ ์ํด์
useKeyFocus
์ปค์คํ ํ ์ผ๋กkeyDown
์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ์๊ณ , ๋ฐฉํฅํค ๊ด๋ จ ๋ฌธ์์ด์ ์์ ์ฒ๋ฆฌํ์์ต๋๋ค.
// src/hooks/useKeyFocus.ts
const KEY_NAME = {
arrowDown: 'ArrowDown',
arrowUp: 'ArrowUp',
enter: 'Enter',
};
const useKeyFocus = (...) => {
...
const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
if (!isOnFocus) return;
if (event.isComposing) return;
if (event.key === KEY_NAME.arrowDown) {
setFocusIndex((currentIndex) =>
Math.min(currentIndex + 1, searchWords.length - 1),
);
return;
}
if (event.key === KEY_NAME.arrowUp) {
setFocusIndex((currentIndex) => Math.max(-1, currentIndex - 1));
return;
}
if (event.key === KEY_NAME.enter) {
setInputText(searchWords[focusIndex].name);
setIsOnFocus(false);
}
},
...
);
return { handleKeyDown };
}
-
ํ๊ธ๊ณผ ๊ฐ์ ์กฐํฉ ๋ฌธ์๋ ๋ฌธ์๋ฅผ ๋ณํํ๋ ๊ณผ์ ์์ OS์ ๋ธ๋ผ์ฐ์ ๋ชจ๋ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ๋๋ฌธ์ keyDown ์ด๋ฒคํธ๊ฐ ์ค๋ณต์ผ๋ก ๋ฐ์๋ฉ๋๋ค.
- ๋ฐ๋ผ์ KeyboardEvent์ isComposing ๊ฐ์ ์ด์ฉํ์ฌ ๋ฌธ์ ๋ณํ ๊ณผ์ ์ค์๋ ์ด๋ฒคํธ๊ฐ ์ํ๋์ง ์๋๋ก ์ฒ๋ฆฌํ์ฌ ์ค๋ณต ๋ฐ์์ ํด๊ฒฐํ์ต๋๋ค.
-
Input ์ฐฝ์ด blur์ธ ์ํ์๋ handleKeyDown ํจ์๊ฐ ์๋๋์ด focusIndex๊ฐ ๋ณ๊ฒฝ๋๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
- ๋ฐ๋ผ์ isOnFocus ์ํ๊ฐ ์๋ ๋๋ ์ํ๋์ง ์๋๋ก ์ฒ๋ฆฌํ์์ต๋๋ค.
- Vite๊ฐ ์๊ท๋ชจ ์ฑ์ ์ ํฉํ๊ณ , ๊ฐ๋ฒผ์์ ๋น๋ ์๋๊ฐ ๋น ๋ฅด๋ค๊ณ ์๊ฐํ์ต๋๋ค.
- CRA๋์ Vite ์ฌ์ฉ์ ๊ถ์ฅํ๋ ์๊ฒฌ์ด ์์ด ํ์ต์ ์ํด Vite๋ฅผ ์ ํํ์ต๋๋ค.
- 'Create React App ๊ถ์ฅ์ Vite๋ก ๋์ฒดโ PR ๋ํ Dan Abramov์ ๋ต๋ณ
- ๊ฐํธํ ํด๋์ค๋ช ์ ํตํ ๋งํฌ์ ์์ ์ด ๊ฐ๋ฅํ์ฌ ๊ธฐ๋ฅ ๊ตฌํ์ ์ง์ค ํ ์ ์๋ค๋ ์ฅ์ ์ด ์์ต๋๋ค.
- ์งง์ ๊ธฐ๊ฐ๋์ ๋งํฌ์ ๊ณผ ๊ธฐ๋ฅ ๋ชจ๋ ์งํํด์ผํ๋ ๊ธฐ์ ๊ณผ์ ์ ์ ํฉํ๋ค๊ณ ํ๋จ๋์์ต๋๋ค.
-
Redux์์ ์ ์ญ๋ณ์๋ก state๋ฅผ ๊ด๋ฆฌํ๋ ๋ถ๋ถ์ด ์๋๋ผ๋ฉด Redux๋ฅผ ์ฌ์ฉํ ํ์๋ฅผ ๋๋ผ์ง ๋ชป ํ์ต๋๋ค.
-
ํน์ ํ ๋ฐฉ์์ผ๋ก ์บ์ฑ์ ๊ตฌํํ์ง ์๋๋ค๋ฉด Redux๋ฅผ ์ฌ์ฉํ ํ์๋ฅผ ๋๋ผ์ง ๋ชป ํ์ต๋๋ค.
๊ฐ๋ณ์ ์ผ๋ก ๋ก์ปฌ ์คํ ๋ฆฌ์ง
, ์ธ์
์คํ ๋ฆฌ์ง
, ์บ์ ์คํ ๋ฆฌ์ง
, Map
, Redux
๋ฅผ ์ด์ฉํ์ฌ ๊ณผ์ ๋ฅผ ์งํํ์์ต๋๋ค.
์ ํฌ๋ ์ด๋ค ๋ฐฉ์์ ์ฌ์ฉํ๋ ๊ฒ์ด ์ต์ ํ์ ์ฌ์ฉ์ฑ์ ์ด์ ์ด ๋๋์ง ๋
ผ์ํด ๋ณด์์ต๋๋ค.
- ๋๊ธฐ ๋ฐฉ์์ผ๋ก ๋์ํ๋ฉฐ ๋ฉ์ธ ์ค๋ ๋ ์ฐ์ฐ์ ์ค๋จ์ํต๋๋ค.
- ์ฉ๋ ์ ํ์ 5MB์ด๋ฉฐ ๋ฌธ์์ด๋ง ์ ์ฅํ ์ ์์ต๋๋ค.
- ํญ ์์์๋ง ์ ํจํ๋ฉฐ ํญ์ด ๋ซํ๋ฉด ์คํ ๋ฆฌ์ง๋ ์ข ๋ฃํฉ๋๋ค.
- ํ์ฌ ํญ์์๋ง ์ฌ์ฉํ๋ IndexedDB ํค๋ฅผ ์ ์ ์ ์ฅํ๋ ๊ฒ๊ณผ ๊ฐ์ด ์์ ์ฉ๋ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ ๋๋ ์ข์ต๋๋ค.
- ๋๊ธฐ(synchronous) ๋ฐฉ์์ผ๋ก ๋์ํ๊ธฐ ๋๋ฌธ์ ๋ฉ์ธ ์ค๋ ๋ ์ฐ์ฐ์ ์ค๋จ์ํต๋๋ค.
- ์ฉ๋ ์ ํ์ 5MB์ด๋ฉฐ ๋ฌธ์์ด๋ง ์ ์ฅํ ์ ์์ต๋๋ค.
- ๋ฉ๋ชจ๋ฆฌ์์ ์บ์ ๋ฐ์ดํฐ๋ฅผ ๊ด๋ฆฌํ์ฌ ์ ์ฅ/์ญ์ ๋ฐ ์ค์ ์ด ๋น๊ต์ ์์ ๋กญ์ต๋๋ค.
- ์๋ก๊ณ ์นจ์ ๋ฐ์ดํฐ๊ฐ ํ๋ฐ๋ฉ๋๋ค.
- ์์ฒญ ๋ฐ ์๋ต์๋ HTTP๋ฅผ ํตํด ์ ์กํ ์ ์๋
๋ชจ๋ ์ข ๋ฅ์ ๋ฐ์ดํฐ๊ฐ ํฌํจ๋ ์ ์์ต๋๋ค.
- ๋น๋๊ธฐ ๋ฐฉ์์ผ๋ก ๋์ํ๋ฉฐ
๋ฉ์ธ ์ค๋ ๋ ์ฐ์ฐ์ ์ค๋จํ์ง ์์ต๋๋ค
. ๋ง์ด ์ ์ฅํ ์ ์์ผ๋ฉฐย ์ ์ด๋ ์๋ฐฑ MB, ๊ฒฝ์ฐ์ ๋ฐ๋ผ ์ GB ์ด์
๊น์ง๋ ๋ ์ ์์ต๋๋ค. (๋ธ๋ผ์ฐ์ ๊ตฌํ์ ๋ค๋ฅผ ์ ์์ง๋ง ์ฌ์ฉ ๊ฐ๋ฅํ ์ ์ฅ ๊ณต๊ฐ์ ์์ ์ผ๋ฐ์ ์ผ๋ก ์ฅ์น์์ ์ฌ์ฉ ๊ฐ๋ฅํ ์ ์ฅ ๊ณต๊ฐ์ ์์ ๋ฐ๋ผ ๊ฒฐ์ )
- ์น ์๋น์ค ์บ์ ๋๋ํ๊ฒ ๋ค๋ฃจ๊ธฐ
- ์๋ฒ ์ธก์์ ์ค์ ์ ๋ฐ๊ฟ์ค์ผ ํ๊ธฐ ๋๋ฌธ์ ํ์ฌ ๊ณผ์ ์์๋ ๊ตฌํํ๊ธฐ ํ๋ค๋ค๊ณ ์๊ฐํ์ต๋๋ค.
๋คํธ์ํฌ๋ก ๋ถ๋ฌ์จ ๋ฆฌ์์ค๋ ํ์ผ์ ์บ์ฑํด์ผ ํ๋ค๋ฉดย ์บ์ ์คํ ๋ฆฌ์ง๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข๋ค๊ณ ๊ฒฐ๋ก ์ ๋ด๋ ธ์ต๋๋ค. ๋๋ถ์ด, ์ต๊ทผ ๊ฒ์์ด ๊ธฐ๋ฅ์ ๊ตฌํํ ๋๋ ์ธ์ ์คํ ๋ฆฌ์ง๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉํฅ์ผ๋ก ๊ฒฐ์ ํ์ต๋๋ค.
- API ํธ์ถ์ ๋ฐ๋ฅธ ์๋ฒ์ ๊ณผ๋ถํ ์ ๋์ ๊ฒ์์ด์ ๋ฐ๋ฅธ API ํธ์ถ ๊ฒฐ๊ณผ๊ฐ ๋ณํ๋ ๊ธฐ๊ฐ์ ๋ฐ๋ผ Expire time์ ๊ฒฐ์ ํ๋ ๊ฒ์ด ์ข๋ค๊ณ ์๊ฐํ์ต๋๋ค.
- ํ์ง๋ง ์ด๋ฒ ๊ณผ์ ์์๋ ์ค์ API ๊ด๋ จ ๋ฐ์ดํฐ๋ฅผ ์ ์ ์์ด, ํ ์คํธ ํธ์์ฑ์ ์ํด ๋ง๋ฃ์๊ฐ์ 10๋ถ์ผ๋ก ์ค์ ํ์ต๋๋ค.
- API ํธ์ถ ํ์๋ฅผ ์ต์ํ ํ๊ธฐ ์ํด์ ๊ฒ์์ฐฝ์ ํ ์คํธ๋ฅผ ์ ๋ ฅํ ๋ ๋ง๋ค ์ฐ์์ ์ผ๋ก ๋ฐ์ํ๋ ์ด๋ฒคํธ๋ฅผ ์ ์ดํ ํ์๊ฐ ์์์ต๋๋ค.
- ์ด๋ฒคํธ๋ฅผ ์ ์ดํ๋ ๋ฐฉ๋ฒ์ผ๋ก ๋๋ฐ์ด์ฑ๊ณผ ์ฐ๋กํ๋ง์ด ์์ด ๋ ์ค ์ ํฉํ ๊ธฐ๋ฒ์ด ๋ฌด์์ธ์ง ๊ฒฐ์ ํด์ผ ํ์ต๋๋ค.
- ๋๋ฐ์ด์ฑ(Debouncing) : ์ฐ์ด์ด ๋ฐ์ํ ์ด๋ฒคํธ๋ฅผ ํ๋์ ๊ทธ๋ฃน์ผ๋ก ๋ฌถ์ด์ ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ผ๋ก, ์ฃผ๋ก ๊ทธ๋ฃน์์ ๋ง์ง๋ง, ํน์ ์ฒ์์ ์ฒ๋ฆฌ๋ ํจ์๋ฅผ ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ผ๋ก ์ฌ์ฉ๋ฉ๋๋ค.
- ์ฐ๋กํ๋ง(Throttling) : ์ฐ์ด์ด ๋ฐ์ํ ์ด๋ฒคํธ์ ๋ํด, ์ผ์ ํ delay ์๊ฐ ๋์ ํธ์ถ๋ ํจ์๋ ๋ฌด์ํ๋ ๋ฐฉ๋ฒ์ ๋๋ค.
- ์ฌ์ฉ์์ ์ ๋ ฅ์ด ์ธ์ ๋ง๋ฌด๋ฆฌ ๋ ์ง ๋ชจ๋ฅด๋ ์ํฉ์ด๊ธฐ ๋๋ฌธ์ ๋๋ฐ์ด์ฑ์ ์ ์ฉํ๋ ๊ฒ์ด ๋ ์ ํฉํ๋ค๊ณ ํ๋จํ์ต๋๋ค.
-
API๋ฅผ ์ด์ฉํด ๊ฒ์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๊ธฐ ์ํด ๋คํธ์ํฌ ์์ฒญ ์ CORS ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.
-
vite.config.ts์ ๋ค์๊ณผ ๊ฐ์ ์ฝ๋๋ฅผ ํตํ proxy ์ค์ ์ผ๋ก ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ์ต๋๋ค.
export default defineConfig({ server: { proxy: { '/api/v1': { target: 'API ์ฃผ์', changeOrigin: true, rewrite: (path) => path.replace(/^\/api\/v1/, ''), }, }, }, });
-
๋ฌธ์ ์ฌํญ
- ํ๊ธ๊ณผ ๊ฐ์ ์กฐํฉ ๋ฌธ์๋ ๋ฌธ์๋ฅผ ๋ณํํ๋ IME ๊ณผ์ ์์ OS์ ๋ธ๋ผ์ฐ์ ๋ชจ๋ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ๋๋ฌธ์ keyDown ์ด๋ฒคํธ๊ฐ ์ค๋ณต์ผ๋ก ๋ฐ์๋์์ต๋๋ค.
-
ํด๊ฒฐ ๋ฐฉ๋ฒ
-
๊ธ์๊ฐ ๋ณํ ์ค์ธ์ง ์๋ ค์ฃผ๋ KeyboardEvent์ isComposing ๊ฐ์ ์ด์ฉํ์ฌ ์ด๋ฒคํธ ํธ๋ค๋ฌ๊ฐ ํ ๋ฒ๋ง ํธ์ถ๋๋๋ก ๊ฐ์ ํ์์ต๋๋ค.
const handleKeyDown = useCallback( ... if(e.isComposing) return; );
-
-
์ถ๊ฐ ์ค๋ช
-
IME composition
IME๋ ์์ด๊ฐ ์๋ ์ธ์ด๋ค์ ๋ค์ํ ๋ธ๋ผ์ฐ์ ์์ ์ง์ํ๋๋ก ์ธ์ด๋ฅผ ๋ณํ์์ผ์ฃผ๊ธฐ ์ํ OS ๋จ๊ณ์ ์ดํ๋ฆฌ์ผ์ด์ ์ ๋๋ค. IME ๊ณผ์ ์์ keyDown ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ฉด, OS์ ๋ธ๋ผ์ฐ์ ์์ ํด๋น ์ด๋ฒคํธ๋ฅผ ๋ชจ๋ ์ฒ๋ฆฌํ๊ธฐ ๋๋ฌธ์ ์ด๋ฒคํธ๊ฐ ์ค๋ณต์ผ๋ก ๋ฐ์ํ๊ฒ ๋ฉ๋๋ค.
-
isComposing
ํ๊ธ๊ณผ ๊ฐ์ด ์์๊ณผ ๋ชจ์์ ์กฐํฉ์ผ๋ก ํ ์์ ์ ์ด๋ฃจ๋ ์กฐํฉ ๋ฌธ์๋ ๋ณํํ๋ ๊ณผ์ ์์ ๊ธ์๊ฐ ์กฐํฉ ์ค์ธ์ง ๋๋ ์ํ์ธ์ง๋ฅผ ์ ์ ์์ต๋๋ค. KeyboardEvent.isComposing ๊ฐ์ ์ด์ฉํ๋ฉด ๊ธ์ ์กฐํฉ ์ค์ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋์ง๋ฅผ ์ ์ ์์ต๋๋ค.
-
-
๋ฌธ์ ์ฌํญ
- ์ถ์ฒ ๊ฒ์์ด๋ฅผ ๋ฐฉํฅํค๋ก ์ด๋ํ์ ๋์ ๋ง์ฐ์ค๋ฅผ ์ฌ๋ ธ์ ๋์ ํญํค๋ก ์ด๋ํ์ ๋์ ํฌ์ปค์ฑ์ด ๋ชจ๋ ๋ค๋ฅธ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
tab
ํค๋ก ๊ฒ์์ด๋ฅผ ์ด๋ํ์์ ๋ CSS๊ฐ ์ ์ฉ๋์ง ์์์ต๋๋ค.
-
ํด๊ฒฐ ๋ฐฉ๋ฒ
- CSS์
focus
์์ฑ์ ์ด์ฉํ์ฌtab
ํค๋ก ํฌ์ปค์ค ์์๋ CSS๋ฅผ ์ ์ฉํ๋๋ก ์์ ํ์์ต๋๋ค. - ๋ฐฉํฅํค๋ก ํฌ์ปค์ค๋ฅผ ์ด๋ํ๋ ์ค ๋ง์ฐ์ค๋ก ํฌ์ปค์ค๋ฅผ ์ด๋ํ ์์๋ ๊ธฐ์กด์ ๋ฐฉํฅํค๋ก ์ด๋ํ๋ ํฌ์ปค์ค๋ ์ด๊ธฐํ ๋๋๋ก ์์ ํ์์ต๋๋ค.
- CSS์
-
๋ฌธ์ ์ฌํญ
- API์ ๊ฒ์๊ฒฐ๊ณผ ์๋ต ์ค์๋ ๋๋ฌธ์๊ฐ ์์ง๋ง ์๋ฌธ์๋ก๋ง ๊ฒ์์ด ๊ฐ๋ฅํ์ต๋๋ค.
- API์์ฒด์์ ๋๋ฌธ์ ๊ฒ์์ด์ ๋ํด ๋น ๋ฐฐ์ด์ ์๋ตํฉ๋๋ค.
-
ํด๊ฒฐ ๋ฐฉ๋ฒ
- ๊ฒ์ํ๋ API๋ฅผ ํธ์ถํ ๋ ์
๋ ฅํ input์
String
์toLowerCase()
ํจ์๋ก ์๋ฌธ์๋ก ๋ณํ ํ ํธ์ถํ๋๋ก ํ์ต๋๋ค.
// components/SearchSection/index.tsx useEffect(() => { const fetchAutocompleteWords = async () => { ... const words = await searchAPI(debouncedInputText.trim().toLowerCase()); ... }; fetchAutocompleteWords(); }, [debouncedInputText, setFocusIndex]);
- ๊ฒ์ํ๋ API๋ฅผ ํธ์ถํ ๋ ์
๋ ฅํ input์
-
๋ฌธ์ ์ฌํญ
- ์ถ์ฒ ๊ฒ์์ด ๋ฆฌ์คํธ๊ฐ ์์ฑ์ด ๋ ํ ๊ฒ์์ด๋ฅผ ๋น ๋ฅด๊ฒ ์ ๋ ฅํ๋ฉด ์ถ์ฒ ๊ฒ์์ด ๋ฆฌ์คํธ๊ฐ ํ์ฌ ์ ๋ ฅ์ค์ธ inputText๋ก ๋ณด์ฌ์ง๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
- ์ถ์ฒ ๊ฒ์์ด ๋ฆฌ์คํธ๋ debounced๋ inputText์ ๋ํ ๊ฒฐ๊ณผ ๊ฐ์ธ๋ฐ, ์ผ์นํ์ง ์๋ inputText๋ก slice๋ฅผ ํ๋ค๋ณด๋ inputText๋ง ๋ณด์ฌ์ง๋ ๋ฌธ์ ์์ต๋๋ค.
-
ํด๊ฒฐ ๋ฐฉ๋ฒ
-
ํ์ฌ ์ ๋ ฅ ์ค์ธ inputText๊ฐ ์๋ debounced๋ inputText๋ฅผ ๋๊ฒจ์ฃผ์ด์ ํด๊ฒฐํ์์ต๋๋ค.
// components/SearchSection/SearchBox/index.tsx { autocompleteWords.map(({ id, name }, index) => ( <SearchWord key={id} inputText={debouncedInputText} // ์ ๋ฌํ๋ ๊ฐ์ debouncedInputText๋ก ๋ณ๊ฒฝ word={name} clickWord={clickWord} isFocused={focusIndex === index} /> )); }
// components/SearchSection/SearchBox/SearchWord/index.tsx <span className="text-black"> {inputText ? <span className="font-bold">{inputText}</span> : null} {word?.slice(inputText?.length)} </span>
-
์์ด๋ก ๊ฒ์ ์ ์ถ์ฒ ๊ฒ์์ด์ inputText์ ๊ฐ์ ๋ถ๋ถ์ ๋ชจ๋ ์๋ฌธ์๋ก ๋ณด์ด๋ ์ด์
-
๋ฌธ์ ์ฌํญ
-
์์ด๋ฅผ ๊ฒ์ํ์ ๋ ์ถ์ฒ ๊ฒ์์ด๋ ๋๋ฌธ์์ธ๋ฐ ๊ฒ์์ด์ ์ ๋ ฅํ ๊ธ์๋ ๋ชจ๋ ์๋ฌธ์๋ก ๋ณด์ฌ์ง๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
-
inputText
์ ๊ฒ์์ด ์ค ์ผ์นํ๋ ๋ถ๋ถ์ ๊ฐ์กฐํ๋ ๋ถ๋ถ์์inputText
๋ฅผ ๊ทธ๋๋ก ๋ณด์ฌ์ฃผ๊ฒ ๋์ด์์์ต๋๋ค.// components/SearchSection/SearchBox/SearchWord/index.tsx // string - input์ ์ ๋ ฅ๋๋ string. // word - ๊ฒ์๊ฒฐ๊ณผ ์ค ํ๋์ string. <span className="text-black"> {inputText ? <span className="font-bold">{inputText}</span> : null} {word.slice(inputText?.length)} </span>
-
-
ํด๊ฒฐ ๋ฐฉ๋ฒ
-
inputText
๋ฅผ ๊ทธ๋๋ก ๋ณด์ฌ์ฃผ์ง ์๊ณ , ๊ฒ์๊ฒฐ๊ณผ์์inpuText
์ ๊ธธ์ด๋งํผ์ ์๋ผ์ ๊ฐ์กฐํ๋๋ก ํด์ ํด๊ฒฐํ์ต๋๋ค.<span className="text-black"> {inputText && ( <span className="font-bold">{word.slice(0, inputText.length)}</span> )} {word?.slice(inputText?.length)} </span>
-