TIL

🧩 비동기 작업에도 우선순위가 있다 + 브라우저 페인팅 vs React 렌더링

inz1234 2025. 5. 8. 23:01

✅ 문제 상황 요약

setVoteLimit(payload.voteLimit)으로 상태를 업데이트한 후,

setTimeout(() => {
  listenersRef.current.onVoteLimitReceived?.();
}, 0);

처럼 setTimeout으로 콜백을 예약했는데, 기대와 다르게 voteLimit의 최신 값이 반영되지 않고 여전히 null로 참조되는 문제가 발생했다.


🤔 내가 기대한 흐름은?

JS 이벤트 루프 기준으로 다음과 같은 순서를 기대했다:

  1. Microtask (예: Promise.then)
  2. React 상태 업데이트 → 컴포넌트 리렌더링
  3. Browser 렌더링 (페인팅)
  4. Macrotask (예: setTimeout)

그래서 setTimeout은 React의 상태 업데이트와 리렌더링이 완료된 후 실행될 거라 기대했다.


❗ 그런데 실제로는..

React는 상태 변경(setState)이 일어나도 즉시 동기적으로 컴포넌트를 리렌더링하지 않는다.
대신 자체적인 스케줄링을 따르며, 이벤트 루프에 등록되는 setTimeout보다 늦게 실행될 수도 있다.

즉:

  • setTimeout(..., 0)은 JS 엔진의 다음 macrotask로 예약됨
  • 하지만 setVoteLimit()의 반영은 React의 내부 타이밍에 따라 처리됨
  • 그래서 setTimeout 콜백이 실행될 때까지 React는 아직 리렌더링을 시작하지 않았을 수 있음

💡 중요한 구분: "렌더링"의 의미

용어 의미

React 렌더링 Virtual DOM 계산 → DOM 변경 요청 (컴포넌트 함수 재실행 포함)
브라우저 렌더링 DOM 변경을 기반으로 화면에 픽셀을 그리는 작업 (페인팅)
이벤트 루프에서 말하는 "렌더링" 브라우저 페인팅을 의미함 (React 리렌더링과 다름)

👉 그래서 마이크로태스크 → 렌더링 → 매크로태스크에서 말하는 렌더링은 React가 아니라 브라우저의 페인팅을 의미함.

  • 즉 나는 JS 이벤트 루프 레벨의 렌더링 타이밍 React 상태 업데이트 타이밍 동일시 했던 것
  • 하지만 React는 자체적인 렌더링 스케줄링을 따르기 때문에, 그 둘은 다르게 움직인다는 것을 알게되었다.

🧪 실패한 시도 예시

setVoteLimit(payload.voteLimit);
setTimeout(() => {
  // 이 시점에 voteLimit은 여전히 null일 수 있음
  listenersRef.current.onVoteLimitReceived?.();
}, 0);
  • setTimeout은 macrotask로 예약되지만,
  • React의 상태 업데이트는 그 이후 프레임에 처리될 수도 있음

→ 결국 이 콜백은 리렌더링 전에 실행되어서 voteLimit은 여전히 null.


✅ 해결 방법: 직접 값을 넘기자

// WebSocket 응답 시
setVoteLimit(payload.voteLimit);
listernersRef.current.onVoteLimitReceived?.(payload.voteLimit); // 👈 바로 넘김
// 콜백 등록
useEffect(() => {
  registerListener("onVoteLimitReceived", (voteLimit) => {
    subscribeAll(voteLimit); // 👈 최신값으로 실행됨
  });
}, []);
  • React 상태로부터 값을 꺼내 쓰는 대신
  • 응답(payload)의 값을 인자로 직접 넘겨주는 방식
  • → 타이밍 이슈 걱정 없이 정확한 값 사용 가능

🧠 배운 점 요약

  • setTimeout(..., 0)보다 늦게 리렌더링이 시작될 수 있음
  • JS 이벤트 루프에서 말하는 "렌더링"은 브라우저의 페인팅을 의미함
  • React 상태 변경 이후 실행이 필요한 로직은 useEffect([state])나 직접 값 전달 방식이 안전함

✅ React의 상태는 예약이고, 반영 시점은 보장되지 않는다.
이벤트 루프의 흐름과 React의 상태 처리 흐름은 다르다.

 

이걸 모르고 setTimeout(() => ...)으로 "다음 프레임이면 되겠지!"라고 믿으면 안 된다.
React의 렌더링은 브라우저의 태스크 큐와 별도로, React가 정한 시점에 일어난다.

참고

 

[자바스크립트] 마이크로 태스크 큐의 비동기 작업 처리와 렌더링 시점을 알아보자.

이전 글 [자바스크립트] 마이크로 태스크 큐 이전 글 [자바스크립트] 프로미스를 이용한 비동기 작업 병렬 처리 이전 글 [자바스크립트] 프로미스 객체 이전 글 [자바스크립트] 비동기로 데이터

gobae.tistory.com

 

React의 useEffect와 useLayoutEffect 이해하기

이 글에서는 React의 useEffect와 useLayoutEffect의 차이점과 사용 방법에 대해 설명합니다. 비동기와 동기 작업을 적절히 분리하여 성능 최적화와 사용자 경험을 개선하는 방법을 다룹니다.

f-lab.kr