7.5 KiB
품목관리 검색 상태 보존 — React UX 개선 요청서
작성일: 2026-03-19 요청자: R&D실 대상: React 프론트엔드 개발자 우선순위: 중간 (UX 개선) 영향 범위: React만 수정 (API 변경 없음)
1. 문제 설명
1.1 현상
품목관리 목록(/production/screen-production 또는 /items)에서:
- 검색어 "실리카" 입력 → 4건 필터링
- 품목 클릭 → 상세 페이지 이동
- 수정 후 저장 → 목록으로 복귀
- 검색어가 초기화되어 전체 1,175건이 다시 표시됨
[목록] 검색: "실리카" (4건)
↓ 클릭
[상세] 품목 조회
↓ 수정 → 저장
[목록] 검색: "" (초기화됨, 1,175건) ← 문제!
1.2 기대 동작
수정/조회 후 목록으로 돌아오면 이전 검색어와 탭 필터가 그대로 유지되어야 한다.
2. 원인 분석
2.1 근본 원인: 검색 상태가 컴포넌트 state에만 존재
ItemListClient.tsx:76에서 검색어가 useState('')로 초기화된다. URL에 반영되지 않으므로 페이지 이동 후 복귀하면 상태가 소멸된다.
// ItemListClient.tsx:76 — 항상 빈 문자열로 시작
const [searchTerm, setSearchTerm] = useState('');
const [selectedType, setSelectedType] = useState<string>('all');
2.2 직접 원인: 목록 복귀 시 router.push 사용
수정/등록 완료 후 검색 파라미터 없이 하드코딩된 경로로 이동한다:
| 파일 | 라인 | 현재 코드 |
|---|---|---|
DynamicItemForm/index.tsx |
408 | router.push('/production/screen-production') |
ItemDetailEdit.tsx |
353, 368 | router.push('/production/screen-production') |
ItemForm/index.tsx |
178 | router.push('/production/screen-production') |
참고:
ItemDetailClient.tsx:568의 "목록으로" 버튼은 이미router.back()을 사용 중 (정상).
3. 수정 방안
2단계 조합으로 해결한다:
3.1 Step 1: 목록 복귀 시 router.back() 사용
수정/등록 완료 후 router.push(...) 대신 router.back()을 사용한다.
브라우저 히스토리의 이전 페이지로 복귀하므로 검색 상태가 자연스럽게 보존된다.
수정 대상 3개 파일:
(1) src/components/items/DynamicItemForm/index.tsx (라인 408)
// Before
router.push('/production/screen-production');
router.refresh();
// After
router.back();
(2) src/components/items/ItemDetailEdit.tsx (라인 353, 368)
// Before (2곳 모두)
onClick={() => router.push('/production/screen-production')}
// After (2곳 모두)
onClick={() => router.back()}
(3) src/components/items/ItemForm/index.tsx (라인 178)
// Before
router.push('/production/screen-production');
router.refresh();
// After
router.back();
3.2 Step 2: URL searchParams 동기화 (선택사항 — 권장)
Step 1만으로도 기본 동작은 해결된다. Step 2는 새로고침/URL 공유 시에도 검색 상태를 보존하기 위한 추가 개선이다.
ItemListClient.tsx에서 검색어와 탭 필터를 URL 쿼리 파라미터에 동기화한다.
변경 1: URL에서 초기값 읽기
// Before (ItemListClient.tsx:76)
const [searchTerm, setSearchTerm] = useState('');
const [selectedType, setSelectedType] = useState<string>('all');
// After
import { useSearchParams } from 'next/navigation';
const searchParams = useSearchParams();
const [searchTerm, setSearchTerm] = useState(searchParams.get('search') || '');
const [selectedType, setSelectedType] = useState<string>(searchParams.get('type') || 'all');
변경 2: 검색/탭 변경 시 URL 업데이트
// ItemListClient.tsx — 새로 추가할 useEffect
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
const pathname = usePathname();
useEffect(() => {
const params = new URLSearchParams();
if (debouncedSearchTerm) params.set('search', debouncedSearchTerm);
if (selectedType !== 'all') params.set('type', selectedType);
const queryString = params.toString();
const newUrl = queryString ? `${pathname}?${queryString}` : pathname;
// 히스토리 교체 (뒤로가기 스택 오염 방지)
window.history.replaceState(null, '', newUrl);
}, [debouncedSearchTerm, selectedType, pathname]);
변경 3: handleView에서 검색 파라미터 전달 (선택)
// ItemListClient.tsx:133 — 상세 페이지 이동 시 returnUrl 전달
const handleView = (itemCode: string, itemType: string, itemId: string) => {
const returnParams = new URLSearchParams();
if (searchTerm) returnParams.set('search', searchTerm);
if (selectedType !== 'all') returnParams.set('type', selectedType);
router.push(
`/production/screen-production/${encodeURIComponent(itemCode)}?mode=view&type=${itemType}&id=${itemId}`
);
};
Step 2를 적용하면 URL이
/items?search=실리카&type=all형태가 되어 새로고침/공유에도 상태가 유지된다.
4. 수정 파일 요약
| 파일 | 수정 내용 | 필수/선택 |
|---|---|---|
src/components/items/DynamicItemForm/index.tsx |
router.push(...) → router.back() |
필수 |
src/components/items/ItemDetailEdit.tsx |
router.push(...) → router.back() (2곳) |
필수 |
src/components/items/ItemForm/index.tsx |
router.push(...) → router.back() |
필수 |
src/components/items/ItemListClient.tsx |
URL searchParams 동기화 | 권장 |
5. 주의사항
5.1 router.back() 사용 시 고려점
- 사용자가 URL 직접 입력으로 수정 페이지에 진입한 경우,
router.back()은 품목 목록이 아닌 이전 페이지(브라우저 탭의 히스토리)로 이동할 수 있다 - 이 경우를 대비하려면
router.back()대신 referrer 체크 후 fallback 패턴을 사용할 수 있다:
// 안전한 뒤로가기 패턴 (선택)
const handleGoBack = () => {
if (window.history.length > 1) {
router.back();
} else {
router.push('/production/screen-production');
}
};
5.2 router.refresh() 제거 여부
DynamicItemForm/index.tsx:409와 ItemForm/index.tsx:179에서 router.refresh()를 호출 중이다. router.back()으로 전환하면 router.refresh()는 불필요하다 (이전 페이지의 캐시된 상태로 복귀하므로). 단, 저장 후 목록 데이터 갱신이 필요하면 ItemListClient의 데이터 fetching이 마운트 시 자동 실행되는지 확인 필요.
5.3 UniversalListPage의 externalSearch prop
UniversalListPage에 이미 externalSearch prop이 존재한다. Step 2 구현 시 이 prop을 활용하면 UniversalListPage 내부의 검색 input 값도 URL과 동기화할 수 있다.
6. 테스트 체크리스트
- 검색어 입력 후 품목 클릭 → 상세 조회 → "목록으로" → 검색어 유지 확인
- 검색어 입력 후 품목 클릭 → 수정 → 저장 → 검색어 유지 확인
- 검색어 입력 후 품목 등록(
?mode=new) → 저장 → 검색어 유지 확인 - 탭 필터(완제품/부품 등) 선택 후 상세 → 복귀 → 탭 유지 확인
- (Step 2) 검색 후 페이지 새로고침 → 검색어 유지 확인
- (Step 2) 검색 상태 URL 복사 → 새 탭에서 열기 → 동일 결과 확인
- 에러 상태에서 "품목 목록으로 돌아가기" 클릭 → 목록 정상 표시 확인
관련 문서
- 품목 정책:
rules/item-policy.md
최종 업데이트: 2026-03-19