본문 바로가기

Frontend

Automatic batching for fewer renders in React 18

https://github.com/reactwg/react-18/discussions/21

 

React 18은 기본적으로 더 많은 일괄 처리를 수행하여 버전을 18로 올리는 것만으로 성능이 향상됩니다. 애플리케이션 및 라이브러리 코드에서 업데이트를 수동으로 일괄 처리할 필요가 없습니다. 이 게시물에서는 일괄 처리가 무엇인지, 이전에는 어떻게 작동했는지, 변경된 사항은 무엇인지 설명합니다. 

 

이것은 대부분의 사용자가 생각할 필요가 없는 심층 기능입니다. 그러나 교육자 및 라이브러리 개발자와 관련이 있을 수 있습니다.

 

What is batching? 

일괄 처리는 React가 성능 향상을 위해 여러 상태 업데이트를 단일 리렌더링으로 그룹화하는 것입니다.

 

Batching is when React groups multiple state updates into a single re-render for better performance.

예를 들어 동일한 클릭 이벤트 내에 두개의 상태 업데이트가 있는 경우, React는 항상 이것을 하나의 리렌더링으로 일괄처리합니다. 다음 코드를 실행하면 상태를 두 번 설정했지만 클릭할 때마다 React가 단일 렌더링만 수행하는 것을 볼 수 있습니다.

function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    setCount(c => c + 1); // Does not re-render yet
    setFlag(f => !f); // Does not re-render yet
    // React will only re-render once at the end (that's batching!)
  }

  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
    </div>
  );
}

 

이는 불필요한 리렌더링을 방지하기 때문에 성능이 좋습니다. 또한 컴포넌트가 하나의 상태 변수만 업데이트된 절반 완료 상태를 렌더링하는 것을 방지하여 버그를 방지합니다. 이것은 레스토랑 웨이터가 당신이 첫 번째 요리를 선택할 때 주방으로 달려가지 않고 당신이 주문을 마칠 때까지 기다리는 것으로 비유할 수 있습니다.

 

그러나 React는 업데이트를 일괄 처리할 때 일관성이 없었습니다. 예를 들어 데이터를 가져온 다음 위의 handleClick에서 상태를 업데이트해야 하는 경우 React는 업데이트를 일괄 처리하지 않고 두 개의 독립적인 업데이트를 수행합니다.

 

이는 React가 브라우저 이벤트(예: 클릭) 동안 일괄 업데이트에만 사용되기 때문입니다. 하지만 여기에서는 이벤트가 이미 처리된 후(페치 콜백에서) 상태를 업데이트하고 있습니다.

 

function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    fetchSomething().then(() => {
      // React 17 and earlier does NOT batch these because
      // they run *after* the event in a callback, not *during* it
      setCount(c => c + 1); // Causes a re-render
      setFlag(f => !f); // Causes a re-render
    });
  }

  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
    </div>
  );
}

 

React 18까지는 React 이벤트 핸들러 동안에만 업데이트를 일괄 처리했습니다. Promise 내부에서의 업데이트, setTimeout, 기본 이벤트핸들러 또는 기타 이벤트는 기본적으로 React에서 일괄 처리되지 않았습니다.

 

What is automatic batching?

createRoot가 포함된 React 18 부터 모든 업데이트는 출처에 관계없이 자동으로 일괄 처리됩니다. 즉 setTimeout, promise, 기본 이벤트 핸들러 또는 기타 이벤트 내부의 업데이트는 React 이벤트 내부의 업데이트와 동일한 방식으로 일괄 처리됩니다. 이렇게하면 렌더링 작업이 줄어들고 따라서 애플리케이션의 성능이 향상될 것으로 기대합니다.

function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    fetchSomething().then(() => {
      // React 18 and later DOES batch these:
      setCount(c => c + 1);
      setFlag(f => !f);
      // React will only re-render once at the end (that's batching!)
    });
  }

  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
    </div>
  );
}

 

참고: React 18 채택의 일부로 여러분이 createRoot로 업그레이드할 것으로 예상됩니다. 렌더링의 이전 동작은 두 버전 모두에서 production 실험을 더 쉽게 수행할 수 있도록 하기 위해서만 존재합니다. 

 

React는 업데이트가 발생하는 위치에 관계없이 자동으로 업데이트를 일괄 처리하므로 다음과 같습니다.

 

function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will only re-render once at the end (that's batching!)
}

 

다음과 동일하게 동작합니다. 

 

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will only re-render once at the end (that's batching!)
}, 1000);

 

다음과 동일하게 동작합니다. 

 

elm.addEventListener('click', () => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will only re-render once at the end (that's batching!)
});

 

React는 일반적으로 안전한 경우에만 업데이트를 일괄 처리합니다. 예를 들어 React는 클릭이나 키 누르기와 같은 각 사용자 시작 이벤트에 대해 다음 이벤트 전에 DOM이 완전히 업데이트되도록 합니다. 예를 들어 form 제출시 비활성화된 양식을 두번 제출할 수 없도록 합니다.

 

What if I don’t want to batch?

일반적으로 일괄 처리는 안전하지만 일부 코드는 상태 변경 직후 DOM에서 무언가를 읽는데 의존할 수 있습니다. 이러한 사용 사례의 경우 ReactDOM.flushSync()를 사용하여 일괄 처리를 옵트아웃할 수 있습니다.

 

import { flushSync } from 'react-dom'; // Note: react-dom, not react

function handleClick() {
  flushSync(() => {
    setCounter(c => c + 1);
  });
  // React has updated the DOM by now
  flushSync(() => {
    setFlag(f => !f);
  });
  // React has updated the DOM by now
}

 

우리는 이것이 일반적이길 기대하지 않습니다.

 

Does this break anything for Hooks?

훅을 사용하는 경우 대부분의 경우 자동 일괄 처리가 작동할 것으로 예상됩니다.

 

Does this break anything for Classes?

React 이벤트 핸들러 중 업데이트는 항상 일괄 처리되므로 해당 업데이트에 대한 변경 사항은 없습니다. 이것이 문제가 될 수 있는 클래스 구성 요소의 극단적인 경우가 있습니다.

 

클래스 컴포넌트에는 이벤트 내에서 상태 업데이트를 동기식으로 읽을 수 있는 구현 문제가 있었습니다. 이는 setSTate 호출 사이에 this.state를 읽을 수 있음을 의미합니다.

 

handleClick = () => {
  setTimeout(() => {
    this.setState(({ count }) => ({ count: count + 1 }));

    // { count: 1, flag: false }
    console.log(this.state);

    this.setState(({ flag }) => ({ flag: !flag }));
  });
};

 

React 18에서는 더 이상 그렇지 않습니다. setTimeout의 모든 업데이트가 일괄 처리되기 때문에 React는 첫 번째 setState의 결과를 동기식으로 렌더링하지 않습니다. 렌더링은 다음 브라우저 틱 중에 발생합니다. 따라서 렌더링은 아직 발생하지 않았습니다.

 

handleClick = () => {
  setTimeout(() => {
    this.setState(({ count }) => ({ count: count + 1 }));

    // { count: 0, flag: false }
    console.log(this.state);

    this.setState(({ flag }) => ({ flag: !flag }));
  });
};

 

이것이 React 18로 업그레이드하는 데 방해가 되는 경우 ReactDOM.flushSync를 사용하여 업데이트를 강제할 수 있지만 드물게 사용하는것이 좋습니다.

 

handleClick = () => {
  setTimeout(() => {
    ReactDOM.flushSync(() => {
      this.setState(({ count }) => ({ count: count + 1 }));
    });

    // { count: 1, flag: false }
    console.log(this.state);

    this.setState(({ flag }) => ({ flag: !flag }));
  });
};

 

상태를 설정해도 useState에서 기존 변수가 업데이트되지 않기 때문에 훅이 있는 함수 컴포넌트에 영향을 미치지 않습니다.

 

function handleClick() {
  setTimeout(() => {
    console.log(count); // 0
    setCount(c => c + 1);
    setCount(c => c + 1);
    setCount(c => c + 1);
    console.log(count); // 0
  }, 1000)

 

이 동작은 Hooks를 채택했을 때 놀라웠을 수 있지만 자동화된 일괄 처리를 위한 길을 열었습니다.

 

What about unstable_batchedUpdates?

일부 React 라이브러리는 이 문서화되지 않은 API를 사용하여 이벤트 핸들러 외부의 setState를 강제로 일괄 처리합니다.

 

import { unstable_batchedUpdates } from 'react-dom';

unstable_batchedUpdates(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
});

 

이 API는 18에 여전히 존재하지만 일괄 처리가 자동으로 발생하므로 더 이상 필요하지 않습니다. 인기 있는 라이브러리가 더 이상 존재에 의존하지 않으면 향후 주요 버전에서 제거될 수 있지만 18에서 제거하지는 않습니다.

'Frontend' 카테고리의 다른 글

Explain dirty checks in React.js  (0) 2023.03.02
프론트엔드에서 상태란?  (0) 2023.01.20
Next.js 환경 변수  (0) 2022.08.11
PWA (점진적 웹 앱)  (0) 2022.06.16
React Query와 상태관리  (0) 2022.06.13