[월간기록]페이지 간 데이터 공유

SANGHYUN KIM·2023년 8월 18일
0
  1. 직장에서 코드리뷰(라고 불리기에는 한 달에 한 번)시간에 공유한 내용을 다시 정리한 글입니다.
  2. CSR환경에서 React-Router-DOM V5.X를 사용했습니다.
  3. UI는 Kendo UI를 활용합니다.

상황

특정 페이지로 진입 시 상태 설정과 뒤로가기 시 이전 페이지의 상태값 유지하기

관리자 페이지 리팩터링을 하면서 놓쳤던 페이지간 상태 공유를 개발해야 하고 다음 2가지 상황에서 적용을 해야했다.

  1. 특정 태그를 누룰 시 태그에 해당하는 검색 조건을 이동하는 페이지에서 받아서 검색 호출
  2. 목록 → 상세 검색 시 목록에서의 검색 조건을 기억하고 뒤로가기 시 검색 조건을 다시 불러오기

그러다가 어느 주말에 개발자 단톡에 올라온 Funnel 구조 이야기를 기점으로 현재 내 상황에 맞는 구조가 어떤 것이 있는지 생각을 해봤다.

생각해볼 수 있는 CASE

Funnel 구조

처음 생각해본 구조는 퍼널이다. 올해 토스의 SLASH23에서 나왔다고 하여 영상을 보고 어떤 형식인지 살펴봤다.
이를 활용하여 관리자의 목록 화면과 상세화면을 Wrapper로 감싸고 Step이라는 단계를 통하여 UI를 바꿔주는 형식으로 구현할 수 있을 것 같았다.

그러나, 상세페이지는 꼭 상위 목록 페이지에서만 접근이 가능하지 않다.

  • 특정화면의 그리드에서 아이디를 클릭하면 A상세페이지로 이동
  • A상세페이지의 문의 관련 아이디를 클릭하면 B상세 페이지로 이동

따라서, 목록과 상세를 하나의 상위 컴포넌트로 묶어서 상태 공유하는 것은 현재 상황과는 부합하지 않다.

Context API & Recoil (전역 상태)

Recoil을 통한 전역적 또는 Context를 통한 지역적으로 관리할 수 있어서 상세페이지가 아니더라도 접근이 가능하고 상태를 넘겨 받을 수 있다.

  • 상태 저장소에 저장을 하고 페이지 이동, 뒤로가기 클릭 시 다시 상태를 불러옴
  • 직접적으로 하위 관계가 아닌 곳에서 이동 할 때 코드 추가 필요

그러나, 상태를 저장한다 = 렌더링이 발생(좋고 나쁨이 아닌 화면 동기화 작업이 존재) + 상태를 인위적으로 지워줄 필요 + 새로고침 이후 상태 증발하는 현상이 나타날 수 있다

따라서, 전역 또는 지역은 부분적으로 부합하다고 생각했다.

Storage

Storage에 저장하는 방법또한 존재하지만, 이 또한 Recoil 및 Context API 처럼 직접적인 하위 페이지로 이동할 땐 저장 이를 벗어나면 삭제해야 하는 코드 추가 필요하다.
따라서, 장단점은 전역 또는 지역 항목과 동일하다고 본다.

Router Location State

React-Router-Dom(V5 기준) useHistory라는 함수(hook)는 window.history 객체를 react에 알맞게 제공한다.
장점으로는 사용 시 브라우저의 hisotry stack을 남김 → 그 페이지로 움직인 “상황”(어떻게 움직였는지)을 기억하고 있다.

단점:

  • 크롬 개발자 도구에서 history입력 시 어떤 값이 저장되는 지 바로 알 수 있기에 기밀정보를 보관하기에는 좋지 않은 장소
  • 상태관리 복잡성: 상태를 맞게 저장하고 가공하는 로직이 추가적으로 필요하나 다른 방법과 비슷
  • 상태 용량: 브라우저 메모리를 사용하지만 다른 것과 비슷

결정 사항 - Router Location State

이유

위 장단점을 비교했을 때 로직에 따른 추가 코드, 특히 다른 방법들을 봤을 때 어떨 때 지우고 저장하는 로직이 상당히 애매하다고 생각했다.

또한, 브라우저의 history stack에 저장하여

  1. 다른 페이지에서 리스트 또는 상세페이지로 들어온 상태와 메뉴 바에서 들어온 리스트 또는 상세페이지와 상태가 공유되지 않음
    1. 메뉴 사이드바에서 접속 시의 상태 A | 그리드의 링크를 타고 들어온 상태 B → 각자 다르게 history stack으로 쌓여서 상태 자체가 분리
  2. 새로고침 시에도 상태가 기억되며 뒤로가기가 가능
    1. (persist화 library 또는 storage를 사용하지 않는다면)상태 관리는 새로고침 시 상태를 초기화
  3. stack에 기록되는 것이 민감한 정보가 아니며 사내망으로 접속을 꼭 해야하기에 안전하다고 생각

구현

구현은 상당히 단순했다. 페이지 넘김 시 새로운 url에 state key값에 넣오 주면 됬다. 이후 받아온 컴포넌트가 이를 조회하여 사용하면 된다.

/** Page 이동을 선언하는 컴포넌트 */
<Column 
	title="답변대기"
	field="managerStandbySum"
	className="ta-c"
	width={100}
 cell={LinkCellFn({
 path:"/xxxxx/xxxxxxxx",
	// historyState을 남길 때 구조 획일화를 위해 search라는 key에 원하는 상태를 객체로 전환하여 전달
	historyState: { search: { ..... }} 
	})}
/>

/** 렌더링되는 컴포넌트 */
const ApprovalManagement = () => {
  const location = useLocation();
  const locationSearchState = location.state.search;
  
  const [search, setSearch] = useState({
    ...INIT,
    ...locationSearchState,
  });
  const { types, status } = locationSearchSate;
  
  // location 안 history state값을 설정
  const initalTypeSetting = locationSearchState?.types ? { key: locationSearchState.types, value: ApprovalType[locationSearchState.types].name } : defaultItem;
  const initalstatusSetting = locationSearchState?.status ? { key: locationSearchState.status, value: ApprovalStatus[locationSearchState.status].name } : defaultItem;
  const initalValues = {
    types: initalTypeSetting,
    status: initalstatusSetting,
    keywordType: KEYWORD_TYPE[0],
    searchDate: {
      start: null,
      end: new Date(),
    }
  };

	........
	return (
		<Form
	    onSubmitClick={onSubmitClick}
	    initialValues={initalValues}
			.....
		/>
	)
	

받은 질문:

Q1. 다른 페이지에서 값을 넘겨받는 것은 이해했는데, 현재 페이지의 상태는 기억하지 않고 넘기나요?

해당 부분에 대해서 생각하지 못 하였기에 현재 페이지의 상태를 저장하고 나가는 방법 또한 history객체를 활용할 수 있다고 생각했다.
그래서 window.history 문서를 봤고 window.history.state를 통하여 현재 url 안의 상태값을 바꿀 수 있다.

  • 1차 시도 window.histoy.state를 조작
    • window.history.replaceState를 이용하여 직접 window의 값을 조작하고 검색할 때마다 작동해야 하기에 useEffect를 사용
      useEffect(() => {
          window.history.replaceState({search}, '', window.location.pathname)
          refetch();
        }, [refetch, search]);
    • 그러나, 이 상태에서 react-router-dom의 useLocation값을 찍었을 때, window.history의 값이 보이지 않음
    • 서로 다른 객체를 활용하고 있는 지를 확인했으나 useHistorywindow.history를 이용하는 것이 맞음( How does React Router location.state works?)
  • 2차 시도 useHistory훅의 api를 이용
    • 생각해볼 수 있는 것은 로직이 일방통행, 즉 useHistory에서 변경된 값 → window.history일방통행 업데이트 진행(window.history에서 업데이트 한 값이 useHistory로 반영되지 않음)
    • 따라서, useHistory().replace를 통하여 hook에서 변경하는 방향으로 변경
      • history.replace(location.pathname, {search});
    • useLocation 훅을 통하여 state를 받아서 queryKey의 search상태와 form의 initalValues를 setting해준 결과 의도대로 작동
  • 부가적으로 initalValues를 설정해주는 것은 현재 아래와 같이 여러번 비교를 해주는 로직으로 세팅해준다. 근데 더 좋은 방법이 있을까 고민했지만 위 긴 로직을 함수로 추상화하여 렌더링 컴포넌트에서 단순화 시켰다. 계속 하다가 좋은 방향이 있으면 그 때 다시 업데이트를 할 예정이다.
const initalFrom = locationSearchState?.from ? new Date(locationSearchState.from) : null;
const initalTo = locationSearchState?.to ? new Date(locationSearchState.to) : new Date();
const initalKeywordType = locationSearchState?.keywordType ? APPROVAL_DROP_DOWN.KEYWORD_TYPE.find(item => item.key === locationSearchState.keywordType ) :  APPROVAL_DROP_DOWN.KEYWORD_TYPE[0];
const initalKeyword = locationSearchState?.keyword ? locationSearchState.keyword : null
const initalValues = {              
  keywordType: {key: 'test', value: 'testing'},
  keyword: initalKeyword
  searchDate: {
    start: initalFrom,
    end: initalTo,
  }
};
// 위 긴 로직을 함수로 묶어서 page 렌더링 파일 내부에서 함수 반환값을 할당. 함수는 상수 관리 파일에서 보관 및 export
const initalValues = activityLogSearchToForm(search, roleGroup, location);

끝맺음

어느정도 진행을 하다가 선배와 이야기를 하다보니 url에 parameter로 남겨놓지 않아아서 몇가지 문제가 같이 있는 것고 이 또한 그 중 하나로 볼수 있을 것 같다고 했다. 만약 url으로 남겼다면 그대로 query를 날리거나, 브라우저 히스토리 설정 및 이용이 쉬웠을 수도 있고, 탭을 구현할 때도 조금 더 수월했을 수도 있을 것 같다고 한다. 공감한다.
그러나, 항상 완벽한 방법은 없다.

profile
꾸준히 공부하자

2개의 댓글

comment-user-thumbnail
2023년 8월 18일

글 잘 봤습니다.

1개의 답글