프로그래밍/ReactJS

리액트 동시성 모드와 서스펜스: 부드러운 사용자 경험을 위한 강력한 조합

shimdh 2025. 10. 17. 09:12
728x90

리액트 애플리케이션을 개발하다 보면, 사용자 경험(UX)을 최적화하는 것이 가장 큰 도전 중 하나입니다. 특히 데이터 로딩이나 복잡한 연산이 발생할 때 UI가 멈추거나 버벅거리는 현상은 사용자 불만을 유발할 수밖에 없습니다. 다행히 리액트는 이러한 문제를 해결하기 위해 동시성 모드(Concurrent Mode)서스펜스(Suspense) 라는 혁신적인 기능을 제공합니다. 이 두 기능은 애플리케이션의 성능을 끌어올리고, 사용자에게 더 부드럽고 반응적인 경험을 선사합니다.

오늘 이 글에서는 동시성 모드와 서스펜스가 어떻게 작동하는지, 그리고 이들을 결합했을 때의 강력한 시너지를 깊이 탐구해 보겠습니다. React 18 이상 버전에서 본격적으로 지원되는 이 기능들을 통해, 당신의 앱도 더 스마트하게 업그레이드할 수 있을 겁니다.

728x90

동시성 모드: 멀티태스킹 UI의 비밀 병기

리액트 동시성 모드는 애플리케이션이 사용자 인터페이스를 방해하지 않으면서 여러 작업을 병렬로 처리할 수 있게 해주는 고급 기능입니다. 간단히 말해, 데이터 페칭 같은 무거운 작업이 백그라운드에서 실행되는 동안에도 UI는 여전히 반응적이고, 사용자가 자유롭게 상호작용할 수 있습니다. 이는 작업의 우선순위를 동적으로 조정함으로써 가능하며, 결과적으로 더 부드러운 UX를 실현합니다.

동시성 모드의 핵심 개념

  1. 동시성(Concurrency):
    리액트는 기본적으로 단일 스레드에서 동작하지만, 동시성 모드는 큰 업데이트를 작은 청크(chunk)로 나누어 처리합니다. 이로 인해 사용자가 앱을 스크롤하거나 버튼을 클릭하는 등의 작업을 방해하지 않고, 여러 태스크를 동시에 '준비'할 수 있습니다. 마치 멀티태스킹 운영체제처럼, UI가 멈추지 않고 효율적으로 돌아갑니다.
  2. 중단 가능한 렌더링(Interruptible Rendering):
    동시성 모드의 가장 매력적인 점은 렌더링 과정을 중단할 수 있다는 것입니다. 예를 들어, 긴 데이터 로딩 중에 사용자가 갑자기 입력 필드를 클릭하면, 리액트는 현재 렌더링을 멈추고 사용자 입력에 우선 응답합니다. 이 기능 덕분에 앱은 항상 최신 상태를 유지하며, 지연이나 프리즈 현상을 최소화합니다.
  3. 우선순위 부여(Prioritization):
    모든 작업이 동등하지 않습니다. 사용자 입력(예: 타이핑) 같은 고우선순위 작업은 즉시 처리되며, 백그라운드 페칭 같은 저우선순위 작업은 나중에 밀려납니다. 이를 통해 리액트는 리소스를 효율적으로 분배하여, 중요한 순간에 앱이 '느껴지지 않을 만큼' 빠르게 반응합니다.

동시성 모드를 활성화하려면 createRoot API를 사용하세요. (React 18부터 기본 지원)

import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'));
root.render(<App />);

서스펜스: 비동기 작업의 우아한 처리기

서스펜스(Suspense)는 컴포넌트가 준비되지 않았을 때(예: 데이터 로딩 중) UI를 '일시 중지'하고, 대체 콘텐츠(fallback)를 보여주는 컴포넌트입니다. API 호출, 코드 스플리팅(동적 임포트), 또는 캐싱된 데이터 페칭과 완벽하게 연동되며, 로딩 상태를 깔끔하게 관리합니다. 더 이상 복잡한 로딩 스피너나 에러 핸들링을 위해 boilerplate 코드를 작성할 필요가 없습니다.

서스펜스 사용의 실제 예시

사용자 프로필을 API에서 가져오는 컴포넌트를 상상해 보세요. React의 Suspense와 함께 React.lazy를 사용하면 지연 로딩을 쉽게 구현할 수 있습니다. (여기서는 코드 스플리팅 예시로, 실제 데이터 페칭은 use 훅이나 캐싱 라이브러리와 결합하는 것을 추천합니다.)

import React, { Suspense, lazy } from 'react';

// 동적 임포트로 UserProfile 컴포넌트 지연 로드 (코드 스플리팅 예시)
const UserProfile = lazy(() => import('./UserProfile'));

function App() {
  return (
    <div>
      <h1>사용자 프로필</h1>
      <Suspense fallback={<div>로딩 중...</div>}>
        <UserProfile />
      </Suspense>
    </div>
  );
}
  • React.lazy: 컴포넌트를 필요할 때만 로드합니다. (네트워크 요청으로 번들 다운로드)
  • Suspensefallback: 로딩 중에 스피너나 플레이스홀더를 보여줍니다.
  • 이 구조는 사용자에게 즉각적인 피드백을 주며, 로딩 시간을 기다리는 동안 앱이 '죽은' 느낌을 주지 않습니다.

데이터 페칭 예시로 확장하면, React 18의 use 훅을 활용할 수 있습니다:

import { Suspense, use } from 'react';

async function fetchUserProfile() {
  const response = await fetch('/api/user');
  return response.json();
}

function UserProfile() {
  const profile = use(fetchUserProfile());
  return <div>{profile.name} ({profile.age}세)</div>;
}

function App() {
  return (
    <Suspense fallback={<div>프로필 로딩 중...</div>}>
      <UserProfile />
    </Suspense>
  );
}

이처럼 서스펜스는 비동기 로직을 컴포넌트 트리에 자연스럽게 녹여냅니다.

서스펜스리스트(SuspenseList): 여러 서스펜스 관리하기

개별 서스펜스를 다루는 Suspense와 달리, SuspenseList는 여러 서스펜스 컴포넌트를 그룹화하여 로딩 순서와 방식을 제어합니다. 복잡한 페이지(예: 대시보드)에서 여러 데이터 소스가 동시에 로드될 때, UI가 한 번에 쏟아지거나 순차적으로 나타나도록 설정할 수 있습니다. 이는 시각적 혼란을 줄이고, 사용자 경험을 세밀하게 튜닝합니다.

서스펜스리스트 사용의 실제 예시

여러 사용자 프로필을 순차적으로 로드하는 리스트를 구현해 보죠. 각 프로필이 독립적으로 로드되지만, SuspenseList가 전체 흐름을 조율합니다.

import React, { Suspense, SuspenseList } from 'react';

function UserProfile({ id }) {
  // 가상의 비동기 데이터 페칭
  const profile = use(fetchUserProfile(id));
  return <div>사용자 {id}: {profile.name}</div>;
}

function UserProfiles({ ids }) {
  return (
    <SuspenseList revealOrder="forwards" tail="collapsed">
      {ids.map(id => (
        <Suspense key={id} fallback={<div>프로필 로딩 중...</div>}>
          <UserProfile id={id} />
        </Suspense>
      ))}
    </SuspenseList>
  );
}

// 사용 예: <UserProfiles ids={[1, 2, 3]} />
  • revealOrder="forwards": 자식 컴포넌트가 로드될 때 순차적으로(위에서 아래로) 나타납니다. ("backwards"나 "together" 옵션도 가능)
  • tail="collapsed": 모든 항목이 로드될 때까지 하나의 fallback만 보여줍니다. (collapsed 대신 "shown"으로 하면 모든 fallback 표시)
  • 이 설정으로 여러 로딩 스피너가 동시에 나타나는 '로딩 지옥'을 피할 수 있습니다.

서스펜스와 동시성 모드의 시너지: 왜 이 조합이 강력한가?

동시성 모드와 서스펜스를 함께 사용하면 리액트 앱이 한 단계 업그레이드됩니다. 주요 이점은 다음과 같습니다:

  1. 향상된 사용자 경험:
    로딩 중 빈 화면 대신 즉각적인 fallback(스피너, 스켈레톤 UI)을 보여주며, 사용자 입력이 항상 우선됩니다. 앱이 '항상 살아 있는' 느낌을 줍니다.
  2. 더 나은 성능 관리:
    중단 가능한 렌더링과 우선순위 덕분에, 고부하 상황에서도 60fps를 유지하기 쉽습니다. 특히 모바일 기기에서 빛을 발합니다.
  3. 간소화된 개발 로직:
    복잡한 로딩 상태(예: isLoading 플래그)를 직접 관리할 필요 없이, 서스펜스의 내장 메커니즘을 활용합니다. 코드가 간결해지고, 버그가 줄어듭니다.

추가 팁: Relay나 Next.js 같은 라이브러리와 결합하면 데이터 페칭이 더 강력해집니다. 동시성 모드는 StrictMode에서 테스트하세요!

결론: 당신의 리액트 앱을 업그레이드할 때

리액트 동시성 모드와 서스펜스(및 서스펜스리스트)는 단순한 기능이 아니라, 현대 웹 앱의 표준입니다. 이들을 이해하고 적용하면 비동기 작업을 우아하게 처리하며, 사용자에게 '마법 같은' 부드러운 경험을 제공할 수 있습니다. 오늘부터 기존 프로젝트에 도입해 보세요 – 로딩 지연이 사라지고, 사용자 리뷰가 달라질 겁니다!

728x90