[React] 뒤로가기 방지 - window.onpopstate, history, createBrowserRoute, router.subscribe, historyAction

yeon·2024년 9월 26일
0

FE

목록 보기
14/15
post-thumbnail

개발 이유

사용자가 개인정보를 입력하고 전자설문을 진행하는 상태에서 도중에 홈페이지에 진입한 후, 홈페이지에서 뒤로가기를 할 경우 이전 전자설문 진행 페이지로 이동되었다.

A 환자가 병원 공용 태블릿으로 전자설문을 진행하다가 중간에 멈추고 홈페이지를 간 후, 태블릿을 B 환자가 이어받아 작성하려다 뒤로 가기를 누를 경우 A 환자의 응답 내용이 표시되는 문제가 예상되었다.

따라서 이를 막기 위해 홈페이지에서 뒤로가기를 눌러도 이전 설문 진행 페이지로 이동하지 않고, 홈페이지가 그대로 유지되도록 뒤로가기 방지 기능을 개발하게 되었다.

1️⃣ 첫 번째 시도: window.onpopstate 사용

window.onpopstate = () => {
      navigate('/');
};

위 코드를 홈페이지 컴포넌트에 적용했는데, 뒤로가기를 막긴 하지만 이전 페이지가 홈페이지가 아닌 다른 전자설문 페이지였음에도 뒤로가기를 누를 경우 무조건 홈페이지 경로(’/’)로 강제이동하게 되는 문제가 발생했다. 홈페이지 컴포넌트가 언마운트되더라도 window.onpopstate가 여전히 작동하여 뒤로가기가 일어날 때마다 무조건 홈페이지로 이동하게 되는 것이다.

전자설문 페이지 간 뒤로가기가 가능하면서 + 홈페이지에서만 뒤로가기가 적용되게 하기 위해서는 위 코드를 사용할 수 없었다.

2️⃣ 두 번째 시도: history 라이브러리 사용

React-router-dom v6 이전까지는 history로 이전 값을 확인할 수 있었는데, navigate로 history를 대체하기 때문에 v6부터 history가 사라졌다.

하지만 navigate는 뒤로가기 -1로 할 수 있을 뿐, 뒤로가기 자체를 감지하고 방지할 수는 없었다.

그래서 history 라이브러리를 설치해 이용했다.

import { createBrowserHistory } from 'history';

const history = createBrowserHistory();
  • createBrowserHistory 함수: 브라우저에서 사용할 수 있는 history 객체를 생성
  • history 객체: 브라우저의 히스토리 상태를 조작하는 데 사용
const navigate = useNavigate();

const blockBackEvent = () => {
	navigate(location);
};
  • navigate 함수: useNavigate 훅을 호출해서 얻음. 주어진 경로(location)로 이동시킬 때 사용
  • blockBackEvent 함수: 뒤로가기를 시도할 때 location으로 강제 리다이렉트
useEffect(() => {
    const historyEvent = history.listen(({ action }) => {
      if (action === 'POP') blockBackEvent();
    });

    return historyEvent;
  }, []);
  • history.listen() 메서드를 사용하여 히스토리의 변화를 감지
  • 브라우저에서 “뒤로가기”를 누르면 action‘POP’으로 설정됨
  • historyEvent는 history.listen()의 리턴값으로, 이벤트 리스너를 제거하기 위한 함수를 반환함. 이 함수는 useEffect가 언마운트될 때 실행되어 리스너를 정리한다.

완성된 뒤로 가기 방지 hook

import { useEffect } from 'react';
import { Pathname, useNavigate } from 'react-router-dom';
import { createBrowserHistory } from 'history';

const history = createBrowserHistory();

export default function useBackBlock(location: Location | Pathname) {
  const navigate = useNavigate();

  const blockBackEvent = () => {
    navigate(location);
  };

  useEffect(() => {
    const historyEvent = history.listen(({ action }) => {
      if (action === 'POP') blockBackEvent();
    });

    return historyEvent;
  }, []);
}

3️⃣ (최종) router.subscribe 사용

history.listen을 대체하는 router.subscribe가 업데이트되어 리팩토링을 진행했다.

createBrowserRouter

import { createBrowserRouter, RouterProvider } from 'react-router-dom';
 
const router = createBrowserRouter([
  {
    path: '/',
    element: <HomePage />,
  },
  {
    path: "about",
    element: <AboutPage />,
  },
  // 추가 라우트 정의
]);

function App() {
	return (
		<RouterProvider router={router} />
	);
}

createBrowserRouter는 React Router v6에서 새롭게 도입된 API로, 라우터 객체를 생성하는 데 사용된다. 라우터 구성을 객체 형태로 선언해서 만들 수 있게 해주며, 선언한 라우터 객체를 RouterProvider 컴포넌트의 속성에 전달해 라우팅을 설정한다.

createBrowserRouter를 사용하면 route에 대한 상세한 정의와 route에 대한 여러 가지 추가 설정을 할 수 있다. 나는 createBrowserRouter 라우터 객체의 내장 메서드를 사용해서 뒤로가기 방지를 구현하기 위해 사용했다.

Trouble Shooting

 import { createBrowserRouter } from 'react-router-dom';
 
 const router = createBrowserRouter([
  {
    path: '/',
    element: <HomePage />,
  },
]);
 
 router.subscribe((state) => {
      if (state.historyAction === NavigationType.Pop) {
          navigate('/');
      }
});

위와 같이 리팩토링했더니, 뒤로가기를 클릭할 때마다 이전 페이지가 뭐든 상관 없이 무조건 홈페이지로 이동하게 되는, window.onpopstate를 사용했을 때와 동일한 문제가 발생했다.

이는 router.subscribe가 컴포넌트의 특정 위치에 있는지와 상관 없이 전역적으로 동작하기 때문이다. router.subscribe의 콜백함수 내에 콘솔을 찍어봤는데 뒤로가기 이벤트가 발생하면 어떤 페이지 경로에 있든 관계 없이 항상 로그가 찍혔다.

react-router-dom에서 제공하는 createBrowserRouter가 전역적인 라우팅 상태를 관리하기 때문에 컴포넌트가 어디에 있든 라우터 상태에 대한 구독은 전역적으로 실행된다. 그래서 위 코드로는 뒤로가기를 누르면 항상 홈페이지로 navigate 되는 것..!

그래서 아래 코드와 같이 현재 경로가 홈페이지 경로일 때만 뒤로가기 방지가 일어나도록 조건을 추가했지만 또 문제가 생겼다.

 router.subscribe((state) => {
      if (state.historyAction === NavigationType.Pop) {
        if (window.location.pathname === '/') {
          navigate('/');
        }
      }
});

window.location.pathname으로 현재 경로를 확인했는데, 뒤로가기를 누른 후 보이는 페이지의 경로가 아니라, 이전 페이지(뒤로가기를 누를 경우 보이는 페이지)가 window.location.pathname가 되었다. 그래서 홈페이지 경로에 있는데도, window.location.pathname은 현재 경로를 이전 페이지 경로로 파악해서 뒤로가기를 막지 못하는 문제가 생겼다.

문제 해결

문제는 router.subscribe가 홈페이지 컴포넌트에만 존재하지만, 이와 상관 없이 어떤 경로나 컴포넌트에 있든 전역적으로 동작하는 것이었다. 그래서 홈페이지 컴포넌트가 언마운트될 때 클린업 함수를 실행하면 되지 않을까 해서 시도해봤더니 문제 해결..!

useEffect(() => {
    const unsubscribe = router.subscribe((state) => {
      if (state.historyAction === NavigationType.Pop) {
        navigate('/');
      }
    });

    // 컴포넌트가 언마운트될 때 해제
    return () => {
      unsubscribe();
    };
  }, []);

이제 홈페이지에서만 뒤로가기가 방지되고, 다른 페이지에서는 뒤로 가기가 정상적으로 실행된다. 😎

최종 뒤로가기 방지 hook

import { useEffect } from 'react';
import { NavigationType, Pathname, useNavigate } from 'react-router-dom';
import homePagerouter from 'routes/routes'; // createBrowserRouter

export default function useBackBlock(propsLocation: Location | Pathname) {
  const navigate = useNavigate();

  const blockBackEvent = () => {
    navigate(propsLocation);
  };

  useEffect(() => {
    const unsubscribe = homePagerouter.subscribe((state) => {
      if (state.historyAction === NavigationType.Pop) {
        blockBackEvent();
      }
    });

    // component unmount unsubscribe for non-homepage
    return () => {
      unsubscribe();
    };
  }, []);
}

느낀점

강제로 사용자의 행동을 막는다는 점에서 맞는 개발 방식인지는 잘 모르겠다. 사용성 개선을 한다면 “잘못된 접근입니다.”와 같은 경고창을 띄우는 것이 최선일 것 같은데, 다른 상용 중인 서비스에서는 뒤로가기 방지를 어떤 방법으로 구현했는지, 일반적인 방식은 어떤 것인지 찾아봐야겠다.

트러블슈팅을 하면서 문제가 발생하면 뭐든 기본기를 숙지한 상태라면 빠르게 해결하겠다는 생각이 들었다. 배웠던 내용이라도 개발할 때는 막상 생각나지 않는 경우가 많으니까 아는 내용이라도 잊지 않게 꾸준히 공부해야겠다. 이 기회에 컴포넌트 생명주기 다시 한 번 복습하기..!

참고 자료

How to controling browser back button with react router dom v6?

[Feature]: allow providing history object when calling createBrowserRouter() · Issue #9422 · remix-run/react-router

JavaScript 뒤로가기를 감지 및 방지해보자

0개의 댓글