프로젝트를 진행하는 중에 모든 새로운 화면들을 모달 컴포넌트를 이용해서 진행중이었다. 그런데 어떤 버튼을 눌렀을 때 모달 컴포넌트가 화면에 마운트 되면서 보여지게 되는데 이 때는 당연히 React Router를 이용해서 주소를 옮기거나 하지 않았다. 그러다보니 브라우저 세션에 저장되는 history
에 어떤 추가가 없기 때문에 모달 창을 열어놓은 상태에서 뒤로가기를 누르면 모달 창의 상위 컴포넌트인 mainPage가 아닌 최초의 로그인 화면으로 가버리는 문제가 있었다. 그래서 모달창을 열어놓은 상태에서 브라우저의 뒤로가기 버튼을 눌렀을 때 모달창만 닫히도록 개선하고 싶었다.
이를 위해서 history 에 대해서 알아야 했으며 이를 활용해서 뒤로가기를 눌렀을 때를 제어할 수 있었다.
브라우저를 자주 사용하다보니 뒤로가기
, 앞으로가기
버튼에 대해서는 익히 잘 알고 있을 것이다. 이전의 url로 다시 이동하고 다시 앞으로 가고 하는 역할을 한다. 그런데 이를 javascript로 제어할 수 있는데 그 때 알아야 할 것이 history API 이다. 공식문서를 통해서 간단한 사용방법을 알 수 있다.
공식문서를 보면 history.pushState()
를 사용하는데 이를 활용해서 문제를 해결할 수 있다. 사용방법은 공식문서를 참고하자.
일단 기본적으로 MainPage.tsx
라는 컴포넌트가 상위 컴포넌트로 있고 여기서 버튼을 누르면 Modal.tsx
컴포넌트가 보여지면서 모달창을 구현했다. 여기서 중요한 것이 url이 어떤 식으로도 변경되지 않았다는 것이다. 그렇기 때문에 뒤로가기를 눌렀을 때 이전의 url인 로그인페이지로 이동하는 것이다.
그래서 생각해낸 로직은 다음과 같다.
1번과 2번을 코드로 어떻게 구현했는지를 보자.
우선 1번
을 구현하기 위해서 컴포넌트의 useEffect
훅을 사용하면 된다. 컴포넌트가 마운트될 때(실행될 때) 실행되는 함수인데 이 안에서 모달창이 켜질때마다 새로운 history 상태를 push 해주는 것이다.
history.pushState({page:"modal"}, document.title);
를 useEffect
안에 추가하면 모달창이 켜질때마다 history에 새로운 상태가 저장이 될 것이다.
그리고 뒤로가기 버튼을 눌렀을 때를 제어하려면 뒤로가기버튼을 눌렀을 때에 대한 이벤트 리스너의 등록이 필요하다. 이 또한 useEffect
훅 안에 구현해서 모달이 활성화돼있을 때만 해당 함수가 실행되도록 했다.
window.addEventListener("popstate", goBack);
이렇게 뒤로가기를 눌렀을 때 popstate
이벤트가 발생하는데 이 때 내가 작성한 goBack
함수가 실행되도록 이벤트리스너를 등록한다.
그렇다면 goBack()
함수를 한 번 봐보자.
const goBack = () => {
closer();
};
단순히 모달을 닫아주는 closer()
함수를 호출한다. 아래 전체 코드를 한 번 봐보자.
//복잡한 interface는 무시하고 그냥 Modal 컴포넌트라고 생각하자
const Modal: FC<modalPros> = ({ content, stateSetter }): JSX.Element => {
...
const goBack = () => {
closer();
};
//모달창에 있는 X 버튼을 누르면 실행되는 모달창을 닫는 함수
const closer = () => {
setTimeout(() => { stateSetter(false); }, 700);
hideAnimatedModal();
}
...
useEffect(() => {
//모달이 켜질때마다 새로운 state를 추가한다.
history.pushState({page:"modal"}, document.title);
showAnimatedModal();
document.addEventListener("keyup", detectESC);
//뒤로가기 버튼을 눌렀을 때 실행하는 이벤트리스너 등록
window.addEventListener("popstate", goBack);
//cleanup 함수로 컴포넌트가 사라질 때 모든 리스너를 제거해준다.
return () => {
document.removeEventListener("keyup", detectESC);
window.removeEventListener("popstate", goBack);
};
}, []);
...
}
그런데 이렇게 했을 경우 문제점이 생긴다.
위 코드는 모달창이 활성화돼있을 때 뒤로가기를 누르면 모달창 closer()
함수가 실행되면서 모달창만 닫히고 mainpage
로 이동하고 또 그냥 모달창을 띄운 상태에서 x버튼을 누르거나 esc키를 눌러도 mainpage
로 이동해서 잘 작동하는 것처럼 보인다.
하지만 잘 분석해보면 모달 창이 켜질때마다 pushState()
함수로 history
에 자꾸 새로운 상태를 추가한다. 그런데 이 상태에서 뒤로가기버튼을 눌렀을 때는 popstate
이벤트가 발생하면서 추가된 새로운 상태가 pop돼서 문제가 없지만 문제는 모달창을 뒤로가기가 아닌 다른 식으로 종료했을 때(esc키를 누르거나 X버튼을 누른 경우) 이 때는 popstate 이벤트가 발생하지 않기 때문에 history에 새로 추가한 state가 그대로 남아있게 된다.
그래서 모달창을 켰다가 뒤로가기버튼이 아닌 X버튼을 눌러서 끄는 식으로 3번 반복을 하면 mainpage
에서 뒤로가기버튼을 3번 눌러야 login
페이지로 이동하는 버그가 발생한다.
그래서 이를 해결하기 위해서는 X버튼을 누르거나 ESC 키를 눌렀을 때도 popstate 이벤트가 발생하도록 해야한다.
다음과 같은 코드를 추가해서 해결해보자.
우선, 뒤로가기버튼을 눌렀는지 안 눌렀는지 상태를 체크할 수 있는 boolean 변수가 하나 필요하다.
let isGoBackClicked = false
코드 한 줄을 Modal 컴포넌트 최상위에 우선 false로 선언한다.
그리고 goBack()
함수에 플래그를 바꿔주는 코드를 삽입한다.
const goBack = () => {
isGoBackClicked = true;
closer();
};
goBack()
함수는 popstate 이벤트가 발생했을 때만 실행되는 이벤트리스너에 등록된 콜백함수이므로 이 함수 안에서 isGoBackClicked
변수를 true로 바꿔주는 것이다. 그리고 이제 마지막으로 해야 할 일은 뒤로가기버튼을 누르지 않고 끄는 경우에도 popstate 이벤트를 발생시켜야 하는 것이다.
그래서 마지막으로 useEffect
함수의 cleanup 함수 부분에 조건을 걸어서 popstate 이벤트를 발생시키는 코드를 추가해야 한다.
useEffect(() => {
...
//뒤로가기를 누르지 않고 종료할 경우엔 state가 새로 있으니
//뒤로가기를 해줘서 popstate 이벤트를 발생시켜줘야 함
if (!isGoBackClicked) {
history.back();
}
...
}, []);
이렇게 해서 완성된 코드는 다음과 같다.
const Modal: FC<modalPros> = ({ content, stateSetter }): JSX.Element => {
//뒤로가기 버튼을 눌렀는지 여부를 저장
let isGoBackClicked = false;
...
/*!
* @author donglee
* @brief 모달 컴포넌트를 종료할 때 실행하는 함수.
*/
const closer = () => {
setTimeout(() => { stateSetter(false); }, 700);
hideAnimatedModal();
}
const goBack = () => {
isGoBackClicked = true;
closer();
};
useEffect(() => {
//모달이 켜질때마다 새로운 state를 history에 추가
history.pushState({page:"modal"}, document.title);
showAnimatedModal();
document.addEventListener("keyup", detectESC);
//뒤로가기 눌렀을 때 goBack 함수 실행
window.addEventListener("popstate", goBack);
return () => {
//이벤트 리스너들 모달창 꺼질 때 제거해주기
document.removeEventListener("keyup", detectESC);
window.removeEventListener("popstate", goBack);
//뒤로가기를 누르지 않고 종료할 경우엔 state가 새로 있으니 뒤로가기를 해줘서 popstate 이벤트를 발생시켜야 함
if (!isGoBackClicked) {
history.back();
}
};
}, []);
...
};
비슷한 고민을 하고 있었는데 도움이 많이 되었습니다!