공식문서 : 잘쓰면 UX 최적화를 진행할 수 있을거 같은데 쉽지 않네 (작성일 : 25/4/27)

React에서 Built-in으로 제공하는 제공하는 내장 컴포넌트가 존재하는데 오늘은 4개 중 Profiler,Suspense에 대해 알아보려고 한다.
<Profiler>를 통해 React 트리의 렌더링 성능을 프로그래밍 방식으로 측정할 수 있습니다.
렌더링 성능을 측정하기 위해 컴포넌트 트리를 Profiler로 감싸준다.
<Profiler id="App" onRender={onRender}>
<App />
</Profiler>
Props
id: 성능 측정을 위해 UI컴포넌트를 식별하는 문자열onRender: React가 프로파일링(profiled)된 트리 안의 컴포넌트들이 업데이트 될때마다 호출하는 onRender콜백, 이 콜백은 어떤 것이 렌더링되었고, 런더링에 얼만큼의 시간이 걸렸는지에 대한 정보를 받는다. NOTE
Profiled는 "성능 측정이 적용된" 정도로 생각하면 좋다.
성능 데이터를 수집하고 있는 컴포넌트 트리를 일컫는다. == "성능 측정을 위해 감싼 트리"
Caveats
Profiling은 추가적인 overhead를 더하기 때문에 기본적으로 배포 환경에서는 비활성화되어있다.
프로덕션 profiling을 사용하려면 프로파일린 기능이 활성화된 특수한 프로덕션 build를 사용해야 한
다.
yarn build --profile
npm run build -- --profile
NOTE
- overhead : 추가로 드는 비용 or 부하
<Profiler>는 가벼운 컴포넌트이지만 사용할 때마다 애플리케이션에 약간의 CPU 및 메모리 오버헤드를 추가하기 때문에 필요할 때만 사용할 것
렌더링된 내용에 대한 정보를 가지고 onRender 콜백을 호출
function onRender(id, phase, actualDuration, baseDuration, startTime, commitTime) {
// Aggregate or log render timings...
}
id : 커밋된 <Profiler>트리의 문자열 id 프로퍼티
phase : mount, update,nested-update 중 하나
트리가 처음으로 마운트되었는지 아니면 props, state, Hooks의 변경으로 다시 렌더링이 되었는지 알 수 있음
actualDuration : 업데이트 동안 <Profiler>와 그 하위 트리를 렌더링하는 데 걸린 시간(ms)
이 값은 서브트리가 memoization을 얼마나 잘 활용하고 있는지를 나타 낸다.
이상적으로 초기 마운트 이후에는 많은 하위 컴포넌들이 특정 props가 변경될 때만 다시 렌더링되기 때문에, 이 값은 크게 줄어야 한다.
baseDuration : 최적화가 전혀 없는 상태에서 <Profiler>의 하위 트리를 전체 다시 렌더링하는데 걸릴 것으로 예상되는 시간(ms)
트리 안의 각 컴포넌트의 가장 최근 렌더링 시간들을 합산해서 계산
이 값은 렌더링의 최악의 비용을 추정한 것 (Ex. 초기 마운트 or memoization이 없는 경우)
actualDuration과 비교해 memoization이 잘 되고 있는지 확인할 수 있다.
startTime : 이번 업데이트를 React가 렌더링하기 시작한 시점의 타임스탬프 (Number Type)
commitTime : 이번 업데이트를 React가 커밋한 시점의 타임스탬프 (Number Type)
이 값은 같은 커밋 안에 있는 모든 프로파일러가 공유하므로 필요하다면 서로 그룹화 할 수 있다.
nested update
컴포넌트가 렌더링 중 또 다른 상태 업데이트를 일으켜 추가로 발생한 렌더링function function MyComponent() { const [count, setCount] = useState(0); useEffect(() => { if (count === 0) { setCount(1); // 렌더링 중에 상태를 바꿔서 "nested update" 발생 } }, [count]); return <div>{count}</div>; }
- 첫 렌더 →
count가0useEffect안에서setCount()를 호출 → 리렌더링 (nested update)
<Profiler>컴포넌트는 코드 안에서 직접 성능 데이터를 수집할 수 있다. (프로그래밍적)
이를 React Developer Tools(크롬 익스텐션)은 시각적으로 성능을 분석할 수 있다.
→ Dev tool을 사용해서 유사한 기능들을 사용할 수 있다.
→←↑↓
📌⚠️
<Suspense>는 자식 요소(children)를 로드하기 전까지 화면에 대체 UI(Fallback)를 보여준다.
<Suspense fallback={<Loading />}>
<SomeComponent /> // children
</Suspense>
Props
children: 렌더링하려는 실제 UI, children의 렌더링이 지연되면 Suspense는 fallback을 대신 렌더링fallback: 실제 UI가 로딩되기 전까지 대신 렌더링되는 대체 UI (Ex. 로딩 스피너, 스켈레톤 UI)fallback의 렌더링이 지연되면 가장 가까운 부모 Suspense가 활성화Caveats
NOTE
렌더링 ? 마운트 ?
렌더링 : 실제 DOM을 그리기 전 어떤 모습으로 그릴지에 대한 청사진 (가상 DOM)
마운트 : 렌더링된 가상 DOM을 가지고 실제 브라우저의 Real DOM을 작업
Suspense가 트리의 컨텐츠를 보여주고 있을 때 또 다시 지연되면 startTransition나 useDeferredValue로 인한 업데이트가 아닌 한 fallback이 다시 보여진다.
이미 화면에 표시된 컨텐츠를 다시 숨겨야 할 경우 컨텐츠 트리 안의 layout Effect들을 정리함
컨텐츠가 다시 표시될 준비가 되면 다시 layout Effect들을 실행
→ DOM 레이아웃을 측정하는 Effect들이 컨텐츠가 숨겨진 상태에서 잘못 동작하지 않도록 보장
NOTE
이 문장은 잘 이해가 안되서 하나씩 좀 뜯어보겠음
- 이미 화면에 표시된 컨텐츠 : 마운트가 완료된 컴포넌트들
- layout Effect를 정리 :
문제는 이렇게 숨김처리를 할때 발생하는데 컴포넌트가 화면에 있을 때는useLayoutEffect같은 Effect를 통해 DOM의 크기, 스크롤 위치 등을 조절하는 "화면 재조정"작업을 하고 있을 수 있음
이 때 컨텐츠가 숨겨져서 화면에 없어졌는데도 이런 작업을 계속 진행할 경우 버그가 발생함 → 퍼포먼스 문제
그러기 때문에 이렇게 layout Effect들은 모두 정리하는 것
- 다시 숨겨야 할 경우? : 사용자 인터렉션에 의해 기존 UI → 새로운 UI 준비 → 이과정에서 fallback UI를 보여줌
Suspense와 통합하여 제공import { Suspense } from 'react';
import Albums from './Albums.js';
export default function ArtistPage({ artist }) {
return (
<>
<h1>{artist.name}</h1>
<Suspense fallback={<Loading />}>
<Albums artistId={artist.id} />
</Suspense>
</>
);
}
function Loading() {
return <h2>🌀 Loading...</h2>;
}
IMPORTANT
Suspense는 특정 상황에서만 활성화
1. Relay, Next.js같은 Suspense가 가능한 프레임워크를 사용한 데이터 페칭
2.lazy를 활용한 로딩 지연 컴포넌트
3.use를 사용한 Promise 값을 읽을 때useEffect + fetch는 렌더링을 멈추지 않고 로딩 스피너를 따로 관리하는 패턴
Suspense는 렌더링 자체를 중단하고 fallback을 보여주는 패턴