이번에도 리액트의 리렌더링 문제를 풀다가 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은 성능 최적화의 핵심 메커니즘으로, "상태가 바뀌지 않았을 때 불필요한 렌더링을 건너뛰는 작업"을 말합니다. 결론부터 말씀드리면, 리액트 19에서도 이 메커니즘은 여전히 매우 중요하게 작동하며, 모든 렌더링 모델(Concurrent, Transition 등)에서 기본 원칙으로 적용됩니다.
-> 제미나이에게 정의를 정리해달라고 했다. 최신 버전에서도 사용되는 것이었다.
하지만 이제 용어를 알았다니 생각보다 모르는 것이 많았다는 사실에 공부가 더 필요하다는 것을 깨닫게 된다..
리액트는 useState나 useReducer에서 상태 업데이트 함수가 호출되면, 이전 상태(Old State)와 새로운 상태(New State)를 비교합니다. 이때 사용하는 기준이 자바스크립트의 Object.is() 알고리즘입니다.
앞선 퀴즈에서 보셨듯이, 이미 값이 같은데도 App 컴포넌트 로그가 찍혔던 이유는 리액트가 안전을 최우선으로 하기 때문입니다.
위의 문제에서 내가 틀렷던 점은 바로 의심의 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"