React batched update 그리고 react-redux

unstable_batchedUpdate api에 관하여

한영재
Tapjoy Korea
7 min readMar 26, 2020

--

들어가기 전에

리덕스를 사용하며 한개의 이벤트에 값을 set 하는 다수의 액션을 dispatch 하는 경우가 왕왕 있다. 이 때 스토어의 각각의 값은 상응하는 액션별로 업데이트 되지만, 이 해당되는 값들을 구독(Subscribe)하는 컴포넌트에서는 리액트의 내부적인 batched update 로직에 의해 효율적으로 props update 를 모아서 받고, 이로 인해 비효율적인 렌더 함수의 호출을 줄일 수 있다. 이글은 이것이 정확히 어디서 발생 되는지, 어떤 api가 이것을 가능케 하는지 정리하는 글이다.

코드를 통해 살펴보자

두개의 액션과 상응하는 값이 있다고 가정해보자.

store {
a: 'old A' --- newly set by Action A
b: 'old B' --- newly set by Action B
}
const Component = ({a, b}) => {
handleTriggerMultipleActions () {
yield put(Action A('new A'))
yield put(Action B('new B'))
}
console.log('A : ', a)
console.log('B : ', b)
return <div onClick={handleTriggerMultipleActions}>Test</div>
}
const mapStateToProps = (state) => ({a: state.a, b: state.b})connect(mapStateToProps)(Component)

볼드처리된 곳에서 콘솔에 어떤 값이 찍힐 것으로 예상 되는가? 아래와 같이 예상 할 수 도 있다.

// 첫 렌더링 (마운트 될 때)
A: old A
B: old B
// 두번째 렌더링 (dispatch Action A)
A: new A
B: old B
// 세번째 렌더링 (dispatch Action B)
A: new A
B: new B

하지만 실제로는 React 의 batch 시스템에 의해 아래와 같이 찍힌다. (비동기 액션이 아닌 경우)

// 첫 렌더링 (마운트 될 때)
A: old A
B: old B
// 두번째 렌더링 (handleTriggerMultipleActions 호출)
A: new A
B: new B

누가 어디서 해주는 것일까?

Redux-dev-tool을 살펴보니, 액션이 각각 순차적으로 호출 되고있고 이에 따라 store가 순차적으로 업데이트가 되고 있었기 때문에 리덕스 자체는 예상대로 동작하였고, 컴포넌트와 직접적으로 연결 해주는 connect(react-redux) 쪽을 더 살펴보다가 다음과 같은 힌트를 발견했다. mapToStateProps 에서 콘솔로그를 찍어보면 액션이 dispatch 될 때마다 구독하고 있는 props를 정상적으로 connect 함수에 넘겨주고 있었다. connect(react-redux) 코드의 일부분을 살펴보자. (코드 링크) * 이 코드는 react-redux v4.x 기준입니다.

내부적으로 connect 를 통해 구독하고 있는 값들이 변할 때를 인지해 리액트의 setState 를 사용하고 있는 것을 볼수 있다. 이 때 setState 가 내부적으로 unstable_batchedUpdates() API 를 사용하기 때문에 batched update 가 발생된다.

추가하여 Dan Abramov 의 답변이 위 내용을 대변해준다. (답변 바로가기)

함정 카드

# react-redux batch-api 의 존재
알고나서 보니 너무나 당연해보이지만 처음에 바로 react 기본 batched update 를 생각하지 못했는데, 그 이유는 react-redux 같은 경우 이런 상황에서 렌더링 최적화(batched update) 를 위해 위 API 를 별도로 제공하고 있다는 것을 인지하고 있었기 때문이다. 그래서 내부적으로 react의 setState 를 사용하고 있을 것이라고 단정 짓지 못하고 죄 없는 redux와 react-redux 를 먼저 의심했었다.

위에서 알 수 있듯, 리액트에 의해 내부적으로 batched update가 되고 있음에도 불구하고 이 react-redux API가 별도로 존재 이유는 다음과 같다.

Since React-Redux needs to work in both ReactDOM and React Native environments, we’ve taken care of importing this API from the correct renderer at build time for our own use. We also now re-export this function publicly ourselves, renamed to batch(). You can use it to ensure that multiple actions dispatched outside of React only result in a single render update, like this:

땡큐 unstable_batchedUpdates()

리액트가 내부적으로 자체 이벤트에 batched update 를 지원할 수 있는 것은 이 API 의 덕분이다. 이로 인해 리액트에서 한 이벤트 핸들러에서 발생하는 모든 업데이트를 단일 렌더 패스로 일괄 처리 할 수 ​​있다. 즉 불필요한 렌더 함수 호출을 줄일 수 있게 된다.

이 API 는 react core 의 것이 아니라 렌더러 페키지(ReactDOM, React Native)의 일부이다.

아래 첨부된 이미지(ReactDOM package test file)를 보면 더욱 명료해 질 것이다. (실제 ReactDOM 소스코드)

ReactDOM package test file

Batched Update 기준

같은 이벤트 핸들러 안에서 처리되는 state update 끼리만 batched update 된다. 이에 관련해 Dan Abromov 의 말을 인용하면 다음과 같다. (인용 원글)

There is yet no batching by default outside of React event handlers.

The key to understanding this is that no matter how many setState() calls in how many components you do inside a React event handler, they will produce only a single re-render at the end of the event.

추가 하여 이외에도 life cycle method 중 ComponentDidMount, ComponentDidUpdate 도 bathing 되니 이 곳에서 connect 된 props 값을 참조하여 액션을 발생시킬 때 사이드 이펙트의 소지가 있으니 유의해야한다.

끝으로

결과적으로 이런 내용들을 정확하게 이해하기 위해서는 사실 React 작동의 주요한 요소인 Virtual DOM과 이벤트 시스템에 알고 있으면 더욱 도움이 된다. 물론 소스코드를 직접 보며 정리해가는 방법이 가장 훌륭하지만 필자가 직접 해보니 큰 각 부분 부분을 모아 큰 그림을 그리기가 켤코 쉽지 않고 많은 시간을 요한다. 혹시 관심이 있으신 분들을 위해 React에서 DOM을 어떻게 렌더링하고 브라우저 이벤트를 처리 방식에 대해 정리한 좋은 글을 공유 하면서 글을 마친다. (링크 바로가기)

--

--