[React]모달(Modal) 컴포넌트 마운트 된 상태에서 브라우저 뒤로가기 눌렀을 때 모달 컴포넌트만 언마운트 시키기 구현

이동현·2021년 7월 14일
9

React

목록 보기
11/16
post-thumbnail

상황

프로젝트를 진행하는 중에 모든 새로운 화면들을 모달 컴포넌트를 이용해서 진행중이었다. 그런데 어떤 버튼을 눌렀을 때 모달 컴포넌트가 화면에 마운트 되면서 보여지게 되는데 이 때는 당연히 React Router를 이용해서 주소를 옮기거나 하지 않았다. 그러다보니 브라우저 세션에 저장되는 history에 어떤 추가가 없기 때문에 모달 창을 열어놓은 상태에서 뒤로가기를 누르면 모달 창의 상위 컴포넌트인 mainPage가 아닌 최초의 로그인 화면으로 가버리는 문제가 있었다. 그래서 모달창을 열어놓은 상태에서 브라우저의 뒤로가기 버튼을 눌렀을 때 모달창만 닫히도록 개선하고 싶었다.

history

이를 위해서 history 에 대해서 알아야 했으며 이를 활용해서 뒤로가기를 눌렀을 때를 제어할 수 있었다.

브라우저를 자주 사용하다보니 뒤로가기, 앞으로가기 버튼에 대해서는 익히 잘 알고 있을 것이다. 이전의 url로 다시 이동하고 다시 앞으로 가고 하는 역할을 한다. 그런데 이를 javascript로 제어할 수 있는데 그 때 알아야 할 것이 history API 이다. 공식문서를 통해서 간단한 사용방법을 알 수 있다.

공식문서를 보면 history.pushState() 를 사용하는데 이를 활용해서 문제를 해결할 수 있다. 사용방법은 공식문서를 참고하자.

해결방법

일단 기본적으로 MainPage.tsx 라는 컴포넌트가 상위 컴포넌트로 있고 여기서 버튼을 누르면 Modal.tsx 컴포넌트가 보여지면서 모달창을 구현했다. 여기서 중요한 것이 url이 어떤 식으로도 변경되지 않았다는 것이다. 그렇기 때문에 뒤로가기를 눌렀을 때 이전의 url인 로그인페이지로 이동하는 것이다.

그래서 생각해낸 로직은 다음과 같다.

  1. 모달 창을 열었을 때 history에 새로운 상태를 push 해줌으로써 현재는 모달이라는 상태에 있다는 것을 브라우저에게 알려준다.
  2. 이렇게 하면 브라우저의 뒤로가기를 눌렀을 때 새로운 모달이라는 상태가 history에 저장됐기 때문에 이전의 상태인 mainpage로 이동할 것이다. 그러면 모달창이 활성화돼있는 상태에서 뒤로가기를 누르면 로그인페이지가 아니라 원래의 메인페이지, 즉 모달창만 닫히는 효과를 볼 수 있다.

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();
      }
    };
  }, []);
  ...
};
출처:
profile
Dom Hardy : 멋쟁이 개발자 되기 인생 프로젝트 진행중

2개의 댓글

comment-user-thumbnail
2022년 9월 17일

비슷한 고민을 하고 있었는데 도움이 많이 되었습니다!

1개의 답글