CSV를 만들어야 할 일이 있어 Papa.unparse
를 사용했는데, 문제 하나를 마주쳤다
setIsLoading(true)
try {
const csv = Papa.unparse({ fields: csvTitle, data: csvData });
const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
// 다운로드 로직
} catch {
// 에러 로직
} finally {
setIsLoading(false)
}
<button loading={isLoading}>생성해줫!</button>
여기서 문제점은 단순했다
setIsLoading(true)
로 로딩 상태를 바꿔도 UI에 스피너가 안 보인다.Js가 끝날 때까지 기다림
여기서 문제가 되는 건 "Js가 끝날 때까지"라는 조건이다.
API 요청과 비교해보면 차이가 뚜렷하다.
fetch()
같은 I/O 비동기 작업은 await
하는 순간 JS 실행이 멈추고 브라우저에 제어권이 넘어간다.
→ 이 타이밍에 React가 그린 로딩 UI가 페인트될 수 있음 ✅
반면 Papa.unparse
는 동기 CPU 작업이다.
setIsLoading(true)
직후에 곧바로 CSV 생성 코드가 실행된다.
→ Js가 계속 돌아가니 브라우저가 페인트할 틈이 없음 ❌
→ 로딩 UI가 안 보이거나 "가짜"처럼 느껴진다.
이럴 때는 브라우저에게 잠깐 숨 쉴 시간을 주면 된다.
바로 requestAnimationFrame(rAF)
을 활용하는 것이다.
export const nextFrame = () =>
new Promise<number>((resolve) => requestAnimationFrame(resolve));
// 사용 예시
setIsLoading(true);
await nextFrame();
// 무거운 작업 동기적
이렇게 하면 흐름이 바뀐다:
setIsLoading(true)
await nextFrame()
→ JS 실행을 한 프레임 동안 멈춤requestAnimationFrame
일까?await Promise.resolve();
await new Promise(r => setTimeout(r, 0));
await new Promise(requestAnimationFrame);
await
하면 확실하게 한 프레임을 양보할 수 있음.new Promise(requestAnimationFrame)
은 다음처럼 동작한다:
new Promise
는 executor 함수를 즉시 실행.rAF(resolve)
호출.rAF
는 다음 페인트 직전에 resolve
를 실행.resolve
되고, await
가 풀림.await new Promise(requestAnimationFrame)
은 다음 프레임까지 대기하는 효과를 가지며 쓰는 순간,
브라우저에게 다음과 같이 말하는 것과 같다
"잠깐, 나 지금 스피너 세팅했으니까 이번 프레임에 꼭 그려줘.
그다음에 내가 CSV 뽑을게."
Js는 싱글스레드라 "다음 프레임" 같은 구체적인 양보 지점 없이는
브라우저가 그림을 그릴 수 없기 때문에 rAF가 사실상 페인트를 강제로 보장하는 수단이 된다.
requestAnimationFrame
이 없다 (브라우저 전용).await fetch()
시 자연스럽게 브라우저가 페인트 기회를 가짐.👉 패턴으로 정리하면:
setIsLoading(true);
await nextFrame(); // 한 프레임 양보
// 동기(=CPU 바운드) 작업 시작
이렇게 하면 한 프레임을 가져오고 UI반영 성공!😊
다만 이 방법은 어디까지나 짧은 동기 작업에 적합하기 때문에 CSV 데이터가 수십 MB 이상처럼 정말 무거워지면, 한 프레임 양보만으로는 부족하다,
결국 몇 ms 단위로 작업을 청크 단위로 나누거나,
아예 Web Worker로 분리해서 메인 스레드와 UI를 지켜주는 게 더 안전할 듯 하다.
저는 requestAnimationFrame을 사용할 정도로 무거운 작업을 진행해본 적이 없어서, Promise로 항상 해결이 되었던 것 같습니다.
이번 기회에 동기 작업과 페인트 타이밍 문제에 대해 잘 알게 되었네요. 글을 명확하게 작성해주셔서 읽기 편했고, 나중에 비슷한 상황이 오면 허둥지둥거리지 않고 requestAnimationFrame에 대해 찾아볼 수 있을 것 같습니다. 좋은 글 감사합니다!!!