✅ 문제 상황 요약
setVoteLimit(payload.voteLimit)으로 상태를 업데이트한 후,
setTimeout(() => {
listenersRef.current.onVoteLimitReceived?.();
}, 0);
처럼 setTimeout으로 콜백을 예약했는데, 기대와 다르게 voteLimit의 최신 값이 반영되지 않고 여전히 null로 참조되는 문제가 발생했다.
🤔 내가 기대한 흐름은?
JS 이벤트 루프 기준으로 다음과 같은 순서를 기대했다:
- Microtask (예: Promise.then)
- React 상태 업데이트 → 컴포넌트 리렌더링
- Browser 렌더링 (페인팅)
- 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
'TIL' 카테고리의 다른 글
🧩 React와 웹소켓 타이밍 이슈, Closure (0) | 2025.05.08 |
---|---|
🧩Webpack + SharedWorker + TypeScript 환경에서 발생한 트러블슈팅 기록 (0) | 2025.03.30 |
🧩 WebSocket, Recoil로 관리해도 될까? (0) | 2025.03.22 |
TimePicker 외부 클릭 시 다시 열리는 현상 (0) | 2025.03.22 |
GitHub Actions + Docker 배포 트러블슈팅: .env 파일이 컨테이너에 들어가지 않는 이유? (0) | 2025.03.21 |