React 18 - 동시성 렌더링(useTransition, useDefferedValue)

Take!·2025년 8월 26일
0

React

목록 보기
5/5

React 18의 동시성 렌더링(Concurrent Rendering)은 렌더링 과정을 멈추거나 우선순위를 조절하여 사용자 경험(UX)을 극대화하는 새로운 렌더링 매커니즘이다.


왜 필요한가?

  • 입력창 타이핑 중에 리스트/차트가 무거워서 자꾸 끊길 때
  • 탭 전환·라우팅 시 데이터를 기다리느라 하얀 화면이 오래 보일 때
  • “한 번에” 그리느라 100ms+ 프리즈가 나오는 거대한 컴포넌트가 있을 때

동시성 렌더링은 이런 문제를 우선순위와 가로채기(interrupt)로 풀어줍니다.


동시성 렌더링의 핵심 개념

  • Interruptible Rendering: 길어진 렌더링을 쪼개고, 급한 이벤트가 오면 가로채서 먼저 처리
  • 우선순위 두 레벨
    - 긴급(urgent): 입력값 반영, 클릭 반응 등 즉각적인 상호작용
    • 전환(transition): 결과 필터링, 긴 리스트 렌더 등 지연 가능한 작업
  • Automatic Batching: Timer/Promise 등 비동기 콜백에서도 setState를 자동 배치해 리렌더 횟수 감소
  • 부분 렌더링: Suspense로 부분별 로딩 UI를 보여주며 점진적으로 완성

참고: 동시성 ≠ 멀티스레드. 여전히 단일 스레드에서 “스케줄링이 똑똑해진 것”입니다.


활성화 조건

  • createRoot 사용 시 자동 활성화
<script>

import { createRoot } from 'react-dom/client'
createRoot(document.getElementById('root')!).render(<App />);

</script>

자주 쓰는 API와 예제

1) useTransition / startTransition

  • 긴급 업데이트(입력 반영)와 전환 업데이트(무거운 필터)를 분리
  • 검색창(입력창) 타이핑 시 리스트/차트/마커가 무거워서 버벅일 때
<script>
import { useState, useTransition, useMemo } from 'react'

function Search() {
	const [text, setText] = useState('');
    const [isPending, startTransition] = useTransition();
    
    const [results, setResults] = useState<string[]>([]);
    
    function onChange(e: React.ChangeEvent<HTMLInputElement>) {
    	const value = e.target.value;
        
        //	1) 즉기 반응 - 검색창 텍스트 입력값
        setText(value);
        
        //	2) 무거운 작업은 전환으로 밀어둠
        startTransition(() => {
        	setResults(expensiveFilter(value));
        });
    }
    
    return (
    	<>
        	<input value={text} onChange={onChange} placeholder="검색어 입력" />
            {isPending && <span>검색 중...</span>}
            <ResultList items={results} />
        </>
    )
}
</script>

2) useDefferredValue

  • 특정 값의 느린 복제본을 만들어 무거운 자식에게 넘김
<script>
import { useDeferredValue, useMemo } from 'react';

function Results({ query }: { query: string }) {
  const deferredQuery = useDeferredValue(query);
  const filtered = useMemo(() => expensiveFilter(deferredQuery), [deferredQuery]);
  return <ResultList items={filtered} />;
}
</script>

3) Suspense (데이터/코드 분할 경계)

  • 부분 로딩을 손쉽게 만들고, SSR에서는 스트리밍과 함께 첫 페인트를 빠르게 합니다.**
  • Next.js(App Router): 서버 컴포넌트 + Suspense로 섹션별 로딩 UI가 기본 동작합니다.
<script>
import { Suspense, lazy } from 'react';
const Chart = lazy(() => import('./Chart'));

export default function Dashboard() {
  return (
    <Suspense fallback={<div>차트 불러오는 중…</div>}>
      <Chart />
    </Suspense>
  );
}
</script>

4) Automatic Batching & flushSync

  • React 18부터는 대부분의 비동기 상황에서 자동 배치됩니다.
<script>
// 이 두 setState는 한 번에 배치되어 리렌더 1회로 처리
setTimeout(() => {
  setOpen(true);
  setCount((c) => c + 1);
}, 0);
</script>
  • 즉시 DOM 반영이 필요할 때만 flushSync를 사용!!!
<script>
import { flushSync } from 'react-dom';

flushSync(() => setOpen(false)); // 드물게 사용
</script>

동시성의 "멘탈 모델"

  • 하나의 큰 렌더를 "조각 작업"으로 나눠두고, 더 급한 일이 생기면 중단 -> 다른 일 -> 재개 합니다.
  • "전환 업데이트"는 취소/재시작될 수 있으므로, 순수함수 렌더링을 지키고, 렌더링 중에 외부 사이드이펙트를 만들지 말 것!

통합 예시 (검색 + 결과 리스트 + Suspense)

<script>
// SearchPage.tsx
import { Suspense, useState, useTransition } from 'react';
import { SearchResult } from './SearchResult';

export default function SearchPage() {
  const [q, setQ] = useState('');
  const [isPending, startTransition] = useTransition();

  return (
    <div>
      <input
        value={q}
        onChange={(e) => {
          const v = e.target.value;
          startTransition(() => setQ(v)); // 전환으로 관리
        }}
        placeholder="React 18"
      />
      {isPending && <small>업데이트 중…</small>}

      <Suspense fallback={<div>결과 불러오는 중…</div>}>
        <SearchResult query={q} />
      </Suspense>
    </div>
  );
}

// SearchResult.tsx (예: Next.js 서버 컴포넌트 or 클라에서 SWR/React Query로)
export async function SearchResult({ query }: { query: string }) {
  const data = await fetchResults(query); // 서버라면 스트리밍/Suspense 가능
  return <ul>{data.map((x) => <li key={x.id}>{x.title}</li>)}</ul>;
}
</script>

주의해야 할 점!

  • 동시성을 멀티스레드로 착각
    - 여전히 단일 스레이드이다. 렌더링이 중단 가능해졌을 뿐이다!
  • 렌더링 중 사이드이펙트 (로그, DOM 조작, 네트워크 호출 등)
    - 전환이 다시 시작될 수 있어 중복 실행 위험. 이펙트는 useEffect에서 실행할 것.
  • 외부 스토어 tearing
    - Redux 등 외부 스토어는 useSyncExternalStore를 기반으로 사용하여 일관성을 보장해야 함!
  • 모든 업데이트를 전환으로 감싸기
    - 입력 지연 발생. 긴급 vs 전환을 분리
  • 무차별 flushSync 남용
    - 동시성 이점 상실 + 프리즈 유발!

도입 체크리스트

  • createRoot(또는 App Router)로 동시성 활성화됨
  • 무거운 업데이트를 startTransition으로 분리
  • 리스트/차트에 useDeferredValue 적용 검토
  • 데이터/코드 경계를 Suspense로 감쌈 (부분 로딩)
  • 외부 스토어는 useSyncExternalStore 기반 확인
  • 자동 배치를 전제로 불필요한 리렌더 제거
  • 렌더 중 사이드이펙트 제거(이펙트로 이동)
profile
확장성 있는 설계와 유지보수가 용이한 클린 코드 지향하는 개발자입니다.

0개의 댓글