-
Notifications
You must be signed in to change notification settings - Fork 1
Refactor/#94 SearchPage 데이터 흐름 개선 #95
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4685673
e2adc5a
89ff95f
4a2a52a
9a4ef9a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<BasePlace[]> | ||
| onSelectPlace: (id: string) => void | ||
| useBackHandler?: boolean | ||
| } | ||
|
|
@@ -26,41 +28,39 @@ export type Props = { | |
| * - useBackHandler가 true면 헤더에 뒤로가기 버튼, false면 검색 아이콘 표시 | ||
| * | ||
| * @param placeholder 검색 input의 placeholder | ||
| * @param places 검색 결과 장소 리스트 | ||
| * @param searchFunc 검색 함수 (input 변경 시 호출) | ||
| * @param onSelectPlace 리스트 아이템 선택 시 호출 | ||
| * @param useBackHandler 헤더에 뒤로가기 버튼 사용 여부 | ||
| * | ||
| * @example | ||
| * <SearchPage | ||
| * placeholder="장소를 검색하세요" | ||
| * places={places} | ||
| * searchFunc={handleSearch} | ||
| * onSelectPlace={(id) => console.log(id)} | ||
| * useBackHandler={true} | ||
| * /> | ||
| */ | ||
| export const SearchPage = ({ | ||
| placeholder, | ||
| places, | ||
| placeholder = '장소 또는 주소를 검색하세요', | ||
| searchFunc, | ||
| onSelectPlace, | ||
| useBackHandler = false, | ||
| }: 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<HTMLInputElement>) => { | ||
| const value = e.target.value | ||
| setInputValue(value) | ||
| if (value.length > 0) { | ||
| searchFunc(value) | ||
| setPlaces(value) | ||
| } | ||
| } | ||
|
|
||
| return ( | ||
| <> | ||
| {isLoading && ( | ||
| {isSelecting && ( | ||
| <Spinner className='absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2' /> | ||
| )} | ||
| <Flex className={'border-b-1 gap-2.5 border-gray-100 p-3.5'}> | ||
|
|
@@ -73,7 +73,7 @@ export const SearchPage = ({ | |
| value={inputValue} | ||
| onChange={handleInputChange} | ||
| className={'w-full text-lg font-medium outline-none'} | ||
| placeholder={placeholder || '장소 또는 주소를 검색하세요'} | ||
| placeholder={placeholder} | ||
| /> | ||
| </Flex> | ||
| {inputValue && ( | ||
|
|
@@ -84,7 +84,7 @@ export const SearchPage = ({ | |
| inputValue={inputValue} | ||
| place={place} | ||
| onClick={() => { | ||
| setIsLoading(true) | ||
| setIsSelecting(true) | ||
| onSelectPlace(place.id) | ||
| }} | ||
|
Comment on lines
86
to
89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 동기 호출로 인해 스피너가 표시되지 않음
🐛 수정 제안 onClick={() => {
- setIsSelecting(true)
- onSelectPlace(place.id)
- setIsSelecting(false)
+ setIsSelecting(true)
+ // onSelectPlace가 Promise를 반환하지 않으면 스피너 로직 제거 고려
+ onSelectPlace(place.id)
+ // 페이지 이동 시 컴포넌트가 언마운트되므로 false 설정 불필요
}}또는 onClick={async () => {
setIsSelecting(true)
try {
await onSelectPlace(place.id)
} finally {
setIsSelecting(false)
}
}}🤖 Prompt for AI Agents |
||
| /> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 = <T, P>( | ||
| fetcher: (params: P) => Promise<T[]>, | ||
| delay: number = 300, | ||
| ) => { | ||
| const [data, setData] = useState<T[]>([]) | ||
| const timeoutRef = useRef<ReturnType<typeof setTimeout> | 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] as const | ||
| } |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
입력값 초기화 시 검색 결과가 유지되는 문제
value.length > 0일 때만setPlaces를 호출하므로, 사용자가 입력을 모두 지워도 이전 검색 결과(places)가 그대로 남아있습니다.inputValue && places.map(...)조건으로 렌더링은 숨겨지지만, 상태는 남아있어 다시 입력 시 잠깐 이전 결과가 보일 수 있습니다.입력이 비워질 때
places를 초기화하는 것이 더 깔끔합니다.🐛 수정 제안
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const value = e.target.value setInputValue(value) if (value.length > 0) { setPlaces(value) + } else { + // places 초기화 로직 필요 (useDebouncedFetch에 clear 함수 추가 필요) } }🤖 Prompt for AI Agents