요구사항
뒤로가기 버튼 클릭, 또는 뒤로가기 동작 (브라우저 동작)이 일어나면,정말 끝내시겠습니다?
라는 모달이 뜨게 됩니다.
단순히 뒤로가기 버튼 클릭에만 이벤트를 주는 것으로는 이 요구사항을 충족하기 어려웠습니다.
가장 어려웠던 부분은 브라우저의 history stack을 고려하며 브라우저의 뒤로가기 동작
을 커스텀 하는 것이였습니다.
보통 브라우저의 뒤로가기 동작 커스텀
을 검색해본다면, 아래와 같은 코드를 찾을 수 있습니다.
function useCustomBack(customBack: () => void) {
const browserPreventEvent = (event: () => void) => {
history.pushState(null, '', location.href);
event();
};
useEffect(() => {
history.pushState(null, '', location.href);
window.addEventListener('popstate', () => {
browserPreventEvent(customBack);
});
return () => {
window.removeEventListener('popstate', () => {
browserPreventEvent(customBack);
});
};
}, []);
}
브라우저의 뒤로가기 동작이 일어날 때 history stack에서 하나가 빠지고 popstate
라는 이벤트가 발생합니다, 이를 감지해 현재 주소를 다시 히스토리 스택에 추가해 실질적으로는 페이지 이동이 되지 않고, 뒤로가기 동작이 일어나지 않는 것 같은 효과를 낼 수 있습니다.
여기서 현재 주소를 히스토리 스택에 추가할 때, 원하는 커스텀 동작을 같이 실행해 뒤로가기 동작이 일어나면 모달이 뜨는 등의 기능을 구현할 수 있습니다.
그런데 단순히 뒤로가기 동작을 막고 모달을 띄우는 것이 아니라, 해당 모달에서 끝내기
버튼을 누르면 페이지가 뒤로가는 기능을 구현해야했습니다.
// 끝내기 버튼 눌렀을 때
const onExit = () => {
router.replace(ROUTER.MISSION.DETAIL(missionId));
};
// 취소 버튼 눌렀을 떄
const onBackMidModalClose = () => {
closeBackMidOutModal(); // 화면에서 모달 제거
onNextStep(prevStep); // 이전 진행상황으로 돌아감
};
// custom back hooks
function useCustomBack(customBack: () => void) {
const browserPreventEvent = (event: () => void) => {
history.pushState(null, '', location.href);
event();
};
useEffect(() => {
history.pushState(null, '', location.href);
window.addEventListener('popstate', () => {
browserPreventEvent(customBack);
});
return () => {
window.removeEventListener('popstate', () => {
browserPreventEvent(customBack);
});
};
}, []);
}
처음에 작성했던 코드는 위와 같았습니다.
문제가 없는 것 처럼 보이지만, 히스토리 스택이 잘못 쌓이는 문제가 존재했습니다.
이전이전 페이지를 A, 이전 페이지를 B, 현재 페이지를 C라고 가정한다면
A -> B -> C 페이지로 차례대로 접근 후,
C 페이지에서 뒤로가 B페이지로 가고, B 페이지에서 다시 뒤로가기를 한다면 A 페이지로 가는 것이 정상적인 동작입니다.
하지만 위의 코드에서는 A 페이지가 아닌 다시 C 페이지로 돌아가는 이상현상이 발생하였습니다.
의도한 동작
A -> B -> C -> B -> A
실제 발생한 동작
A -> B -> C -> B -> C
해당 문제는,
C페이지에서 B페이지로 뒤로가기를 통해 이동할 때 replace를 통해 히스토리 스택에서 C 페이지를 삭제하고 넘어갔다고 생각하였는데, 히스토리에서 없어지지 않아 발생한 문제였습니다.
기본적으로 hisory.replace (=== router.replace)를 이용하면, 이동을 하기 전에 쌓인 히스토리를 덮어 씌우기 때문에 C -> B로 이동할 떄 C의 히스토리가 사라지는게 맞습니다.
실제로도 replace를 통해 이동했기 때문에, 하나의 C 히스토리는 삭제되었습니다.
하지만, 뒤로가기 커스텀 동작을 통해 C의 히스토리 스택이 두개가 쌓여있는 상태였기 때문에 두개의 히스토리중 하나의 히스토리만 삭제되어 히스토리 스택에서 C가 사라지지 않는 것 처럼 보였던 것이였습니다.
어떻게 히스토리 스택이 쌓이고 없어지길래 이러한 상황이 연출될지 시뮬레이션 해본 결과, 이러한 결론이 나왔습니다.
밑의 전체 동작을 하는 동안 히스토리가 어떻게 쌓이는지 확인해보겠습니다.
전체 동작
A -> B -> C ->(뒤로가기) B ->(뒤로가기) A
History Stack : [A]
History Stack : [A, B]
History Stack : [A, B, C]
C 페이지에서 뒤로가기 커스텀 동작을 통해 추가적으로 새로운 히스토리가 쌓입니다.
관련 코드
useEffect(() => {
history.pushState(null, '', location.href);
}, []);
이로 인해 히스토리 스택에 C가 하나 더 추가됩니다.
History Stack : [A, B, C, C]
뒤로가기를 시도하면 popstate
이벤트가 발생하고, 히스토리 스택에서 하나가 빠집니다.
그리고, popstate에 걸어둔 event listener로 인해 browserPreventEvent
메소드가 실행되고 다시 C 페이지가 히스토리 스택에 추가됩니다.
그리고 browserPreventEvent
메소드에서 event
메소드가 실행되어 `
관련 코드
// custom back hooks
function useCustomBack(customBack: () => void) {
const browserPreventEvent = (event: () => void) => {
history.pushState(null, '', location.href);
event();
};
useEffect(() => {
history.pushState(null, '', location.href); // 관련 x
window.addEventListener('popstate', () => {
browserPreventEvent(customBack);
});
return () => {
window.removeEventListener('popstate', () => {
browserPreventEvent(customBack);
});
};
}, []);
}
History Stack : [A, B, C, C]
정말 끝내시겠습니다?
모달에서 끝내기 버튼 클릭 or 취소 버튼 클릭// 취소 버튼 눌렀을 떄
const onBackMidModalClose = () => {
closeBackMidOutModal(); // 화면에서 모달 제거
onNextStep(prevStep); // 이전 진행상황으로 돌아감
};
취소 버튼을 클릭하면 모달이 닫히고, 현재 페이지에 그대로 머물게 됩니다.
History Stack : [A, B, C, C]
// 끝내기 버튼 눌렀을 때
const onExit = () => {
router.replace(ROUTER.MISSION.DETAIL(missionId));
};
끝내기 버튼을 클릭하면 replace를 통해 B페이지로 돌아갑니다.
History Stack : [A, B, C, B]
이때 히스토리 스택에 위와 같이 C가 존재하게 됩니다.
이런 상황에서 B 페이지에서 뒤로가기를 하면 다시 C 페이지로 돌아가는 문제점이 생깁니다.
따라서, 이떄 히스토리 스택에 C 페이지가 없도록 처리해주어야 했습니다!
브라우저의 뒤로가기 동작 후, 바로 히스토리 스택에 제물 스택 (C)를 추가해주는 것이 문제였습니다.
위와같은 플로우로 히스토리 스택이 쌓이도록 수정해야 했습니다.
// 끝내기 버튼 눌렀을 때
const onExit = () => {
router.back();
};
// 취소 버튼 눌렀을 떄
const onBackMidModalClose = () => {
closeBackMidOutModal(); // 화면에서 모달 제거
history.pushState({}, '', location.href); // History Stack에 현재 페이지 추가
onNextStep(prevStep);
};
// custom back hooks
function useCustomBack(customBack: () => void) {
const browserPreventEvent = (event: () => void) => {
// history.pushState(null, '', location.href); // 제거, Modal에서 처리
event();
};
useEffect(() => {
history.pushState(null, '', location.href);
window.addEventListener('popstate', () => {
browserPreventEvent(customBack);
});
return () => {
window.removeEventListener('popstate', () => {
browserPreventEvent(customBack);
});
};
}, []);
}
따라서 코드를 아래와 같이 수정하여 해결할 수 있었습니다 :D