Notice
Recent Posts
Recent Comments
관리 메뉴

즐겁게, 코드

미리 만나보는 automatic batching 본문

🎨 프론트엔드/React.js

미리 만나보는 automatic batching

Chamming2 2021. 12. 27. 22:03

TL;DR

1. 리액트에서는 중복되는 상태 업데이트를 배칭으로 처리하고 있다.
2. 기존에는 비동기 프로세스 안에서 발생하는 중복된 상태 업데이트에는 배칭을 적용하지 않았지만, 버전 18부터는 항상 배칭을 적용한다.


2013년 처음 세상에 등장한 리액트는 현재 최고의 전성기를 누리고 있고, 이제는 버전 18의 출시를 눈앞에 두고 있습니다.

많은 개발자들의 안도의 한숨..!

버전 17에서는 내부적인 최적화 및 안정화에 초점을 맞춰 별다른 변화를 체감하지 못했지만, 현재 베타로 출시된 리액트 버전 18에서는 몇 가지 눈에 띄는 변경사항이 있는데요, 오늘은 그 중 automatic-batching 이라는 새로 추가된 성질을 소개해보려 합니다.

시작하기 전에

시작하기 전, 리액트 버전 18의 설정방법을 간단히 소개하고자 합니다.

1. 최신 버전 설치

먼저 CRA로 리액트 프로젝트를 구축한 다음 package.json 의 디펜던시 목록에서 reactreact-dom 을 제거합니다.

"dependencies": {
    ...
    "react-scripts": "5.0.0",
  },

그 후 최신 버전의 리액트를 설치합니다.

yarn add react@rc react-dom@rc
"dependencies": {
    ...
    "react": "^18.0.0-rc.0",
    "react-dom": "^18.0.0-rc.0",
    "react-scripts": "5.0.0",
  },

2. index.js 설정

버전 18부터는 ReactDOM.render 대신, createRoot 메서드로 어플리케이션을 감쌀 DOM 요소를 지정합니다.

const container = document.getElementById("root");
const root = ReactDOM.createRoot(container)

그리고 컨테이너의 자식으로 어플리케이션을 렌더링합니다.

const container = document.getElementById("root");
const root = ReactDOM.createRoot(container)
root.render(<App/>);

이제 리액트 18을 만나볼 준비가 모두 끝났습니다!

이전 버전 리액트의 배칭(batching) 업데이트

개발자에게 사용 여부가 달린 ErrorBoundary, Suspense 등을 제쳐두고 배칭에 대해 다루는 이유는 이번 버전부터는 특정 상황에서 리액트가 상태를 업데이트하는 방식이 완전히 변해, 꼭 짚고 넘어갈만 하다고 여겼기 때문입니다.

 

이전 버전(~17)까지의 상태 업데이트는 사실 동기적으로 일어나지 않는다는 사실을 알고 계셨나요?

한번 코드로 확인해 보겠습니다.

import React, { useState } from 'react'

const Counter = () => {
  const [count, setCount] = useState(0)
  console.log(count)

  const increase = () => {
    setCount(count + 1)
  }

  return (
    <div>
      <button onClick={increase}>+</button>
      <p>{count}</p>
    </div>
  )
}

export default Counter

버튼을 누르면 상태값이 1 증가하고 현재 카운트 값을 콘솔에 출력하는 간단한 예제입니다.

예상했던 대로 버튼을 한 번 누르면 상태값이 한번 변화하면서 재렌더링이 수행되는 모습입니다.
이제 위의 코드를 조금 수정해 버튼을 누를 때마다 상태를 세 번 수정하도록 해보겠습니다.

import React, { useState } from 'react'

const Counter = () => {
  const [count, setCount] = useState(0)
  console.log(count)

  const increase = () => {
    setCount(count + 1)
    setCount(count + 2)
    setCount(count + 3)
  }

  return (
    <div>
      <button onClick={increase}>+</button>
      <p>{count}</p>
    </div>
  )
}

export default Counter

하지만 이전과 달리 버튼을 누를 때마다 콘솔에는 숫자가 한번씩만 출력되는데요, 이렇게 생각하실 수 있습니다.

🧐 엥, 원래 예상대로라면 업데이트 함수를 3번 호출하면 렌더링이 3번 일어나 콘솔에도 숫자가 3번씩 출력되어야 하지 않나요?

이는 버그가 아닌 의도된 성질로, 불필요한 렌더링을 최소화하기 위해 업데이트하는 상태와 스코프가 동일한 업데이트 함수가 여러번 호출되면 마지막으로 실행된 업데이트 함수만을 리액트 내부의 업데이트 큐에 삽입하기 때문입니다.

const increase = () => {
    // setCount(count + 1)
    // setCount(count + 2)
    // 동일한 스코프에서 동일한 상태값을 업데이트하는 여러 업데이트 함수는 마지막 변경사항만이 업데이트 큐에 삽입됩니다.
    setCount(count + 3) 
  }

하지만, 비동기 함수 내에서는 상태의 업데이트가 조금 다르게 이루어집니다.

const asyncIncrease = () => {
    setTimeout(() => {
      setCount(count + 1)
      setCount(count + 2)
      setCount(count + 3)
    }, 1000)
  }

버튼을 한 번 누르면 콘솔이 한 번 출력되던 이전과는 달리, 이제는 업데이트 함수를 호출한 만큼 렌더링을 다시 수행하는 모습입니다!

 

이는 리액트 버전 17까지는 클릭 등 브라우저 이벤트가 발생할 때만 배치(Batch) 업데이트를 실행하고, setTimeout 이나 fetch 등의 Web API에 대해서는 재렌더링을 배치로 관리하지 않았기 때문에 생긴 현상인데요, 리액트 18에서는 이제 어떤 코드에서 여러 업데이트 함수를 실행하든 재렌더링을 배치로 관리하게 됩니다.

리액트 18의 automatic batching

이제 리액트 18에서 일어나는 동작을 확인해 보겠습니다.
동기 프로세스 내에서는 변화가 없지만 비동기 프로세스 안에서의 상태 업데이트 방식이 변했는데요, 한번 코드로 확인해 보겠습니다.

import React, { useState } from 'react'

const Counter = () => {
  const [count, setCount] = useState(0)
  console.log(count)

  const asyncIncrease = () => {
    setTimeout(() => {
      setCount(count + 1)
      setCount(count + 2)
      setCount(count + 3)
    }, 1000)
  }

  return (
    <div>
      <button onClick={asyncIncrease}>+</button>
      <p>{count}</p>
    </div>
  )
}

export default Counter

리액트 버전 17에서 테스트했을 때는 버튼을 누를 때마다 세 번씩 재렌더링이 수행되었는데요, 과연 이번에는 어떨까요?

재렌더링이 다시 한번씩만 수행되는 모습입니다!

이렇게 리액트 18부터는 비동기 함수 내부에서의 렌더링 역시 배치로 처리된다는 것을 알 수 있었는데요, 만약 리액트 18에서 이전처럼 비동기 배치를 적용하지 않으려면 어떻게 해야 할까요?

ReactDOM.flushSync()

기존처럼 비동기 작업 내에서 배치 업데이트를 사용하지 않으려면 ReactDOM.flushSync() 메서드를 활용할 수 있습니다.

흔한 경우는 아니겠지만 만약 이전처럼 비동기 함수 내의 중첩된 상태 업데이트에 배치가 필요하지 않을 시, flushSync 함수를 통해 이전 버전처럼 동작하도록 할 수 있습니다.

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

const Counter = () => {
  const [count, setCount] = useState(0)
  console.log(count)

  const asyncIncrease = () => {
    setTimeout(() => {
      flushSync(() => {
        setCount(count + 1)
      })
      flushSync(() => {
        setCount(count + 2)
      })
      flushSync(() => {
        setCount(count + 3)
      })
    }, 1000)
  }

  return (
    <div>
      <button onClick={asyncIncrease}>+</button>
      <p>{count}</p>
    </div>
  )
}

export default Counter

ReactDOM.flushSync()를 사용한 결과, 리액트 ~17 처럼 재렌더링이 여러번 수행되는 모습입니다.

지금까지 리액트의 배칭 업데이트 혹시 리액트 18에서 달라진 배칭 기법이 더 궁금하시다면, 이 깃허브 이슈에서 자세히 확인하실 수 있을 거에요! 😆

반응형
Comments
소소한 팁 : 광고를 눌러주시면, 제가 뮤지컬을 마음껏 보러다닐 수 있어요!
와!! 바로 눌러야겠네요! 😆