React 렌더링을 측정할 때 남길 것
2023년에 작성한 React Render Performance 원문은 memo, useMemo, useCallback과 렌더링 과정을 공부한 기록이었습니다. 샘플 목록의 측정 화면도 있었지만 실행 환경과 반복 조건이 충분히 남아 있지 않았습니다. 당시 숫자를 서비스의 개선 성과로 옮기지 않고 다시 측정할 때 필요한 기준을 정리했습니다.
컴포넌트 호출과 DOM 변경은 다릅니다
React의 render는 컴포넌트를 호출해 결과를 계산하는 과정입니다. commit에서 필요한 DOM 변경을 반영합니다. 컴포넌트가 다시 호출됐다는 사실만으로 모든 DOM을 다시 만들거나 화면을 다시 그렸다고 볼 수는 없습니다. Render and Commit
flowchart LR
Trigger["상태 변경"] --> Render["render: 컴포넌트 결과 계산"]
Render --> Commit["commit: 필요한 DOM 변경"]
Commit --> Browser["브라우저의 스타일, 레이아웃과 표시"]
Measure["React Profiler"] -.-> Render
Timeline["브라우저 Performance 기록"] -.-> Browser원문에서는 메모이제이션이 된 props면 commit을 생략한다고 넓게 설명한 부분이 있었습니다. 특정 값의 참조가 같다는 것만으로 전체 commit이나 브라우저의 작업이 없어지는 것은 아닙니다. React 계산 비용과 레이아웃, 이미지 디코딩, 네트워크 대기를 구분해서 봐야 합니다.
먼저 재현 동작 하나를 정합니다
“화면이 느리다” 대신 검색어 한 글자 입력, 필터 하나 변경, 항목 선택처럼 반복할 행동을 정합니다. 같은 데이터, 뷰포트와 빌드 조건에서 전후를 비교해야 원인을 좁힐 수 있습니다.
| 남길 항목 | 필요한 이유 |
|---|---|
| 코드 버전과 빌드 종류 | 개발 도구와 최적화 조건 구분 |
| 브라우저, 장치와 CPU 제한 | 다른 실행 환경과 비교하지 않기 |
| 데이터 개수와 콘텐츠 | 계산량과 행 높이 차이 구분 |
| 재현 순서와 캐시 상태 | 초기 로딩과 반복 동작 구분 |
| 반복 결과와 분포 | 우연히 빠른 한 번을 성과로 쓰지 않기 |
| 사용자가 기다린 시간과 React 측정값 | 서로 다른 지표를 한 숫자로 묶지 않기 |
Profiler 값은 무엇을 잰 것인지 붙입니다
actualDuration은 해당 갱신에서 Profiler 영역의 렌더링에 사용한 시간입니다. baseDuration은 최근 측정값을 바탕으로 전체 하위 트리를 다시 렌더링할 비용을 추정하는 값입니다. 두 값을 페이지 전체 로딩 시간으로 쓰지 않습니다. profiling 자체의 비용도 있으며 기본 production 빌드에서는 별도 설정 없이 같은 측정을 기대할 수 없습니다. Profiler API
다음은 기록할 위치를 보여 주는 예시입니다. 실제 측정 결과는 포함하지 않았습니다.
import { Profiler, type ProfilerOnRenderCallback } from 'react'
const recordRender: ProfilerOnRenderCallback = (
id, phase, actualDuration, baseDuration,
) => {
console.table({ id, phase, actualDuration, baseDuration })
}
<Profiler id="filtered-list" onRender={recordRender}>
<FilteredList />
</Profiler>콘솔 출력 비용까지 결과에 섞일 수 있으므로 정밀 측정에서는 수집 방식도 맞춰야 합니다. React Profiler에서 비용이 작다면 브라우저 기록으로 다른 병목을 살펴보겠습니다.
memo를 붙이기 전에 바뀌는 props를 찾습니다
memo의 기본 비교는 각 prop에 대한 Object.is 비교입니다. 부모가 매번 객체나 함수를 새로 만들면 비교 결과가 달라질 수 있습니다. 그러나 객체의 참조를 고정하는 일이 실제로 비싼 하위 렌더링을 줄이는지 확인하지 않고 모든 값을 메모이제이션하지는 않습니다. 컴포넌트 자신의 state와 사용하는 Context가 바뀌는 경우도 따로 봅니다. memo의 비교와 갱신 조건
useMemo는 계산 결과를 재사용하고 useCallback은 함수의 참조를 재사용할 수 있습니다. 둘 다 그 자체로 자식의 렌더링을 막는 장치는 아닙니다. 의존성을 빠뜨려 오래된 값을 유지하는 문제도 비용보다 먼저 피해야 합니다.
목록의 key도 성능용 스위치로만 보지 않습니다. 삽입과 정렬 때 같은 항목의 상태를 유지하도록 항목의 정체성을 표현해야 합니다. index를 id처럼 쓰면 입력 상태가 다른 항목에 붙을 수 있습니다. 상태 보존과 초기화
최적화 뒤에는 같은 행동을 다시 확인합니다
한 번에 변경 한 가지를 적용하고 같은 동작을 반복합니다. 렌더링 시간만 줄었는지, 입력과 스크롤의 반응도 나아졌는지, 코드 복잡도가 얼마나 늘었는지를 함께 기록하겠습니다.
React Compiler를 사용하는 환경이라면 자동 메모이제이션 적용 여부도 확인해야 합니다. 수동 memo가 늘 필요한 것으로 가정하지 않습니다. 현재 memo 문서의 Compiler 안내
과거 샘플의 숫자만으로 이력서에 개선율을 넣지는 않았습니다. 코드 버전, 재현 조건과 반복 결과가 연결됐을 때 해당 범위의 결과로 설명하는 것이 이 기록의 기준입니다.