리액트의 Bailout(상태 변경 최적화)

이명진·2026년 4월 23일

TIL

목록 보기
26/27

이번에도 리액트의 리렌더링 문제를 풀다가 Bailout(상태 변경 최적화) 에 대해서 알게 되었고 공부하고 정리하게 되었다..

문제는 아래와 같다


import * as React from "react";
import { useState } from "react";
import { createRoot } from "react-dom/client";
import { screen, fireEvent } from "@testing-library/dom";

function A() {
  console.log('render A')
  return null
}

function App() {
  const [_state, setState] = useState(false)
  console.log('render App')
  return <div>
    <button onClick={() => {
      console.log('click')
      setState(true)
    }}>click me</button>
    <A />
  </div>
}

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

(async function () {
  const action = await screen.findByText("click me");
  fireEvent.click(action);
  await wait(100);
  fireEvent.click(action);
  await wait(100);
  fireEvent.click(action);
})();

function wait(duration = 100) {
  return new Promise((resolve) => setTimeout(resolve, duration));
}

여기서 나는 기본적으로 리액트는 변경된 부분이 없을때는 리렌더링 되지 않는다 라는 생각으로 정답을 아래와 같이 접근하였다

"render App"
"render A"
"click"
"render App"
"render A"
"click"
"click"

클릭 이벤트가 벌어질때 true로만 변경하니 처음에는 false -> true로 변경되어서 리렌더링이 일어나고 이제 true 로 다시 변경되니 리렌더링이 일어나지 않을 거라고 생각햐고 위와 같이 생각하였는데 아니였다 ..

여기에서도 Bailout(상태 변경 최적화) 메커니즘이 있었다 .

리엑트 Bailout(상태 변경 최적화) 가 뭘까 ?

리액트의 Bailout은 성능 최적화의 핵심 메커니즘으로, "상태가 바뀌지 않았을 때 불필요한 렌더링을 건너뛰는 작업"을 말합니다. 결론부터 말씀드리면, 리액트 19에서도 이 메커니즘은 여전히 매우 중요하게 작동하며, 모든 렌더링 모델(Concurrent, Transition 등)에서 기본 원칙으로 적용됩니다.

-> 제미나이에게 정의를 정리해달라고 했다. 최신 버전에서도 사용되는 것이었다.
하지만 이제 용어를 알았다니 생각보다 모르는 것이 많았다는 사실에 공부가 더 필요하다는 것을 깨닫게 된다..

Bailout의 기본 원리: Object.is()

리액트는 useState나 useReducer에서 상태 업데이트 함수가 호출되면, 이전 상태(Old State)새로운 상태(New State)를 비교합니다. 이때 사용하는 기준이 자바스크립트의 Object.is() 알고리즘입니다.

  • 값이 같으면 (True): 리액트는 "변경 사항 없음"으로 판단하고 리렌더링을 시도하지 않습니다. 이를 Bailout이라고 합니다.
  • 값이 다르면 (False): 컴포넌트를 다시 실행(Render Phase)합니다.

“의심의 1회 리렌더링" (Edge Case Bailout)

앞선 퀴즈에서 보셨듯이, 이미 값이 같은데도 App 컴포넌트 로그가 찍혔던 이유는 리액트가 안전을 최우선으로 하기 때문입니다.

  • 상황: 상태가 true인데 다시 true로 업데이트한 경우.
  • 작동: 리액트는 혹시 모를 사이드 이펙트나 계산 로직을 확인하기 위해 해당 컴포넌트만 딱 한 번 더 실행해 봅니다.
  • 결과: 실행 결과가 이전과 완전히 동일하다고 확정되면, 자식 컴포넌트(A)를 렌더링하거나 브라우저 DOM을 업데이트하지 않고 그 자리에서 렌더링 프로세스를 즉시 종료합니다. 이것이 바로 '진정한 의미의 Bailout'입니다.

주의해야 할 점 (Bailout이 깨지는 경우)

  • 객체/배열 리터럴:
    setState({ value: 1 }); // 매번 새로운 객체 참조가 생성되어 Object.is가 false를 반환함.
  • 부모 컴포넌트의 리렌더링:
    부모 컴포넌트가 리렌더링되면, 자식 컴포넌트는 자신의 state가 변하지 않았더라도 기본적으로 다시 렌더링됩니다. (이를 막으려면 React.memo나 컴파일러의 도움이 필요합니다.)

위의 문제에서 내가 틀렷던 점은 바로 의심의 1회 리렌더링 과정이었다

내가 생각한 대로 첫번째 클릭시 (false → true)
값이 변했기 때문에 App과 자식 인 A가 리렌더링 된다.

두번째 클릭 (true → true)
이때가 핵심! 리액트는 값이 이전과 같으면 리렌더링을 하지 않으려 하지만, "정말로 안 해도 되는지" 확인하기 위해 App 컴포넌트를 한 번 더 실행(Render)하게 된다.

컴포넌트를 실행해보고 "어? 결과가 이전과 똑같네?"라고 판단되면, 그 아래 자식(A)으로 내려가지는 않고 거기서 멈추게 된다

그래서 부모만 리렌더링 일어나고 자식은 일어나지 않는다.

최종 결과

"render App" 
"render A" 
"click" 
"render App" // state: false -> true (첫 번째 클릭) 
"render A" 
"click" 
"render App" // state: true -> true (두 번째 클릭: Bailout 발생 직전 체크) "click"
profile
프론트엔드 개발자 초보에서 고수까지!

0개의 댓글