RETROSPECTIVE · 회고
모바일 뒤로가기로 닫히는 모달 만들기 (pushState와 popstate)
Frontend
모바일에서 모달을 띄운 상태로 뒤로가기를 누르면, 모달이 닫히는 게 아니라 이전 페이지로 이동해버린다. 사용자 입장에서는 "닫기"를 누른 건데 페이지를 잃는 셈이다.
아이디어
모달이 열릴 때 같은 URL로 히스토리를 하나 쌓아두고, 뒤로가기(popstate)가 오면 모달만 닫는다.
useEffect(() => {
if (!open) return;
let pushed = false;
let closedByPop = false;
const onPop = () => {
closedByPop = true;
onClose("popstate");
};
// StrictMode의 mount→unmount→mount에서 push/back이 꼬이지 않게 한 틱 미룸
const timer = setTimeout(() => {
history.pushState({ ...history.state, __modal: true }, "");
pushed = true;
window.addEventListener("popstate", onPop);
}, 0);
return () => {
clearTimeout(timer);
window.removeEventListener("popstate", onPop);
// 버튼·ESC로 닫혔다면 쌓아둔 히스토리를 직접 소비
if (pushed && !closedByPop) history.back();
};
}, [open]);
함정 1: 버튼으로 닫았을 때 남는 히스토리
확인 버튼으로 닫으면 쌓아둔 항목이 그대로 남아서, 다음 뒤로가기가 아무 일도 안 하는 것처럼 보인다. 그래서 cleanup에서 history.back()으로 직접 소비한다.
함정 2: 닫자마자 페이지 이동
await modal.confirm(...) 직후 router.push()를 하면, 뒤늦게 실행된 history.back()이 방금 한 이동을 되돌려버린다. 모달 Promise는 히스토리 정리(popstate)가 끝난 뒤에 resolve하도록 순서를 보장했다.
function afterHistoryRestored(fn) {
let done = false;
const finish = () => {
if (done) return;
done = true;
window.removeEventListener("popstate", finish);
fn();
};
window.addEventListener("popstate", finish);
setTimeout(finish, 500); // 안전장치
}
Next.js App Router와 같이 쓸 때
Next.js 14.1부터 App Router는 네이티브 window.history.pushState를 지원하고 내부 상태를 함께 관리하기 때문에, 같은 URL로 항목을 쌓아도 라우터와 충돌하지 않았다. 헤드리스 브라우저로 "열기 → 뒤로가기 → 페이지 유지", "확인으로 닫기 → 뒤로가기 → 이전 페이지" 두 시나리오를 검증했다.