[BADATA] ์œ„์น˜ ๊ฒ€์ƒ‰ ์ฐฝ์— ๋””๋ฐ”์šด์‹ฑ ์“ฐ๋กœํ‹€๋ง ์ค‘์ฒฉ ๋ฌธ์ œ ํ•ด๊ฒฐํ•˜๊ธฐ

2025. 10. 21. 00:30ยท๐Ÿงก Projects/๐Ÿงก Projects: Web
728x90

 

์œ„์น˜ ๊ฒ€์ƒ‰์ฐฝ ๋””๋ฐ”์šด์Šค·AbortController ์ ์šฉ์œผ๋กœ ์ค‘๋ณต ์š”์ฒญ ์ œ๊ฑฐ PR

 

Performance: ์œ„์น˜ ๊ฒ€์ƒ‰์ฐฝ ๋””๋ฐ”์šด์Šค·AbortController ์ ์šฉ์œผ๋กœ ์ค‘๋ณต ์š”์ฒญ ์ œ๊ฑฐ by arty0928 · Pull Request #298

#๏ธโƒฃ ์—ฐ๊ด€๋œ ์ด์Šˆ close: #297 1) ๋„์ž… ๋ฐฐ๊ฒฝ ์œ„์น˜ ๊ฒ€์ƒ‰์ฐฝ ์ž…๋ ฅ ์‹œ ๋””๋ฐ”์šด์‹ฑ๊ณผ ์“ฐ๋กœํ‹€๋ง์„ ์ค‘์ฒฉ ์ ์šฉํ•˜์—ฌ, ์‚ฌ์šฉ์ž๊ฐ€ ์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ๋“ฑ ํ•œ๊ธ€์„ ํƒ€์ดํ•‘ํ•  ๋•Œ๋งˆ๋‹ค ์ตœ๋Œ€ 22ํšŒ์˜ API ํ˜ธ์ถœ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. R

github.com

 

๋“ค์–ด๊ฐ€๋ฉฐ..

CS ์Šคํ„ฐ๋””์—์„œ ๋””๋ฐ”์šด๋”ฉ/์“ฐ๋กœํ‹€๋ง ๊ฐœ๋…์„ ๋ฐฐ์šด ๊น€์—, BADATA ํ”„๋กœ์ ํŠธ์—์„œ ์œ„์น˜ ๊ฒ€์ƒ‰์„ ์ง„ํ–‰ํ•˜๋Š” ๊ณผ์ •์—์„œ ์ ์šฉํ•œ ๋””๋ฐ”์šด์‹ฑ ์ฝ”๋“œ๋ฅผ ๋‹ค์‹œ ํ™•์ธํ–ˆ๋‹ค. 
๊ทธ๋Ÿฐ๋ฐ ๋””๋ฐ”์šด์‹ฑ๊ณผ ์“ฐ๋กœํ‹€๋ง์„ ์ค‘์ฒฉํ•ด์„œ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์–ด input ์ฐฝ์— ์œ„์น˜๋ฅผ ํƒ€์ดํ•‘ํ•  ๋•Œ๋งˆ๋‹ค API ์š”์ฒญ์ด ๊ฐ€๋Š” ์ƒํ™ฉ์ด์—ˆ๋‹ค.

 


๋ฌธ์ œ ์ƒํ™ฉ

'์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ'๋ฅผ ์ฒœ์ฒœํžˆ ํƒ€์ดํ•‘ํ•˜๋ฉด ํƒ€์ดํ•‘ ํ•˜๋‚˜๋‹น API ์š”์ฒญ์ด ๋ณด๋‚ด์ ธ์„œ ์š”์ฒญํšŸ์ˆ˜๊ฐ€ 22ํšŒ๊ฐ€ ๋ณด๋‚ด์ง„๋‹ค.

 

INP(Interaction to Next Paint)๊ฐ€ 111ms ์†Œ์š”๋˜๊ณ  ์žˆ๋‹ค. 

๋˜ํ•œ ํƒ€์ž„๋ผ์ธ์—์„œ ๋ณผ ์ˆ˜ ์žˆ๋“ฏ์ด ๋…ธ๋ž€ ๋ง‰๋Œ€ ์ฆ‰ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๊ฐ€ ์—ฐ์†๋˜์–ด ๋‚˜ํƒ€๋‚œ๋‹ค. ์ฆ‰, '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ'๋ฅผ ๋งค๋ฒˆ ์ž…๋ ฅํ•  ๋•Œ๋งˆ๋‹ค JS ๊ฐ€ ์‹คํ–‰๋˜๊ณ  ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด ๋ถ€๋ถ„์€ ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰ํ•  ๋•Œ ๋งˆ๋‹ค ์•„๋ž˜ ๊ด€๋ จ ํ‚ค์›Œ๋“œ๊ฐ€ ํฌํ•จ๋œ ๋ฆฌ์ŠคํŠธ ๋‚ด์—ญ์„ UI๋กœ ๋ณด์—ฌ์ค˜์•ผ ํ•˜๋ฏ€๋กœ ๋„คํŠธ์›Œํฌ ์š”์ฒญ ์ตœ์†Œํ™” ์ดํ›„ ๋‹ค์‹œ ํ™•์ธํ•ด์•ผ ํ•  ๋“ฏ ํ•˜๋‹ค. 

 

 

 

 

๋ Œ๋”๋ง ์ด 23ํšŒ, SearchPosPage์˜ ๋ Œ๋”๋ง ๋น„์šฉ์ด ๊ฐ€์žฅ ์ปธ๋‹ค.

 

์ฆ‰, ๋””๋ฐ”์šด์‹ฑ ์ ์šฉ ์ „์—์„œ๋Š” ์ด 3๊ฐ€์ง€์˜ ๋ฌธ์ œ์ ์ด ๋ฐœ์ƒํ•œ๋‹ค.

1. ์ž…๋ ฅํ•  ๋•Œ๋งˆ๋‹ค ์ฆ‰์‹œ API ํ˜ธ์ถœ

2. ๋ถˆํ•„์š”ํ•œ ๋„คํŠธ์›Œํฌ ๋น„์šฉ์ด ๋ฐœ์ƒ

3. ๊ฒ€์ƒ‰์ฐฝ์— ์ž…๋ ฅํ•  ๋•Œ๋งˆ๋‹ค ๋งค๋ฒˆ ๋ฆฌ๋ Œ๋”๋ง ๋ฐœ์ƒ

 

์ฆ‰, ๋””๋ฐ”์šด์Šค + ์Šค๋กœํ‹€์„ ์ค‘์ฒฉํ•ด์„œ ์‚ฌ์šฉํ•ด์„œ '๋ฉˆ์นซํ•  ๋•Œ๋งˆ๋‹ค 1ํšŒ์”ฉ' ํ˜ธ์ถœ์ด ์ผ์–ด๋‚œ ๊ฒƒ์ด์—ˆ๋‹ค.

 

 


 

๋ฌธ์ œ 1 : ๋””๋ฐ”์šด์Šค + ์“ฐ๋กœํ‹€์˜ ์ค‘์ฒฉ ์‚ฌ์šฉ

์ง€๊ธˆ ์ฝ”๋“œ ํ๋ฆ„์—์„œ ์™œ 2๊ฐœ๋ฅผ ๊ฐ™์ด ์“ฐ๋ฉด ์•ˆ ์ข‹์„๊นŒ?

import { useCallback, useEffect, useRef, useState } from 'react';

import {
  searchPlaces,
  type PlaceSearchResult,
  type SearchPlacesParams,
} from '@/features/rental/search/utils/address/searchPlaces';

// ๋””๋ฐ”์šด์‹ฑ ํ›…
const useDebounce = (value: string, delay: number) => {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
};

// ์Šค๋กœํ‹€๋ง ํ›…
const useThrottle = <T extends unknown[]>(callback: (...args: T) => void, delay: number) => {
  const lastRunRef = useRef(0);

  return useCallback(
    (...args: T) => {
      const now = Date.now();
      if (now - lastRunRef.current >= delay) {
        lastRunRef.current = now;
        callback(...args);
      }
    },
    [callback, delay],
  );
};

// ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰ ํ›…
export const useSearchPlaces = () => {
  const [keyword, setKeyword] = useState('');
  const [searchResults, setSearchResults] = useState<PlaceSearchResult[]>([]);
  const [isLoading, setIsLoading] = useState(false);
  const [isLoadingMore, setIsLoadingMore] = useState(false);
  const [hasNext, setHasNext] = useState(true);
  const [page, setPage] = useState(1);

  // ๋””๋ฐ”์šด์Šค๋œ ํ‚ค์›Œ๋“œ (500ms) - ๊ณต๋ฐฑ ์ œ๊ฑฐ
  const debouncedKeyword = useDebounce(keyword.trim(), 500);

  // ๊ฒ€์ƒ‰ ์‹คํ–‰ ํ•จ์ˆ˜
  const performSearch = useCallback(
    async (searchKeyword: string, pageNum: number = 1, append: boolean = false) => {
      const trimmedKeyword = searchKeyword.trim();
      if (!trimmedKeyword) {
        setSearchResults([]);
        return;
      }

      if (pageNum === 1) {
        setIsLoading(true);
      } else {
        setIsLoadingMore(true);
      }

      try {
        const params: SearchPlacesParams = {
          keyword: trimmedKeyword,
          page: pageNum,
          size: 15,
        };

        const results = await searchPlaces(params);

        if (append) {
          setSearchResults((prev) => [...prev, ...results]);
        } else {
          setSearchResults(results);
        }

        setHasNext(results.length === 15 && pageNum < 3);
      } catch (error) {
        console.error('๊ฒ€์ƒ‰ ์˜ค๋ฅ˜:', error);
        if (!append) {
          setSearchResults([]);
        }
        // API ํ˜ธ์ถœ ์ œํ•œ ์—๋Ÿฌ์ธ ๊ฒฝ์šฐ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆผ
        if (error instanceof Error && error.message.includes('API ํ˜ธ์ถœ ์ œํ•œ')) {
          console.warn('API ํ˜ธ์ถœ ์ œํ•œ์œผ๋กœ ์ธํ•ด ๊ฒ€์ƒ‰์ด ์ผ์‹œ์ ์œผ๋กœ ์ค‘๋‹จ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
        }
      } finally {
        setIsLoading(false);
        setIsLoadingMore(false);
      }
    },
    [],
  );

  // ์Šค๋กœํ‹€๋ง๋œ ๊ฒ€์ƒ‰ ํ•จ์ˆ˜ (300ms)
  const throttledSearch = useThrottle(performSearch, 300);

  // ๋””๋ฐ”์šด์Šค๋œ ํ‚ค์›Œ๋“œ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ ๊ฒ€์ƒ‰ ์‹คํ–‰
  useEffect(() => {
    if (debouncedKeyword) {
      setPage(1);
      setHasNext(true);
      throttledSearch(debouncedKeyword, 1, false);
    } else {
      setSearchResults([]);
    }
  }, [debouncedKeyword, throttledSearch]);

  // ๋‹ค์Œ ํŽ˜์ด์ง€ ๋กœ๋“œ ํ•จ์ˆ˜
  const loadNextPage = useCallback(() => {
    if (!hasNext || isLoadingMore || isLoading) return;

    const nextPage = page + 1;
    setPage(nextPage);
    performSearch(keyword.trim(), nextPage, true);
  }, [hasNext, isLoadingMore, isLoading, page, keyword, performSearch]);

  // ํ‚ค์›Œ๋“œ ์„ค์ • ํ•จ์ˆ˜
  const setKeywordOptimized = useCallback((value: string) => {
    setKeyword(value);
  }, []);

  return {
    keyword,
    setKeyword: setKeywordOptimized,
    searchResults,
    isLoading,
    isLoadingMore,
    hasNext,
    loadNextPage,
  };
};

 

ํ˜„์žฌ ํ๋ฆ„์„ ์ •๋ฆฌํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

 

keyword ์ž…๋ ฅ → debounce(500ms) → debouncedKeyword ๋ณ€ํ•จ → useEffect → throttle(300ms) ๋กœ performSearch ํ˜ธ์ถœ

 

1. ๋””๋ฐ”์šด์Šค

์‚ฌ์šฉ์ž๊ฐ€ ๋‹จ์–ด ์‚ฌ์ด๋งˆ๋‹ค 0.5์ดˆ ๋‚ด์™ธ๋กœ ์ž ๊น์”ฉ ๋ฉˆ์ถ”๋ฉด, ๊ทธ ๋ฉˆ์ถค๋‹ค๊ฐ€ 1ํšŒ ํ˜ธ์ถœ๋œ๋‹ค. 

ํŠนํžˆ ํ•œ๊ธ€๋กœ '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ'๋ฅผ ํƒ€์ดํ•‘ํ•˜๋Š” ๊ณผ์ •์—์„œ ์กฐํ•ฉ ์ค‘๊ฐ„์ค‘๊ฐ„์— 500ms ์ด์ƒ ๋ฉˆ์ถค์ด ์ƒ๊ธฐ๊ธฐ ์‰ฌ์›Œ์„œ, ์กฐํ•ฉ ๋‹จ๊ณ„๋ณ„๋กœ ์—ฌ๋Ÿฌ๋ฒˆ ์š”์ฒญ๋˜๊ณ  ์žˆ์—ˆ๋‹ค. (๋‚ด๊ฐ€ ์„ฑ๋Šฅ์„ ๋ณด๋ ค๊ณ  ์ผ๋ถ€๋Ÿฌ ํ•œ์žํ•œ์ž ํƒ€์ดํ•‘ํ•œ๊ฒƒ๋„ ์žˆ์ง€๋งŒ...)

ใ„ฑ → ๊ฐ€ → ๊ฐ• ์œผ๋กœ ๊ฐ’์ด ์—ฌ๋Ÿฌ ์ค‘๊ฐ„ ์ƒํƒœ๋ฅผ ๊ฑฐ์น˜๋ฉด์„œ ๋‹จ์–ด/์กฐํ•ฉ ๊ณผ์ •์—์„œ ์ž์ฃผ ๋ฉˆ์ถค์ด ๋ฐœ์ƒํ•˜๊ณ , ๋””๋ฐ”์šด์Šค๊ฐ€ ๊ทธ๋•Œ๊ทธ๋•Œ ๋ฐœํ™”ํ•œ๋‹ค.

 

2. ์“ฐ๋กœํ‹€

๋””๋ฐ”์šด์Šค ๋’ค์—๋„ ์“ฐ๋กœํ‹€์„ ํ•œ๋ฒˆ ๋” ๊ฑธ๋ฉด, '๋ณ€ํ™” ์‹œ์  ๊ทผ์ฒ˜'์—์„œ ์ถ”๊ฐ€ ํ˜ธ์ถœ์ด ๋ฐœ์ƒํ•œ๋‹ค. ๋””๋ฐ”์šด์Šค๋ฅผ ํ†ต๊ณผํ•œ ํ˜ธ์ถœ์ด ์Šค๋กœํ‹€ ๊ฒฝ๊ณ„์™€ ๋งŒ๋‚˜๋ฉด 1ํšŒ ํ˜ธ์ถœ์ด 2ํšŒ๊ฐ€ ๋˜๊ธฐ๋„ ํ•œ๋‹ค.

 

=> ์ฆ‰, ๋””๋ฐ”์šด์‹ฑ์œผ๋กœ ์ธํ•ด์„œ '๋ฉˆ์ถค'๋งˆ๋‹ค 1ํšŒ + ์“ฐ๋กœํ‹€ ๊ฒฝ๊ณ„์—์„œ 1ํšŒ ์ถ”๊ฐ€ 

 

 

๋˜ํ•œ react 18์˜ strictmode์˜ ์ดํŽ™ํŠธ 2ํšŒ ์‹คํ–‰๊นŒ์ง€ ๊ฒน์ณ์„œ ๋„คํŠธ์›Œํฌ ์š”์ฒญ ์ˆซ์ž๊ฐ€ ๋” ๋Š˜์–ด๋‚œ ๊ฒƒ์ด์—ˆ๋‹ค.

 

๋ฌธ์ œ 2. React 18 StrictMode(๊ฐœ๋ฐœ ๋ชจ๋“œ)์˜ '๋”๋ธ” ๋งˆ์šดํŠธ' 

 

๊ฐœ๋ฐœ ๋ชจ๋“œ์—์„œ StrictMode๋Š” ๋งˆ์šดํŠธ → ์–ธ๋งˆ์šดํŠธ → ์žฌ๋งˆ์šดํŠธ๋ฅผ ์ฆ‰์‹œ ์ˆ˜ํ–‰ํ•œ๋‹ค.

์ด๋•Œ ref(์Šค๋กœํ‹€์˜ lastRunRef)๋„ ์ดˆ๊ธฐํ™”๊ฐ€ ๋˜๋Š”๋ฐ,

์ฒซ ๋งˆ์šดํŠธ์—์„œ ์Šค๋กœํ‹€์ด 1ํšŒ ํ—ˆ์šฉ, ์–ธ๋งˆ์šดํŠธ๋กœ ref = 0 ์ด ๋ฆฌ์…‹, ์žฌ๋งˆ์šดํŠธ ํ›„ ๋‹ค์‹œ 1ํšŒ ํ—ˆ์šฉ

→ ๊ฐ™์€ ํšจ๊ณผ๊ฐ€ 2๋ฒˆ ๋ฐœ์ƒํ•˜๊ณ  ์žˆ์—ˆ๋‹ค.

 

์ด ๋ถ€๋ถ„์€ ๊ฐœ๋ฐœ ๋ชจ๋“œ์—์„œ๋งŒ ๋ฐœ์ƒํ•˜๊ณ  ํ”„๋กœ๋•์…˜ ๋นŒ๋“œ์—์„œ๋Š” ์‚ฌ๋ผ์ง€๋‹ˆ ์‹ ๊ฒฝ์“ธ ํ•„์š”๋Š” ์—†์—ˆ๋‹ค.

 

 

๋ฌธ์ œ 3. AbortController ์˜ ๋ถ€์žฌ๋กœ ๋ชจ๋“  ์š”์ฒญ์ด ๋๊นŒ์ง€ ์ง„ํ–‰

 

ํ˜„์žฌ ๋””๋ฐ”์šด์Šค + ์Šค๋กœํ‹€ ์ค‘์ฒฉ์œผ๋กœ ์ƒ๊ฐ๋ณด๋‹ค ์ž์ฃผ ์š”์ฒญ์ด ์ง„ํ–‰๋˜๊ณ  ์žˆ๋Š”๋ฐ Abort๊ฐ€ ์—†์–ด์„œ ๋ชจ๋“  ์š”์ฒญ์ด ๋๊นŒ์ง€ ์ง„ํ–‰๋˜๊ณ  ์žˆ๋‹ค. ์ฆ‰ ํ˜„์žฌ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์ด 22๋ฒˆ ์ง„ํ–‰๋œ ๊ฒƒ์€ 

(๋””๋ฐ”์šด์Šค ๋ฉˆ์ถค ํšŸ์ˆ˜) × (์Šค๋กœํ‹€ ๊ฒฝ๊ณ„/StrictMode ์ฆํญ) + (ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ) + (์ทจ์†Œ ์—†์Œ์œผ๋กœ ์ „๋ถ€ ์™„๋ฃŒ) ์˜ ๊ฒฐ๊ณผ์˜€๋‹ค.

 

 

 


 

 

ํ•ด๊ฒฐ 1. ์“ฐ๋กœํ‹€์€ ์ œ๊ฑฐํ•˜์—ฌ ๋””๋ฐ”์šด์Šค๋งŒ ์ ์šฉํ•˜๊ธฐ

๊ฒ€์ƒ‰์€ “์ตœ์ข… ํ™•์ •๊ฐ’ 1ํšŒ ํ˜ธ์ถœ”์ด ๋ชฉํ‘œ๋‹ค.
๊ทธ๋Ÿฐ๋ฐ ์Šค๋กœํ‹€(์ฃผ๊ธฐ ์ œํ•œ)์€ ์ค‘๊ฐ„๊ฐ’๋„ ์ฃผ๊ธฐ์ ์œผ๋กœ ์‹คํ–‰์‹œํ‚ค๊ธฐ ๋•Œ๋ฌธ์—, ๋””๋ฐ”์šด์Šค ๋’ค์— ์Šค๋กœํ‹€์„ ๋˜ ๊ฑธ๋ฉด ๋ฉˆ์ถค๋งˆ๋‹ค 1ํšŒ + ๊ฒฝ๊ณ„์—์„œ 1ํšŒ ์ถ”๊ฐ€ ๊ฐ™์€ ์˜๋„์น˜ ์•Š์€ ํ˜ธ์ถœ์ด ์„ž์˜€๋‹ค.

 

์“ฐ๋กœํ‹€ ์ ์šฉํ•œ ๊ฒƒ์„ ๋ชจ๋‘ ์ง€์šฐ๊ณ , ๋””๋ฐ”์šด์Šค๋งŒ ๋‚จ๊ฒจ์„œ '๋ฉˆ์ถค ๊ตฌ๊ฐ„๋งˆ๋‹ค 1ํšŒ'๋กœ ์ˆ˜๋ ด์‹œํ‚จ๋‹ค.

-// ์Šค๋กœํ‹€๋ง ํ›…
-const useThrottle = <T extends unknown[]>(callback: (...args: T) => void, delay: number) => {
-  const lastRunRef = useRef(0);
-  return useCallback((...args: T) => {
-    const now = Date.now();
-    if (now - lastRunRef.current >= delay) {
-      lastRunRef.current = now;
-      callback(...args);
-    }
-  }, [callback, delay]);
-};
@@
-// ์Šค๋กœํ‹€๋ง๋œ ๊ฒ€์ƒ‰ ํ•จ์ˆ˜ (300ms)
-const throttledSearch = useThrottle(performSearch, 300);
@@
-// ๋””๋ฐ”์šด์Šค๋œ ํ‚ค์›Œ๋“œ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ ๊ฒ€์ƒ‰ ์‹คํ–‰
-useEffect(() => {
-  if (debouncedKeyword) {
-    setPage(1);
-    setHasNext(true);
-    throttledSearch(debouncedKeyword, 1, false);
-  } else {
-    setSearchResults([]);
-  }
-}, [debouncedKeyword, throttledSearch]);
+// ๋””๋ฐ”์šด์Šค๋œ ํ‚ค์›Œ๋“œ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ ๊ฒ€์ƒ‰ ์‹คํ–‰ (์Šค๋กœํ‹€ ์ œ๊ฑฐ)
+useEffect(() => {
+  if (!debouncedKeyword) {
+    setSearchResults([]);
+    return;
+  }
+  setPage(1);
+  setHasNext(true);
+  performSearch(debouncedKeyword, 1, false);
+}, [debouncedKeyword, performSearch]);

 

 

 

ํ•ด๊ฒฐ 2. StrictMode 2ํšŒ ์ค‘๋ณต ํ˜ธ์ถœ ๊ฐ€๋“œ

 export const useSearchPlaces = () => {
   const [page, setPage] = useState(1);
+  // ๊ฐ™์€ ์š”์ฒญ(ํ‚ค์›Œ๋“œ, ํŽ˜์ด์ง€, append ์—ฌ๋ถ€) ์ค‘๋ณต ๋ฐฉ์ง€ ๊ฐ€๋“œ
+  const lastReqRef = useRef<string | null>(null);

  const performSearch = useCallback(
    async (searchKeyword: string, pageNum: number = 1, append: boolean = false) => {
       const trimmedKeyword = searchKeyword.trim();
       if (!trimmedKeyword) {
         setSearchResults([]);
         return;
       }
+      // StrictMode ์žฌ๋งˆ์šดํŠธ/์ดํŽ™ํŠธ ์žฌ์‹คํ–‰ ์‹œ ๊ฐ™์€ ์š”์ฒญ์€ ์Šคํ‚ต
+      const reqKey = `${trimmedKeyword}::${pageNum}::${append ? 'append' : 'replace'}`;
+      if (lastReqRef.current === reqKey) return;
+      lastReqRef.current = reqKey;
     },
     [],
   );

 

ํ˜„์žฌ StrictMode์—์„œ ์ดํŽ™ํŠธ/๋งˆ์šดํŠธ๊ฐ€ ์˜๋„์ ์œผ๋กœ 2ํšŒ ๋ฐœ์ƒ๋˜๊ณ  ์žˆ์—ˆ๋Š”๋ฐ, 

๋™์ผ ์š”์ฒญ์„ ์ค‘๋ณต์œผ๋กœ ๋ณด๋‚ด์ง€ ์•Š๋„๋ก ref๋ฅผ ๋„์ž…ํ•˜์—ฌ ํ‚ค ๋‹จ์œ„๋กœ ์ฐจ๋‹จํ•˜์˜€๋‹ค. 

 

๊ฐœ๋ฐœ๋ชจ๋“œ์—์„œ ๋™์ผ ๋ Œ๋” ์‚ฌ์ดํด ๋‚ด์—์„œ ๊ฐ™์€ ์ดํŽ™ํŠธ๋ฅผ ์—ฐ์† 2ํšŒ ์‹คํ–‰ํ•  ๋•Œ, ํ›…์— ์ „๋‹ฌ๋˜๋Š” ์ธ์ž( debouncedKeyword, page, append) ๋Š” ๊ทธ๋Œ€๋กœ์—ฌ์„œ ๋™์ผํ•œ Key๊ฐ€ ์ƒ์„ฑ๋œ๋‹ค.

 

useRef๋กœ ๋งˆ์ง€๋ง‰ ์š”์ฒญ์˜ ํ‚ค๋ฅผ ์ €์žฅํ•ด๋‘๊ณ , ๊ฐ™์€ ํ‚ค๋กœ performSearch๊ฐ€ ์—ฐ์† ํ˜ธ์ถœ๋˜๋ฉด ์ฆ‰์‹œ return ํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ๋‹ค.

const reqKey = `${trimmedKeyword}::${pageNum}::${append ? 'append' : 'replace'}`;

 

  • trimmedKeyword: ์„œ๋ฒ„์— ์ „๋‹ฌ๋  ์‹ค์งˆ ๊ฒ€์ƒ‰์–ด(๊ณต๋ฐฑ ์˜ํ–ฅ ์ œ๊ฑฐ)
  • pageNum: ํŽ˜์ด์ง€๋„ค์ด์…˜ ์š”์ฒญ ๊ตฌ๋ถ„
  • append/replace: ๋ฌดํ•œ์Šคํฌ๋กค(append)๊ณผ ์ตœ์ดˆ๊ฒ€์ƒ‰/๊ฐฑ์‹ (replace) ํ๋ฆ„์„ ๊ตฌ๋ถ„

 

๋‹ค๋งŒ, StrictMode๋Š” ๋งˆ์šดํŠธ ์ž์ฒด๋ฅผ ํ•œ๋ฒˆ ์™„์ „ํžˆ ํ•ด์ œ(unmount)ํ–ˆ๋‹ค๊ฐ€ ๋‹ค์‹œ ๋งˆ์šดํŠธํ•œ๋‹ค.

์ด๋•Œ useRef ์—ญ์‹œ ์ปดํฌ๋„ŒํŠธ ์ธ์Šคํ„ด์Šค ๊ธฐ๋ฐ˜์ด๋ฏ€๋กœ ์žฌ๋งˆ์šดํŠธ ์‹œ ์ดˆ๊ธฐํ™”๋œ๋‹ค.

์ฆ‰, ์ฒซ ๋งˆ์šดํŠธ์˜ ์—ฐ์† 2ํšŒ ํ˜ธ์ถœ์€ ref๋กœ ์žก์„ ์ˆ˜ ์žˆ์ง€๋งŒ, ์žฌ๋งˆ์šดํŠธ ํ›„์˜ 1ํšŒ ํ˜ธ์ถœ์€ ์ƒˆ ์ธ์Šคํ„ด์Šค์ด๋ฏ€๋กœ ์ •์ƒ์ ์œผ๋กœ 1ํšŒ ๋” ๋ฐœ์ƒํ•œ๋‹ค.

๋‹ค๋งŒ, ์ด๊ฑด ํ”„๋กœ๋•์…˜ ๋นŒ๋“œ์—์„œ๋Š” ๋ฐœ์ƒํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์ด ์ •๋„ ๊ฐ€๋“œ๋กœ ๋งˆ๋ฌด๋ฆฌํ•˜์˜€๋‹ค.

 

 

ํ•ด๊ฒฐ 3. AbortController๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์ค‘๊ฐ„ ์š”์ฒญ์„ ์ทจ์†Œํ•œ๋‹ค.

๋””๋ฐ”์šด์Šค๋ฅผ ์ ์šฉํ•ด๋„ '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ'๋ฅผ ํƒ€์ดํ•‘ํ•˜๋Š” ๊ณผ์ •์—์„œ ๋ฉˆ์ถค์ด ์žˆ๋‹ค๋ฉด ์ค‘๊ฐ„์— ์š”์ฒญ์ด ํ•œ๋ฒˆ ๋‚ ๋ผ๊ฐ€๊ฒŒ ๋œ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์ดํ›„ '์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ'๋ฅผ ๋‹ค์‹œ ์š”์ฒญํ•˜๋ฉด ์ด์ „ ์š”์ฒญ์ด ์•„์ง ๋๋‚˜์ง€ ์•Š์€ ์ƒํƒœ(in-flight) ๋กœ ๋‚จ๊ณ , ๊ทธ ์‚ฌ์ด ์ƒˆ ์š”์ฒญ์ด ๋‹ค์‹œ ๋ฐœ์‚ฌ๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋‚จ์•„์žˆ๋‹ค.
์š”์ฒญ์ด ๊ธธ๋‹ค๋ฉด, ํ˜น์€ ๋„คํŠธ์›Œํฌ ์†๋„๊ฐ€ ๋А๋ฆฌ๋‹ค๋ฉด, ์˜ค๋ž˜๋œ ์‘๋‹ต์ด ๋‚˜์ค‘์— ๋„์ฐฉํ•˜์—ฌ ์ตœ์‹  ๊ฒฐ๊ณผ๋ฅผ ๋ฎ์–ด์“ฐ๋Š” ๋ ˆ์ด์Šค๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค. ๋˜ํ•œ ๋ถˆํ•„์š”ํ•œ ์˜ˆ์ „ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ์ง€์†ํ•˜๋ฏ€๋กœ ๋„คํŠธ์›Œํฌ ๋‚ญ๋น„๋„ ์ปธ๋‹ค.

 

์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด:

  • AbortController๋ฅผ ๋„์ž…ํ•˜์—ฌ ์š”์ฒญ์„ ํ‚ค ๋‹จ์œ„๊ฐ€ ์•„๋‹ˆ๋ผ '๋ฌผ๋ฆฌ์ ์œผ๋กœ' ์ทจ์†Œํ•˜๋„๋ก ๋ณ€๊ฒฝํ•˜์˜€๋‹ค.
    • controllerRef๋กœ ํ˜„์žฌ ์ง„ํ–‰ ์ค‘์ธ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ์ €์žฅํ•ด๋‘๊ณ ,
    • ์ƒˆ ์š”์ฒญ ์ „์—” ๋ฐ˜๋“œ์‹œ abort()๋กœ ๊ธฐ์กด ์š”์ฒญ์„ ์ค‘๋‹จํ•œ ๋’ค,
    • ์ƒˆ๋กœ์šด AbortController๋ฅผ ๋ฐœ๊ธ‰ํ•˜์—ฌ ํ•ด๋‹น signal๋ฅผ fetch์— ์—ฐ๊ฒฐํ–ˆ๋‹ค.
  • ๋˜ํ•œ, ์ดํŒฉํŠธ ํด๋ฆฐ์—… ์‹œ์ ์—๋„ abort()๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ StrictMode ์žฌ์‹คํ–‰/์–ธ๋งˆ์šดํŠธ ๊ตฌ๊ฐ„์—์„œ ๋‚จ์•„ ์žˆ๋Š” in-flight ์š”์ฒญ์„ ์ •๋ฆฌํ•˜์˜€๋‹ค.

1. useSearchPlaces ํ›… : ์ดํŽ™ํŠธ/์žฌ์‹คํ–‰/์–ธ๋งˆ์šดํŠธ ์‹œ ์ง„ํ–‰ ์ค‘ ์š”์ฒญ ์ทจ์†Œ(cleanUp)

// NEW: ์ด์ „ ์š”์ฒญ ์ทจ์†Œ์šฉ ์ปจํŠธ๋กค๋Ÿฌ
const controllerRef = useRef<AbortController | null>(null);

const performSearch = useCallback(
  async (searchKeyword: string, pageNum: number = 1, append: boolean = false) => {
    // ... (์ค‘๋žต)

    // NEW: ์ด์ „ ์š”์ฒญ ์ทจ์†Œ
    controllerRef.current?.abort();
    // NEW: ์ƒˆ ์ปจํŠธ๋กค๋Ÿฌ ์ƒ์„ฑ
    const controller = new AbortController();
    controllerRef.current = controller;

    try {
      const params: SearchPlacesParams = {
        keyword: trimmedKeyword,
        page: pageNum,
        size: 15,
        // NEW: ์ง„ํ–‰ ์ค‘ ์š”์ฒญ ์ทจ์†Œ๋ฅผ ์œ„ํ•ด signal ์ „๋‹ฌ
        signal: controller.signal,
      };
      const results = await searchPlaces(params);
      // ...
    } catch (error) {
      // NEW: AbortError๋Š” ์ •์ƒ ์ทจ์†Œ → ์กฐ์šฉํžˆ ๋ฌด์‹œ
      if ((error as any)?.name !== 'AbortError') {
        console.error('๊ฒ€์ƒ‰ ์˜ค๋ฅ˜:', error);
        // ...
      }
    } finally {
      setIsLoading(false);
      setIsLoadingMore(false);
    }
  },
  [],
);

// NEW: debouncedKeyword ์ดํŽ™ํŠธ ํด๋ฆฐ์—…์—์„œ ์ง„ํ–‰ ์ค‘ ์š”์ฒญ ์ทจ์†Œ
useEffect(() => {
  if (!debouncedKeyword) {
    setSearchResults([]);
    controllerRef.current?.abort();  // ← ์—ฌ๊ธฐ
    return;
  }
  setPage(1);
  setHasNext(true);
  performSearch(debouncedKeyword, 1, false);

  return () => {
    controllerRef.current?.abort();  // ← ์—ฌ๊ธฐ
  };
}, [debouncedKeyword, performSearch]);

 

 

2. searchPlaces ์œ ํ‹ธ : fetch์— signal์„ ์ „๋‹ฌ + AbortError ๋Š” ๊ทธ๋Œ€๋กœ ์œ„๋กœ ๋˜์ง€๊ธฐ

export interface SearchPlacesParams {
  keyword: string;
  page?: number;
  size?: number;
  signal?: AbortSignal;  // NEW
}

export const searchPlaces = async (params: SearchPlacesParams): Promise<PlaceSearchResult[]> => {
  const { keyword, page = 1, size = 15, signal } = params;  // NEW

  try {
    const response = await fetch(
      `https://dapi.kakao.com/v2/local/search/keyword.json?query=${encodeURIComponent(keyword)}&page=${page}&size=${size}`,
      {
        headers: { Authorization: `KakaoAK ${process.env.NEXT_PUBLIC_KAKAO_MAP_REST_API_KEY}` },
        signal,  // NEW: fetch ์ทจ์†Œ์šฉ signal ์—ฐ๊ฒฐ
      },
    );
    // ...
  } catch (error: any) {
    if (error?.name === 'AbortError') {
      throw error;  // NEW: ์ทจ์†Œ๋Š” ์ •์ƒ ํ๋ฆ„ → ์ƒ์œ„์—์„œ ์กฐ์šฉํžˆ ๋ฌด์‹œ
    }
    console.error('ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰ ์˜ค๋ฅ˜:', error);
    throw error;    // ๊ธฐ์กด ์—๋Ÿฌ๋Š” ์ƒ์œ„์—์„œ ์ฒ˜๋ฆฌํ•˜๋„๋ก ์ „ํŒŒ
  }
};

 

  • ์œ ํ‹ธ ๋ ˆ๋ฒจ์—์„œ๋Š” signal์„ fetch ์˜ต์…˜์œผ๋กœ ์ „๋‹ฌํ•ด ์‹ค์ œ ์ทจ์†Œ๊ฐ€ ๋™์ž‘ํ•˜๋„๋ก ํ–ˆ๊ณ ,
  • ์ทจ์†Œ ์‹œ ๋ฐœ์ƒํ•˜๋Š” AbortError๋Š” ์ •์ƒ ์ทจ์†Œ๋กœ ๊ฐ„์ฃผํ•ด ๊ทธ๋Œ€๋กœ ์ „ํŒŒํ•˜์—ฌ ์ƒ์œ„ ํ›…์—์„œ ์กฐ์šฉํžˆ ๋ฌด์‹œํ•˜๋„๋ก ๋งŒ๋“ค์—ˆ๋‹ค.
  • ๋ฐ˜๋Œ€๋กœ ๋„คํŠธ์›Œํฌ ์‹คํŒจ/429๋Š” ์—๋Ÿฌ๋กœ ์ „ํŒŒํ•ด ์˜๋„๋œ ์—๋Ÿฌ ํ•ธ๋“ค๋ง์„ ์œ ์ง€ํ–ˆ๋‹ค.’

 


 

์ ์šฉ ๊ฒฐ๊ณผ

 

 

 

๋„คํŠธ์›Œํฌ ํƒญ์„ ๋ณด๋‹ˆ keyword.json?... ์ด 1ํšŒ๋งŒ ํ˜ธ์ถœ๋˜์–ด ๋””๋ฐ”์šด์Šค์™€ AbortController๊ฐ€ ์ž˜ ์ ์šฉ๋œ ๊ฒƒ์„ ํ™•์ธํ–ˆ๋‹ค. ์—ฌ๋Ÿฌ ์š”์ฒญ์ด ๋‚˜๊ฐ€๋˜ ๋ฌธ์ œ๋„ ์‚ฌ๋ผ์กŒ๊ณ , ์ค‘๊ฐ„ ์š”์ฒญ๋„ ์ž˜ ์ทจ์†Œ๋˜์—ˆ๋‹ค.

 

๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ž…๋ ฅ์— ๋Œ€ํ•œ ์‘๋‹ต๋„ ๋นจ๋ผ์ ธ์„œ INP๊ฐ€ 111ms์—์„œ 48ms๋กœ ๊ฐœ์„ ๋˜์—ˆ๋‹ค.

๋˜ํ•œ ์ด ์ˆ˜ํ–‰ ๋ฒ”์œ„์ธ full range๋„ 5995ms์—์„œ 2506ms๋กœ ์ ˆ๋ฐ˜์ด์ƒ์ด ๊ฐ์†Œํ•˜์˜€๋‹ค.

 

๋„คํŠธ์›Œํฌ ๋ฌธ์ œ๋Š” ํ•ด๊ฒฐํ–ˆ์ง€๋งŒ ๋…ธ๋ž€์ƒ‰ ๋ง‰๋Œ€๊ฐ€ ์—ฌ์ „ํžˆ ์ž์ฃผ ๋ณด์ธ๋‹ค. ์ฆ‰, UI ๋ Œ๋”๋ง์ด ์ž์ฃผ ๋ฐœ์ƒํ•˜๊ณ  ์žˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ๋‚ด๊ฐ€ ์›ํ•˜๋Š” ๋™์ž‘์€ ํ‚ค์›Œ๋“œ๊ฐ€ ํฌํ•จ๋˜๋Š” ์œ„์น˜๋ฅผ ์•„๋ž˜์— ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ณด์—ฌ์ค˜์•ผ ํ•˜๋Š” ๋™์ž‘์ด๋ผ UI๊ฐ€ ๋ฆฌ๋ Œ๋”๋ง์ด ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์ด ๋งž๋‹ค. 

 

 

728x90
'๐Ÿงก Projects/๐Ÿงก Projects: Web' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€
  • [ ๋ฌด๋ฌผ ] ๊ทผ์ฒ˜ ์‚ฌ์šฉ์ž ์ฐพ๊ธฐ #3 : ๋‚˜์™€ ๋™์ผํ•œ ํ”„๋กœํ•„ ์œ ํ˜• ํด๋ฆญํ•˜๋ฉด ์Šคํƒฌํ”„ ์ ๋ฆฝ ๋ฐ ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ์• ๋‹ˆ๋ฉ”์ด์…˜
  • [ ๋ฌด๋ฌผ ] Prisma ORM ์œผ๋กœ NextJS ๋ฐฑ์—”๋“œ ์„œ๋ฒ„ ๊ตฌ์ถ•ํ•˜๊ธฐ
  • [์œ ๋ ˆ์นด] github์— html ๋ฐฐํฌ ์‹œ ์˜์ƒ, ์‚ฌ์ง„ ์•ˆ ๋ณด์ด๋Š” ๋ฌธ์ œ ํ•ด๊ฒฐ
  • [web] SNS Instagram ์ปค๋ฎค๋‹ˆํ‹ฐ ๋งŒ๋“ค๊ธฐ(post,like, comments,signin) (Nextjs, tailwind, firebase)
eyes from es
eyes from es
  • eyes from es
    eyes from es
    eyes from es
  • ์ „์ฒด
    ์˜ค๋Š˜
    ์–ด์ œ
    • ๋ถ„๋ฅ˜ ์ „์ฒด๋ณด๊ธฐ
      • โค๏ธ ๊ฟ€ํŒ ๋ชจ์Œ
        • โค๏ธ ๊ฐ“์ƒ ๊ฟ€ํŒ
        • โค๏ธ ํ”„๋กœ๊ทธ๋ž˜๋ฐ
      • ๐Ÿงก Projects
        • ๐Ÿงก Projects: Web
        • ๐ŸŽคPreterview
        • ๐Ÿงก Projects: App
        • ๐Ÿงก๋Œ€์™ธํ™œ๋™
        • ๐Ÿงก OSCCA ์˜คํ”ˆ์†Œ์Šค ์ปจํŠธ๋ฆฌ๋ทฐ์…˜ ์•„์นด๋ฐ๋ฏธ
      • ๐Ÿ’› Frontend
        • ๐Ÿ’› Frontend : React
        • ๐Ÿ’› Frontend : JavaScript
        • ๐Ÿ’› Frontend : TypeScript
      • ๐Ÿ’š Backend
      • ๐Ÿ’™ OS: ์šด์˜์ฒด์ œ
        • ๐Ÿ’™ Linux
      • ๐Ÿ’œ ์ฝ”๋”ฉํ…Œ์ŠคํŠธ
        • ๐Ÿ’œ ์ž๋ฃŒ๊ตฌ์กฐ
        • ๐Ÿ’œ ์•Œ๊ณ ๋ฆฌ์ฆ˜
        • ๐Ÿ’œ ๋ฐฑ์ค€
        • ๐Ÿ’œSWEA
        • ๐Ÿ’œํ”„๋กœ๊ทธ๋ž˜๋จธ์Šค
      • ๐Ÿ”ด Study
        • ๐Ÿ”ด๋ฉด์ ‘ ์Šคํ„ฐ๋””
        • ๐Ÿ”ด ๊ธฐ์—…๋ถ„์„
        • ๐Ÿ”ด ์—๋Ÿฌ๋…ธํŠธ(Error Note)๐Ÿงฑ
        • ๐Ÿ”ด ITNews(Coding)
        • ๐Ÿ”ด ITNews(Tech)
      • ๐ŸŸ  ์ธ์ƒ ๊ณ„ํš
        • ๐ŸŸ  ์˜ฌํ•ด ๋ชฉํ‘œ
      • ๐ŸŸก TIL
        • ๐ŸŸก TIL ์ผ๊ธฐ
  • ๋ธ”๋กœ๊ทธ ๋ฉ”๋‰ด

    • ํ™ˆ
    • ํƒœ๊ทธ
    • ๋ฐฉ๋ช…๋ก
  • ๋งํฌ

  • ๊ณต์ง€์‚ฌํ•ญ

  • ์ธ๊ธฐ ๊ธ€

  • ํƒœ๊ทธ

    css
    ์‚ผ์„ฑ์ „์ž
    ์•Œ๊ณ ๋ฆฌ์ฆ˜
    ๋‰ด์Šค์Šคํฌ๋žฉ
    Ai
    IT์ด์Šˆ
    ์Šคํ„ฐ๋””
    ์ฝ”๋”ฉํ…Œ์ŠคํŠธ
    ๋ฐฉํ•™์Šคํ„ฐ๋””
    ์Šค๋งˆํŠธ์‹ฑ์Šค
    SW์ด์Šˆ
    ์ฝ”ํ…Œ
    ์›น๊ฐœ๋ฐœ
    ์ŠคํŒŒ๋ฅดํƒ€์ฝ”๋”ฉํด๋Ÿฝ
    ๊ฐœ๋ฐœ๊ณต๋ถ€
    ์ฝ”๋”ฉ
    ๋™ํ–ฅ๋ถ„์„
    ๊ธฐ์—…๋ถ„์„
    ๋ถ„์„๋ ˆํฌํŠธ
    ๊ฐœ๋ฐœ
    html
    C
    ๋ฐฑ์ค€
    ๋‰ด์Šค๋ฃธ
    ๋„ค์นด๋ผ์ฟ ๋ฐฐ
    ์ฝ”๋“œ์Šคํ„ฐ๋””
    ์ตœ๊ทผ์ด์Šˆ
    ์ฝ”๋“œ๋ฆฌ๋ทฐ
    ๋ฌธ์ œํ’€์ด
    ์ž๋ฃŒ๊ตฌ์กฐ
  • ์ตœ๊ทผ ๋Œ“๊ธ€

  • ์ตœ๊ทผ ๊ธ€

  • hELLOยท Designed By์ •์ƒ์šฐ.v4.10.5
eyes from es
[BADATA] ์œ„์น˜ ๊ฒ€์ƒ‰ ์ฐฝ์— ๋””๋ฐ”์šด์‹ฑ ์“ฐ๋กœํ‹€๋ง ์ค‘์ฒฉ ๋ฌธ์ œ ํ•ด๊ฒฐํ•˜๊ธฐ
์ƒ๋‹จ์œผ๋กœ

ํ‹ฐ์Šคํ† ๋ฆฌํˆด๋ฐ”