useHistoryState, 뒤로가기/새로고침 페이지 상태 유지

웹브라우저의 기본 경험을 React SPA에서 구현할 때 사용하는 간단한 훅입니다.

import { useState, useCallback } from 'react';
import { useHistory } from 'react-router-dom';

const useHistoryState = (initialState, key) => {
  const history = useHistory();
  const stateValue = history.location.state?.[key];

  const [historyState, setHistoryState] = useState(
    stateValue === undefined ? initialState : stateValue,
  );

  const setState = useCallback(
    (state, replace = false) => {
      const value = state instanceof Function ? state(historyState) : state;

      setHistoryState(() => value);
      history.replace({
        state: replace ? value : { ...history.location.state, [key]: value },
      });
    },
    [history, historyState, key],
  );

  return [historyState, setState];
};

export default useHistoryState;

아래는 Next.js에서의 useHistoryState.js 코드입니다.
Next.js에서는 SSR과 CSR의 렌더링 차이에 대한 우려로 Router에 state를 추가할 수 있는 기능을 제공하지 않는데요. 브라우저 기본 경험처럼 구현해야 한다는 점이 우선이라 판단해서 아래처럼 꼼수 적용하는 방법을 추가했습니다.

import { useState, useCallback } from 'react';

const historyStorage = (history => {
  history.replaceState = (replaceState => (state = {}, title, url) =>
    replaceState.call(history, { ...history.state, ...state }, title, url))(
    history.replaceState,
  );

  const get = key => history.state?.page?.[key];
  const set = (key, value, replace = false) => {
    history.replaceState({
      page: replace
        ? { [key]: value }
        : { ...history.state?.page, [key]: value },
    });
  };

  return { set, get };
})(typeof window !== 'undefined' ? window.history : {});

const useHistoryState = (initialState, key) => {
  const stateValue = historyStorage.get(key);

  const [historyState, setHistoryState] = useState(
    stateValue === undefined ? initialState : stateValue,
  );

  const setState = useCallback(
    (state, replace = false) => {
      const value = state instanceof Function ? state(historyState) : state;

      setHistoryState(() => value);
      historyStorage.set(key, value, replace);
    },
    [historyState, key],
  );

  return [historyState, setState];
};

export default useHistoryState;

아래처럼 setState와 유사하게 사용합니다.

// useHistoryState(기본값, 키이름)
const [sort, setSort] = useHistoryState('desc', 'sort');
const [showPanel, setShowPanel] = useHistoryState(false, 'showPanel');

// or
const [pageForm, setPageForm] = useHistoryState({
  title: '',
  content: '',
  tags: [],
}, 'pageForm');


return (
  <>
    <select value={sort} onChange={({ target }) => setSort(() => target.value)}>
      <option value="desc">내림차순</option>
      <option value="acs">오름차순</option>
    </select>
    
    <label>
      <input
        type="checkbox"
        checked={showPanel}
        onChange={({ target }) => setShowPanel(() => target.checked)}
      />
      패널 표시
    </label>
  </>
);

이렇게 뒤로가기/앞으로가기, 새로고침 시 input에 입력한 값을 유지시켜서 브라우저 기본 동작처럼 자연스럽게 동작하도록 구현할 때 사용합니다. 그리고 SWR과 조합하여 뒤로가기 시 캐시를 활용하고 API 요청을 하지 않도록 처리합니다.