안녕하세요!
너무 오랜만에 글을 쓰는 것 같지만.. 이직하고 회사에 적응하느라 좀 시간이 필요했네요!
오늘은 useHistory라는 커스텀 훅을 생성하면서 생성한 배경, 생긴 이슈, 처리한 이슈, 라이브러리 배포까지 정리해서 공유해보려고 합니다. 대단한 훅은 아니지만 이해했다 정도? 로 봐주시면 감사드리겠습니다.
useHistory의 배경
아래와 같은 예시 로직 코드가 있었다고 가정해 봅시다!
const TestCode = () => { const [isSelect, setIsSelect] = useState(false); const { data, isLoading } = useQuery( 'textApi', async () => { const { data: resData } = await dataFetch.get() if (resData.length > 10) { setIsSelect(false) } else { setIsSelect(true) } return resData; } ); return <></> }
이런식으로 받아와야하는 데이터의 길이가 10이 넘는다면 isSelect는 false, 10이 되지 않는다면 true의 값을 넣어주고 있습니다. query 특성상 mount 때마다 로직이 돌아가게 되죠!
그 뿐 아니라 useState도 똑같이 mount 시점에 초기값으로 변경되게 될 것입니다. 저 로직에 아래와 같은 clickEvent를 추가했다고 가정해 봅시다.
return ( <section> <button onClick={() => setIsSelect(true)}>SELECT</button> </section> )
우리는 분명 button을 통해서 상태값을 true로 변경해주었습니다. 하지만 이전화면으로 갔다가 다시 돌아온다면 상태값은 true로 남아있지 않습니다. 우리는 이미 true를 했고, 실수로든 아니면 의도적으로든 뒤로갔다 돌아왔을때 상태값을 기억하고 싶을 수 있습니다.
이러한 케이스를 해결하기위해서 useHistoryState를 생성하게 되었습니다. 그렇다면 왜 그 많은 state management 방식중에 history를 사용했느냐?
생각했던 state의 저장방식은 사실 두가지 정도가 있었습니다.
1. Query Params 사용
왜 Query Params를 사용하려 하였느냐? 막연히 뒤로가기 시 state는 매번 초기화 되지만 query는 그대로 남아있기 때문에 값만 체크해서 query 에 true, false를 넣어주면 어떨까? 라는 생각을 하였습니다.
하지만 이 생각은 오래가지 못했습니다. 해당 내용을 생각한 계기가 검색페이지에서 주로 사용했었던 내용으로 검색했던 기록을 보관, 검색 query를 넣고 공유할때 유리했기 때문입니다. 하지만 해당 경우는 다릅니다. query에 select=true 를 넣고 이동하게 되면 나는 누르지 않았지만 버튼은 눌린것으로 공유될 것입니다. 과연 이게 사용성에 유리한 방식일까요? 그렇지 않다 생각했습니다.
2. Global State 사용
그렇다면 Global State는 어떨까요? isSelect의 여부를 Global State에 넣고 컴포넌트를 불러올때에도 들어가있는 데이터를 불러오기 때문에 한번 Select한 경우에 isSelect는 true의 값을 갖고있을 것입니다.
하지만 이 값은 뒤로가기, 앞으로 가기 동작에서만 그렇게 반응하지 않을 것입니다. 한번 바뀐 Global State는 어디서 부르든, 뒤로가기, 앞으로 가기 동작이 아닌 다른 동작에서도 똑같은 값을 유지할 것입니다. 이것도 사용성에도 좋은 것이라 할 수 없죠.
그렇다면 분기처리를 해서 isSelect를 다시 false로 초기화 하면 되는거 아니야? 라고 할 수도 있습니다. 하지만 코드 한줄을 더 쓰는것보단 빼는걸 추구하는 타입이라 그렇게 하고싶진 않았습니다..
그렇기 때문에 마지막으로 고려했던 내용이 Web API history를 활용해서 history state를 관리하는건 어떨까? 라는 생각을 하게 되었습니다.
History Web API
history Web API[공식문서]에서는 이렇게 설명하고 있습니다.
History
인터페이스는 브라우저의 세션 기록, 즉 현재 페이지를 불러온 탭 또는 프레임의 방문 기록을 조작할 수 있는 방법을 제공합니다.
우리가 딱 원하는 그림입니다. 우리는 현재 페이지의 기록을 조작하고 싶은 것입니다. 이 페이지에 state를 변경한다면 이전, 다음 페이지 동작시 우리가 원하는 state를 얻을 수 있습니다.
replaceState()를 활용해서 현재 페이지 state에 값을 넣어두는 구조로 구현을 생각하고 있습니다. 또한 추가로 고려해야하는게 지금 사용하는 프로젝트가 SSR형식이라 무턱대로 history 를 부른다고 해서 되는 구조가 아닐뿐더러 에러만 나게 될 것입니다. window 가 없기때문이죠!
useHistoryState 구현하기
useHistoryState
훅 탐구: 브라우저 히스토리를 이용한 상태 관리
이 커스텀 훅은 페이지 새로고침 후에도 애플리케이션의 상태를 유지하고 싶을 때, 서버 사이드 저장소나 쿠키를 사용하지 않고도 유용합니다. 아래에서는 이 훅이 어떻게 작동하는지와 어떤 기술을 사용하는지에 대해 자세히 살펴보겠습니다.
파트 1: 문제 정의
React 애플리케이션에서 상태를 다룰 때, 페이지를 새로고침하거나 다른 페이지로 이동한 후에도 그 상태를 유지하는 것이 종종 중요합니다.
useHistoryState
는 브라우저의 히스토리 객체에 상태 데이터를 저장함으로써 이 문제를 해결하려고 합니다.파트 2: 의존성 및 초기 설정
훅은 React에서 다음을 가져옵니다:
import { useState, useCallback, useEffect } from 'react';
useState
: 로컬 컴포넌트 상태를 관리합니다.
useCallback
: 콜백 함수를 메모이제이션합니다.
useEffect
: 사이드 이펙트를 처리합니다.
파트 3: 인터페이스와 타입
코드는 먼저 히스토리 객체의 상태에 데이터를 저장하고 검색하는
StorageProps
인터페이스와 훅의 초기 상태와 키를 설정하기 위한 StateProps
인터페이스를 정의합니다.interface StorageProps<T> { key: string; value: T; replace: boolean; } interface StateProps<T> { initialState: T; key: string; }
파트 4: 환경 체크
코드가 브라우저 환경에서 실행되고 있는지 아닌지를 알아야 합니다. 이는
window
와 history
객체가 브라우저에서만 사용 가능하기 때문입니다.const isBrowser = typeof window !== 'undefined';
파트 5: 히스토리 저장 유틸리티
훅은 브라우저의 히스토리에 데이터를 가져오고 설정하는 유틸리티 객체
historyStorage
를 사용합니다.let historyStorage: { get: any; set: any };
파트 6: 히스토리 객체 메소드 재정의
기존의
replaceState
메소드를 재정의하여 새로운 상태 값을 기존 것과 병합합니다. 이렇게 하면 동일한 히스토리 객체에 여러 상태 키를 저장할 수 있습니다.history.replaceState = ( (replaceState) => (state = {}, title, url) => { return replaceState.call( history, { ...history.state, ...state }, title, url, ); } )(history.replaceState);
파트 7: useHistoryState
훅 구현
훅의 주요 함수는
initialState
과 key
를 입력으로 받습니다. 이는 상태 값과 그것을 업데이트하는 setState
함수를 튜플로 반환합니다. 훅은 상태 관리를 위해 useState
, 브라우저의 히스토리와 동기화를 위해 useEffect
, setState
함수를 메모이제이션하기 위해 useCallback
을 사용합니다.const [historyState, setHistoryState] = useState<T>(initialData);
파트 8: 브라우저 히스토리와 상태 동기화
useEffect
훅은 로컬 상태와 히스토리 객체의 상태가 다르다면 로컬 상태를 업데이트하는 데 사용됩니다.useEffect(() => { if (isBrowser) { const stateValue = historyStorage.get(key); if (stateValue !== undefined && stateValue !== historyState) { setHistoryState(stateValue); } } }, [key]);
파트 9: 상태 업데이트
setState
함수는 컴포넌트의 로컬 상태와 브라우저의 히스토리 상태 모두를 업데이트합니다.const setState = useCallback( (state: T, replace = false) => { if (!isBrowser) return; const value = state instanceof Function ? state(historyState) : state; setHistoryState(value); historyStorage.set({ key, value, replace }); }, [historyState, key], );
결론
useHistoryState
훅은 페이지 새로고침에도 유지되는 React 상태를 브라우저 히스토리에 저장하는 간단하고 효과적인 방법을 제공합니다. 여러 페이지의 애플리케이션을 만들든 복잡한 상태를 가진 싱글 페이지 앱을 만들든, 이 훅은 여러분의 코드 에 큰 도움이 되지 않을까 생각됩니다. 감사합니다 :D