이번 글에서는 Autowini 필터 검색 페이지의 좌측 Accordion 메뉴에서 발생한 UX 문제를 계기로, TanStack Query의 placeholderData 옵션을 활용해 구조를 개선한 리팩토링 경험을 정리해보려 합니다.
문제 상황
Autowini 필터 검색 페이지의 좌측 영역은 Accordion UI로 구성되어 있으며, 필터 조건이 변경될 때마다 서버에서 필터 데이터를 다시 조회하도록 구현되어 있었습니다.
이 과정에서 다음과 같은 UX 문제가 발생했습니다.
- 필터 파라미터 변경
- API 재호출
- 로딩 중 data가 undefined
- Accordion 내부 리스트가 비어짐
- 모든 메뉴가 접혔다가 다시 펼쳐지는 현상 발생
사용자 입장에서는 필터를 조작할 때마다 화면이 깜빡이거나 필터가 초기화되는 것처럼 느껴졌습니다.
기존 해결 방식
UX 문제를 빠르게 해결하기 위해, 초기에는 React의 useState를 사용해 이전 필터 데이터를 보존하는 방식으로 대응했습니다.
핵심 아이디어는 단순했습니다. "새 데이터를 불러오는 동안에는 이전 데이터를 계속 보여주자."
/**
* @description 필터 검색 데이터 조회 Hook
*/
export const useFilterSearch = ({
itemType,
filterSearchParams,
}: {
itemType: string;
filterSearchParams: FilterSearchParams;
}) => {
const [filterData, setFilterData] = useState<FilterResponse['data'] | null>(
null
);
const { data, isLoading, isError } = useQuery({
queryKey: ['useFilterSearch', itemType, filterSearchParams],
queryFn: () =>
apiClient.get<FilterResponse>(`${API_URL}`, {
params: filterSearchParams,
}),
});
// 로딩 중 Accordion 메뉴가 비어 보이는 현상 방지
useEffect(() => {
if (data?.data?.data && !isError) {
setFilterData(data.data.data);
}
}, [data, isError]);
return {
filterData,
filterDataIsLoading: isLoading,
filterDataIsError: isError,
};
};
이 방식으로 Accordion 메뉴가 접히는 문제는 해결되었고, 사용자 경험 역시 즉각적으로 개선되었습니다.
문제점과 해결
기존 구현에서는 UX 문제를 해결하기 위해 useState를 사용해 이전 필터 데이터를 별도로 관리하고 있었습니다. 하지만 이 방식은 구조적으로 몇 가지 한계를 가지고 있었습니다.
- React Query가 이미 캐시를 관리하고 있음
- 동일한 데이터를 useState로 한 번 더 관리
- 불필요한 메모리 사용
- useEffect 추가로 인한 코드 복잡도 증가
즉, React Query를 사용하고 있음에도 상태를 이중으로 관리하고 있었고, 이 로직을 더 단순하게 만들 수 있지 않을까 하는 고민이 들었습니다.
이 문제를 해결하기 위해 활용한 것이 TanStack Query의 placeholderData 옵션입니다. placeholderData는 쿼리 키가 변경되어 새 데이터를 fetching 하는 동안에도 이전 데이터를 그대로 유지할 수 있도록 도와주는 옵션입니다.
이 옵션을 적용하면, 로딩 중에도 data가 undefined가 되지 않고 Accordion 내부 데이터가 유지되며 메뉴 상태가 자연스럽게 보존됩니다.
결과적으로, 기존에 useState와 useEffect로 직접 처리하던 로직을 React Query에 위임할 수 있었고, UX 개선과 함께 코드 구조도 훨씬 단순해졌습니다.
/**
* @description 필터 검색 데이터 조회 Hook
*/
export const useFilterSearch = ({
itemType,
filterSearchParams,
}: {
itemType: string;
filterSearchParams: FilterSearchParams;
}) => {
const { data, isLoading, isError } = useQuery({
queryKey: ['useFilterSearch', itemType, filterSearchParams],
queryFn: () =>
apiClient.get<FilterResponse>(`${API_URL}`, {
params: filterSearchParams,
}),
// 새 데이터 로딩 전까지 이전 데이터를 그대로 유지
placeholderData: (previousData) => previousData,
});
return {
filterData: data?.data?.data,
filterDataIsLoading: isLoading,
filterDataIsError: isError,
};
};
개선 결과 및 느낀 점
이번 개선을 통해 기존에 사용하던 useState와 useEffect를 제거하면서 로직 흐름을 단순화할 수 있었고, 상태를 이중으로 관리하던 불필요한 구조를 정리해 메모리 사용 측면에서도 개선 효과를 얻을 수 있었습니다.
그동안 TanStack Query를 비교적 잘 활용하고 있다고 생각했지만, placeholderData처럼 상황에 맞게 활용할 수 있는 옵션을 놓치고 있었다는 점에서 아직도 더 공부할 여지가 많다는 것을 느끼게 되었습니다.
'지식 정리 📝' 카테고리의 다른 글
| Lighthouse 90점대인데 체감은 느린 이유? Vercel Region 최적화로 해결 (2) | 2026.02.03 |
|---|---|
| Autowini 프론트엔드 분리 프로젝트 CI/CD 파이프라인 구축 (Bitbucket 기반) 2 (0) | 2026.01.19 |
| React SPA에서 메타 태그 중복 문제, 라이브러리 제작으로 해결하기 (feat. react-head-safe) (0) | 2026.01.09 |
| React i18n 다국어 처리 빌드 크기 최적화 2 (0) | 2025.12.30 |
| Autowini 프론트엔드 분리 프로젝트 CI/CD 파이프라인 구축 (Bitbucket 기반) 1 (1) | 2025.11.27 |