React 18의 새로운 기능인 Concurrent Features와 Suspense에 대해 설명해주세요.
힌트
렌더링 우선순위와 비동기 데이터 로딩을 생각해보세요.
정답 및 해설
React 18의 새로운 기능인 Concurrent Features와 Suspense에 대해 설명해주세요.
React 18은 Concurrent Mode를 기반으로 렌더링 방식을 근본적으로 개선했습니다. 기존 React는 렌더링이 시작되면 중단 없이 끝까지 실행되었지만, Concurrent Features를 통해 렌더링을 중단하고, 재개하고, 우선순위에 따라 처리할 수 있게 되었습니다.
Concurrent Features란?
Concurrent(동시성) 란 여러 작업을 동시에 처리하는 것처럼 보이게 하는 개념입니다. React 18은 렌더링 작업에 우선순위를 부여하여, 긴급한 업데이트(사용자 입력 등)가 덜 긴급한 업데이트(데이터 페칭 결과 표시 등)보다 먼저 처리되도록 합니다.
기존 React의 한계
// React 17 이하 — 한번 렌더링이 시작되면 멈출 수 없음
function HeavyComponent() {
// 무거운 연산이 실행되는 동안 UI가 블로킹됨
const result = heavyComputation(); // 수백 ms 소요
return <div>{result}</div>;
}
React 17 이하에서는 위처럼 무거운 컴포넌트가 렌더링 중이면 사용자 입력(타이핑, 클릭 등)이 지연되는 UI 블로킹 문제가 발생했습니다.
React 18의 접근 방식
React 18은 렌더링을 작은 단위로 쪼개어 중간에 더 긴급한 작업이 있으면 잠시 멈추고, 긴급한 작업이 끝나면 재개하는 방식을 사용합니다.
useTransition
useTransition은 특정 상태 업데이트를 비긴급(non-urgent) 으로 표시하는 훅입니다. 반환값으로 isPending(전환 중 여부)과 startTransition(전환 래퍼 함수)을 제공합니다.
import { useState, useTransition } from 'react';
function SearchComponent() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
function handleChange(e) {
// 입력값 업데이트는 긴급 — 즉시 처리
setQuery(e.target.value);
// 검색 결과 업데이트는 비긴급 — 전환으로 처리
startTransition(() => {
const filtered = searchItems(e.target.value);
setResults(filtered);
});
}
return (
<div>
<input value={query} onChange={handleChange} />
{isPending ? (
<p>검색 중...</p>
) : (
<ul>
{results.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
)}
</div>
);
}
isPending이 true인 동안 사용자에게 로딩 표시를 보여주고, 전환이 완료되면 새로운 결과를 표시합니다. 이 방식으로 타이핑 중에도 입력창이 즉각 반응하면서, 무거운 검색 결과 렌더링은 백그라운드에서 처리됩니다.
startTransition
startTransition은 훅 없이도 사용할 수 있는 함수형 API입니다. 컴포넌트 외부나 이벤트 핸들러에서 직접 사용할 수 있습니다.
import { startTransition } from 'react';
// 라우터나 외부 라이브러리에서 활용
function navigateTo(page) {
startTransition(() => {
// 페이지 전환은 비긴급으로 처리
setCurrentPage(page);
});
}
useTransition vs startTransition 비교
| 구분 | useTransition | startTransition |
|---|---|---|
| 사용 위치 | 함수 컴포넌트 내부 | 어디서든 사용 가능 |
| isPending | 제공 (로딩 상태 추적) | 제공 안 함 |
| 주요 용도 | 컴포넌트 내 전환 | 라이브러리, 유틸 함수 |
useDeferredValue
useDeferredValue는 특정 값의 업데이트를 지연시켜, 더 긴급한 렌더링이 먼저 처리되도록 합니다. debounce와 비슷하지만, 고정된 딜레이 없이 React가 동적으로 타이밍을 결정합니다.
import { useState, useDeferredValue, memo } from 'react';
// memo로 감싸야 최적화 효과를 볼 수 있음
const HeavyList = memo(function HeavyList({ query }) {
// 무거운 필터링 연산
const items = hugeList.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
);
return (
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
});
function SearchPage() {
const [query, setQuery] = useState('');
// deferredQuery는 query보다 한 박자 늦게 업데이트됨
const deferredQuery = useDeferredValue(query);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="검색..."
/>
{/* 입력창은 즉시 반응, 목록은 지연 업데이트 */}
<HeavyList query={deferredQuery} />
</div>
);
}
useTransition vs useDeferredValue
| 구분 | useTransition | useDeferredValue |
|---|---|---|
| 제어 대상 | 상태 업데이트 함수 | 값(value) |
| 사용 시점 | 업데이트의 출처를 알 때 | 제어권이 없을 때(props 등) |
| isPending | 제공 | 제공 안 함 |
| 전형적 사용 | 자신의 setState | 외부에서 받은 props 값 |
Suspense
Suspense는 컴포넌트가 렌더링 준비가 되지 않았을 때 폴백(fallback) UI를 보여주는 메커니즘입니다. React 16.6에서 코드 스플리팅용으로 도입되었고, React 18에서 데이터 페칭과 서버사이드 렌더링까지 지원 범위가 확장되었습니다.
기본 사용법
import { Suspense, lazy } from 'react';
// 동적 import로 코드 스플리팅
const HeavyDashboard = lazy(() => import('./HeavyDashboard'));
function App() {
return (
<Suspense fallback={<div>대시보드 로딩 중...</div>}>
<HeavyDashboard />
</Suspense>
);
}
데이터 페칭과 Suspense
React 18에서는 데이터 페칭 라이브러리(SWR, React Query, Relay 등)와 함께 Suspense를 활용할 수 있습니다.
// SWR과 함께 사용하는 예시
import { Suspense } from 'react';
import useSWR from 'swr';
function UserProfile({ userId }) {
// suspense: true 옵션으로 데이터 준비 전에 Suspense에 알림
const { data: user } = useSWR(`/api/users/${userId}`, fetcher, {
suspense: true,
});
// 이 시점에서 user는 반드시 존재함 (로딩 중이면 여기까지 오지 않음)
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
function App() {
return (
<Suspense fallback={<UserSkeleton />}>
<UserProfile userId={1} />
</Suspense>
);
}
Suspense 중첩
여러 Suspense 경계를 중첩하여 세밀한 로딩 UI를 구성할 수 있습니다.
function Dashboard() {
return (
<div>
{/* 사이드바는 독립적으로 로딩 */}
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
{/* 메인 콘텐츠 영역 */}
<Suspense fallback={<ContentSkeleton />}>
<MainContent />
{/* 중첩된 Suspense — 댓글은 별도로 로딩 */}
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
</Suspense>
</div>
);
}
서버사이드 Suspense (React 18)
React 18에서는 Streaming SSR과 함께 서버에서도 Suspense를 활용할 수 있습니다. 서버는 준비된 부분을 먼저 스트리밍으로 전송하고, 준비되지 않은 부분은 나중에 전송합니다.
// Next.js 13+ App Router에서의 서버 컴포넌트 예시
import { Suspense } from 'react';
async function SlowData() {
// 서버에서 느린 데이터 페칭
const data = await fetchSlowData();
return <div>{data.content}</div>;
}
export default function Page() {
return (
<main>
<h1>빠른 콘텐츠</h1> {/* 즉시 전송 */}
<Suspense fallback={<p>느린 데이터 로딩 중...</p>}>
<SlowData /> {/* 준비되면 스트리밍 */}
</Suspense>
</main>
);
}
정리
| 기능 | 역할 | 주요 사용 사례 |
|---|---|---|
| useTransition | 상태 업데이트를 비긴급으로 표시 + isPending 제공 | 검색, 탭 전환, 필터링 |
| startTransition | 상태 업데이트를 비긴급으로 표시 | 라이브러리, 이벤트 핸들러 |
| useDeferredValue | 값의 업데이트를 지연 | 외부 props로 전달받은 값 |
| Suspense | 비동기 콘텐츠 로딩 중 폴백 UI 표시 | 코드 스플리팅, 데이터 페칭 |
React 18의 Concurrent Features는 복잡한 UI에서 사용자 경험을 크게 향상시킬 수 있는 강력한 도구입니다. 모든 상황에 적용할 필요는 없으며, 무거운 렌더링이나 사용자 입력 반응성이 중요한 곳에 선택적으로 적용하는 것이 좋습니다.