React

[React] 뒤로가기 시 상태 유지하기 ( useNavigationType, popstate )

설탕시럽 2024. 11. 15. 18:15

TLDR

이번에는 Tab Navigation을 서비스를 개발하며 개선한 부분에 대해서 얘기해보려고 합니다. 그저 "탭의 상태" 에 따른 표시되는 UI가 변경되는 정도의 컴포넌트로 인식하고 있었지만, 사용자측면을 고려하고 베타테스트를 진행하면서 저 "상태"를 자연스럽게 관리하는 것에 대해 고민하고 개선한 내용과 그 로직에 대해 공유하려고 합니다.

 

초기 로직 : UI와 그 기능에 집중

처음 개발한 Tab Navigation은 UI와 Highligt Animation 등에 더 신경을 많이 썼습니다. 다른 페이지에서도 활용 될 수 있게끔, 탭의 갯수와 상관없이 UI를 적용할 수 있게끔 고민했고, 탭의 상태는 크게 신경쓰지 않았습니다.

컴포넌트의 Props를 통해 tab의 정보를 배열로 받았으며, tab navigation에서 선택된 탭의 상태를 useState를 통해 관리 했으며, Props를 통해 공유했습니다.

 

문제 1 : 활성화 된 탭의 상태가 계속해서 초기화 된다

useState로 활성화 된 탭을 관리하니 당연한 일이지만, 다른 페이지로 이동하거나 해당페이지 내 상세페이지로 들어갔다가 나올 때면 해당 탭이 초기화 되어있어서 사용자 불편함을 유발했습니다. 웹에서도 불편하겠지만, 웹뷰를 통해 앱으로 서비스를 지원하는 경우 그 불편함과 어색함이 더 부각되어 빠르게 개선이 필요했습니다.

그래서 URL params에 활성화된 tab을 기록하여 뒤로가기 stack에 남게끔 수정했습니다. URL params에 남아있다면 다른 어딘가로 이동했다가 와도 그 기록을 통해 기존에 활성화 되어 있던 탭으로 돌아 갈 수 있게끔 하여, 사용자의 불편함을 해결하였습니다.

 

import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';

function TabNavigation() {
	const [ query, setQuery ] = useSearchParams();
    
    useEffect(() => {
    	query.set('tab', tab상태);
        setQuery(query);
    }, [tab 상태])
    
    ...
}

 

문제 2 : 뒤로가기 stack이 너무 많이 쌓임

위 방법을 통해 해결한건 사용자 입장에서 다른 불편함을 유발했습니다. 탭이 변경될 때 마다 뒤로가기 stack이 쌓이게 된 것 입니다. 뒤로가기를 눌러도 이전 탭, 화면으로 돌아가는게 아닌 tab navigation 화면그대로 URL 만 되돌아 가는 현상이 발생했고 URL이 보이지 않는 앱 입장에서는 더욱 치명적으로 다가왔습니다.

위 사진은 쌓여있는 뒤로가기 stack의 예시입니다. 사용자 입장에서 tab Navigation을 다룬후 pageC로 이동했다가 다시 뒤로가기를 통해 pageA로 가려면? pageB?tab=3 -> tab=2 -> tab=1 -> pageB 라는 과정을 통해 pageB에 뒤로가기 4번 시도하는 동안 갖혀있게 됩니다.

 

문제 재 정의

URL params를 활용하는 방식의 한계를 알았으니 다른 방식으로의 수정이 필요했습니다. 기존 useState방식은 상태가 유지되지 못하니, 전역상태와 같은 방법을 고민하기 이전에 해결해야하는 문제에 대해 구체적으로 정의가 필요했습니다.

 

Tab Navigation 의 상태저장 예시

  1. tab navigation page로 정상적으로 이동했다면? "탭의 상태가 초기화" 되야 한다.
  2. 다른 페이지 / 심지어 외부페이지 이동했다가 "뒤로가기"를 해서 페이지로 돌아온다면 "탭의 상태 유지"가 되야한다
  3. 새로고침 시, 탭 유지

 

LocalStorge

서비스 내부에는 아예 외부 다른 페이지로 이동하는 링크가 있습니다. 이런 부분까지 고려한다면, 상태 관리는 localStorage에서 하는 게 맞다고 판단했습니다.

 

뒤로가기 상태 구분하기

tab navigation이 있는 페이지에서 위 문제를 해결하기 위해서는? 페이지를 접근할때 뒤로가기로 접근했는지, 아니면 페이지로 새롭게 접근했는지를 구분할 수 있어야 합니다.

  • window.addEventListener('popstate') : 뒤로가기 할 때 실행
  • useNavigationType(): react-router-dom 내 hooks로 어떤 동작을 통해 페이지로 접근했는지 확인

해당 페이지에 어떤 동작으로 접근했는지 확인하기 위해 window.history.state를 사용하려고 한적이 있으나, React SPA 때문인지 예상대로 동작하지 않았습니다. performance.navigation.type 도 마찬가지로 원하는데로 분류할 수 없어서 위 두가지 방법을 엮어서 뒤로가기 상태를 구분했습니다.

 

코드

import React, { useState, useEffect } from 'react';
import { useLocation, useNavigationType } from 'react-router-dom';

const TabContent = ({ tab }) => (
  <div>
    <h2>{tab} 페이지 내용</h2>
  </div>
);

const TabNavigation = () => {
  const isValidTab = (tab) => ['tab1', 'tab2', 'tab3'].includes(tab);

  // 현재 네비게이션 타입과 위치를 가져옵니다.
  const navigationType = useNavigationType();
  const location = useLocation();

  const initialTab =
    navigationType === 'POP'
      ? localStorage.getItem('activeTab') || 'tab1' // 뒤로가기인 경우 저장된 탭 유지
      : 'tab1'; // 새로고침 또는 직접 접근 시 기본값 설정

  const [activeTab, setActiveTab] = useState(
    isValidTab(initialTab) ? initialTab : 'tab1'
  );
  
   useEffect(() => {
    if (navigationType !== 'POP') {
      setActiveTab('tab1');
      localStorage.removeItem('activeTab');
    }
  }, []);

  useEffect(() => {
    const restoredTab = localStorage.getItem('activeTab');
    if (navigationType === 'POP' && isValidTab(restoredTab)) {
      setActiveTab(restoredTab);
    }

    const handleTabChange = (tab) => {
      setActiveTab(tab);
      localStorage.setItem('activeTab', tab);
    };

    return () => {
      handleTabChange(activeTab); // 컴포넌트 언마운트 시 현재 탭 저장
    };
  }, [location, navigationType]);

  const handleTabClick = (tab) => {
    setActiveTab(tab);
    localStorage.setItem('activeTab', tab); // 탭 상태를 localStorage에 저장
  };

  return (
    <div>
      <div>
        <button
          onClick={() => handleTabClick('tab1')}
          className={activeTab === 'tab1' ? 'active' : ''}
        >
          탭 1
        </button>
        <button
          onClick={() => handleTabClick('tab2')}
          className={activeTab === 'tab2' ? 'active' : ''}
        >
          탭 2
        </button>
        <button
          onClick={() => handleTabClick('tab3')}
          className={activeTab === 'tab3' ? 'active' : ''}
        >
          탭 3
        </button>
      </div>
      <div>
        <TabContent tab={activeTab} />
      </div>
    </div>
  );
};

const App = () => (
  <div>
    <h1>탭 네비게이션 예제</h1>
    <TabNavigation />
  </div>
);

export default App;

 

마무리

지금까지 TabNavigation 의 상태유지를 위해 개선한 내용에 대해 소개해 드렸습니다. 이번 문제를 해결하기 위해 React에서 페이지 상태를 분류할 수 있는 다양한 이벤트타입과 hooks, 로직 등에 대해서 새롭게 알게된 부분이 많았던 것 같습니다.