[React] 뒤로가기 막기, 감지하기 (history / react-router-dom)
Code/React - Node

[React] 뒤로가기 막기, 감지하기 (history / react-router-dom)

반응형

Photo by 愚木混株 cdd20 on Unsplash

오늘은 모달 React 에서 react-router-dom v6 이상의 환경에서 뒤로가기를 감지하거나 막는 방법에 대해 서술하려고 한다.

자사 서비스는 모바일 웹 / 앱에 최적화 되어있어 모바일 유저의 UX를 중요하게 여기고 있다.

문제는 웹의 경우에는 뒤로가기를 진행하면 페이지 자체가 이동되는 게 자연스럽다고 생각했으나, 모바일 기기 특히 안드로이드에서는 뒤로가기를 시도했을 때 기대하는 바가 다르다는 것이었다. 안드로이드는 페이지 이동이 아닌 레이어 정도의 뒤로가기 느낌이었다.

이전까지는 뒤로가기에 대한 핸들링은 페이지 단으로만 처리를 하고 있었는데 (커버하지 않았다) 가장 핵심 기능인 일정표 페이지에서 뒤로가기 기능을 통해 모달이 켜지거나 꺼지고, 모드가 바뀌는 등의 기능이 추가되었다.

react-router-dom v6 이전 버전에서는 useHistory 훅을 통해 history의 변화를 감지할 수 있었는데, v6로 변경되며 useHistory가 useNavigate 로 대체 되었다. 다만 문제는 기존의 useHistory의 기능이 모두 useNavigate로 흡수된 것이 아니었기 때문에 뒤로가기를 감지할 방법이 마땅치 않았다.

이를 해결하기 위해 여러가지 방법을 찾아 보았고, 적용한 결과물이 영 맘에 들지않아 이런 저런 구조도 만져보고.. 더 좋은 방법은 없을까 고민도 했었는데(기능이 점점 커지며 기능파트에서 제일 구조적으로 고민인 부분) 구조적인 고민은 우선 뒤로하고 방법에 대해 포스팅하겠다.

우선 유저의 방문 기록을 볼 수 있다면 좋을 것 같다는 생각이 들어 찾아보니 개인정보이기 때문에 웹 API로도 지원하지 않는 기능이었다.

그리고 찾은 방법

 

history.listen 리스너 활용

src/util/history.ts

import { createBrowserHistory } from 'history';

export const history = createBrowserHistory();

history 의 createBrowserHistory 를 통해 history객체를 생성한다.

 

Example.tsx

import {history} from '../../util/history';
import {useLocation} from 'react-router-dom';

const ExamplePage = () => {
   const {pathname} = useLocation()

   useEffect(() => {
       const unlistenHistoryEvent = history.listen(({action}) => {
          if (action !== 'POP') return;
          history.push(pathname);
       });
       return unlistenHistoryEvent;
    }, [])

  return <div>{...}</div>
}

history.listen 은 history에 변화가 생기면 작동하는 리스너이다.


history 액션

  • push (히스토리 스택에 페이지가 추가된 경우, 페이지 이동)
  • pop (히스토리 스택에 페이지가 제거된 경우, 뒤로가기)

간단 예제, 위 코드를 설명하자면,

ExamplePage 에서 history 액션이 발생하면 리스너가 작동된다.

1. pop 인 경우 (뒤로 가기를 시도한 경우) history.push 로 현재 페이지를 히스토리 스택에 추가한다. 
2. 뒤로가기가 작동 되었으나 push 를 통해 현재 페이지에 머무르게 된다.
3. 뒤로갈 수가 없다.

간단히 말하자면 뒤로감기가 감지될 때 뒤로가지만 말고 history에 현재 페이지를 다시 추가해서 눈속임을 하는 것이다.
이 플로우만 이해한다면 다양한 조건의 뒤로가기 핸들링도 구현할 수 있다.

 

해당 작업이 가장 많이 진행된 곳은 서비스의 가장 핵심이라고 할 수 있는 일정표 페이지이다.
일정표 관련 기능(수정/삭제/교체/추가/이동/열람/공유) 등은 모두 한 개의 페이지에서 작동된다.
드로워 내부에 존재하는 일정표 열람 / 편집 모드, poi (장소)의 상세 컨텐츠 모달, 이미지 모달, 드로워의 오픈 여부 등과 같은 경우에 대해 뒤로가기 로직을 반영해야 했다. 


반응형


좌 상태 -> 뒤로가기 -> 우 상태

드로워 오픈 -> 닫힘
편집 모드 -> 열람 모드
장소 이미지 상세 열기 -> 이미지 모달 닫힘
장소 상세 페이지 열기 -> 장소 상세 닫힘
..이외 다수


기능상 이런 조건들이 동시에 겹겹이 쌓여있을 수 있기 때문에 각 기능들 중에서도 우선순위를 정해 리스너가 실행되었을 때 논리적 흐름과 맞는 우선순위대로 처리해줘야 했다.

구현 결과물

동영상을 기준으로 진입과 이탈 플로우를 설명하자면 아래와 같다.

진입 플로우

: 일정표 진입 (열람 모드) -> 편집모드 진입 -> 장소 검색 페이지 진입 -> 장소 상세 페이지 진입 -> 드로워 펼치기 -> 이미지 자세히 보기 열림

위 상태에서 뒤로가기를 진행할 경우, 이탈 플로우

: 이미지 자세히 보기 닫힘 -> 장소 상세 페이지 닫힘 -> 드로워 반으로 닫힘 -> 장소 검색 페이지 닫힘 -> 편집 모드 이탈 -> 일정표 (열람 모드) -> 실제 페이지 이동으로 뒤로가기 

 

코드로만 보면 그렇게 어렵지 않을 수 있는데 전역에서 사용하는 컴포넌트들이 재사용되기도 하고, 페이지 별로 뒤로가기 액션의 요구사항이 달랐기때문에 컴포넌트마다 history 리스너를 구현하기도 페이지마다 리스너를 구현하기도 조금씩 다 애매한 부분이 있어서 구조에 대한 고민이 제일 많이 드는 파트였다. 

물론 아직도 뒤로가기 기능을 작업할 때마다 '이게 최선일까..'하는 생각을 항상 갖고 있다.
왜 업무를 하다 보면 늘 마음 속에 간직하고 있는 투두나 백로그가 있을테다, 요즘 항상 간직하고 있는 건 바로 이 뒤로가기 관련 컴포넌트들의 구조에 대한 고민이다.

웹앱이지만, 서비스가 앱의 동작 방식을 지향하고 있는데 특히 이 부분은 정보가 잘 없는지 적절한 레퍼런스를 찾지 못한 상태라 다른 웹앱 서비스에서는 이런 복잡한 뒤로가기를 어떻게 관리하고 있는지 너무 궁금하다.

아마 레퍼런스 찾기 힘든 이유가 웹(모바일x)에서는 뒤로가기를 했을 때 페이지 이동이 되는 것이 자연스럽기 때문에 굳이 이렇게 웹앱 친화적으로 구현하는 경우는 없기 때문이 아닐까.. 하는 생각이 든다.

구조상 컴포넌트 별로 처리를 해주기도 해야하고..페이지에서도 처리해야 하고 이렇다 보니 페이지 이동을 해야 하는데 나가지 못하고 갇히질 않나 조건을 잘못 쓰면 엉뚱하게 작동하기도 하고 참 여러가지로 애먹었던 부분이다.

참 복잡하고 머리 아팠지만 이때 뒤로가기 이슈를 원천 차단하겠다며 머리를 쥐어짜내고 고통을 느낀 덕에 히스토리 관리에 더 자신이 생길 수 있었다. 아주 유의미한 싸움이었다.

네비게이션 이제 안 무섭다!!!!!

반응형