전체 목록
ReactMedium#018

React 18의 새로운 기능인 Concurrent Features와 Suspense에 대해 설명해주세요.

#React#React18#Concurrent#Suspense

답변 포인트

렌더링 우선순위와 비동기 데이터 로딩을 생각해보세요.

정답 및 해설

빠른 요약

Concurrent Features: 렌더링을 중단/재개하여 우선순위에 따라 처리합니다. - useTransition: 비긴급 업데이트를 표시 - startTransition: UI 블로킹 없이 상태 업데이트 - useDeferredValue: 긴급하지 않은 값의 업데이트 지연 Suspense: 비동기 데이터 로딩 중 폴백 UI를 표시합니다.

React 18은 Concurrent Mode를 기반으로 렌더링 방식을 근본적으로 개선했습니다. 기존 React는 렌더링이 시작되면 중단 없이 끝까지 실행되었지만, Concurrent Features를 통해 렌더링을 중단하고, 재개하고, 우선순위에 따라 처리할 수 있게 되었습니다.

Concurrent Features란?

Concurrent(동시성) 란 여러 작업을 동시에 처리하는 것처럼 보이게 하는 개념입니다. React 18은 렌더링 작업에 우선순위를 부여하여, 긴급한 업데이트(사용자 입력 등)가 덜 긴급한 업데이트(데이터 페칭 결과 표시 등)보다 먼저 처리되도록 합니다.

기존 React의 한계

JavaScript
// 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(전환 래퍼 함수)을 제공합니다.

JavaScript
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>
  );
}

isPendingtrue인 동안 사용자에게 로딩 표시를 보여주고, 전환이 완료되면 새로운 결과를 표시합니다. 이 방식으로 타이핑 중에도 입력창이 즉각 반응하면서, 무거운 검색 결과 렌더링은 백그라운드에서 처리됩니다.

startTransition

startTransition은 훅 없이도 사용할 수 있는 함수형 API입니다. 컴포넌트 외부나 이벤트 핸들러에서 직접 사용할 수 있습니다.

JavaScript
import { startTransition } from 'react';

// 라우터나 외부 라이브러리에서 활용
function navigateTo(page) {
  startTransition(() => {
    // 페이지 전환은 비긴급으로 처리
    setCurrentPage(page);
  });
}

useTransition vs startTransition 비교

구분useTransitionstartTransition
사용 위치함수 컴포넌트 내부어디서든 사용 가능
isPending제공 (로딩 상태 추적)제공 안 함
주요 용도컴포넌트 내 전환라이브러리, 유틸 함수

useDeferredValue

useDeferredValue는 특정 값의 업데이트를 지연시켜, 더 긴급한 렌더링이 먼저 처리되도록 합니다. debounce와 비슷하지만, 고정된 딜레이 없이 React가 동적으로 타이밍을 결정합니다.

JavaScript
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

구분useTransitionuseDeferredValue
제어 대상상태 업데이트 함수값(value)
사용 시점업데이트의 출처를 알 때제어권이 없을 때(props 등)
isPending제공제공 안 함
전형적 사용자신의 setState외부에서 받은 props 값

Suspense

Suspense는 컴포넌트가 렌더링 준비가 되지 않았을 때 폴백(fallback) UI를 보여주는 메커니즘입니다. React 16.6에서 코드 스플리팅용으로 도입되었고, React 18에서 데이터 페칭과 서버사이드 렌더링까지 지원 범위가 확장되었습니다.

기본 사용법

JavaScript
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를 활용할 수 있습니다.

JavaScript
// 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를 구성할 수 있습니다.

JavaScript
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를 활용할 수 있습니다. 서버는 준비된 부분을 먼저 스트리밍으로 전송하고, 준비되지 않은 부분은 나중에 전송합니다.

JavaScript
// 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에서 사용자 경험을 크게 향상시킬 수 있는 강력한 도구입니다. 모든 상황에 적용할 필요는 없으며, 무거운 렌더링이나 사용자 입력 반응성이 중요한 곳에 선택적으로 적용하는 것이 좋습니다.

관련 질문

같은 카테고리/태그 기준