Skip to content

Latest commit

ย 

History

History
683 lines (496 loc) ยท 31.9 KB

README.md

File metadata and controls

683 lines (496 loc) ยท 31.9 KB

๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ TEAM ๋ณด๋žŒ์‚ผ์กฐ

์ธํ„ด์‹ญ ๊ธฐ๊ฐ„๋™์•ˆ ๋ณด๋žŒ์ฐฌ 3์กฐ๊ฐ€ ๋˜์ž!

Name ํ™ฉ์ˆ˜ํ˜„ ์ด์ค€ํ˜ธ ๋ฐ•์ˆ˜ํ˜„ ์ด์ƒ๋ฏผ ์œ ๋™ํ˜
Profile
GitHub @rjsej12 @wujuno @pySoo @sangminlee98 @robin14dev
Name ๊ฐ•๋ช…์ฃผ ๋ฐ•๊ฒธ์˜ ์ •์ •์ˆ˜ ๊ณ ์˜์šฑ ์ถ”ํ—Œ์žฌ
Profile
GitHub @myungju030 @seoltang @wjdwjdtn92 @free-ko @Chuhj

์›ํ‹ฐ๋“œ ํ”„๋ฆฌ์˜จ๋ณด๋”ฉ ํ”„๋ก ํŠธ์—”๋“œ ์ธํ„ด์‹ญ 2์ฃผ์ฐจ ๊ณผ์ œ

๊ฒ€์ƒ‰์ฐฝ ๊ตฌํ˜„ + ๊ฒ€์ƒ‰์–ด ์ถ”์ฒœ ๊ธฐ๋Šฅ ๊ตฌํ˜„ + ์บ์‹ฑ ๊ธฐ๋Šฅ ๊ตฌํ˜„

์ง„ํ–‰ ๊ธฐ๊ฐ„: 2023-05-02 ~ 2023-05-05

๋ชฉ์ฐจ


๋ฐฐํฌ ๋งํฌ

https://pre-onboarding-10th-2-3.netlify.app/


๋™์ž‘ ํ™”๋ฉด

demo


์‚ฌ์šฉํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

Area Tech Stack
Frontend

ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

๐Ÿ“ฆ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

๊ณผ์ œ ์ˆ˜ํ–‰ ๋‚ด์šฉ

Overview

  • ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ ๋ฐ ์žฌ์‚ฌ์šฉ์„ฑ
    • ํŠน์ • ํ‚ค์›Œ๋“œ๋“ค์€ ์ƒ์ˆ˜ํ™” ํ•˜์—ฌ ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์šฉ์ดํ•˜๋„๋ก ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
      • ์ถ”์ฒœ๊ฒ€์ƒ‰์–ด๋ฅผ ์ €์žฅํ•˜๋Š” ์บ์‹œ์Šคํ† ๋ฆฌ์ง€์˜ ๋งŒ๋ฃŒ ์‹œ๊ฐ„๊ณผ key
      • API ์ฃผ์†Œ์™€ ์—๋Ÿฌ๋ฉ”์‹œ์ง€
      • ์ตœ์‹ ๊ฒ€์ƒ‰์–ด๋ฅผ ์ €์žฅํ•˜๋Š” ์„ธ์…˜์Šคํ† ๋ฆฌ์ง€์˜ key
    • ์žฌ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•œ ์ปค์Šคํ…€ ํ›…์„ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
      • ๋””๋ฐ”์šด์Šค ๊ธฐ๋Šฅ์ด ๋‹ด๊ฒจ์žˆ๋Š” useDebounce
      • ์ž…๋ ฅ๋œ ํ…์ŠคํŠธ๋ฅผ ์„ธ์…˜์Šคํ† ๋ฆฌ์ง€์— ์ €์žฅํ•˜๋Š” useRecentSearchWords
      • ๋ฐฉํ–ฅํ‚ค ์ด๋ฒคํŠธ์— ๋”ฐ๋ผ ์ธ๋ฑ์Šค๋ฅผ ๋ณ€๊ฒฝํ•ด์ฃผ๋Š” useKeyFocus
  • ์„ฑ๋Šฅ ์ตœ์ ํ™”
    • ๋กœ์ปฌ ์บ์‹ฑ์„ ๊ตฌํ˜„ํ•˜์—ฌ, ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•˜๋Š” ์‹œ๊ฐ„์„ ์ค„์˜€์Šต๋‹ˆ๋‹ค.
    • ๋””๋ฐ”์šด์‹ฑ์„ ํ†ตํ•ด API์˜ ํ˜ธ์ถœ์„ ์ค„์˜€์Šต๋‹ˆ๋‹ค.
  • ์‚ฌ์šฉ์ž ๊ฒฝํ—˜
    • ์ž…๋ ฅ์‹œ ๋นจ๊ฐ„์ค„์„ ์—†์• ์ฃผ๋Š” spellCheck
    • ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ Œ๋”๋ง๋˜์—ˆ์„ ๋•Œ ์ž๋™์œผ๋กœ ์ปค์„œ๊ฐ€ focus๋˜๋Š” autoFocus

์งˆํ™˜๋ช… ๊ฒ€์ƒ‰์‹œ API ํ˜ธ์ถœ ํ†ตํ•ด์„œ ๊ฒ€์ƒ‰์–ด ์ถ”์ฒœ ๊ธฐ๋Šฅ ๊ตฌํ˜„

  • 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 [];
        }
    };

API ํ˜ธ์ถœ๋ณ„๋กœ ๋กœ์ปฌ ์บ์‹ฑ ๊ตฌํ˜„

๋กœ์ปฌ ์บ์‹ฑ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด 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ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์บ์‹œ ์Šคํ† ๋ฆฌ์ง€์— ์บ์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

์ž…๋ ฅ๋งˆ๋‹ค API ํ˜ธ์ถœํ•˜์ง€ ์•Š๋„๋ก API ํ˜ธ์ถœ ํšŸ์ˆ˜๋ฅผ ์ค„์ด๋Š” ๊ธฐ๋Šฅ ๊ตฌํ˜„

  • ์ด๋ฒคํŠธ๋ฅผ ๊ทธ๋ฃนํ™”ํ•˜์—ฌ ํŠน์ • ์‹œ๊ฐ„์ด ์ง€๋‚œ ํ›„ ํ•˜๋‚˜์˜ ์ด๋ฒคํŠธ๋งŒ ๋ฐœ์ƒํ•˜๋„๋ก ํ•˜๋Š” Debounceย ๊ธฐ์ˆ ์„ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

    1. 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๊ฐ€ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.
    2. ํŠน์ • ์‹œ๊ฐ„์„ ์ •ํ•ด๋†“๊ณ , ํ•ด๋‹น ์‹œ๊ฐ„ ๋‚ด์— ์ž…๋ ฅํ•˜๋Š” ๋ชจ๋“  ํ…์ŠคํŠธ๋ฅผ ํ•˜๋‚˜๋กœ ๋ชจ์€ ๋‹ค์Œ์— ํ•ด๋‹น ์‹œ๊ฐ„์ด ์ง€๋‚˜๋ฉด ์ถ•์ ๋œ ํ…์ŠคํŠธ์˜ ๋ณ€๊ฒฝ ์œ ๋ฌด์— ๋”ฐ๋ผ 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๋œ ์ถ”์ฒœ ๊ฒ€์ƒ‰์–ด๋ฅผ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.

tab ํ‚ค๋กœ ์ด๋™: ์ถ”์ฒœ ๊ฒ€์ƒ‰์–ด๋ฅผ ๋Œ€ํ™”ํ˜• ์š”์†Œ <button> ํƒœ๊ทธ๋กœ ์„ค์ •

์‹œ๋งจํ‹ฑ ๋งˆํฌ์—…์„ ์œ„ํ•ด ์ถ”์ฒœ ๊ฒ€์ƒ‰์–ด๋ฅผ <li> ํƒœ๊ทธ๋กœ ์„ค์ •ํ•˜์˜€์œผ๋‚˜, <li> ํƒœ๊ทธ๋Š” ๋น„๋Œ€ํ™”ํ˜• ์š”์†Œ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ ‘๊ทผ์„ฑ ํŠธ๋ฆฌ์— ๋‚˜ํƒ€๋‚˜์ง€ ์•Š์•„ ํ‚ค๋ณด๋“œ tab ํ‚ค๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์—†์—ˆ์Šต๋‹ˆ๋‹ค. ์ ‘๊ทผ์„ฑ ํŠธ๋ฆฌ์— ํฌํ•จ๋  ์ˆ˜ ์žˆ๋„๋ก ๋น„๋Œ€ํ™”ํ˜• ์š”์†Œ์ธ <li> ํƒœ๊ทธ๋ฅผ ๋Œ€ํ™”ํ˜• ์š”์†Œ์ธ <button> ํƒœ๊ทธ๋กœ ๋ฐ”๊พธ์–ด ํ‚ค๋ณด๋“œ tab ํ‚ค๋ฅผ ํ†ตํ•ด ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.

tabindex ์†์„ฑ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์€ ์ด์œ 

๋น„๋Œ€ํ™”ํ˜• ์š”์†Œ๋ฅผ ์‚ฌ์šฉํ•ด ๋งŒ๋“  ๋Œ€ํ™”ํ˜• ์ปดํฌ๋„ŒํŠธ๋Š” ์ ‘๊ทผ์„ฑ ํŠธ๋ฆฌ์— ๋‚˜ํƒ€๋‚˜์ง€ ์•Š์œผ๋ฏ€๋กœ, ๋ณด์กฐ ๊ธฐ์ˆ ์ด ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ๋กœ ํƒ์ƒ‰ํ•˜๊ฑฐ๋‚˜ ์กฐ์ž‘ํ•˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์ƒํ˜ธ์ž‘์šฉ ๊ฐ€๋Šฅํ•œ ํ•ญ๋ชฉ์€ ๋Œ€ํ™”ํ˜• ์š”์†Œ๋ฅผ ์‚ฌ์šฉํ•ด ์ ์ ˆํ•œ ์˜๋ฏธ์™€ ํ•จ๊ป˜ ๋‚˜ํƒ€๋‚ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ฐธ๊ณ : ์ ‘๊ทผ์„ฑ ๊ณ ๋ ค์‚ฌํ•ญ - MDN

May-05-2023 21-54-26

๋ฐฉํ–ฅํ‚ค๋กœ ์ด๋™: keyDown ์ด๋ฒคํŠธ ์ด์šฉํ•œ useKeyFocus ์ปค์Šคํ…€ ํ›…

  • 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 ์ƒํƒœ๊ฐ€ ์•„๋‹ ๋•Œ๋Š” ์ˆ˜ํ–‰๋˜์ง€ ์•Š๋„๋ก ์ฒ˜๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๊ณ ๋ฏผํ–ˆ๋˜ ์‚ฌํ•ญ๋“ค

๋นŒ๋“œ ํˆด ์„ ์ •์— ๊ด€ํ•œ ๋…ผ์˜

CRA vs Vite

CSS ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ ์ •์— ๊ด€ํ•œ ๋…ผ์˜

Tailwind

  • ๊ฐ„ํŽธํ•œ ํด๋ž˜์Šค๋ช…์„ ํ†ตํ•œ ๋งˆํฌ์—… ์ž‘์—…์ด ๊ฐ€๋Šฅํ•˜์—ฌ ๊ธฐ๋Šฅ ๊ตฌํ˜„์— ์ง‘์ค‘ ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์งง์€ ๊ธฐ๊ฐ„๋™์•ˆ ๋งˆํฌ์—…๊ณผ ๊ธฐ๋Šฅ ๋ชจ๋‘ ์ง„ํ–‰ํ•ด์•ผํ•˜๋Š” ๊ธฐ์—… ๊ณผ์ œ์— ์ ํ•ฉํ•˜๋‹ค๊ณ  ํŒ๋‹จ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

Redux ์‚ฌ์šฉ ์—ฌ๋ถ€์— ๊ด€ํ•œ ๋…ผ์˜

  • Redux์—์„œ ์ „์—ญ๋ณ€์ˆ˜๋กœ state๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๋ถ€๋ถ„์ด ์•„๋‹ˆ๋ผ๋ฉด Redux๋ฅผ ์‚ฌ์šฉํ•  ํ•„์š”๋ฅผ ๋Š๋ผ์ง€ ๋ชป ํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ํŠน์ •ํ•œ ๋ฐฉ์‹์œผ๋กœ ์บ์‹ฑ์„ ๊ตฌํ˜„ํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด Redux๋ฅผ ์‚ฌ์šฉํ•  ํ•„์š”๋ฅผ ๋Š๋ผ์ง€ ๋ชป ํ–ˆ์Šต๋‹ˆ๋‹ค.

๋กœ์ปฌ ์บ์‹ฑ ๋ฐฉ๋ฒ•์— ๊ด€ํ•œ ๋…ผ์˜

๊ฐœ๋ณ„์ ์œผ๋กœ ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€, ์„ธ์…˜ ์Šคํ† ๋ฆฌ์ง€, ์บ์‹œ ์Šคํ† ๋ฆฌ์ง€, Map, Redux๋ฅผ ์ด์šฉํ•˜์—ฌ ๊ณผ์ œ๋ฅผ ์ง„ํ–‰ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ €ํฌ๋Š” ์–ด๋–ค ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ตœ์ ํ™”์™€ ์‚ฌ์šฉ์„ฑ์— ์ด์ ์ด ๋˜๋Š”์ง€ ๋…ผ์˜ํ•ด ๋ณด์•˜์Šต๋‹ˆ๋‹ค.

๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€

  • ๋™๊ธฐ ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•˜๋ฉฐ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ์—ฐ์‚ฐ์„ ์ค‘๋‹จ์‹œํ‚ต๋‹ˆ๋‹ค.
  • ์šฉ๋Ÿ‰ ์ œํ•œ์€ 5MB์ด๋ฉฐ ๋ฌธ์ž์—ด๋งŒ ์ €์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์„ธ์…˜ ์Šคํ† ๋ฆฌ์ง€

  • ํƒญ ์•ˆ์—์„œ๋งŒ ์œ ํšจํ•˜๋ฉฐ ํƒญ์ด ๋‹ซํžˆ๋ฉด ์Šคํ† ๋ฆฌ์ง€๋„ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค.
  • ํ˜„์žฌ ํƒญ์—์„œ๋งŒ ์‚ฌ์šฉํ•˜๋Š” IndexedDB ํ‚ค๋ฅผ ์ž ์‹œ ์ €์žฅํ•˜๋Š” ๊ฒƒ๊ณผ ๊ฐ™์ด ์ž‘์€ ์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•  ๋•Œ๋Š” ์ข‹์Šต๋‹ˆ๋‹ค.
  • ๋™๊ธฐ(synchronous) ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ์—ฐ์‚ฐ์„ ์ค‘๋‹จ์‹œํ‚ต๋‹ˆ๋‹ค.
  • ์šฉ๋Ÿ‰ ์ œํ•œ์€ 5MB์ด๋ฉฐ ๋ฌธ์ž์—ด๋งŒ ์ €์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Map

  • ๋ฉ”๋ชจ๋ฆฌ์ƒ์— ์บ์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•˜์—ฌ ์ €์žฅ/์‚ญ์ œ ๋ฐ ์„ค์ •์ด ๋น„๊ต์  ์ž์œ ๋กญ์Šต๋‹ˆ๋‹ค.
  • ์ƒˆ๋กœ๊ณ ์นจ์‹œ ๋ฐ์ดํ„ฐ๊ฐ€ ํœ˜๋ฐœ๋ฉ๋‹ˆ๋‹ค.

์บ์‹œ ์Šคํ† ๋ฆฌ์ง€

  • ์š”์ฒญ ๋ฐ ์‘๋‹ต์—๋Š” HTTP๋ฅผ ํ†ตํ•ด ์ „์†กํ•  ์ˆ˜ ์žˆ๋Š” ๋ชจ๋“  ์ข…๋ฅ˜์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋น„๋™๊ธฐ ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•˜๋ฉฐ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ์—ฐ์‚ฐ์„ ์ค‘๋‹จํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • ๋งŽ์ด ์ €์žฅํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐย ์ ์–ด๋„ ์ˆ˜๋ฐฑ MB, ๊ฒฝ์šฐ์— ๋”ฐ๋ผ ์ˆ˜ GB ์ด์ƒ๊นŒ์ง€๋„ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (๋ธŒ๋ผ์šฐ์ € ๊ตฌํ˜„์€ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์ง€๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ €์žฅ ๊ณต๊ฐ„์˜ ์–‘์€ ์ผ๋ฐ˜์ ์œผ๋กœ ์žฅ์น˜์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ €์žฅ ๊ณต๊ฐ„์˜ ์–‘์— ๋”ฐ๋ผ ๊ฒฐ์ •)

๋ฒˆ์™ธ) Cache-Control

๊ฒฐ๋ก 

๋„คํŠธ์›Œํฌ๋กœ ๋ถˆ๋Ÿฌ์˜จ ๋ฆฌ์†Œ์Šค๋‚˜ ํŒŒ์ผ์„ ์บ์‹ฑํ•ด์•ผ ํ•œ๋‹ค๋ฉดย ์บ์‹œ ์Šคํ† ๋ฆฌ์ง€๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค๊ณ  ๊ฒฐ๋ก ์„ ๋‚ด๋ ธ์Šต๋‹ˆ๋‹ค. ๋”๋ถˆ์–ด, ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•  ๋•Œ๋Š” ์„ธ์…˜ ์Šคํ† ๋ฆฌ์ง€๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉํ–ฅ์œผ๋กœ ๊ฒฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

Expire time ๊ฒฐ์ •์— ๊ด€ํ•œ ๋…ผ์˜

  • API ํ˜ธ์ถœ์— ๋”ฐ๋ฅธ ์„œ๋ฒ„์˜ ๊ณผ๋ถ€ํ™” ์ •๋„์™€ ๊ฒ€์ƒ‰์–ด์— ๋”ฐ๋ฅธ API ํ˜ธ์ถœ ๊ฒฐ๊ณผ๊ฐ€ ๋ณ€ํ•˜๋Š” ๊ธฐ๊ฐ„์— ๋”ฐ๋ผ Expire time์„ ๊ฒฐ์ •ํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ํ•˜์ง€๋งŒ ์ด๋ฒˆ ๊ณผ์ œ์—์„œ๋Š” ์‹ค์ œ API ๊ด€๋ จ ๋ฐ์ดํ„ฐ๋ฅผ ์•Œ ์ˆ˜ ์—†์–ด, ํ…Œ์ŠคํŠธ ํŽธ์˜์„ฑ์„ ์œ„ํ•ด ๋งŒ๋ฃŒ์‹œ๊ฐ„์„ 10๋ถ„์œผ๋กœ ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

๋””๋ฐ”์šด์‹ฑ(Debouncing) vs ์“ฐ๋กœํ‹€๋ง(Throttling)

  • API ํ˜ธ์ถœ ํšŸ์ˆ˜๋ฅผ ์ตœ์†Œํ™” ํ•˜๊ธฐ ์œ„ํ•ด์„œ ๊ฒ€์ƒ‰์ฐฝ์— ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•  ๋•Œ ๋งˆ๋‹ค ์—ฐ์†์ ์œผ๋กœ ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ๋ฅผ ์ œ์–ดํ•  ํ•„์š”๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
  • ์ด๋ฒคํŠธ๋ฅผ ์ œ์–ดํ•˜๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ๋””๋ฐ”์šด์‹ฑ๊ณผ ์“ฐ๋กœํ‹€๋ง์ด ์žˆ์–ด ๋‘˜ ์ค‘ ์ ํ•ฉํ•œ ๊ธฐ๋ฒ•์ด ๋ฌด์—‡์ธ์ง€ ๊ฒฐ์ •ํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    • ๋””๋ฐ”์šด์‹ฑ(Debouncing) : ์—ฐ์ด์–ด ๋ฐœ์ƒํ•œ ์ด๋ฒคํŠธ๋ฅผ ํ•˜๋‚˜์˜ ๊ทธ๋ฃน์œผ๋กœ ๋ฌถ์–ด์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ, ์ฃผ๋กœ ๊ทธ๋ฃน์—์„œ ๋งˆ์ง€๋ง‰, ํ˜น์€ ์ฒ˜์Œ์— ์ฒ˜๋ฆฌ๋œ ํ•จ์ˆ˜๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
    • ์“ฐ๋กœํ‹€๋ง(Throttling) : ์—ฐ์ด์–ด ๋ฐœ์ƒํ•œ ์ด๋ฒคํŠธ์— ๋Œ€ํ•ด, ์ผ์ •ํ•œ delay ์‹œ๊ฐ„ ๋™์•ˆ ํ˜ธ์ถœ๋œ ํ•จ์ˆ˜๋Š” ๋ฌด์‹œํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.
  • ์‚ฌ์šฉ์ž์˜ ์ž…๋ ฅ์ด ์–ธ์ œ ๋งˆ๋ฌด๋ฆฌ ๋  ์ง€ ๋ชจ๋ฅด๋Š” ์ƒํ™ฉ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋””๋ฐ”์šด์‹ฑ์„ ์ ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋” ์ ํ•ฉํ•˜๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.

API ํ˜ธ์ถœ์‹œ CORS ์˜ค๋ฅ˜

  • API๋ฅผ ์ด์šฉํ•ด ๊ฒ€์ƒ‰ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ค๊ธฐ ์œ„ํ•ด ๋„คํŠธ์›Œํฌ ์š”์ฒญ ์‹œ CORS ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

  • vite.config.ts์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ฝ”๋“œ๋ฅผ ํ†ตํ•œ proxy ์„ค์ •์œผ๋กœ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค.

    export default defineConfig({
      server: {
        proxy: {
          '/api/v1': {
            target: 'API ์ฃผ์†Œ',
            changeOrigin: true,
            rewrite: (path) => path.replace(/^\/api\/v1/, ''),
          },
        },
      },
    });

ํ•œ๊ธ€ ์ž…๋ ฅ ์‹œ keyDown ์ด๋ฒคํŠธ ์ค‘๋ณต ๋ฐœ์ƒ ์ด์Šˆ

  • ๋ฌธ์ œ์‚ฌํ•ญ

    • ํ•œ๊ธ€๊ณผ ๊ฐ™์€ ์กฐํ•ฉ ๋ฌธ์ž๋Š” ๋ฌธ์ž๋ฅผ ๋ณ€ํ™˜ํ•˜๋Š” 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๋ฅผ ์ ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
    • ๋ฐฉํ–ฅํ‚ค๋กœ ํฌ์ปค์Šค๋ฅผ ์ด๋™ํ•˜๋˜ ์ค‘ ๋งˆ์šฐ์Šค๋กœ ํฌ์ปค์Šค๋ฅผ ์ด๋™ํ•  ์‹œ์—๋Š” ๊ธฐ์กด์˜ ๋ฐฉํ–ฅํ‚ค๋กœ ์ด๋™ํ•˜๋˜ ํฌ์ปค์Šค๋Š” ์ดˆ๊ธฐํ™” ๋˜๋„๋ก ์ˆ˜์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์˜์–ด ๋Œ€๋ฌธ์ž๋กœ ๊ฒ€์ƒ‰ ์‹œ ์ถ”์ฒœ ๊ฒ€์ƒ‰์–ด๊ฐ€ ์—†๋Š” ์ด์Šˆ

  • ๋ฌธ์ œ์‚ฌํ•ญ

    • API์˜ ๊ฒ€์ƒ‰๊ฒฐ๊ณผ ์‘๋‹ต ์ค‘์—๋Š” ๋Œ€๋ฌธ์ž๊ฐ€ ์žˆ์ง€๋งŒ ์†Œ๋ฌธ์ž๋กœ๋งŒ ๊ฒ€์ƒ‰์ด ๊ฐ€๋Šฅํ–ˆ์Šต๋‹ˆ๋‹ค.
    • API์ž์ฒด์—์„œ ๋Œ€๋ฌธ์ž ๊ฒ€์ƒ‰์–ด์— ๋Œ€ํ•ด ๋นˆ ๋ฐฐ์—ด์„ ์‘๋‹ตํ•ฉ๋‹ˆ๋‹ค.
  • ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

    • ๊ฒ€์ƒ‰ํ•˜๋Š” API๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ ์ž…๋ ฅํ•œ input์„ String์˜ toLowerCase()ํ•จ์ˆ˜๋กœ ์†Œ๋ฌธ์ž๋กœ ๋ณ€ํ™˜ ํ›„ ํ˜ธ์ถœํ•˜๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.
    // components/SearchSection/index.tsx
    
    useEffect(() => {
      const fetchAutocompleteWords = async () => {
        ...
        const words = await searchAPI(debouncedInputText.trim().toLowerCase());
        ...
      };
    
      fetchAutocompleteWords();
    }, [debouncedInputText, setFocusIndex]);

๊ฒ€์ƒ‰์–ด๋ฅผ ๋นจ๋ฆฌ ์ž…๋ ฅํ–ˆ์„ ๋•Œ ์ถ”์ฒœ ๊ฒ€์ƒ‰์–ด ๋ฆฌ์ŠคํŠธ๊ฐ€ inputText๋กœ ๋‚˜ํƒ€๋‚˜๋Š” ์ด์Šˆ

  • ๋ฌธ์ œ์‚ฌํ•ญ

    • ์ถ”์ฒœ ๊ฒ€์ƒ‰์–ด ๋ฆฌ์ŠคํŠธ๊ฐ€ ์ƒ์„ฑ์ด ๋œ ํ›„ ๊ฒ€์ƒ‰์–ด๋ฅผ ๋น ๋ฅด๊ฒŒ ์ž…๋ ฅํ•˜๋ฉด ์ถ”์ฒœ ๊ฒ€์ƒ‰์–ด ๋ฆฌ์ŠคํŠธ๊ฐ€ ํ˜„์žฌ ์ž…๋ ฅ์ค‘์ธ 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>