React 18의 새로운 기능인 Concurrent Features와 Suspense에 대해 설명해주세요.
답변 포인트
렌더링 우선순위와 비동기 데이터 로딩을 생각해보세요.
정답 및 해설
빠른 요약
Concurrent Features: 렌더링을 중단/재개하여 우선순위에 따라 처리합니다. - useTransition: 비긴급 업데이트를 표시 - startTransition: UI 블로킹 없이 상태 업데이트 - useDeferredValue: 긴급하지 않은 값의 업데이트 지연 Suspense: 비동기 데이터 로딩 중 폴백 UI를 표시합니다.
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에서 사용자 경험을 크게 향상시킬 수 있는 강력한 도구입니다. 모든 상황에 적용할 필요는 없으며, 무거운 렌더링이나 사용자 입력 반응성이 중요한 곳에 선택적으로 적용하는 것이 좋습니다.