React 18, 성능을 다시 정의하다: Automatic Batching과 Transition API 실전 활용

React 18은 프론트엔드 개발의 흐름을 다시 쓰고 있습니다. 이번 포스팅에서는 React 18에서 새롭게 등장한 Automatic Batching과 Transition API를 중심으로, 성능 최적화의 실전 예제와 함께 그 진가를 낱낱이 파헤쳐 보겠습니다.

React 18, 성능을 다시 정의하다

목차


1. 서론: 프론트엔드 진화의 분기점, React 18

웹은 끊임없이 진화하고 있습니다. 그리고 그 중심에는 항상 ‘사용자 경험(UX)’이라는 절대적인 기준이 자리 잡고 있죠. 최근 몇 년 동안 프론트엔드 개발은 보다 빠르고, 자연스럽고, 지능적인 UI를 구현하는 방향으로 발전해왔습니다. 이러한 흐름 속에서 React 18은 단순한 버전 업그레이드가 아니라, 새로운 패러다임의 출발점을 의미합니다.

React 18은 성능 최적화를 넘어, **사용자가 체감하는 부드러움**과 **개발자가 체감하는 생산성**을 모두 끌어올리기 위해 설계되었습니다. 특히, 이번 버전에서는 다음과 같은 두 가지 핵심 기능이 주목받고 있습니다.

  • Automatic Batching : 상태 업데이트를 자동으로 묶어 리렌더링을 줄이는 기능
  • Transition API : 긴 작업을 '느린 작업'으로 분리해, 주요 인터랙션을 부드럽게 유지하는 기능

이 두 가지 기능은 단순한 최적화 기법이 아닙니다. **React의 기본 렌더링 모델 자체를 재설계**하는 데 가깝습니다. 따라서 이 글에서는 단순 기능 소개를 넘어, 실제로 어떻게 활용할 수 있는지까지 실전 예제를 통해 깊이 있게 다루어볼 예정입니다.

아직 React 18을 본격적으로 도입하지 않은 프로젝트라도 걱정하지 마세요. 점진적으로, 안전하게 새로운 기능을 적용하는 전략도 함께 소개해드릴 예정입니다.

지금부터 React 18이 제시하는 ‘성능 최적화의 새로운 기준’을 함께 탐험해봅시다.


2. React 18 핵심 개요

React는 2013년 등장 이후 꾸준히 사용자 인터페이스 개발의 판도를 바꿔왔습니다. 그리고 React 18은 그 흐름 속에서 가장 근본적인 진화를 이룬 버전이라 할 수 있습니다. 단순한 API 추가나 성능 개선을 넘어, '병렬성(Concurrency)'을 지원하는 아키텍처를 도입하면서 웹 어플리케이션의 체감 품질을 한 단계 끌어올렸습니다.

2-1. React 18의 주요 목표

  • 병렬성 지원 (Concurrency) : UI 업데이트를 더 세밀하게 제어하여, 사용자의 인풋에 즉각적이고 부드럽게 반응할 수 있게 함.
  • 기본 최적화 강화 : Automatic Batching으로 렌더링 횟수를 줄여 리소스 낭비 방지.
  • 개발자 경험 개선 : 새 Transition API, Suspense 개선 등을 통해 코드를 더 직관적이고 읽기 쉽게 작성할 수 있도록 지원.

2-2. 병렬성이란 무엇인가?

React 18에서 말하는 '병렬성'은 CPU 수준의 멀티스레드 처리를 의미하는 것이 아닙니다. 대신 렌더링 작업을 나누어 **필요할 때 작업을 중단하고**, 더 중요한 업데이트를 먼저 처리할 수 있는 '선택적 렌더링(Interruptible Rendering)'을 가능하게 합니다.

이로 인해, 사용자는 대규모 렌더링 작업 중에도 인터페이스가 멈추지 않고 부드럽게 반응하는 경험을 하게 됩니다. 예를 들어, 대량의 리스트를 렌더링하는 동안에도 검색 입력창은 즉각적으로 반응할 수 있게 됩니다.

2-3. React 18에서 새롭게 추가된 주요 기능 요약

기능 설명
Automatic Batching 여러 상태 업데이트를 자동으로 묶어 한 번에 렌더링 처리
Transition API UI 업데이트를 '긴 작업'과 '짧은 작업'으로 분리하여 사용자 체감 성능 최적화
Suspense 개선 데이터 패칭, 코드 스플리팅 등 비동기 작업을 더 자연스럽게 통합
Concurrent Features startTransition, useDeferredValue 등 새로운 병렬성 API 제공

React 18은 이처럼 '사용자가 체감하는 부드러움'과 '개발자가 느끼는 효율성'을 동시에 잡기 위한 다양한 기능을 품고 있습니다. 이제 본격적으로, Automatic Batching과 Transition API를 중심으로 각 기능을 실전 예제와 함께 깊이 탐구해보겠습니다.


3. Automatic Batching: 렌더링 최적화의 혁신

React는 기본적으로 하나의 이벤트 핸들러 안에서 발생하는 여러 상태(state) 업데이트를 하나로 묶어(batch) 처리합니다. 이를 통해 불필요한 렌더링을 줄이고 성능을 향상시켜왔죠. 그러나 React 17까지는 이벤트 핸들러 외부(예: 비동기 콜백)에서는 자동 배칭이 적용되지 않았습니다.

React 18에서는 이러한 제약이 사라졌습니다. 비동기 함수 내부, setTimeout, Promise 등의 컨텍스트에서도 **자동으로 상태 업데이트를 묶어 처리**할 수 있게 되었습니다. 이를 'Automatic Batching'이라고 부릅니다.

3-1. 기존 방식과의 비교

React 17 이하에서는 다음과 같은 코드가 각각 별도로 렌더링을 유발했습니다.

function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
}
// 이벤트 핸들러 내부에서는 batching OK

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
}, 1000);
// setTimeout 내에서는 각각 따로 렌더링 발생 (React 17)

반면, React 18에서는 다음과 같이 동작합니다.

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
}, 1000);
// setTimeout 내부에서도 자동으로 batching 적용 (React 18)

즉, 이제 개발자는 별도로 batching을 신경 쓰지 않고도, 비동기 상황에서도 자연스럽게 최적화된 렌더링을 기대할 수 있습니다.

3-2. 동작 원리 간단 요약

  • React 18은 setState 호출이 발생하는 'tick'을 감지합니다.
  • 같은 tick 내 여러 상태 변경을 자동으로 묶어, 한 번만 렌더링합니다.
  • tick 종료 시점에 React가 한번에 업데이트를 처리합니다.

3-3. 실전 예제: 네트워크 요청 응답 후 상태 업데이트

실제 상황에서 네트워크 요청 이후 여러 상태를 업데이트해야 하는 경우를 생각해봅시다.

import { useState } from 'react';

function ProfileLoader() {
  const [profile, setProfile] = useState(null);
  const [loading, setLoading] = useState(false);

  const fetchProfile = async () => {
    setLoading(true);
    const res = await fetch('/api/profile');
    const data = await res.json();
    setProfile(data);
    setLoading(false);
  };

  return (
    <div>
      <button onClick={fetchProfile}>Load Profile</button>
      {loading ? <p>Loading...</p> : <p>{profile?.name}</p>}
    </div>
  );
}

위 코드에서 `setProfile`과 `setLoading(false)`는 fetch 호출 이후 연달아 실행되지만, React 18의 Automatic Batching 덕분에 단 한 번만 렌더링이 발생합니다.

3-4. 주의할 점

Automatic Batching은 강력하지만, 모든 경우에 무조건 적용되는 것은 아닙니다.

  • **외부 라이브러리**가 자체적으로 setTimeout 등을 제어할 경우, batching이 깨질 수 있습니다.
  • **수동 Batching**이 필요한 경우는 flushSync() API를 이용해 제어할 수 있습니다.

예를 들어, 사용자가 클릭하자마자 즉시 업데이트를 반영해야 한다면 다음처럼 수동 플러시를 사용할 수 있습니다.

import { flushSync } from 'react-dom';

flushSync(() => {
  setCount(c => c + 1);
});
// 이 안에서는 즉시 업데이트 발생

Automatic Batching은 강력한 기본값이지만, 필요할 때는 세밀한 제어도 가능하다는 점을 기억해두세요.


4. Transition API: 사용자 경험을 부드럽게 만드는 비밀

웹 애플리케이션을 개발하다 보면, 사용자의 입력(Input)과 관련된 업데이트는 최대한 빠르게 처리하고, 검색 결과 필터링처럼 상대적으로 덜 중요한 작업은 약간 느려도 괜찮은 경우가 많습니다.

이런 상황을 명확히 구분하여 **우선순위를 제어**할 수 있게 해주는 것이 바로 Transition API입니다.

4-1. Transition API란 무엇인가?

Transition은 사용자의 인터랙션 중 일부를 '덜 긴급한 작업'으로 분리해서 처리할 수 있도록 하는 메커니즘입니다. React 18에서는 startTransition 함수를 통해 긴 작업을 명시적으로 구분할 수 있습니다.

이를 통해 주요 인터페이스(예: 입력창 반응)는 즉각적으로 유지하고, 긴 작업(예: 대량 리스트 필터링)은 별도로 처리하면서도 UX를 부드럽게 유지할 수 있습니다.

4-2. 기본 사용법

import { startTransition } from 'react';

startTransition(() => {
  // 긴 작업
  setSearchResults(filteredData);
});

위처럼 startTransition 안에 긴 작업을 감싸면, React는 이 작업을 낮은 우선순위로 처리하여 주요 인터랙션이 끊기지 않게 합니다.

4-3. 실전 예제: 검색 입력 필터링 최적화

Transition이 실제로 얼마나 유용한지, 실전 예제를 통해 살펴봅시다.

import { useState, startTransition } from 'react';

function SearchComponent({ items }) {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState(items);

  const handleChange = (e) => {
    const value = e.target.value;
    setQuery(value);

    startTransition(() => {
      const filtered = items.filter(item => 
        item.toLowerCase().includes(value.toLowerCase())
      );
      setResults(filtered);
    });
  };

  return (
    <div>
      <input type="text" value={query} onChange={handleChange} placeholder="Search..." />
      <ul>
        {results.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

이 예제에서는 사용자가 검색어를 입력할 때마다 `query`는 즉시 업데이트되고, 리스트 필터링(`results`)은 Transition을 통해 부드럽게 비동기 처리됩니다.

4-4. 주의사항: Transition 사용 시 알아야 할 것

  • Transition 안에서는 상태 업데이트가 '지연될 수 있음'을 감안해야 합니다.
  • Transition 중에는 isPending 상태를 활용해 로딩 UI를 제공할 수도 있습니다.
  • Transition을 남용하면 오히려 UX가 어색해질 수 있으므로, '덜 중요한 업데이트'에만 사용하는 것이 좋습니다.

4-5. 추가 팁: useTransition 훅 활용

Transition을 더욱 편리하게 관리할 수 있도록 React 18은 useTransition 훅도 제공합니다.

import { useTransition } from 'react';

const [isPending, startTransition] = useTransition();

startTransition(() => {
  setSearchResults(filteredData);
});

if (isPending) {
  // 로딩 스피너 등 표시 가능
}

이렇게 isPending 값을 통해, Transition이 진행 중인지 여부를 감지하여 UX를 세심하게 개선할 수 있습니다.


5. Suspense와 Concurrent Rendering의 시너지

React 18의 진정한 힘은 단일 기능에만 있지 않습니다. SuspenseConcurrent Rendering을 함께 활용하면, 데이터 패칭이나 무거운 렌더링 작업을 신속하고 부드럽게 처리할 수 있습니다.

특히 Suspense는 이제 서버 데이터 패칭까지 지원할 준비를 마쳤고, Concurrent Mode를 통해 긴 작업 중에도 UI의 즉각 반응성을 유지할 수 있습니다.

5-1. Suspense 기본 개념

Suspense는 '로딩 중' 상태를 선언적으로 처리할 수 있는 컴포넌트입니다.

기본 문법은 다음과 같습니다.

import { Suspense } from 'react';

<Suspense fallback={<LoadingSpinner />}>
  <MyComponent />
</Suspense>

이렇게 하면 MyComponent가 로딩 상태일 때 LoadingSpinner를 자동으로 표시할 수 있습니다.

5-2. Concurrent Rendering이란 무엇인가?

Concurrent Rendering은 React가 렌더링을 "나눠서" 처리할 수 있게 만드는 기술입니다. 이를 통해 긴 렌더링 작업도 중단, 재개, 취소가 가능해지며, 사용자 인풋은 끊김 없이 자연스럽게 반응합니다.

쉽게 말하면, '시간이 오래 걸리는 렌더링' 때문에 사용자의 클릭이나 입력이 멈추는 일이 사라진다는 의미입니다.

5-3. Suspense와 Concurrent Rendering 조합 예제

React 18에서는 데이터 패칭까지도 Suspense 안에서 자연스럽게 사용할 수 있습니다. 다음은 간단한 데이터 로딩 컴포넌트 예제입니다.

import { Suspense } from 'react';

function fetchData() {
  let status = 'pending';
  let result;
  const suspender = fetch('/api/data')
    .then(res => res.json())
    .then(
      r => {
        status = 'success';
        result = r;
      },
      e => {
        status = 'error';
        result = e;
      }
    );

  return {
    read() {
      if (status === 'pending') {
        throw suspender;
      } else if (status === 'error') {
        throw result;
      }
      return result;
    }
  };
}

const resource = fetchData();

function DataDisplay() {
  const data = resource.read();
  return <div>{data.message}</div>;
}

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <DataDisplay />
    </Suspense>
  );
}

이 패턴을 사용하면, 데이터가 로딩 중일 때 Suspense의 fallback UI를 보여주고, 데이터가 준비되면 자연스럽게 컴포넌트를 렌더링합니다.

5-4. Suspense + Transition 최적 조합

Transition API와 함께 사용하면 더 강력해집니다. 예를 들어, 사용자가 입력을 시작하면 Transition을 통해 비동기 데이터 요청을 보내고, Suspense로 자연스럽게 로딩 상태를 처리할 수 있습니다.

import { useState, startTransition, Suspense } from 'react';

function SearchPage() {
  const [resource, setResource] = useState(initialResource);

  const handleSearch = (query) => {
    startTransition(() => {
      setResource(fetchData(query));
    });
  };

  return (
    <div>
      <input type="text" onChange={e => handleSearch(e.target.value)} />
      <Suspense fallback={<div>Searching...</div>}>
        <DataDisplay resource={resource} />
      </Suspense>
    </div>
  );
}

이렇게 하면, 사용자가 검색어를 입력하는 즉시 UI는 부드럽게 반응하고, 새로운 결과가 준비되는 동안 깔끔한 로딩 화면이 표시됩니다.

5-5. 주의사항

  • Suspense는 네이티브 데이터 패칭 지원이 아직 베타 단계입니다. (React Query, SWR 같은 라이브러리 활용 추천)
  • Transition과 결합할 때, 느린 네트워크에서는 적절한 로딩 표시 전략이 필요합니다.

Suspense와 Concurrent Rendering은 단독으로도 강력하지만, 서로 조합할 때 그 진정한 가치를 발휘합니다. React 18은 이 두 가지를 통해 진정한 '부드러운 UX'를 실현할 수 있게 되었습니다.


6. 실전 활용 전략: 프로젝트에 React 18 도입하기

React 18은 강력한 기능을 제공하지만, 기존 프로젝트에 무턱대고 도입하기에는 신중함이 필요합니다. 이 단락에서는 React 18을 안전하게 도입하고, Automatic Batching과 Transition API를 효과적으로 활용하는 전략을 소개합니다.

6-1. 기존 프로젝트에 React 18 적용 시 고려사항

  • 패키지 업데이트 : React 18을 사용하려면 React, ReactDOM 버전을 함께 업그레이드해야 합니다.
  • Strict Mode 활성화 : 개발 환경에서는 Strict Mode를 켜서 숨겨진 문제를 조기에 발견하세요.
  • 타사 라이브러리 호환성 확인 : 특히 상태 관리, 라우팅, 서버 사이드 렌더링(SSR) 관련 라이브러리는 React 18 대응 여부를 확인해야 합니다.

6-2. 점진적 적용 방법

React 18의 특징은 점진적 도입이 가능하다는 점입니다. 모든 코드를 한 번에 바꿀 필요가 없습니다.

  • 필요한 곳부터 startTransition이나 useTransition을 적용
  • Suspense를 부분적으로 사용하여 데이터 패칭 최적화
  • Automatic Batching의 이점을 자연스럽게 활용

초기에는 작은 컴포넌트 단위로 새 기능을 적용하고, 점진적으로 확장하는 것이 좋습니다.

6-3. Automatic Batching 도입 체크리스트

체크 포인트 설명
비동기 상태 업데이트 setTimeout, Promise 내 setState가 자동 배칭되는지 확인
flushSync 사용 여부 즉시 렌더링이 필요한 경우 flushSync 활용
버그 및 예외 케이스 배칭이 예상과 다르게 동작하는 경우 디버깅 및 라이브러리 업데이트 고려

6-4. Transition API 적용 체크리스트

  • 긴 작업 분리 : 입력(Input)과 무거운 작업을 명확히 구분
  • isPending 상태 활용 : Transition 진행 중 로딩 UI 제공
  • 불필요한 Transition 남용 금지 : 실제 사용자 체감이 필요한 작업에만 적용

6-5. 성능 측정 방법

React 18 기능 적용 이후에는 성능 개선 효과를 정량적으로 확인하는 것이 중요합니다.

  • React DevTools Profiler 사용 : 렌더링 시간, 렌더링 횟수 비교
  • Web Vitals 지표 측정 : LCP(Largest Contentful Paint), FID(First Input Delay) 개선 여부 확인
  • 실사용자 경험(Real User Monitoring) 데이터 분석

Automatic Batching과 Transition 적용 후, 실제로 렌더링이 줄어들고 체감 반응성이 좋아졌는지 숫자로 확인하는 습관을 들이세요.

6-6. 버그 대응 전략

React 18을 도입한 후 예상치 못한 버그가 발생할 수도 있습니다. 이때는 다음과 같은 원칙을 따르세요.

  • Strict Mode로 재현 테스트
  • flushSync를 사용해 의도적 배칭 해제
  • React 공식 문서와 릴리즈 노트 확인
  • 최신 React 버전 및 관련 라이브러리 업데이트 적용

특히, Concurrent Features는 아직 발전 중인 영역이기 때문에, 세심한 테스트와 단계별 적용이 필수입니다.


7. 마치며: 진짜 '성능 최적화'란 무엇인가

React 18은 단순히 기능 몇 가지를 추가한 버전이 아닙니다. **사용자 경험(UX)**이라는 본질적인 목표를 향해, 프레임워크의 뿌리부터 다시 설계한 결과물입니다.

Automatic Batching은 렌더링의 최적화를 자동화했고, Transition API는 사용자 인터랙션과 무거운 작업의 경계를 명확히 나누었습니다. Suspense와 Concurrent Rendering은 부드럽고 끊김 없는 웹 경험을 가능하게 했습니다.

하지만 진정한 성능 최적화는 단순히 "빠른" 것만을 의미하지 않습니다. **사용자가 느끼는 부드러움**, **개발자가 유지 보수하기 쉬운 코드**, 그리고 **미래 변화에 유연하게 대응할 수 있는 설계**가 함께 어우러질 때 비로소 완성됩니다.

이제 질문을 던져봅니다.

당신이 만드는 웹은, 단순히 빠른가요? 아니면, 정말 부드럽고 매끄러운가요?

React 18은 준비되었습니다. 이제, 그 다음은 여러분의 선택입니다.

Comments