react · high priority
React Concurrent — 중단 가능한 렌더링과 Suspense
Lane 우선순위 · `useTransition` · `useDeferredValue` · Streaming SSR
학습 개요
탄생 배경
쉬운 설명
복잡한 개념을 실생활 비유로 설명합니다.
“응급실 트리아지와 호출 보드”
예전 응급실(Legacy) 은 도착 순서대로만 처리해 중증 환자가 기다렸습니다. Concurrent 트리아지는 호출 보드(Lane) 에 우선순위를 적어두고, 가벼운 환자(Transition) 를 보다가 중증(Sync) 이 오면 진료를 멈추고 그쪽으로 갑니다. Streaming SSR 은 큰 검사 결과 한 장을 다 기다리지 않고 *준비된 항목부터 봉투에 담아* 환자에게 차례로 전달하는 것이고, Selective Hydration 은 환자가 직접 만진 항목부터 정리해 주는 것입니다.
핵심 개념
Legacy (`ReactDOM.render`) vs Concurrent (`createRoot`)
ReactDOM.render(...)진입점- 한 번 시작한 렌더는 끝까지 동기
- 이벤트 핸들러 외부의
setState는 배칭 안 됨 - 우선순위 표현 수단이 사실상 없음
- Suspense 는 코드 분할용으로만 안정 지원
createRoot(container).render(<App/>)- 렌더가 노드 단위로 yield → 입력 우선
- setTimeout / Promise 안에서도 자동 배칭
useTransition/useDeferredValue로 Lane 표현- Suspense 가 데이터·Streaming SSR 까지 확장
"Concurrent Mode" 라는 별도 모드는 없다
초기엔 전역 토글로 켜는 모드였지만, React 18 부터는 *Concurrent Features* 로 이름이 바뀌고 createRoot 로 마운트하는 순간 기본 활성화됩니다. useTransition·Suspense·자동 배칭 등 *기능을 쓰는 순간* 동시성 의미론에 들어갑니다.
1// React 18, createRoot 로 마운트한 앱2function App() {3 const [a, setA] = useState(0);4 const [b, setB] = useState(0);5 return (6 <button7 onClick={() => {8 setTimeout(() => {9 setA((x) => x + 1);10 setB((x) => x + 1); // ← 같은 tick 안의 두 setState11 }, 0);12 }}13 >14 go15 </button>16 );17}18// 클릭 한 번에 컴포넌트는 몇 번 리렌더되는가?
정답
1 번. (React 17 에서는 2 번이었다)
해설
React 18 의 *automatic batching* 은 setTimeout/Promise/native event 에서 호출된 setState 도 같은 tick 안에 묶어 한 번만 렌더합니다. 17 까지는 React 이벤트 핸들러 바깥의 setState 는 각각 동기 렌더를 트리거했습니다.
흔한 오답
- setTimeout 안의 setState 는 항상 즉시 렌더된다고 생각 — 18 에서는 묶임.
flushSync를 안 써도 모든 업데이트가 자동으로 묶일 거라고 오해 — 의도적으로 즉시 반영해야 하는 경우엔flushSync(() => setX(...))가 필요.
실무 적용
어떤 상황에서 사용하는가
검색창에 글자를 칠 때마다 무거운 결과 리스트가 즉시 다시 그려져 1~2 글자씩 끊긴다는 사용자 리포트. 동시에 모바일에서 첫 페인트가 느리다는 LCP 경고.
어떻게 적용하는가
(1) `setQuery` 는 그대로, 결과 갱신은 `startTransition` 으로. (2) 결과 컴포넌트는 `useDeferredValue(query)` 로 한 박자 뒤 렌더. (3) 결과 영역을 `<Suspense fallback={<Skeleton/>}>` 로 감싸 새 결과 도착 전 *이전 결과* 유지. (4) 서버 측은 App Router 의 `loading.tsx` + 페이지 분할로 Streaming SSR 활용. (5) React Profiler "Concurrent" 모드로 SyncLane vs TransitionLane commit 비율 확인.
흔한 실수와 안티패턴
- 입력처럼 *즉시 보여야 하는* setState 까지 Transition 으로 감싸면 오히려 끊겨 보인다.
- Suspense 경계 안에서 *매 렌더 새 promise* 를 만들면 무한 서스펜드 — 캐시·`use cache` 필요.
- `flushSync` 로 우회해 측정/포커스를 강제 동기화하면 Concurrent 이점을 잃음. 정말 필요한 곳만.
- Strict Mode 의 이중 호출을 우회하려고 ref 에 부수효과를 숨기는 것 — 더 깊은 버그를 만듦.
흔한 오해
"Concurrent Mode 는 React 가 알아서 빠르게 만들어 준다."
교정체감 속도(smoothness) 를 위한 *우선순위 표현 도구* 이지, JS 실행을 빠르게 하는 것은 아니다.
왜 중요Concurrent 가 줄여주는 것은 메인 스레드 점유로 인한 *대기* 이지 계산 자체의 비용이 아니다. 무거운 함수는 여전히 메모이즈/Web Worker 로 분리해야 한다.
"`useTransition` 은 debounce 와 같다."
교정debounce 는 호출 지연, Transition 은 우선순위 하향. 원리도 효과도 다르다.
왜 중요Transition 은 *즉시 시작* 하되 React 가 양보·중단할 수 있게 한다. debounce 는 멈춘 사이엔 함수가 아예 실행되지 않는다.
"Suspense 는 데이터 페칭 라이브러리에서만 쓰는 것."
교정React 19 의 `use(promise)` + Server Components 의 `await` 가 1급 시민이라 *프레임워크 위에서* 누구나 쓰는 도구가 됐다.
왜 중요Next.js App Router · Remix(이젠 React Router v7) · TanStack Start 모두 Suspense 를 데이터/로딩 UX 의 표준 모델로 채택했다.
면접 질문
답변 방향 힌트
"중단 가능성" + "Lane 비트마스크" + "automatic batching" 세 축으로.
반드시 언급할 키워드
- 17: 동기 재귀, 콜 스택이 진행 상태, 중단 불가
- 18: Fiber 노드 단위로 yield, 시간 슬라이스 ~5ms
- 우선순위는 단일 숫자 → 32 비트 Lane 마스크
- automatic batching: setTimeout/Promise 안의 setState 도 묶임
- Strict Mode 의 이중 호출은 부수효과 검증을 위한 의도된 동작
- `createRoot` 가 진입점, 모드가 아니라 기본 동작
예상 꼬리 질문
- Lane 모델이 expirationTime 을 대체한 이유는?
- `flushSync` 는 언제 어떤 비용을 감수하고 쓰나요?
자기 점검
`useTransition` 으로 감싼 setState 가 진행 중일 때 같은 setState 가 또 호출되면 어떻게 되는가?
기대 키워드
자주 하는 오해
"큐에 쌓여 순서대로 실행" 으로 답하기 쉽지만, Concurrent 에서는 진행 중이던 렌더가 *폐기되고* 최신 상태로 다시 시작된다.
Suspense fallback 이 의도치 않게 다시 보이는 흔한 원인은?
기대 키워드
자주 하는 오해
"Suspense 가 항상 fallback 을 우선 표시한다" 는 오해. transition/deferred 안의 갱신은 fallback 대신 *이전 콘텐츠* 를 유지한다.
automatic batching 이 17 과 결정적으로 달라진 점 한 가지를 한 문장으로.
기대 키워드
자주 하는 오해
"이벤트 핸들러 안에서만 묶인다" 는 17 의 모델이고, 18 부터는 어디서 호출되든 같은 tick 안의 setState 가 묶인다.
학습 자료
- React 19 — Release NotesActions, `use(promise)`, `useActionState`, document metadata, Server Components 안정화 등 Concurrent 위에 얹힌 19 의 변화 정리.Docreact.dev · 2024
- `useTransition` — Reference훅 API · isPending 사용법 · 비동기 액션과의 결합 · Suspense fallback 억제 동작.Docreact.dev
- `useDeferredValue` — Reference값을 받는 쪽에서 한 박자 늦추는 패턴, isStale 비교, Suspense 와의 결합.Docreact.dev
- `<Suspense>` — Reference경계 의미론, fallback 재표시 조건, Streaming SSR · Selective Hydration 설명.Docreact.dev
- React 18 — New Suspense SSR ArchitectureStreaming SSR + Selective Hydration 이 등장한 배경과 흐름을 1차 자료로 설명한 RFC 노트.BlogReact Working Group
- 토스 — React 18 Concurrent Features 활용기실서비스에서 `useTransition`/`Suspense` 를 적용하며 만난 패턴과 함정.Blog토스 기술블로그