이번에도 렌더링 관련해서 문제를 풀다가 막힌 부분이 있어서 정리글을 쓰게 되었다.
이번 문제는 아래와 같다
import * as React from 'react'
import { useState } from 'react'
import { createRoot, flushSync } from 'react-dom/client'
import { screen, fireEvent } from "@testing-library/dom";
function App() {
const [state, setState] = useState(0)
const onClick = () => {
console.log('handler')
flushSync(() => {
setState(state => state + 1)
})
console.log('handler ' + state)
}
console.log('render ' + state)
return <div>
<button onClick={onClick}>click me</button>
</div>
}
const root = createRoot(document.getElementById("root"));
root.render(<App />);
(async function () {
const action = await screen.findByText('click me')
fireEvent.click(action);
})()
내가 생각한 결과 값은 아래와 같았다.
첫 랜더링
“render 0”.
클릭함수 실행
“handler”
flushSync 실행
“handler 1” <- flushSync 로 바로 state 가 변경되어서 콘솔이 바로 변경되어서 찍힐줄 알았다
그리고 리렌더링 결과
“render 1”
결과
“render 0”.
“handler”
“handler 1”
“render 1”
위의 답은 틀렸다..
해설을 보고 이해할수 있었다.
일단 flushSync에 대해서 다시 정리 해본다.
flushSync는 react-dom 패키지에서 제공하는 상태 업데이트를 강제로 즉시(동기적으로) 처리하여 DOM에 반영하도록 만드는 함수이다.
flushSync로 감싸진 상태 업데이트는 React가 렌더링을 미루지 않고, 그 코드가 끝나는 즉시 화면을 새로고침(Commit)해버린다.
React 공식 문서에서는 flushSync를 가급적 사용하지 말 것을 권장한다.
왜냐하면 React의 성능 최적화(Batching)를 깨뜨리기 때문이다.
하지만 특정상황에 사용된다.
스크롤 위치나 포커스 맞추기: 목록 맨 끝에 새로운 아이템을 추가(state 업데이트)하자마자, 그 새로운 아이템의 위치를 계산해서 스크롤을 맨 아래로 내려야 할 때.
서드파티 라이브러리 연동: React가 아닌 외부 라이브러리(예: 직접 DOM을 조작하는 지도 API, D3 차트 등)와 React의 상태를 완벽하게 같은 타이밍에 맞춰야 할 때.
브라우저 API 호환성: 클립보드 복사, 프린트 등 사용자 상호작용 직후에 즉각적인 DOM 변화가 필요한 브라우저 API를 다룰 때.
이제 다시 문제로 돌아가보자
결과 값은 다시 생각해보고 문제를 풀면 아래와 같다
"render 0"
"handler"
"render 1"
"handler 0"
왜 이런 결과가 나오는지 확인해보자..
1. flushSync는 'DOM'만 먼저 바꾼다.
flushSync를 호출하면 React는 그 즉시 컴포넌트를 리렌더링하고 DOM을 업데이트하게 된다. 그래서 flushSync 콜백이 끝나자마자 "render 1"이 먼저 찍히는 것 하지만 flushSync가 이미 실행 중인 함수(onClick) 내부에 있는 변수(state) 값까지 실시간으로 바꿔주지는 않는다.
그래서 즉시 리렌더링 해서 바깥의 콘솔이 찍히게 된다.
그리고 리렌더링 이후에 아래 코드가 실행된다
console.log('handler ' + state) <- 이부분
여기에서는 클로저의 마법이 들어간다.
2. 클로저(Closure)의 마법
onClick 함수가 처음 생성될 당시의 state 값은 0. 자바스크립트 함수는 선언될 당시의 환경을 기억하게 된다 (이것이 클로저!)
실행 순서 정리 !