FEInterview Prep

react · high priority

React Concurrent — 중단 가능한 렌더링과 Suspense

Lane 우선순위 · `useTransition` · `useDeferredValue` · Streaming SSR

advanced 난이도5시간토스카카오네이버배민라인당근
시작 전
이해도
매우 낮음

학습 개요

탄생 배경

쉬운 설명

복잡한 개념을 실생활 비유로 설명합니다.

응급실 트리아지와 호출 보드

예전 응급실(Legacy) 은 도착 순서대로만 처리해 중증 환자가 기다렸습니다. Concurrent 트리아지는 호출 보드(Lane) 에 우선순위를 적어두고, 가벼운 환자(Transition) 를 보다가 중증(Sync) 이 오면 진료를 멈추고 그쪽으로 갑니다. Streaming SSR 은 큰 검사 결과 한 장을 다 기다리지 않고 *준비된 항목부터 봉투에 담아* 환자에게 차례로 전달하는 것이고, Selective Hydration 은 환자가 직접 만진 항목부터 정리해 주는 것입니다.

핵심 개념

Legacy (`ReactDOM.render`) vs Concurrent (`createRoot`)

Legacy (~React 17)
  • ReactDOM.render(...) 진입점
  • 한 번 시작한 렌더는 끝까지 동기
  • 이벤트 핸들러 외부의 setState 는 배칭 안 됨
  • 우선순위 표현 수단이 사실상 없음
  • Suspense 는 코드 분할용으로만 안정 지원
Concurrent (React 18+)
  • createRoot(container).render(<App/>)
  • 렌더가 노드 단위로 yield → 입력 우선
  • setTimeout / Promise 안에서도 자동 배칭
  • useTransition/useDeferredValue 로 Lane 표현
  • Suspense 가 데이터·Streaming SSR 까지 확장

"Concurrent Mode" 라는 별도 모드는 없다

초기엔 전역 토글로 켜는 모드였지만, React 18 부터는 *Concurrent Features* 로 이름이 바뀌고 createRoot 로 마운트하는 순간 기본 활성화됩니다. useTransition·Suspense·자동 배칭 등 *기능을 쓰는 순간* 동시성 의미론에 들어갑니다.

먼저 출력 순서를 예측해보세요
예측 문제tsx
1// React 18, createRoot 로 마운트한 앱
2function App() {
3 const [a, setA] = useState(0);
4 const [b, setB] = useState(0);
5 return (
6 <button
7 onClick={() => {
8 setTimeout(() => {
9 setA((x) => x + 1);
10 setB((x) => x + 1); // ← 같은 tick 안의 두 setState
11 }, 0);
12 }}
13 >
14 go
15 </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 가 또 호출되면 어떻게 되는가?

기대 키워드

폐기재시작최신 입력 우선Lane

자주 하는 오해

"큐에 쌓여 순서대로 실행" 으로 답하기 쉽지만, Concurrent 에서는 진행 중이던 렌더가 *폐기되고* 최신 상태로 다시 시작된다.

Suspense fallback 이 의도치 않게 다시 보이는 흔한 원인은?

기대 키워드

이미 보이던 트리재서스펜드transition 으로 감싸지 않음deferred 미사용

자주 하는 오해

"Suspense 가 항상 fallback 을 우선 표시한다" 는 오해. transition/deferred 안의 갱신은 fallback 대신 *이전 콘텐츠* 를 유지한다.

automatic batching 이 17 과 결정적으로 달라진 점 한 가지를 한 문장으로.

기대 키워드

setTimeoutPromisenative event같은 tick한 번만 렌더

자주 하는 오해

"이벤트 핸들러 안에서만 묶인다" 는 17 의 모델이고, 18 부터는 어디서 호출되든 같은 tick 안의 setState 가 묶인다.

학습 자료