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 |
| 링크 | 사내 저장소, 공개 링크 없음 |
맡은 범위
- 본인 구현. 가상 그리드 예제 페이지(
ba37fc8), 이미지를 누르면 열리는 상세 페이지(f773683), 가상 목록과 그리드의 스크롤 복원 예제 수정(bf9425b), 외부 이미지 API 사용량을 넘었을 때의 안내 문구. 가상 목록의 속성을 만드는useVirtualListPropsGetter와 공통ScrollRestoration의 관련 부분을 고쳤습니다. - 보조 작업. 패키지 스크립트와 의존성, MSW 버전 정리, 로그인 샘플 보강. 독립 성과로 두지 않았습니다.
- 기존 기반. 스타터킷의 전체 구조와 공통 내비게이션, 라우터의 location key로 캐시를 나누는
useCache는 이미 있던 공통 코드입니다. 그 최초 설계를 제 역할로 옮기지 않았습니다.
문제와 해결
코드는 본인 변경이 있는 예제 파일에서 발췌했습니다.
라이브러리 설치 목록을 따라 볼 수 있는 화면 흐름으로 바꾸기
스타터킷에는 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에 저장값을 넘기고 언마운트 콜백에서 현재 값을 저장하게 했습니다.
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의 계약과 샘플의 업무 흐름을 따로 읽을 수 있게 했습니다. 실제로 이 예제를 가져다 쓴 프로젝트 수나 개발 시간 단축률은 근거가 없어 쓰지 않았습니다.
결과와 근거
- 조회부터 상세 복귀까지 따라 읽을 수 있는 이미지 예제와 복원 코드를 추가했습니다. 근거는
VirtualGridPage.tsx,PhotoDetailPage.tsx,useImageApi.ts,useVirtualListPropsGetter.ts,ScrollRestoration.tsx와 커밋ba37fc8,f773683,bf9425b입니다. - 스크롤 위치와 행 높이의 저장은
cacheSlice.ts와 기존useCache훅을 거칩니다. - 측정 캐시의 내부 필드 접근과 interval 보정은 당시 구현의 제약으로 함께 적었습니다.
- 사내 적용 범위, 팀 규모, 개발 시간 단축 효과는 측정한 값이 없어 넣지 않았습니다.
배운 것
- 스크롤 복원은 좌표 하나를 되돌리는 일이 아니라 그 좌표를 만든 측정값과 목록이 같은지까지 묶은 상태를 되돌리는 일이었습니다. 위치보다 같은 목록인지와 측정값이 유효한지를 먼저 확인하는 순서는 관련 글에 따로 정리했습니다.
- 공통 기반의 예제는 라이브러리 한 개의 사용법보다 화면 사이의 연결을 보여 줄 때 참고할 가치가 생깁니다. 조회 조건, 응답 변환, 상세 이동, 복귀를 한 예제에 둔 이유입니다.
- 라이브러리 내부에 기대는 우회 코드는 예제에 들어가는 순간 그대로 복제됩니다. 그런 코드는 한곳에 모으고 제약을 함께 적어 두어야 한다는 것을 이 작업에서 배웠습니다.