무엇
flushSync는 콜백 안에서 일어난 상태 갱신을 동기적으로 즉시 처리하라고 React에 강제하는 API다. 그 순간 DOM이 바로 업데이트된다.
인자는 함수 하나다. React가 그 함수를 곧바로 실행하고, 안에서 예약된 갱신을 즉시 반영한다. 반환값은 undefined다.
import { flushSync } from 'react-dom'
flushSync(() => {
setCount(count + 1)
})
// 이 줄에서 이미 DOM이 갱신돼 있다
무슨 일이 일어나나
React는 보통 여러 갱신을 모아(batch) 한 번에 처리한다. flushSync는 이 자동 배칭을 깨고, 콜백 안의 작업을 그 자리에서 밀어붙여 DOM까지 반영한다.
여파가 넓다. 콜백 밖에 대기 중이던 갱신까지 함께 밀려나올 수 있고, 대기 중인 Effect가 실행되며 그 갱신이 동기로 적용되기도 한다. 진행 중인 Suspense 경계는 fallback 상태로 되돌아갈 수 있다. 즉 "지금 당장 화면을 맞춘다"의 대가로 React가 미뤄 두던 최적화를 포기하는 셈이다.
사용법
상태를 바꾼 직후에 갱신된 DOM을 읽어야 할 때 쓴다. 평소라면 다음 렌더까지 기다려야 하지만, flushSync는 그 자리에서 DOM을 맞춰 준다.
import { flushSync } from 'react-dom'
function addAndScroll(listRef: React.RefObject<HTMLUListElement>) {
// 먼저 항목을 추가하고 DOM을 즉시 반영
flushSync(() => {
setItems((prev) => [...prev, newItem])
})
// 이미 새 항목이 그려졌으므로 스크롤 위치를 잰다
const list = listRef.current
if (list) list.scrollTop = list.scrollHeight
}
이 흐름을 flushSync 없이 두면, 스크롤 계산 시점엔 아직 새 항목이 DOM에 없어 위치가 어긋난다.
실무 예시
React 바깥의 코드가 최신 DOM을 읽어야 하는 순간이 대표적이다. 예를 들어 브라우저 인쇄 대화상자는 열리기 직전 DOM을 그대로 캡처하는데, 그 전에 상태를 확실히 반영해 둬야 한다.
import { useState, useEffect } from 'react'
import { flushSync } from 'react-dom'
export function PrintApp() {
const [isPrinting, setIsPrinting] = useState(false)
useEffect(() => {
function handleBeforePrint() {
// 인쇄 대화상자가 뜨기 전에 DOM을 즉시 맞춘다
flushSync(() => setIsPrinting(true))
}
window.addEventListener('beforeprint', handleBeforePrint)
return () => window.removeEventListener('beforeprint', handleBeforePrint)
}, [])
return <h1>인쇄 중: {isPrinting ? '예' : '아니오'}</h1>
}
왜 중요한가
일상적으로 쓸 API는 아니다. 자동 배칭을 강제로 깨는 탈출구라, 상태 변경 직후 DOM을 반드시 읽어야 하거나 React 바깥 코드와 시점을 맞춰야 하는 드문 경우에만 쓴다. 공식 문서도 마지막 수단이라고 못박는다. 존재와 부작용을 알아 두되, 먼저 손이 가서는 안 되는 도구다.
Reference