์์น ๊ฒ์์ฐฝ ๋๋ฐ์ด์ค·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๊ฐ ๋ฆฌ๋ ๋๋ง์ด ๋ฐ์ํ๋ ๊ฒ์ด ๋ง๋ค.
