한국어

React의 자동 배치 기능에 대한 종합 가이드입니다. 이점, 한계, 그리고 더 부드러운 애플리케이션 성능을 위한 고급 최적화 기법을 알아봅니다.

React 배치(Batching): 상태 업데이트 최적화로 성능 향상하기

끊임없이 발전하는 웹 개발 환경에서 애플리케이션 성능 최적화는 매우 중요합니다. 사용자 인터페이스 구축을 위한 선도적인 자바스크립트 라이브러리인 React는 효율성을 높이기 위한 여러 메커니즘을 제공합니다. 그중 하나가 바로 종종 보이지 않는 곳에서 작동하는 배치(batching)입니다. 이 글에서는 React 배치의 개념, 이점, 한계, 그리고 더 부드럽고 반응성 좋은 사용자 경험을 제공하기 위한 상태 업데이트 최적화 고급 기법에 대해 종합적으로 탐구합니다.

React 배치란 무엇인가?

React 배치는 React가 여러 상태 업데이트를 하나의 리렌더링으로 그룹화하는 성능 최적화 기법입니다. 즉, 각 상태 변경마다 컴포넌트를 여러 번 리렌더링하는 대신, React는 모든 상태 업데이트가 완료될 때까지 기다렸다가 단일 업데이트를 수행합니다. 이는 리렌더링 횟수를 크게 줄여 성능을 개선하고 사용자 인터페이스의 반응성을 높입니다.

React 18 이전에는 배치가 React 이벤트 핸들러 내에서만 발생했습니다. setTimeout, 프로미스(promise), 네이티브 이벤트 핸들러 내의 상태 업데이트와 같이 이러한 핸들러 외부의 상태 업데이트는 배치 처리되지 않았습니다. 이로 인해 예기치 않은 리렌더링과 성능 병목 현상이 종종 발생했습니다.

React 18에 자동 배치가 도입되면서 이 한계가 극복되었습니다. 이제 React는 다음과 같은 더 많은 시나리오에서 상태 업데이트를 자동으로 배치합니다:

React 배치의 이점

React 배치의 이점은 상당하며 사용자 경험에 직접적인 영향을 미칩니다:

React 배치의 작동 방식

React의 배치 메커니즘은 조정(reconciliation) 프로세스에 내장되어 있습니다. 상태 업데이트가 트리거되면 React는 즉시 컴포넌트를 리렌더링하지 않습니다. 대신 업데이트를 큐에 추가합니다. 짧은 시간 내에 여러 업데이트가 발생하면 React는 이를 단일 업데이트로 통합합니다. 그런 다음 이 통합된 업데이트를 사용하여 컴포넌트를 한 번만 리렌더링하여 모든 변경 사항을 한 번에 반영합니다.

간단한 예시를 살펴보겠습니다:


import React, { useState } from 'react';

function ExampleComponent() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  const handleClick = () => {
    setCount1(count1 + 1);
    setCount2(count2 + 1);
  };

  console.log('Component re-rendered');

  return (
    <div>
      <p>Count 1: {count1}</p>
      <p>Count 2: {count2}</p>
      <button onClick={handleClick}>Increment Both</button>
    </div>
  );
}

export default ExampleComponent;

이 예시에서 버튼을 클릭하면 setCount1setCount2가 동일한 이벤트 핸들러 내에서 호출됩니다. React는 이 두 상태 업데이트를 배치 처리하고 컴포넌트를 단 한 번만 리렌더링합니다. 콘솔에는 클릭당 한 번만 "Component re-rendered"가 기록되어 배치가 작동하는 것을 보여줍니다.

배치되지 않는 업데이트: 배치가 적용되지 않는 경우

React 18에서는 대부분의 시나리오에 자동 배치가 도입되었지만, 배치를 우회하고 React가 즉시 컴포넌트를 업데이트하도록 강제하고 싶은 상황이 있을 수 있습니다. 이는 일반적으로 상태 업데이트 직후 업데이트된 DOM 값을 읽어야 할 때 필요합니다.

React는 이를 위해 flushSync API를 제공합니다. flushSync는 React가 보류 중인 모든 업데이트를 동기적으로 플러시하고 즉시 DOM을 업데이트하도록 강제합니다.

다음은 예시입니다:


import React, { useState } from 'react';
import { flushSync } from 'react-dom';

function ExampleComponent() {
  const [text, setText] = useState('');

  const handleChange = (event) => {
    flushSync(() => {
      setText(event.target.value);
    });
    console.log('Input value after update:', event.target.value);
  };

  return (
    <input type="text" value={text} onChange={handleChange} />
  );
}

export default ExampleComponent;

이 예시에서는 flushSync를 사용하여 입력 값이 변경된 직후 text 상태가 업데이트되도록 합니다. 이를 통해 다음 렌더링 주기를 기다리지 않고 handleChange 함수에서 업데이트된 값을 읽을 수 있습니다. 하지만 flushSync는 성능에 부정적인 영향을 미칠 수 있으므로 신중하게 사용해야 합니다.

고급 최적화 기법

React 배치가 상당한 성능 향상을 제공하지만, 애플리케이션의 성능을 더욱 향상시키기 위해 사용할 수 있는 추가적인 최적화 기법이 있습니다.

1. 함수형 업데이트 사용하기

이전 값을 기반으로 상태를 업데이트할 때는 함수형 업데이트를 사용하는 것이 가장 좋습니다. 함수형 업데이트는 특히 비동기 작업이나 배치 업데이트가 포함된 시나리오에서 가장 최신 상태 값으로 작업하고 있음을 보장합니다.

다음 대신:


setCount(count + 1);

다음을 사용하세요:


setCount((prevCount) => prevCount + 1);

함수형 업데이트는 오래된 클로저(stale closures)와 관련된 문제를 방지하고 정확한 상태 업데이트를 보장합니다.

2. 불변성

상태를 불변(immutable)으로 취급하는 것은 React에서 효율적인 렌더링을 위해 매우 중요합니다. 상태가 불변일 때, React는 이전 상태와 새 상태 값의 참조를 비교하여 컴포넌트를 리렌더링해야 하는지 신속하게 판단할 수 있습니다. 참조가 다르면 React는 상태가 변경되었음을 알고 리렌더링이 필요하다고 판단합니다. 참조가 같다면 React는 리렌더링을 건너뛰어 귀중한 처리 시간을 절약할 수 있습니다.

객체나 배열로 작업할 때 기존 상태를 직접 수정하지 마세요. 대신, 원하는 변경 사항을 적용한 객체나 배열의 새 복사본을 만드세요.

예를 들어, 다음 대신:


const updatedItems = items;
updatedItems.push(newItem);
setItems(updatedItems);

다음을 사용하세요:


setItems([...items, newItem]);

전개 연산자(...)는 기존 항목과 새 항목이 끝에 추가된 새 배열을 만듭니다.

3. 메모이제이션(Memoization)

메모이제이션은 비용이 많이 드는 함수 호출 결과를 캐싱하고 동일한 입력이 다시 발생할 때 캐시된 결과를 반환하는 강력한 최적화 기법입니다. React는 React.memo, useMemo, useCallback을 포함한 여러 메모이제이션 도구를 제공합니다.

다음은 React.memo를 사용하는 예시입니다:


import React from 'react';

const MyComponent = React.memo(({ data }) => {
  console.log('MyComponent re-rendered');
  return <div>{data.name}</div>;
});

export default MyComponent;

이 예시에서 MyComponentdata prop이 변경될 경우에만 리렌더링됩니다.

4. 코드 스플리팅(Code Splitting)

코드 스플리팅은 애플리케이션을 필요에 따라 로드할 수 있는 더 작은 청크로 나누는 관행입니다. 이는 초기 로드 시간을 줄이고 애플리케이션의 전반적인 성능을 향상시킵니다. React는 동적 import와 React.lazySuspense 컴포넌트를 포함하여 코드 스플리팅을 구현하는 여러 방법을 제공합니다.

다음은 React.lazySuspense를 사용하는 예시입니다:


import React, { Suspense } from 'react';

const MyComponent = React.lazy(() => import('./MyComponent'));

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

export default App;

이 예시에서 MyComponentReact.lazy를 사용하여 비동기적으로 로드됩니다. Suspense 컴포넌트는 컴포넌트가 로드되는 동안 폴백(fallback) UI를 표시합니다.

5. 가상화(Virtualization)

가상화는 큰 목록이나 테이블을 효율적으로 렌더링하는 기법입니다. 모든 항목을 한 번에 렌더링하는 대신, 가상화는 현재 화면에 보이는 항목만 렌더링합니다. 사용자가 스크롤하면 새 항목이 렌더링되고 이전 항목은 DOM에서 제거됩니다.

react-virtualizedreact-window와 같은 라이브러리는 React 애플리케이션에서 가상화를 구현하기 위한 컴포넌트를 제공합니다.

6. 디바운싱(Debouncing)과 스로틀링(Throttling)

디바운싱과 스로틀링은 함수가 실행되는 빈도를 제한하는 기법입니다. 디바운싱은 특정 비활성 기간이 지난 후에 함수 실행을 지연시킵니다. 스로틀링은 주어진 시간 내에 함수를 최대 한 번만 실행합니다.

이러한 기법은 스크롤 이벤트, 리사이즈 이벤트, 입력 이벤트와 같이 빠르게 발생하는 이벤트를 처리하는 데 특히 유용합니다. 이러한 이벤트를 디바운싱하거나 스로틀링함으로써 과도한 리렌더링을 방지하고 성능을 향상시킬 수 있습니다.

예를 들어, lodash.debounce 함수를 사용하여 입력 이벤트를 디바운싱할 수 있습니다:


import React, { useState, useCallback } from 'react';
import debounce from 'lodash.debounce';

function ExampleComponent() {
  const [text, setText] = useState('');

  const handleChange = useCallback(
    debounce((event) => {
      setText(event.target.value);
    }, 300),
    []
  );

  return (
    <input type="text" onChange={handleChange} />
  );
}

export default ExampleComponent;

이 예시에서 handleChange 함수는 300밀리초의 지연으로 디바운싱됩니다. 이는 사용자가 300밀리초 동안 타이핑을 멈춘 후에만 setText 함수가 호출된다는 것을 의미합니다.

실제 사례 및 케이스 스터디

React 배치 및 최적화 기법의 실제적인 영향을 설명하기 위해 몇 가지 실제 사례를 살펴보겠습니다:

배치 문제 디버깅하기

배치가 일반적으로 성능을 향상시키지만, 배치와 관련된 문제를 디버깅해야 하는 시나리오가 있을 수 있습니다. 다음은 배치 문제를 디버깅하기 위한 몇 가지 팁입니다:

상태 업데이트 최적화를 위한 모범 사례

요약하자면, 다음은 React에서 상태 업데이트를 최적화하기 위한 몇 가지 모범 사례입니다:

결론

React 배치는 React 애플리케이션의 성능을 크게 향상시킬 수 있는 강력한 최적화 기법입니다. 배치가 어떻게 작동하는지 이해하고 추가적인 최적화 기법을 사용함으로써 더 부드럽고, 반응성이 좋으며, 더 즐거운 사용자 경험을 제공할 수 있습니다. 이러한 원칙을 받아들이고 React 개발 관행에서 지속적인 개선을 위해 노력하세요.

이러한 가이드라인을 따르고 애플리케이션의 성능을 지속적으로 모니터링함으로써, 전 세계 사용자가 사용하기에 효율적이고 즐거운 React 애플리케이션을 만들 수 있습니다.