반응형

1. 발생 이슈
활동 일지 작성 페이지에서 사용자가 입력 중인 데이터가 실수로 유실되는 것을 방지하기 위해 이탈 방지 로직을 구현했으나, 다음과 같은 기술적 결함이 발생했습니다.
- 히스토리 스택 오염: 데이터 입력 시마다 브라우저 히스토리에 불필요한 엔트리가 계속 추가됨.
- 뒤로가기 기능 오작동: 이탈 확인 창에서 '확인'을 눌러도 이전 페이지로 이동하지 못하고 현재 페이지에 갇힘.
- 라이브러리 제약:
useBlocker등 최신 API 도입이 어려운 환경에서 브라우저 네이티브 API(popstate) 제어 로직의 불안정성 노출.
2. 원인 분석
- 잘못된 의존성 배열 설정:
useEffect내 로직이formData와selectedFiles의 변화에 반응하도록 설정되어, 값이 수정될 때마다 이벤트 리스너가 재등록되고pushState가 중복 실행됨. - 이벤트 루프 제어 실패:
handlePopState내부에서pushState와back()호출 순서가 꼬이면서 브라우저가 정상적인 히스토리 이동 경로를 찾지 못함.
3. 해결 방법
계층화된 아키텍처 원칙에 따라 컴포넌트 마운트 시점에 로직을 분리하고, 불필요한 리렌더링 및 히스토리 추가를 방지하도록 리팩토링했습니다.
① 이탈 방지 플래그 관리 (useRef)
상태 변화 시마다 이벤트를 재등록하지 않도록 useRef를 사용하여 최신 데이터 변경 여부를 관리합니다.
TypeScript
const isFormDirtyRef = useRef(false);
// 폼 데이터 변경 감지
useEffect(() => {
if (initialFormDataRef.current) {
const hasChanges = JSON.stringify(formData) !== JSON.stringify(initialFormDataRef.current);
isFormDirtyRef.current = hasChanges || selectedFiles.length > 0;
}
}, \[formData, selectedFiles\]);\
② 브라우저 히스토리 및 이벤트 최적화 (useEffect)
컴포넌트 로드 시 단 한 번만 더미 엔트리를 추가하고, 확인 버튼 클릭 시에만 리스너를 제거 후 이동하도록 로직을 단순화했습니다.
TypeScript
useEffect(() => {
const handlePopState = (e: PopStateEvent) => {
if (isFormDirtyRef.current && !submitting) {
const confirmed = window.confirm("작성 중인 내용이 있습니다. 페이지를 떠나시겠습니까?");
if (!confirmed) {
// 취소 시: 현재 페이지 유지 (뒤로 간 스택을 다시 앞으로 밀어넣음)
window.history.pushState(null, "", window.location.href);
} else {
// 확인 시: 이벤트 리스너 제거 후 실제 뒤로가기 수행
isFormDirtyRef.current = false;
window.removeEventListener("popstate", handlePopState);
window.history.back();
}
}
};
window.addEventListener("popstate", handlePopState);
window.history.pushState(null, "", window.location.href); // 초기 1회만 더미 엔트리 추가
return () => {
window.removeEventListener("popstate", handlePopState);
};
}, \[submitting\]); // formData 의존성 제거로 중복 실행 방지\`
4. 결과 및 배운점
결과
- 사용자 경험 개선: 데이터 수정 후 뒤로가기 시 알림창이 정상적으로 뜨며, '확인' 클릭 시 즉시 이전 페이지로 이동함.
- 성능 및 안정성: 브라우저 히스토리에 더미 스택이 쌓이지 않아 뒤로가기 버튼을 길게 눌러도 깨끗한 이력을 유지함.
- 예외 처리 완성: 새로고침(
beforeunload)과 뒤로가기(popstate)를 모두 완벽히 차단하여 데이터 유실 방지.
배운점 (Key Takeaways)
- 상태 참조 최적화:
useEffect내에서 실시간 상태값이 필요할 때, 의존성 배열에 넣어 함수를 재실행하기보다useRef를 활용하는 것이 부수 효과(Side Effect)를 제어하는 데 더 효율적임을 학습함. - 브라우저 동작 원리: SPA 환경에서 브라우저의 히스토리 스택이 어떻게 작동하는지 깊이 이해하게 되었으며, 네이티브 API 제어의 중요성을 깨달음.
반응형
'TroubleShooting' 카테고리의 다른 글
| 외부 API 연동 시 세션(loginInfo) 초기화 현상 (1) | 2026.03.06 |
|---|---|
| 음성인식 모바일 호환성 트러블슈팅 (0) | 2026.03.06 |
| 리액트 API 호출 구조 리팩토링: useEffect에서 React Query로의 전환 (0) | 2026.01.07 |
| Jenkins-SonarQube 연동 에러: "The ‘report’ parameter is missing" (HTTP 400) (0) | 2026.01.06 |