[React 19] 공식문서 톺아보기 - React 컴포넌트
• Study• 수십명이 읽음
리액트에서 사용하는 Fragment, Profiler, StrictMode, Suspense에 대해 공부하였습니다.
Fragment
<Fragment>
는 종종<>...</>
구문으로 사용하고, 래퍼 노드 없이 엘리먼트를 그룹화할 수 있다.
가상 DOM 비교
{
show ? (
<>
<Counter />
</> // Fragment 사용
) : (
<Counter /> // 직접 렌더링
);
}
{
show ? (
<>
<Counter />
</> // Fragment 사용
) : (
<Counter /> // 직접 렌더링
);
}
- 같은 컴포넌트라고 생각하여 state를 초기화하지 않는다.
- Fragment나 배열로 감싸더라도 한 단계 깊이까지는 동일하다고 판단한다.
{
show ? (
<>
<>
<Counter />
</>
</> // 중첩 Fragment
) : (
<Counter />
);
}
{
show ? (
<>
<>
<Counter />
</>
</> // 중첩 Fragment
) : (
<Counter />
);
}
- 두 단계 깊이 차이의 경우 state를 초기화한다.
여러 엘리먼트 반환하기
<>
<PostTitle />
<PostBody />
</>
<>
<PostTitle />
<PostBody />
</>
- JSX에서는 하나의 엘리먼트를 반환해야하므로 Fragment로 묶어 반환한다.
- 레이아웃이나 스타일에 영향을 주지 않는다.
리스트 렌더링
return posts.map(post =>
<Fragment key={post.id}>
<PostTitle title={post.title} />
<PostBody body={post.body} />
</Fragment>
);
return posts.map(post =>
<Fragment key={post.id}>
<PostTitle title={post.title} />
<PostBody body={post.body} />
</Fragment>
);
- key값을 할당해야할때 위와같이
<></>
문법을 사용하는 대신 명시적으로Fragment
를 작성해야 한다.
Profiler
<Profiler>
를 통해 React 트리의 렌더링 성능을 프로그래밍 방식으로 측정할 수 있다.- 프로파일링은 추가적인 오버헤드를 더하기 때문에, 프로덕션에서는 기본적으로 비활성화 되어있다.
사용예시
<Profiler id='App' onRender={onRender}>
<App />
</Profiler>
<Profiler id='App' onRender={onRender}>
<App />
</Profiler>
id
: 성능을 측정하는 UI 컴포넌트를 식별하기 위한 문자열onRender
: 프로파일링된 트리 내의 컴포넌트가 업데이트될 때마다 React가 호출한다.- 렌더링된 내용과 소요된 시간에 대한 정보를 받는다.
onRender
function onRender(
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime,
) {
// 렌더링 시간 집계 혹은 로그...
}
function onRender(
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime,
) {
// 렌더링 시간 집계 혹은 로그...
}
id
: 커밋된<Profiler>
트리의 문자열id
프로퍼티- 프로파일러를 다중으로 사용하고 있는 트리 내에서 어떤 부분이 커밋 되었는지 식별할 수 있게 해준다.
phase
:"mount"
,"update"
혹은"nested-update"
. 트리가 최초로 마운트되었는지 또는 Props, State, Hook의 변경으로 인해 리렌더링 되었는지 알 수 있다.actualDuration
: 현재 업데이트에 대해<Profiler>
와 자식들을 렌더링하는데 소요된 시간baseDuration
: 최적화 없이 전체<Profiler>
하위 트리에 대해 걸리는 시간, 이 값은 최악의 렌더링 비용(예: 최초 마운트 또는 메모이제이션이 없는 트리)을 추정한다.startTime
: React가 현재 업데이트 렌더링을 시작한 시점commitTime
: React가 현재 업데이트를 커밋한 시점
StrictMode
<StrictMode>
를 통해 개발 중에 컴포넌트에서 일반적인 버그를 빠르게 찾을 수 있도록 합니다.- 개발환경에서만 실행되며, 컴포넌트가 순수한지, Effect 클린업 함수가 필요한지 찾을 수 있다.
- Strict Mode는 실수로 작성된 순수하지 않은 코드를 찾아내기 위해 몇 가지 함수(순수 함수여야 하는 것만)를 개발 환경에서 두 번 호출한다.
- 컴포넌트 함수 본문 (단, 최상위 로직만 해당하며, 이벤트 핸들러 내부의 코드는 포함하지 않는다.)
useState
,set
함수,useMemo
, 또는useReducer
에 전달한 함수constructor
,render
,shouldComponentUpdate
와 같은 일부 클래스 컴포넌트 메소드- Strict Mode가 켜져 있으면 React는 모든 Effect에 대해 개발 환경에서 한 번 더 셋업+클린업 사이클을 실행한다.
ref 콜백 다시 실행
ref={(node) => {
const list = itemsRef.current;
const item = {animal: animal, node};
list.push(item);
console.log(`✅ Adding animal to the map. Total animals: ${list.length}`);
if (list.length > 10) {
console.log('❌ Too many animals in the list!');
}
return () => {
// 🚩 No cleanup, this is a bug!
}
}}
ref={(node) => {
const list = itemsRef.current;
const item = {animal: animal, node};
list.push(item);
console.log(`✅ Adding animal to the map. Total animals: ${list.length}`);
if (list.length > 10) {
console.log('❌ Too many animals in the list!');
}
return () => {
// 🚩 No cleanup, this is a bug!
}
}}
- 엄격모드는 개발단계에서 잠재적인 문제를 감지하기 위해 컴포넌트를 의도적으로 두 번 렌더링하는데 이 과정에서 ref 콜백도 두번 실행된다.
- 첫 번째 렌더링:
ref
콜백이null
로 호출된다. (기존 참조 정리) - 두 번째 렌더링: 실제 DOM 요소나 컴포넌트 인스턴스로 호출된다.
- 첫 번째 렌더링:
- ref 콜백에서 다음과 같은 코드를 작성한다면 필수로 클린업 함수를 작성해야한다.
- 이벤트 리스너 추가
- 외부 라이브러리 인스턴스 생성
- 타이머/구독(subscription) 관리
- useEffect와의 차이점
ref
콜백의 클린업은 DOM 요소의 연결/해제시 실행되지만,useEffect
의 클린업은 의존성 배열이 변경되거나 언마운트 시만 실행된다.
Suspense
<Suspense>
는 자식 요소를 로드하기 전까지 화면에 대체 UI Fallback를 보여준다.
<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>
<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>
fallback
: 실제 UI가 로딩되기 전까지 대신 렌더링되는 대체 UI- Suspense는
children
의 렌더링이 지연되면 자동으로fallback
으로 전환하고, 데이터가 준비되면children
으로 다시 전환한다. - 만약
fallback
의 렌더링도 지연되면, 가장 가까운 부모 Suspense가 활성화된다.
- Suspense는
Suspense 도중 생성된 state는 유지되지 않는다
function MyComponent() {
const [count, setCount] = useState(0);
const data = use(fetchData()); // 여기서 suspend
return (
<div>
{data} / Count: {count}
</div>
);
}
function MyComponent() {
const [count, setCount] = useState(0);
const data = use(fetchData()); // 여기서 suspend
return (
<div>
{data} / Count: {count}
</div>
);
}
React는 컴포넌트가 처음으로 마운트 되기 전에, 지연된 렌더링을 하는 동안 어떤 State도 유지하지 않습니다. 컴포넌트가 로딩되면 React는 일시 중지된 트리를 처음부터 다시 렌더링합니다.
- React가 컴포넌트를 렌더링하다가 중간에
fetchData
를 만나면 일시중단한다.- 중단하는 동안 fallback ui가 표시된다.
- 컴포넌트 실행중에
useState(0)
도 실행 도중에 멈추고count
는 메모리에 저장되지 않는다.
- 데이터 fetch가 끝나고 다시 렌더링되면
useState(0)
부터 다시 시작한다.
useLayoutEffect와 Suspense
useLayoutEffect(() => {
console.log('레이아웃 측정 실행');
return () => console.log('레이아웃 측정 정리'); // ✅ 로딩 중일 때 호출됨
}, []);
useLayoutEffect(() => {
console.log('레이아웃 측정 실행');
return () => console.log('레이아웃 측정 정리'); // ✅ 로딩 중일 때 호출됨
}, []);
- 컴포넌트가 로딩중이면 클린업함수를 실행한다.
- 로딩중인 컴포넌트는 숨겨진 상태로 DOM요소에 접근할 수 없다.
- 로딩 완료후 다시 실행한다.
Suspense를 활성화하는 조건
- Relay와 Next.js 같이 Suspense가 가능한 프레임워크를 사용한다.
lazy
를 활용한 지연 로딩 컴포넌트- 컴포넌트가 아직 로드되지 않았을 때 렌더링을 일시 중단(suspend)한다.
use
를 사용해서 캐시된 Promise 값 읽기- React는 컴포넌트 렌더링 중 use(somePromise)를 만나면 Suspense 상태로 전환한다.
- Suspense는 Effect 또는 이벤트 핸들러 내부에서 가져오는 데이터를 감지하지 않는다.
로딩순서 만들기
- Suspense로 감싸진 구성요소중 하나라도 지연되면 로딩이 표시되고 모두 보일 준비가 되면 함께 보여진다.
- 따라서 오래걸리는 컴포넌트를 함께 Suspense로 묶게되면 다함께 느려지므로 Suspense를 세분화한다.
- 여러 Suspense 컴포넌트를 중첩하여 로딩 순서를 만들 수 있다.
- Suspense를 중첩하여 먼저 준비가 된 컴포넌트부터 보여줄 수 있다.
- 먼저 보여줄 컴포넌트 경계를 Suspense를 통해서 지정할 수 있다.
useDeferredValue와 Suspense
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
- 함께 사용할 경우 새 결과가 준비될때까지 이전 결과를 보여줄 수 있다.
- 사용자의 빠른 입력에 입력마다 데이터요청을 하는게 아니라 완성된 입력상태에 대해서 요청이 되고 그것에 대한 fallback을 보여준다.
startTransition와 Suspense
const handleTabChange = (newTab) => {
startTransition(() => {
setTab(newTab); // 탭 전환 지연 (중요하지 않은 업데이트)
});
};
// ...
<Suspense fallback={<div>Loading...</div>}>
{tab === 'home' ? <HomeTab /> : <ProfileTab />}
</Suspense>;
const handleTabChange = (newTab) => {
startTransition(() => {
setTab(newTab); // 탭 전환 지연 (중요하지 않은 업데이트)
});
};
// ...
<Suspense fallback={<div>Loading...</div>}>
{tab === 'home' ? <HomeTab /> : <ProfileTab />}
</Suspense>;
- 사용자가 탭을 클릭 →
startTransition
으로 상태 업데이트 지연. - 새 탭의 컴포넌트(
<ProfileTab />
)가 로딩되면Suspense
가fallback
UI 표시. - 로딩 완료 후 실제 컨텐츠 렌더링.
startTransition
는 낮은 우선순위를 갖게해줘서 다른 급한 동작을 중간에 수행할 수 있게 해준다.Suspense
는 로딩중이라는 것을 시각적으로 보여준다.- 두개를 같이 쓰게되면 사용자는 로딩상태를 인지하면서도 화면이 멈추지 않고 다른 작업을 계속할 수 있다.
Suspense
만 쓰게 되면 사용자는 다른 작업을 할수 없고 로딩이 길면 안좋은 UX가 된다.startTransition
만 쓰게 되면 로딩표시가 없어 반응이 없다고 느낄 수 있다.- 무조건 두개를 같이 쓴다고 위와같은 효과가 나타나는게 아니라
Suspense
는 특정 조건을 만족해야 fallback UI를 보여준다.
주의사항
Suspense
는 Promise를 throw하는 비동기 함수와만 작동한다.Suspense
는 SSR에서 스트리밍 렌더링과 함께 사용될 수 있지만,startTransition
은 클라이언트 사이드에서만 유효하다.
SSR과 Suspense
- 스트리밍 서버 렌더링 API 중 하나(또는 프레임워크)를 사용하는 경우, React는 서버의 에러를 처리하기 위해
<Suspense>
를 사용하기도 한다. - 컴포넌트가 서버에서 에러를 발생시키더라도 리액트는 렌더링을 중단하지 않고, 가장 가까운
<Suspense>
의fallback
UI를 HTML에 넣어 보낸다. - 그리고 클라이언트에서 재시도를 해서 성공하면 정상적으로 표시하고 실패하면 에러바운더리가 에러 ui 표시한다.
- 이러한 과정을 통해 서버에서의 에러로 인해 전체 페이지가 흰 화면이 되지 않도록 방지하고 클라이언트에서 복구할 기회를 준다.
- 또한 이를 사용하여 일부 컴포넌트를 의도적으로 서버에서 렌더링하지 않도록 선택할 수 있다.
if (typeof window === 'undefined') { throw Error('Chat should only render on the client.'); }
if (typeof window === 'undefined') { throw Error('Chat should only render on the client.'); }