React 18 automatic batching에 대해 알아보자

정유정 | yujeong choung·2023년 3월 26일
1

React

목록 보기
1/1

최근 사내에서 리액트 버전을 17에서 18로 올리게 되었다.

리액트 18 버전은 일년전 이맘때쯤 짜잔하고 등장하였는데 개인 공부로 문서를 읽어본적은 있어도 제대로 다뤄본적은 없는 것 같아 더 늦어지기전에 하나씩 조금 더 들여다보고 최대한 모든 기능을 활용해보려 한다.

우선 첫번째 스타트는 React 18의 automatic batching (자동 배칭)이다!

자동배칭으로 바로 다이빙하기 이전에 간단하게 배칭과 관련된 state에 대한 이야기도 조금 해보면 좋을 것 같다.


setState는 왜 비동기적으로 처리될까??

아래 코드에서 alert은 어떤값을 보여줄까?

정답은 0이다.그렇다면 왜 1이 아닌 0을 보여주는것일까?

바로 setState가 리액트내부에서 비동기적으로 처리되기 때문이다.

그렇다면 또 다시 꼬리 질문으로 왜 setState가 비동기적으로 처리가 되는지에 대한 의문이 든다 🤔

이에 대한 답변으로 2017년도에 react 이슈로( RFClarification: why is setState asynchronous? ) 비슷한 질문이 올라온적이 있다.

이에 대해 2018년도에 올라온 Dan의 답변을 요약해보자면 아래와 같은 이유로 setState는 비동기로 작용한다고 한다.

  • batch (일괄) 업데이트를 위해서 reconciliation(재조정)을 연기하는것이 효율적이다. 즉, setState() 를 동기적으로 업데이트 하여 매번 리렌더링하는 것은 비효율적이다.
    • 예를들어, 브라우저 click 핸들러 안에서, 자식과 부모가 모두 setState를 호출하는 경우, 자식을 두 번 렌더링하지 않고 더티로 표시한 후 브라우저 이벤트를 종료하기 전에 자식과 부모를 한번에 렌더링한다.

물론 질문자는 해당 상황에 대해 인지하고 setState() 는 reconciliation이 끝날때까지 기다리는것이 아니라 동기적으로 변경해주면서 rendering은 비동기적으로 만들 수는 없는지에 대해서도 언급하였다.

이에 대해 Dan은 명확한 답은 없다고 하지만 생각할 수 있는 이유들 몇가지를 던져주었다.

  1. 내부 일관성 보장 (Guaranteeing Internal Consistency)

    • state는 동기적으로 업데이트 해준다고 하더라도, props는 부모 컴포넌트가 렌더링이 되기전까지 변하된 state값을 가져올 수 없다.
    • 이러한 이유때문에 동기적으로 렌더링을 해준다면 batching을 전혀 실행할 수 없게 된다.
  2. 동시 업데이트 활성화 (Enabling Concurrent Updates)

    • 동시성 기능을 활용하면 하나의 작업을 작은 여러개의 작업으로 나누어 동시에 진행할 수 있도록 만들어 준다.
    • 예를들어, 모닝루틴에 커피를 타서 빵을 먹는다가 있다고 했을 때 동시성이 없다면 무조건 뜨거운물을 끓이고 해당 물에 커피 가루를 탄다음에 컵에 커피를 한잔 만들어 낼때까지 빵을 준비하는 일은 할 수 없는 것이다. 하지만 동시성이 존재한다면 뜨거운물을 끓이면서 빵을 굽는등 여러가지 작업을 왔다갔다 하며 동시에 진행할 수 있음을 의미한다.
    • 즉 동시성을 리액트에 적용해본다면 렌더링을 쪼개서 우선순위를 두고 우선적으로 상태 업데이트가 진행되야하는 부분을 먼저 수행할 수 있게 되는 것이다.
    • 이러한 동시성 기능을 가능하게 하기 위해서라도 상태 업데이트를 동기적으로 수행할 수 없게 된다. 리액트는 이벤트 핸들러, 네트워크 응답, 애니메이션등의 출처에 따라 setState() 호출에 각각 다른 우선순위를 할당할 수 있는데 상태 업데이트를 동기적으로 변경하게 된다면 그 작업의 수행 시간과 우선순위와는 상관없이 순서대로 반영된다는 이야기이기 때문이다.

    One way we’ve been explaining “async rendering” is that React could assign different priorities to setState() calls depending on where they’re coming from: an event handler, a network response, an animation, etc

    • 동시성을 활용할 수 있는 react18의 concurrent APIs는 아래와 같다.
      • React.Suspense
      • React.startTransition
      • React.useTransition
      • React.useDeferredValue

이러한 이유로 인하여 setState()는 비동기적으로 작용하고 있는데 위에서 계속해서 나왔던 배칭이 무엇인지 조금 더 알아보고 오늘 글의 주제인 React 18 automatic batching과 무슨 차이가 있는지 알아보는 시간을 가져보자


Batching

리액트는 기본적으로 batching을 활용한다. 배칭이 무엇인가 하면

batching (배칭)이란, 이벤트 핸들러나 hooks안에서 상태 업데이트를 묶어서 동작하도록 만들어준다. 즉, 렌더링이 한번만 일어나게 해준다

react 17에서의 batching

리액트 17에서도 batching은 존재하였지만 지원되는 범위가 리액트 18가 달랐다.

  • 오직 리액트 이벤트 핸들러 내부에서만 배칭이 적용 되었다 (camel case인 onClick, onMouseMove, onLoad 등등..)
  • promises, setTimeout, native event handlers들 안에서는 배칭이 되지 않았다.

예시와 함께 조금 더 살펴보도록 하자

현재 리액트는 버전 17이며 아이스크림 버튼에는 아래 코드의 handleIceCreamClick 핸들러가 연결되어있다.

이때 버튼을 클릭하게 된다면 콘솔창에 "Component Rendering 🚀"useEffect가 불렸습니다 | 오빠: ${brother} 동생: ${sister} 몇번씩 찍히게 될까?

리액트 17버전에서도 리액트 이벤트 핸들러 내부에서는 배칭이 존재했기에 버튼 한번 클릭시 한번씩만 찍히는것을 볼 수 있다.

하지만 코드를 아래처럼 수정한다면?

async await을 활용하여 handleIceCreamClick을 항상 프라미스를 반환하는 함수로 변경하고 해당 함수내에서 setState를 실행해보았다.

이 경우에는 리액트 17버전에서는 배칭이 적용되지 않아 렌더링이 버튼 클릭한번당 setState의 갯수(setBrother, setSister)만큼 2번씩 발생하는 것을 볼 수 있다.

react 18에서의 batching (automatic batching)

그런데 새롭게 버전업이 된 react 18에서는 automatic batching으로 인해 react 17에서 보던 배칭과는 다른점을 볼 수 있다.

automatic batching의 출현

automatic batching, 자동 배칭이란, 말 그대로 모든 업데이트가 자동으로 배칭됨을 의미한다. 리액트 이벤트 핸들러 내부뿐만 아니라 promises, setTimeout, native event handlers들 안에서도 배칭이 적용된다.

그런데 여기에는 조건이 존재한다.

  • createRoot 를 사용했을때만 automatic batching이 적용된다.
  • 이전 legacy인 render 를 사용하게 되면 리액트 18버전이어도 automatic batching이 적용되지 않는다!

기존 react 17에서도 리액트 이벤트 핸들러 내부에서는 배칭이 적용되었기에 위에서 사용했던 promise 예제만을 react 18버전에서 확인해본다면 아래와같이 렌더링이 버튼클릭당 한번씩만 되는 것을 볼 수 있다.

  • 위에서와의 차이점을 확인하기 위해 console.log에 react 18이라는 것을 명시해줬다.

batching을 하고싶지 않다면? (flushSync)

배칭을 강제적으로 하지 않게 만드는것은 흔하게 발생해서는 안되는 일이지만 일부 코드는 상태 변경 직후에 DOM에서 무언가를 읽어야 할 수도 있을 것이다.

이러한 경우에는 ReactDOM.flushSync()를 사용하여 배칭(일괄 처리)를 선택적으로 하지 않을수도 있다.

위와 같은 경우에는 아이스크림 버튼을 클릭했을 때 배칭이 적용되지 않아 flushSync 후에 렌더링이 한번 진행되고 두번째 flushSync 후에 또 다시 렌더링이 진행되는것을 볼 수 있다.

추가적으로 약간의 호기심이 생겨서 flushSync안에 여러개의 setState를 넣어보았는데 처음에는 flushSync안에 setState 갯수 만큼 렌더링이 일어날까? 라는 의문점을 갖고 있었다.

하지만 해당 부분은 flushSync안의 setState 갯수가 아닌 flushSync가 하나의 렌더링으로 처리된다는 것을 알게 되었다!


배칭에 대해서는 익히 들어왔는데 배칭과 오토 배칭에 차이에 대해서는 이번 기회를 통해 정확히 알게 되었다. 이 글을 읽게되는 모두가 배칭과, 오토 배칭 그리고 그 차이에 대해 조금이나마 알아가는 시간이었으면 한다.

틀린 내용이 있다면 언제든 댓글 달아주세요 ☺️

Reference

profile
언제나 새로운 도전을 꿈꾸는 개발자

1개의 댓글

comment-user-thumbnail
2024년 3월 14일

리액트 18 버전만 썼어서 몰랐는데, 17 버전 대비 배칭에 큰 차이가 있었군요! 좋은 글 감사합니다!

답글 달기