From 2b1b629a2514765c0b90adff82526188e46ab28f Mon Sep 17 00:00:00 2001 From: arty0928 Date: Tue, 7 Oct 2025 02:10:34 +0900 Subject: [PATCH 1/6] =?UTF-8?q?perf:=20[BADA-388]=20=EB=94=94=EB=B0=94?= =?UTF-8?q?=EC=9A=B4=EC=8A=A4=EB=A7=8C=20=EB=82=A8=EA=B8=B0=EA=B3=A0=20?= =?UTF-8?q?=EC=93=B0=EB=A1=9C=ED=8B=80=20=EC=A0=9C=EA=B1=B0=20#297?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../search/hook/useSearchPlacesHooks.ts | 33 ++++--------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/src/features/rental/search/hook/useSearchPlacesHooks.ts b/src/features/rental/search/hook/useSearchPlacesHooks.ts index 7ebd0944..95145465 100644 --- a/src/features/rental/search/hook/useSearchPlacesHooks.ts +++ b/src/features/rental/search/hook/useSearchPlacesHooks.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { searchPlaces, @@ -23,22 +23,6 @@ const useDebounce = (value: string, delay: number) => { return debouncedValue; }; -// 스로틀링 훅 -const useThrottle = (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(''); @@ -99,19 +83,16 @@ export const useSearchPlaces = () => { [], ); - // 스로틀링된 검색 함수 (300ms) - const throttledSearch = useThrottle(performSearch, 300); - // 디바운스된 키워드가 변경될 때 검색 실행 useEffect(() => { - if (debouncedKeyword) { - setPage(1); - setHasNext(true); - throttledSearch(debouncedKeyword, 1, false); - } else { + if (!debouncedKeyword) { setSearchResults([]); + return; } - }, [debouncedKeyword, throttledSearch]); + setPage(1); + setHasNext(true); + performSearch(debouncedKeyword, 1, false); + }, [debouncedKeyword, performSearch]); // 다음 페이지 로드 함수 const loadNextPage = useCallback(() => { From 673a1f78abc28b992ff7c314628d5072d0e1f99f Mon Sep 17 00:00:00 2001 From: arty0928 Date: Tue, 7 Oct 2025 02:19:13 +0900 Subject: [PATCH 2/6] =?UTF-8?q?perf:=20[BADA-388]=20StrictMode=202?= =?UTF-8?q?=ED=9A=8C=20=ED=98=B8=EC=B6=9C=20=EC=A4=91=EB=B3=B5=20=EA=B0=80?= =?UTF-8?q?=EB=93=9C=20#297?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rental/search/hook/useSearchPlacesHooks.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/features/rental/search/hook/useSearchPlacesHooks.ts b/src/features/rental/search/hook/useSearchPlacesHooks.ts index 95145465..6ce3d3ee 100644 --- a/src/features/rental/search/hook/useSearchPlacesHooks.ts +++ b/src/features/rental/search/hook/useSearchPlacesHooks.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { searchPlaces, @@ -32,6 +32,9 @@ export const useSearchPlaces = () => { const [hasNext, setHasNext] = useState(true); const [page, setPage] = useState(1); + // 같은 요청 (키워드, 페이지)이 중복으로 발생하는 것을 방지 가드 + const lastReqRef = useRef(null); + // 디바운스된 키워드 (500ms) - 공백 제거 const debouncedKeyword = useDebounce(keyword.trim(), 500); @@ -44,6 +47,13 @@ export const useSearchPlaces = () => { return; } + // StrictMode 재마운트/이펙트 재실행 시 같은 요청 스킵 + const reqKey = `${trimmedKeyword}::${pageNum}::${append ? 'append' : 'replace'}`; + if (lastReqRef.current === reqKey) { + return; + } + lastReqRef.current = reqKey; + if (pageNum === 1) { setIsLoading(true); } else { From 42fb4e60308c5a57fe9829e6f3c4bf9dcbc602eb Mon Sep 17 00:00:00 2001 From: arty0928 Date: Tue, 7 Oct 2025 03:08:56 +0900 Subject: [PATCH 3/6] =?UTF-8?q?perf:=20[BADA-388]=20AbortController=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=EC=9C=BC=EB=A1=9C=20=EC=9D=B4=EC=A0=84=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=B7=A8=EC=86=8C=20#297?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../search/hook/useSearchPlacesHooks.ts | 33 +++++++++++++++---- .../search/utils/address/searchPlaces.ts | 10 ++++-- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/features/rental/search/hook/useSearchPlacesHooks.ts b/src/features/rental/search/hook/useSearchPlacesHooks.ts index 6ce3d3ee..2cd9e0a2 100644 --- a/src/features/rental/search/hook/useSearchPlacesHooks.ts +++ b/src/features/rental/search/hook/useSearchPlacesHooks.ts @@ -38,6 +38,9 @@ export const useSearchPlaces = () => { // 디바운스된 키워드 (500ms) - 공백 제거 const debouncedKeyword = useDebounce(keyword.trim(), 500); + // 이전 요청 취소용 컨트롤러 + const controllerRef = useRef(null); + // 검색 실행 함수 const performSearch = useCallback( async (searchKeyword: string, pageNum: number = 1, append: boolean = false) => { @@ -54,6 +57,13 @@ export const useSearchPlaces = () => { } lastReqRef.current = reqKey; + // 이전 요청 취소 + controllerRef.current?.abort(); + // 새 컨트롤러 생성 + const controller = new AbortController(); + controllerRef.current = controller; + + // API 호출 if (pageNum === 1) { setIsLoading(true); } else { @@ -65,6 +75,7 @@ export const useSearchPlaces = () => { keyword: trimmedKeyword, page: pageNum, size: 15, + signal: controller.signal, }; const results = await searchPlaces(params); @@ -77,13 +88,15 @@ export const useSearchPlaces = () => { 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 호출 제한으로 인해 검색이 일시적으로 중단되었습니다.'); + if ((error as Error)?.name !== 'AbortError') { + console.error('검색 오류:', error); + if (!append) { + setSearchResults([]); + } + // API 호출 제한 에러인 경우 사용자에게 알림 + if (error instanceof Error && error.message.includes('API 호출 제한')) { + console.warn('API 호출 제한으로 인해 검색이 일시적으로 중단되었습니다.'); + } } } finally { setIsLoading(false); @@ -97,11 +110,17 @@ export const useSearchPlaces = () => { useEffect(() => { if (!debouncedKeyword) { setSearchResults([]); + // 현재 진행 중인 요청이 있으면 취소 + controllerRef.current?.abort(); return; } setPage(1); setHasNext(true); performSearch(debouncedKeyword, 1, false); + // 이 이펙트가 재실행/언마운트 될 때 진행 중 요청 취소 + return () => { + controllerRef.current?.abort(); + }; }, [debouncedKeyword, performSearch]); // 다음 페이지 로드 함수 diff --git a/src/features/rental/search/utils/address/searchPlaces.ts b/src/features/rental/search/utils/address/searchPlaces.ts index 2322f19f..f8ed9dd2 100644 --- a/src/features/rental/search/utils/address/searchPlaces.ts +++ b/src/features/rental/search/utils/address/searchPlaces.ts @@ -14,11 +14,12 @@ export interface SearchPlacesParams { keyword: string; page?: number; size?: number; + signal?: AbortSignal; } // 키워드 검색 함수 (페이지네이션 지원) export const searchPlaces = async (params: SearchPlacesParams): Promise => { - const { keyword, page = 1, size = 15 } = params; + const { keyword, page = 1, size = 15, signal } = params; if (!keyword.trim()) { return []; @@ -31,6 +32,7 @@ export const searchPlaces = async (params: SearchPlacesParams): Promise Date: Tue, 7 Oct 2025 04:04:46 +0900 Subject: [PATCH 4/6] =?UTF-8?q?perf:=20[BADA-388]=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20useCallback=20=EC=9D=98=EC=A1=B4=EC=84=B1?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0=20#297?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rental/search/hook/useSearchPosHooks.ts | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/src/features/rental/search/hook/useSearchPosHooks.ts b/src/features/rental/search/hook/useSearchPosHooks.ts index 90caecae..2cd92ccb 100644 --- a/src/features/rental/search/hook/useSearchPosHooks.ts +++ b/src/features/rental/search/hook/useSearchPosHooks.ts @@ -50,28 +50,25 @@ export const useSearchPos = () => { } = useSearchPlaces(); // 검색 결과 선택 시 호출되는 함수 - const handleSelectPlace = useCallback( - (place: PlaceSearchResult) => { - createAddressMutation.mutate(place, { - onSuccess: () => { - setTimeout(() => { - queryClient.invalidateQueries({ queryKey: ['addressHistory', 5, sort] }); - refetch(); - }, 500); - }, - }); - - const searchParams = new URLSearchParams({ - lat: place.y.toString(), - lng: place.x.toString(), - address: place.road_address_name || place.address_name, - placeName: place.place_name, - }); - - router.push(`/rental?${searchParams.toString()}`); - }, - [createAddressMutation, refetch, sort, queryClient, router], - ); + const handleSelectPlace = useCallback((place: PlaceSearchResult) => { + createAddressMutation.mutate(place, { + onSuccess: () => { + setTimeout(() => { + queryClient.invalidateQueries({ queryKey: ['addressHistory', 5, sort] }); + refetch(); + }, 500); + }, + }); + + const searchParams = new URLSearchParams({ + lat: place.y.toString(), + lng: place.x.toString(), + address: place.road_address_name || place.address_name, + placeName: place.place_name, + }); + + router.push(`/rental?${searchParams.toString()}`); + }, []); // 주소 이력 클릭 시 호출되는 함수 const handleAddressHistoryClick = useCallback( From 18e7a0648541c4edf257ec85da230f82467bcabe Mon Sep 17 00:00:00 2001 From: arty0928 Date: Tue, 7 Oct 2025 04:12:59 +0900 Subject: [PATCH 5/6] =?UTF-8?q?perf:=20[BADA-388]=20=EC=A3=BC=EC=86=8C=20?= =?UTF-8?q?=EC=9D=B4=EB=A0=A5=20=EC=A1=B4=EC=9E=AC=20=EC=97=AC=EB=B6=80=20?= =?UTF-8?q?useMemo=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EB=B0=B0=EC=97=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20#297?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rental/search/hook/useSearchPosHooks.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/features/rental/search/hook/useSearchPosHooks.ts b/src/features/rental/search/hook/useSearchPosHooks.ts index 2cd92ccb..f12aee79 100644 --- a/src/features/rental/search/hook/useSearchPosHooks.ts +++ b/src/features/rental/search/hook/useSearchPosHooks.ts @@ -179,17 +179,14 @@ export const useSearchPos = () => { } }, [throttledHandleScroll]); - // 주소 이력 존재 여부 + // 주소 이력 존재 여부 (실제 내용이 바뀔 때만 재계산) const hasAddressHistory = useMemo(() => { - return ( - Array.isArray(addressHistoryInfinite?.pages) && - addressHistoryInfinite.pages.some( - (page) => - Array.isArray(page?.content?.getAddressResponses) && - page.content.getAddressResponses.length > 0, - ) + if (!addressHistoryInfinite?.pages) return false; + + return addressHistoryInfinite.pages.some( + (page) => page?.content?.getAddressResponses?.length > 0, ); - }, [addressHistoryInfinite?.pages]); + }, [addressHistoryInfinite]); return { // 상태 From 4cbea621164579856df739fda84238f7a7acf332 Mon Sep 17 00:00:00 2001 From: arty0928 Date: Sun, 12 Oct 2025 17:10:15 +0900 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20[BADA-388]=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=83=81=ED=83=9C=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=20=ED=82=A4=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD(stale=20closure)=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=BA=90=EC=8B=9C=20=EC=B5=9C=EC=8B=A0=ED=99=94=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B0=9C=EC=84=A0=20#297?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rental/search/hook/useSearchPosHooks.ts | 53 ++++++++++++------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/src/features/rental/search/hook/useSearchPosHooks.ts b/src/features/rental/search/hook/useSearchPosHooks.ts index f12aee79..2037eb1f 100644 --- a/src/features/rental/search/hook/useSearchPosHooks.ts +++ b/src/features/rental/search/hook/useSearchPosHooks.ts @@ -49,26 +49,39 @@ export const useSearchPos = () => { loadNextPage, } = useSearchPlaces(); - // 검색 결과 선택 시 호출되는 함수 - const handleSelectPlace = useCallback((place: PlaceSearchResult) => { - createAddressMutation.mutate(place, { - onSuccess: () => { - setTimeout(() => { - queryClient.invalidateQueries({ queryKey: ['addressHistory', 5, sort] }); - refetch(); - }, 500); - }, - }); - - const searchParams = new URLSearchParams({ - lat: place.y.toString(), - lng: place.x.toString(), - address: place.road_address_name || place.address_name, - placeName: place.place_name, - }); - - router.push(`/rental?${searchParams.toString()}`); - }, []); + // 주소 이력 쿼리 키 + const addressHistoryKey = (limit: number, sort: string /*, userId?: string */) => + ['addressHistory', limit, sort /*, userId */] as const; + + const LIMIT = 5; + + // 장소 선택 시 호출되는 함수 + const handleSelectPlace = useCallback( + async (place: PlaceSearchResult) => { + try { + // 1) 주소 생성(저장) + await createAddressMutation.mutateAsync(place); + + // 2) 캐시 무효화 + 즉시 재패치 (타임아웃 제거) + const key = addressHistoryKey(LIMIT, sort); + await queryClient.invalidateQueries({ queryKey: key }); + await queryClient.refetchQueries({ queryKey: key }); + + // 3) 라우팅 + const searchParams = new URLSearchParams({ + lat: String(place.y), // Kakao: y=lat, x=lng + lng: String(place.x), + address: place.road_address_name || place.address_name, + placeName: place.place_name, + }); + router.push(`/rental?${searchParams.toString()}`); + } catch (e) { + // 필요 시 토스트/로그 + console.error(e); + } + }, + [createAddressMutation, queryClient, sort, router], + ); // 주소 이력 클릭 시 호출되는 함수 const handleAddressHistoryClick = useCallback(