- claudedocs 폴더 구조 재정리: archive/sessions, guides/migration·mobile·universal-list, refactoring 분류 - 오래된 세션 컨텍스트/체크리스트 문서 정리 (아카이브 이동 또는 삭제) - AuthContext → authStore(Zustand) 전환 시작, RootProvider 간소화 - GenericCRUDDialog 공통 다이얼로그 컴포넌트 추가 - PermissionDialog 삭제 → GenericCRUDDialog로 대체 - RankDialog/TitleDialog GenericCRUDDialog 기반으로 리팩토링 - toast-utils.ts 삭제 (미사용) - fileDownload.ts 개선, excel-download.ts 정리 - menuStore/themeStore Zustand 셀렉터 최적화 - useColumnSettings/useTableColumnStore 기능 보강 - 세금계산서/견적/작업자화면/결재 등 소규모 개선 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5.3 KiB
5.3 KiB
UniversalListPage 검색창 리렌더링 문제 해결 가이드
문제 현상
- 검색창에 글자 하나만 입력해도 전체 페이지가 리렌더링됨
- 검색어가 초기화되거나 데이터가 새로고침됨
- 정상적인 검색이 불가능함
원인 분석
핵심 차이점: clientSideFiltering
| 설정 | 동작 | 검색 시 fetchData 호출 |
|---|---|---|
clientSideFiltering: true |
클라이언트에서 필터링 | ❌ 호출 안함 |
clientSideFiltering: false |
서버에서 필터링 | ✅ 매번 호출 |
UniversalListPage 내부 코드 (index.tsx:298-305):
useEffect(() => {
if (!config.clientSideFiltering && !isLoading && !isMobileLoading) {
fetchData(isMobileAppend);
}
}, [currentPage, searchValue, filters, activeTab, dateRangeKey]);
clientSideFiltering: false일 때 검색어(searchValue) 변경마다 fetchData가 호출됨.
무한 루프 발생 조건
-
getList 내부에서 setState 호출
// ❌ 잘못된 패턴 actions: { getList: async (params) => { const result = await getStocks(params); if (result.success) { setStockStats(result.data); // ← 상태 변경! setTotalItems(result.pagination.total); // ← 상태 변경! } return result; }, }, -
config가 useMemo로 감싸져 있고 상태 의존성이 있을 때
- getList에서 setState → 컴포넌트 리렌더링
- stats/tableFooter useMemo 재평가
- config useMemo 재평가 (stats 의존성)
- UniversalListPage에 새 config 전달
- dateRangeKey 재계산 → useEffect 트리거
- fetchData 호출 → 무한 루프!
해결 방법
방법 1: 수주관리 패턴 (권장)
클라이언트 사이드 필터링으로 전환
// ===== 데이터 상태 (외부 관리) =====
const [stocks, setStocks] = useState<StockItem[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [filterValues, setFilterValues] = useState<Record<string, string | string[]>>({});
// 데이터 로드 함수
const loadData = useCallback(async () => {
setIsLoading(true);
const result = await getStocks({ page: 1, perPage: 9999 }); // 전체 로드
if (result.success) {
setStocks(result.data);
}
setIsLoading(false);
}, [startDate, endDate]);
// 클라이언트 사이드 필터링
const filteredStocks = stocks.filter((stock) => {
if (searchTerm) {
const searchLower = searchTerm.toLowerCase();
if (!stock.itemName.toLowerCase().includes(searchLower)) return false;
}
return true;
});
// config는 useMemo 없이 일반 객체로!
const config: UniversalListConfig<StockItem> = {
// ...
clientSideFiltering: true, // ← 핵심!
actions: {
getList: async () => ({
success: true,
data: filteredStocks, // ← 이미 필터링된 데이터
totalCount: filteredStocks.length,
}),
},
searchFilter: (stock, searchValue) => {
return stock.itemName.toLowerCase().includes(searchValue.toLowerCase());
},
customFilterFn: (items, fv) => {
// 필터 로직
return items;
},
};
return (
<UniversalListPage
config={config}
initialData={filteredStocks}
initialTotalCount={filteredStocks.length}
onFilterChange={setFilterValues}
onSearchChange={setSearchTerm}
/>
);
방법 2: 서버 사이드 필터링 유지 (주의 필요)
getList 내부에서 절대 setState 호출 금지
// ✅ 올바른 패턴
actions: {
getList: async (params) => {
const result = await getStocks(params);
if (result.success) {
// ❌ setStockStats, setTotalItems 호출 금지!
return {
success: true,
data: result.data,
totalCount: result.pagination.total,
totalPages: result.pagination.lastPage,
};
}
return { success: false, error: result.error };
},
},
config useMemo 의존성 최소화
// 상태에 의존하는 값들을 config 외부로 분리
const config = useMemo(() => ({
// 상태에 의존하지 않는 설정만 포함
title: '목록',
idField: 'id',
clientSideFiltering: false,
// ...
}), []); // 빈 의존성 배열!
// 상태에 의존하는 설정은 별도로 전달
return (
<UniversalListPage
config={config}
stats={stats} // 별도 prop으로 전달
tableFooter={tableFooter} // 별도 prop으로 전달
/>
);
체크리스트
문제 발생 시 확인 사항
clientSideFiltering값 확인getList내부에서setState호출 여부- config가
useMemo로 감싸져 있는지 - useMemo 의존성에 상태값이 포함되어 있는지
onSearchChange콜백이 상태를 업데이트하는지
권장 패턴 (수주관리 참고)
src/app/[locale]/(protected)/sales/order-management-sales/page.tsx
clientSideFiltering: true- config를 useMemo 없이 일반 객체로 정의
- 외부에서 데이터 관리 (
useState) initialDataprop으로 데이터 전달onSearchChange,onFilterChange콜백 사용
관련 파일
src/components/templates/UniversalListPage/index.tsx- useEffect 의존성 확인 (Line 298-305)src/app/[locale]/(protected)/sales/order-management-sales/page.tsx- 정상 동작 패턴 참고
작성일
2026-01-28