Navigation

React Starter Kit

사내 React Starter Kit은 새 React 프로젝트를 시작할 때 라우팅, 상태 관리, API 호출, 모달, 스크롤 복원, 테스트 구성을 참고할 수 있게 만든 공통 기반입니다. 저는 이미 있던 공통 구조 위에 이미지 목록과 상세 탐색 예제를 추가하고 가상 그리드에서 상세로 갔다 돌아올 때 이전 위치가 보이도록 복원 코드를 보강했습니다. 라이브러리 사용법보다 화면과 화면 사이의 연결을 따라 읽을 수 있는 예제를 남기는 것이 목표였습니다.

핵심 기여

과제본인 기여확인한 동작
따라 읽을 수 있는 화면 흐름가상 그리드와 이미지 상세 예제 추가URL 조회 조건, 응답 변환, 그리드, 상세 이동이 한 흐름으로 연결
높이가 늦게 정해지는 이미지 목록이미지 로드 시점의 셀 재측정이미지가 로드되면 해당 셀의 높이를 다시 측정
상세에서 돌아왔을 때의 위치스크롤 위치와 행 높이를 함께 저장하고 복원목록을 떠날 때 저장한 높이를 측정 캐시에 다시 채움
공통 UI와 샘플의 경계그리드는 값과 콜백만 받고 페이지가 이동을 결정공통 컴포넌트에 샘플 데이터의 의미가 들어가지 않음

개요

항목내용
기간2024.03 ~ 2024.04
역할기존 공통 기반의 예제 확장과 스크롤 복원 코드 보강
대상신규 React 프로젝트를 시작하는 사내 개발자
협업이미 있던 스타터킷의 구조와 공통 컴포넌트 위에서 작업했습니다
기술React, TypeScript, React Router, React Query, Zustand, react-virtualized, MSW
링크사내 저장소, 공개 링크 없음

맡은 범위

문제와 해결

코드는 본인 변경이 있는 예제 파일에서 발췌했습니다.

라이브러리 설치 목록을 따라 볼 수 있는 화면 흐름으로 바꾸기

스타터킷에는 React Router, React Query, react-virtualized 같은 라이브러리가 설치되어 있었고 개별 기능의 샘플도 있었습니다. 하지만 실제 서비스에서 막히는 지점은 라이브러리 하나의 사용법보다 그 사이의 연결이었습니다. 조회 조건은 어디에 두고 응답은 어디에서 화면 데이터로 바꾸며 상세에서 돌아오면 무엇을 되살려야 하는지가 한 화면 안에 드러나야 했습니다.

그래서 이미지 검색, 가상 그리드, 상세 이동을 한 예제로 묶었습니다. URL 조회 조건을 이미지 API에 넘기고 응답 변환 계층이 만든 화면 데이터를 그리드에 전달했습니다. 이미지를 누르면 상세 주소로 이동하고 뒤로가기로 돌아오면 목록 위치를 복원합니다.

flowchart LR
    Query["URL 조회 조건"] --> API["이미지 API"]
    API --> Convert["응답 변환 계층"]
    Convert --> Grid["가상 그리드와 높이 측정"]
    Grid -->|"이미지 선택"| Detail["이미지 상세"]
    Detail -->|"뒤로가기"| Restore["위치와 측정값 복원"]
    Restore --> Grid

예제는 외부 이미지 API를 썼기 때문에 사용량 한도를 넘으면 이미지를 받아 오지 못합니다. 상세 화면에 한도 초과 안내 문구를 넣어 예제가 실패한 이유를 알 수 있게 했습니다. 예제가 외부 서비스에 기대는 한계는 그대로 남겼습니다.

이미지 높이가 늦게 정해지는 가상 그리드

가상 그리드는 화면에 보이는 셀만 그리고 나머지는 높이 정보로 자리만 계산합니다. 이미지 셀은 처음에 기본 높이로 그려지고 이미지가 로드된 뒤에야 실제 높이가 정해집니다. 측정이 끝나기 전의 높이로 자리를 계산하면 이미지가 로드될 때마다 아래 셀이 밀리고 스크롤 위치의 의미도 달라집니다.

스타터킷의 가상 목록은 window 스크롤을 따라가는 WindowScroller와 셀 높이를 기억하는 CellMeasurerCache를 조합합니다. 그리드 예제에서는 이미지의 onLoad에서 해당 셀의 measure를 호출해 실제 높이로 다시 측정하게 했습니다. 측정된 높이는 측정 캐시에 남고 이 값이 다음 절의 복원 재료가 됩니다.

flowchart TD
    Render["기본 높이로 셀 렌더링"] --> Load["이미지 로드 완료"]
    Load --> Measure["onLoad에서 measure 호출"]
    Measure --> Cache["CellMeasurerCache에 실제 높이 기록"]
    Cache --> Layout["아래 셀 위치 재계산"]

복원에 위치와 측정값을 함께 남기기

상세를 보고 돌아왔을 때 scrollTop 값만 되돌리면 같은 위치가 나오지 않았습니다. 돌아온 시점에 측정 캐시가 새로 만들어지면 모든 셀이 기본 높이로 계산되고 같은 scrollTop이 다른 셀을 가리키기 때문입니다. 위치는 측정값을 전제로 한 좌표였습니다.

목록을 떠날 때 scrollTop과 행 높이 맵을 함께 저장하고 돌아오면 저장된 높이를 측정 캐시에 먼저 채운 뒤 위치를 적용했습니다. 저장 공간은 기존 useCache 훅을 썼고 이 훅은 React Router의 location key를 캐시 키로 써서 같은 기록 항목으로 돌아왔을 때 저장값을 찾습니다. 가상 목록의 속성을 만드는 useVirtualListPropsGetter에 저장값을 넘기고 언마운트 콜백에서 현재 값을 저장하게 했습니다.

VirtualGridPage: 목록을 떠날 때 복원 정보를 저장한다ts
const {
  cellCache, scrollTop, virtualListHandler, getRowsHeight, getScrollTop
} = useVirtualListPropsGetter({
  scrollTop: cache.data?.scrollTop,
  rowsHeight: cache.data?.rowsHeight,
  callback: {
    unMount: () => cache.save({
      scrollTop: getScrollTop(),
      rowsHeight: getRowsHeight(),
    }),
    loadMoreRows: () => fetchNextPage(),
  },
});
flowchart LR
    Leave["목록을 떠남"] --> Save["location key별 저장: scrollTop, 행 높이 맵"]
    Save --> Back["뒤로가기로 복귀"]
    Back --> Fill["저장된 높이를 측정 캐시에 채움"]
    Fill --> Apply["저장된 scrollTop 적용"]

공통 ScrollRestoration은 페이지마다 복원 전략과 스크롤 컨테이너를 등록받는 구조였고 bf9425b에서 가상 목록과 그리드 예제가 이 구조로 복원되도록 연결하고 관련 코드를 고쳤습니다.

다만 당시 코드에는 측정 캐시의 내부 필드에 직접 높이를 넣고 짧은 interval로 위치를 다시 적용하는 부분이 있었습니다. 라이브러리 버전이 바뀌면 깨질 수 있는 방식이라 모든 비동기 목록의 복원을 보장하는 일반 해법으로 소개하지 않았습니다. 같은 목록인지 확인하는 조건과 강제 위치를 해제하는 시점은 가상 스크롤 복원 글에서 따로 정리했습니다.

공통 UI와 샘플 페이지의 역할 나누기

스타터킷은 여러 프로젝트가 참고하는 코드입니다. 샘플의 데이터 구조나 이동 규칙이 공통 컴포넌트 안으로 들어가면 다음 프로젝트에서 그 컴포넌트를 쓸 때 이미지 예제의 가정까지 따라옵니다.

그래서 가상 그리드는 행 데이터와 측정 캐시, 스크롤과 추가 조회 콜백만 받게 하고 무엇을 그리고 어디로 이동할지는 샘플 페이지가 정하게 했습니다. 상세 주소와 이미지 데이터의 의미는 페이지에만 있고 그리드는 전달받은 값으로 셀을 그리고 이벤트를 위로 알립니다.

flowchart TD
    Page["샘플 페이지: 조회 조건, 상세 이동 결정"] -->|"rows, 측정 캐시, 콜백"| Grid["공통 가상 그리드"]
    Grid -->|"셀 클릭, 추가 조회 요청"| Page
    Page --> Router["상세 주소로 이동"]

이 경계로 공통 UI의 계약과 샘플의 업무 흐름을 따로 읽을 수 있게 했습니다. 실제로 이 예제를 가져다 쓴 프로젝트 수나 개발 시간 단축률은 근거가 없어 쓰지 않았습니다.

결과와 근거

배운 것