리액트 18의 핵심 기능인 동시성 렌더링(Concurrent Rendering), useTransition, useDeferredValue의 동작 원리와 실전 사용법을 배웁니다.
⚡ 리액트 18 — 동시성의 시대
리액트 18은 동시성 렌더링(Concurrent Rendering)을 도입했습니다. 이것은 리액트의 렌더링 방식을 근본적으로 바꾼 업그레이드입니다. 이전에는 렌더링이 시작되면 완료될 때까지 중단할 수 없었습니다. 무거운 렌더링이 발생하면 UI가 멈추는(jank) 현상이 생겼습니다.
동시성 렌더링은 렌더링을 일시 중지하고 재개하며 우선순위를 부여할 수 있습니다. 긴급한 업데이트(사용자 입력)와 덜 긴급한 업데이트(검색 결과)를 구분해서 처리합니다.
🔄 useTransition — 긴급하지 않은 업데이트 표시
검색창에서 타이핑할 때마다 수천 개의 결과를 필터링하면 타이핑이 버벅거립니다. useTransition은 필터링 업데이트를 "긴급하지 않음"으로 표시해, 타이핑(긴급)이 먼저 처리되도록 우선순위를 부여합니다:
import { useState, useTransition, memo } from 'react'
// 수천 개 아이템 리스트 (일부러 무겁게)
const BigList = memo(function BigList({ filter }: { filter: string }) {
const items = Array.from({ length: 10000 }, (_, i) => `아이템 ${i + 1}`)
.filter((item) => item.includes(filter))
return (
<ul className="h-64 overflow-y-auto space-y-0.5">
{items.map((item, i) => (
<li key={i} className="px-3 py-1 text-sm text-gray-700">{item}</li>
))}
</ul>
)
})
function SearchWithTransition() {
const [query, setQuery] = useState('')
const [filter, setFilter] = useState('') // BigList에 내려줄 값
const [isPending, startTransition] = useTransition()
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.value
setQuery(value) // 입력창 업데이트 — 즉시 (긴급)
// 필터링 업데이트 — 느려도 되는 것 (비긴급)
startTransition(() => {
setFilter(value)
})
}
return (
<div>
<div className="relative">
<input
value={query}
onChange={handleChange}
placeholder="검색..."
className="border rounded-lg px-4 py-2 w-full"
/>
{isPending && (
<span className="absolute right-3 top-2.5 text-xs text-gray-400">필터링 중...</span>
)}
</div>
<BigList filter={filter} />
</div>
)
}
startTransition 안의 상태 업데이트는 우선순위가 낮아집니다. 타이핑 중에는 입력창이 즉시 반응하고, 목록 필터링은 타이핑이 멈출 때 처리됩니다.
⏳ useDeferredValue — 값 기반 지연
useDeferredValue는 useTransition과 비슷하지만, 상태 setter 대신 값을 지연합니다. 외부에서 받은 값(Props)을 지연할 때 유용합니다:
import { useDeferredValue, useMemo } from 'react'
function FilterableList({ query }: { query: string }) {
// query가 빠르게 바뀌어도 deferredQuery는 여유롭게 업데이트
const deferredQuery = useDeferredValue(query)
const filteredItems = useMemo(() => {
return Array.from({ length: 10000 }, (_, i) => `아이템 ${i + 1}`)
.filter((item) => item.includes(deferredQuery))
}, [deferredQuery]) // deferredQuery가 바뀔 때만 재계산
const isStale = query !== deferredQuery // 오래된 값이면 true
return (
<ul style={{ opacity: isStale ? 0.7 : 1, transition: 'opacity 0.2s' }}>
{filteredItems.map((item, i) => (
<li key={i}>{item}</li>
))}
</ul>
)
}
목록이 오래된 값을 보여줄 때 살짝 흐리게 만들어 사용자에게 업데이트 중임을 알릴 수 있습니다.
🆚 useTransition vs useDeferredValue
| 구분 | useTransition | useDeferredValue |
|---|---|---|
| 제어 위치 | 상태 업데이트 하는 곳 | 값을 소비하는 곳 |
| 언제 사용 | 자신이 상태를 바꿀 수 있을 때 | Props로 받은 값을 지연할 때 |
| isPending 접근 | 가능 | 직접 비교 필요 (query !== deferredQuery) |
✅ 정리
- 리액트 18의 동시성 렌더링: 우선순위를 부여해 긴급한 업데이트 먼저 처리.
useTransition: 느려도 되는 상태 업데이트를startTransition으로 감쌉니다.useDeferredValue: Props 값을 지연해서 받아 무거운 렌더링을 늦춥니다.- 두 API 모두 측정 후 필요할 때만 사용 — 대부분의 앱에서는 필요하지 않습니다.
관련 주제
- 데이터 페칭 로딩 에러 성공 상태
- 재시도 로직
- 스피너 토스트 피드백
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 리액트 입문 — 컴포넌트로 만드는 웹
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
댓글
0/1000
불러오는 중...
