From 4685673c74b9596e323e4a6f40e98e37722d6939 Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Thu, 15 Jan 2026 17:10:42 +0900 Subject: [PATCH 1/5] =?UTF-8?q?refactor:=20useSearch=20=ED=9B=85=EC=9D=84?= =?UTF-8?q?=20=EB=B2=94=EC=9A=A9=EC=A0=81=EC=9D=B8=20useDebouncedFetch?= =?UTF-8?q?=EB=A1=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=EB=B0=8F=20?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/_hooks/useDebouncedFetch.ts | 54 ++++++++++++++++++++++++ apps/web/app/_hooks/useSearch.ts | 53 ----------------------- 2 files changed, 54 insertions(+), 53 deletions(-) create mode 100644 apps/web/app/_hooks/useDebouncedFetch.ts delete mode 100644 apps/web/app/_hooks/useSearch.ts diff --git a/apps/web/app/_hooks/useDebouncedFetch.ts b/apps/web/app/_hooks/useDebouncedFetch.ts new file mode 100644 index 0000000..f9671db --- /dev/null +++ b/apps/web/app/_hooks/useDebouncedFetch.ts @@ -0,0 +1,54 @@ +import { useCallback, useEffect, useRef, useState } from 'react' + +/** + * 비동기 함수(fetcher)를 디바운싱하여 실행하고, 그 결과를 상태로 관리하는 훅 + * + * - 입력이 멈춘 후 일정 시간(delay) 뒤에 API를 호출합니다. + * - 검색어 자동완성, 필터링 등 잦은 요청을 방지해야 할 때 유용합니다. + * + * @template T 결과 데이터의 타입 + * @template P 파라미터의 타입 + * + * @param fetcher 데이터를 가져오는 비동기 함수 (Promise 반환) + * @param delay 디바운스 지연 시간 (ms, 기본값: 300ms) + * + * @returns data - 비동기 작업의 결과 데이터 (초기값: []) + * @returns trigger - 디바운스가 적용된 실행 함수 + * + * @example + * const { data: places, trigger: searchPlaces } = useDebouncedFetch(getSearchPlaceByKakao, 500); + * // searchPlaces('강남역') 호출 시 500ms 후 API 호출 -> places 업데이트 + */ +export const useDebouncedFetch = ( + fetcher: (params: P) => Promise, + delay: number = 300, +) => { + const [data, setData] = useState([]) + const timeoutRef = useRef | null>(null) + + const trigger = useCallback( + (params: P) => { + if (timeoutRef.current) clearTimeout(timeoutRef.current) + + timeoutRef.current = setTimeout(async () => { + try { + const result = await fetcher(params) + setData(result) + } catch (error) { + console.error('Debounced fetch failed:', error) + setData([]) + } + }, delay) + }, + [fetcher, delay], + ) + + // 언마운트 시 타이머 클리어 + useEffect(() => { + return () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current) + } + }, []) + + return { data, trigger } +} diff --git a/apps/web/app/_hooks/useSearch.ts b/apps/web/app/_hooks/useSearch.ts deleted file mode 100644 index c2d1aa9..0000000 --- a/apps/web/app/_hooks/useSearch.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from 'react' - -/** - * - 입력된 query를 기준으로 장소 검색 API 호출 - * - 300ms 디바운싱 적용 (빠른 입력 시 불필요한 요청 방지) - * - API 결과를 상태로 관리하여 컴포넌트에서 바로 사용 가능 - * - * @example - * const { searchResult: cafeResult, searchFunc: cafeSearchFunc } = useSearch< - * SearchPlace, - * KakaoSearchFuncParams - * >(getSearchPlaceByKakao) - * - * - * @template T 검색 결과 아이템의 타입 (예: SearchPlace) - * @template P 검색 함수에 들어갈 파라미터 타입 (예: KakaoSearchFuncParams) - * @param fetcher Promise를 반환하는 비동기 검색 함수 - * - * @returns searchResult - 검색된 장소 리스트 - * @returns searchFunc - 검색을 수행하는 함수 - */ - -export const useSearch = (fetcher: (params: P) => Promise) => { - const [searchResult, setSearchResult] = useState([]) - const timeoutRef = useRef | null>(null) - - const searchFunc = useCallback( - (params: P) => { - if (timeoutRef.current) clearTimeout(timeoutRef.current) - - timeoutRef.current = setTimeout(async () => { - try { - // 주입받은 fetcher 실행 - const result = await fetcher(params) - setSearchResult(result) - } catch (error) { - console.error('Search failed:', error) - setSearchResult([]) - } - }, 300) - }, - [fetcher], // fetcher가 변경되면 함수 재생성 - ) - - // 언마운트 시 타이머 클리어 - useEffect(() => { - return () => { - if (timeoutRef.current) clearTimeout(timeoutRef.current) - } - }, []) - - return { searchResult, searchFunc } -} From e2adc5a9c3892eae8d42654f36d276d85006b8c0 Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Thu, 15 Jan 2026 17:15:38 +0900 Subject: [PATCH 2/5] =?UTF-8?q?refactor:=20SearchPage=20=EB=82=B4=EB=B6=80?= =?UTF-8?q?=EB=A1=9C=20=EC=83=81=ED=83=9C=20=EC=9D=B4=EB=8F=99=20=EB=B0=8F?= =?UTF-8?q?=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0=20-=20=EB=B6=80?= =?UTF-8?q?=EB=AA=A8=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EA=B4=80=EB=A6=AC=ED=95=98=EB=8D=98=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EB=A5=BC=20SearchPage=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= =?UTF-8?q?=20-=20=EB=B6=80=EB=AA=A8=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=EC=9D=98=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=EB=A6=AC=EB=A0=8C=EB=8D=94=EB=A7=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/_components/SearchPage/SearchPage.tsx | 24 ++++++++--------- .../Step/PlaceSearch/PlaceSearch.tsx | 26 +++++++++---------- apps/web/app/places/search/page.tsx | 22 +++++++--------- 3 files changed, 34 insertions(+), 38 deletions(-) diff --git a/apps/web/app/_components/SearchPage/SearchPage.tsx b/apps/web/app/_components/SearchPage/SearchPage.tsx index bd54d06..c1c9d3c 100644 --- a/apps/web/app/_components/SearchPage/SearchPage.tsx +++ b/apps/web/app/_components/SearchPage/SearchPage.tsx @@ -4,15 +4,17 @@ import { Icon } from '@repo/ui/components/Icon' import { Flex, VerticalScrollArea } from '@repo/ui/components/Layout' import { SearchPlaceListItem } from './SearchPlaceListItem' import { HeaderBackButton } from '@/_components/HeaderBackButton' +import { useDebouncedFetch } from '@/_hooks/useDebouncedFetch' + +export type BasePlace = { + id: string + name: string + address: string +} export type Props = { placeholder?: string - places: { - id: string - name: string - address: string - }[] - searchFunc: (inputValue: string) => void + searchFunc: (inputValue: string) => Promise onSelectPlace: (id: string) => void useBackHandler?: boolean } @@ -26,7 +28,6 @@ export type Props = { * - useBackHandler가 true면 헤더에 뒤로가기 버튼, false면 검색 아이콘 표시 * * @param placeholder 검색 input의 placeholder - * @param places 검색 결과 장소 리스트 * @param searchFunc 검색 함수 (input 변경 시 호출) * @param onSelectPlace 리스트 아이템 선택 시 호출 * @param useBackHandler 헤더에 뒤로가기 버튼 사용 여부 @@ -34,19 +35,18 @@ export type Props = { * @example * console.log(id)} * useBackHandler={true} * /> */ export const SearchPage = ({ - placeholder, - places, + placeholder = '장소 또는 주소를 검색하세요', searchFunc, onSelectPlace, useBackHandler = false, }: Props) => { + const { data: places, trigger } = useDebouncedFetch(searchFunc) const [inputValue, setInputValue] = useState('') const [isLoading, setIsLoading] = useState(false) @@ -54,7 +54,7 @@ export const SearchPage = ({ const value = e.target.value setInputValue(value) if (value.length > 0) { - searchFunc(value) + trigger(value) } } @@ -73,7 +73,7 @@ export const SearchPage = ({ value={inputValue} onChange={handleInputChange} className={'w-full text-lg font-medium outline-none'} - placeholder={placeholder || '장소 또는 주소를 검색하세요'} + placeholder={placeholder} /> {inputValue && ( diff --git a/apps/web/app/places/new/_components/Step/PlaceSearch/PlaceSearch.tsx b/apps/web/app/places/new/_components/Step/PlaceSearch/PlaceSearch.tsx index df38405..4eaca47 100644 --- a/apps/web/app/places/new/_components/Step/PlaceSearch/PlaceSearch.tsx +++ b/apps/web/app/places/new/_components/Step/PlaceSearch/PlaceSearch.tsx @@ -1,6 +1,5 @@ import { useCallback } from 'react' import { SearchPage } from '@/_components/SearchPage' -import { useSearch } from '@/_hooks/useSearch' import type { UseFormSetValue } from 'react-hook-form' import type { NewPlaceRequest } from '@/_apis/schemas/place' import { type CampusType, CAMPUS_LOCATION } from '@/_constants/campus' @@ -13,27 +12,26 @@ type Props = { } export const PlaceSearch = ({ campus, setValue, nextStep }: Props) => { - const { searchResult, searchFunc } = useSearch(searchCafeAndRestaurant) - - const places = [...searchResult].map((item) => ({ - id: item.id, - name: item.place_name, - address: item.address_name, - })) - const handleSearch = useCallback( - (query: string) => { + async (query: string) => { const { longitude: x, latitude: y } = CAMPUS_LOCATION[campus] - searchFunc({ query, location: { x, y } }) + const result = await searchCafeAndRestaurant({ + query, + location: { x, y }, + }) + return result.map((item) => ({ + id: item.id, + name: item.place_name, + address: item.address_name, + })) }, - [campus, searchFunc], + [campus], ) return ( { + onSelectPlace={(id: string) => { setValue('kakaoPlaceId', id) nextStep() }} diff --git a/apps/web/app/places/search/page.tsx b/apps/web/app/places/search/page.tsx index fb07d96..d4349f3 100644 --- a/apps/web/app/places/search/page.tsx +++ b/apps/web/app/places/search/page.tsx @@ -3,29 +3,27 @@ import { useRouter } from 'next/navigation' import { SearchPage } from '@/_components/SearchPage' import { getPlacesBySearch } from '@/_apis/services/place' -import { useSearch } from '@/_hooks/useSearch' import { CLIENT_PATH } from '@/_constants/path' -import type { PlaceBySearch } from '@/_apis/schemas/place' const Page = () => { const { replace } = useRouter() - const { searchResult, searchFunc } = useSearch( - getPlacesBySearch, - ) - const newPlaces = searchResult.map((place) => ({ - id: place.placeId, - name: place.placeName, - address: place.address, - })) + const handleSearch = async (query: string) => { + const result = await getPlacesBySearch(query) + return result.map((place) => ({ + id: place.placeId, + name: place.placeName, + address: place.address, + })) + } return ( { replace(CLIENT_PATH.PLACE_DETAIL(id)) }} - places={newPlaces} /> ) } From 89ff95fd8e5be783ed6b865cb8c341b445169918 Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Thu, 15 Jan 2026 21:35:55 +0900 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20useDebouncedFetch=20=ED=9B=85?= =?UTF-8?q?=EC=9D=98=20=EB=B0=98=ED=99=98=20=EA=B0=92=EC=9D=84=20=EB=B0=B0?= =?UTF-8?q?=EC=97=B4=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/_components/SearchPage/SearchPage.tsx | 4 ++-- apps/web/app/_hooks/useDebouncedFetch.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/app/_components/SearchPage/SearchPage.tsx b/apps/web/app/_components/SearchPage/SearchPage.tsx index c1c9d3c..1f4aedc 100644 --- a/apps/web/app/_components/SearchPage/SearchPage.tsx +++ b/apps/web/app/_components/SearchPage/SearchPage.tsx @@ -46,7 +46,7 @@ export const SearchPage = ({ onSelectPlace, useBackHandler = false, }: Props) => { - const { data: places, trigger } = useDebouncedFetch(searchFunc) + const [places, setPlaces] = useDebouncedFetch(searchFunc) const [inputValue, setInputValue] = useState('') const [isLoading, setIsLoading] = useState(false) @@ -54,7 +54,7 @@ export const SearchPage = ({ const value = e.target.value setInputValue(value) if (value.length > 0) { - trigger(value) + setPlaces(value) } } diff --git a/apps/web/app/_hooks/useDebouncedFetch.ts b/apps/web/app/_hooks/useDebouncedFetch.ts index f9671db..69810b8 100644 --- a/apps/web/app/_hooks/useDebouncedFetch.ts +++ b/apps/web/app/_hooks/useDebouncedFetch.ts @@ -50,5 +50,5 @@ export const useDebouncedFetch = ( } }, []) - return { data, trigger } + return [data, trigger] as const } From 4a2a52a9d323f6881b37c101d13d366fe214561c Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Thu, 15 Jan 2026 22:01:18 +0900 Subject: [PATCH 4/5] =?UTF-8?q?refactor:=20isLoading=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EB=AA=85=EC=9D=84=20isSelecting=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=98=EC=97=AC=20=EC=9D=98=EB=AF=B8=20=EB=AA=85?= =?UTF-8?q?=ED=99=95=ED=99=94=20-=20isLoading=EC=9D=80=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=ED=8E=98=EC=B9=AD(fetching)=EA=B3=BC=20?= =?UTF-8?q?=ED=98=BC=EB=8F=99=EB=90=A0=20=EC=97=AC=EC=A7=80=EA=B0=80=20?= =?UTF-8?q?=EC=9E=88=EC=96=B4=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/_components/SearchPage/SearchPage.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/web/app/_components/SearchPage/SearchPage.tsx b/apps/web/app/_components/SearchPage/SearchPage.tsx index 1f4aedc..4814780 100644 --- a/apps/web/app/_components/SearchPage/SearchPage.tsx +++ b/apps/web/app/_components/SearchPage/SearchPage.tsx @@ -48,7 +48,7 @@ export const SearchPage = ({ }: Props) => { const [places, setPlaces] = useDebouncedFetch(searchFunc) const [inputValue, setInputValue] = useState('') - const [isLoading, setIsLoading] = useState(false) + const [isSelecting, setIsSelecting] = useState(false) const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value @@ -60,7 +60,7 @@ export const SearchPage = ({ return ( <> - {isLoading && ( + {isSelecting && ( )} @@ -84,8 +84,9 @@ export const SearchPage = ({ inputValue={inputValue} place={place} onClick={() => { - setIsLoading(true) + setIsSelecting(true) onSelectPlace(place.id) + setIsSelecting(false) }} /> ))} From 9a4ef9af4e20bcea024f0c0500bc7aa8c88897ad Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Thu, 15 Jan 2026 22:11:17 +0900 Subject: [PATCH 5/5] =?UTF-8?q?refactor:=20setIsSelecting(false)=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20-=20=EB=B0=B0=EC=B9=98=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EB=95=8C=EB=AC=B8=EC=97=90=20=EC=8A=A4=ED=94=BC?= =?UTF-8?q?=EB=84=88=EA=B0=80=20=EC=8B=A4=EC=A0=9C=EB=A1=9C=20=EB=A0=8C?= =?UTF-8?q?=EB=8D=94=EB=A7=81=EB=90=98=EC=A7=80=20=EC=95=8A=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/_components/SearchPage/SearchPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/web/app/_components/SearchPage/SearchPage.tsx b/apps/web/app/_components/SearchPage/SearchPage.tsx index 4814780..ba0a15b 100644 --- a/apps/web/app/_components/SearchPage/SearchPage.tsx +++ b/apps/web/app/_components/SearchPage/SearchPage.tsx @@ -86,7 +86,6 @@ export const SearchPage = ({ onClick={() => { setIsSelecting(true) onSelectPlace(place.id) - setIsSelecting(false) }} /> ))}