# UniversalListPage 검색창 리렌더링 문제 해결 가이드

## 문제 현상
- 검색창에 글자 하나만 입력해도 전체 페이지가 리렌더링됨
- 검색어가 초기화되거나 데이터가 새로고침됨
- 정상적인 검색이 불가능함

## 원인 분석

### 핵심 차이점: clientSideFiltering

| 설정 | 동작 | 검색 시 fetchData 호출 |
|------|------|----------------------|
| `clientSideFiltering: true` | 클라이언트에서 필터링 | ❌ 호출 안함 |
| `clientSideFiltering: false` | 서버에서 필터링 | ✅ 매번 호출 |

**UniversalListPage 내부 코드 (index.tsx:298-305):**
```javascript
useEffect(() => {
  if (!config.clientSideFiltering && !isLoading && !isMobileLoading) {
    fetchData(isMobileAppend);
  }
}, [currentPage, searchValue, filters, activeTab, dateRangeKey]);
```

`clientSideFiltering: false`일 때 검색어(`searchValue`) 변경마다 `fetchData`가 호출됨.

### 무한 루프 발생 조건

1. **getList 내부에서 setState 호출**
   ```javascript
   // ❌ 잘못된 패턴
   actions: {
     getList: async (params) => {
       const result = await getStocks(params);
       if (result.success) {
         setStockStats(result.data);  // ← 상태 변경!
         setTotalItems(result.pagination.total);  // ← 상태 변경!
       }
       return result;
     },
   },
   ```

2. **config가 useMemo로 감싸져 있고 상태 의존성이 있을 때**
   - getList에서 setState → 컴포넌트 리렌더링
   - stats/tableFooter useMemo 재평가
   - config useMemo 재평가 (stats 의존성)
   - UniversalListPage에 새 config 전달
   - dateRangeKey 재계산 → useEffect 트리거
   - fetchData 호출 → 무한 루프!

## 해결 방법

### 방법 1: 수주관리 패턴 (권장)

**클라이언트 사이드 필터링으로 전환**

```typescript
// ===== 데이터 상태 (외부 관리) =====
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 호출 금지**

```typescript
// ✅ 올바른 패턴
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 의존성 최소화**

```typescript
// 상태에 의존하는 값들을 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`)
- `initialData` prop으로 데이터 전달
- `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
