2 minute read

requestAnimationFrame의 콜백 함수 실행 시점이 자꾸 햇갈려서 정리해보았다.

환경

  • iOS webview

현상

iOS 11번가 앱의 웹뷰에서 풀스크린 쇼츠 플레이어(정식 이름은 11Play)를 이탈(URL 이동)할 때, <video>가 잠깐 검은 상자로 깜빡이는 문제를 잡기 위해

showImage();                    // 비디오 위에 썸네일 DOM 삽입

return new Promise((resolve) => { // Promise가 resolve 되면 플레이어 이탈 허용
  requestAnimationFrame(() =>     // rAF #1
    requestAnimationFrame(resolve)  // rAF #2
  );
})

처럼 rAF 두 번 대기 패턴을 썼다.

“한 번만 기다려도 되지 않나?” 싶지만, 렌더링 파이프라인의 정확한 순서를 보면 두 프레임을 기다려야 하는 이유가 분명해진다.


1. 한 프레임(대체로 60 Hz) 안에서 브라우저가 하는 일

VSyncₙ                                                
① rAF 큐 실행            <- rAF #1                    
② Style 계산                                          
③ Layout                                              
④ Paint                                               
⑤ Commit (메인 → 컴포지터 스레드 복사)                      
⑥ Composite / Draw (GPU 커맨드 제출)                     
( GPU swap 예약 )       <- 여기까지 끝나야 ‘화면 픽셀’ 확정    
-----------------
VSyncₙ₊₁     ← 패널이 프레임을 교체
① rAF 큐 실행            <- rAF #2
... 반복
  • rAF 콜백: 앞선 VSync가 끝난 직후, 새 프레임을 그리기 위한 렌더링 파이프라인 시작 직전
  • Style -> (…) -> Composite: rAF 뒤에 연속으로 실행.
  • 화면 표시: 다음 VSync 시점에 GPU에서 완성된 버퍼로 디스플레이 패널에 보낼 데이터를 스왑.

2. rAF #1에서는 아직 “픽셀이 GPU에 없다”

시점 무엇이 끝났나? showImage() 함수가 의도한 DOM 변경이 어디까지 반영됐나?
rAF #1 진입 아무것도 렌더링 안 됨 단순히 “DOM 플래그”만 설정
Style → Paint 레이아웃·픽셀 버퍼 생성 CPU 메모리에 그림이 생김
Commit 레이어 트리 컴포지터로 복사 GPU까지 아직 안 감
Composite / Draw GPU 명령 제출 이제야 GPU 버퍼 업데이트
VSync₁ 버퍼 스왑 사용자·탭 스냅샷에 보임

따라서 rAF #1 안에서 resolve()로 탐색을 허용하면, iOS의 WKWebView가 URL 이동을 위해 탭 스냅샷을 찍을 때, 렌더링 파이프라인의 Commit 시점 이전에 스냅샷을 찍어서 검은 사각형이 재발한다.


3. rAF #2는 “레이어 커밋까지 끝난 후” 호출된다

  • rAF #2가 실행되려면 이미 Composite → GPU swap 예약이 끝나 브라우저가 다음 VSync만 기다리는 상태여야 한다.
  • 즉, rAF #2 실행 시점에는 이미 showImage()가 만든 픽셀이 GPU 버퍼에 존재함이 보장된다.
  • 이 시점에 URL 이동·스냅샷·Process Swap이 시작돼도 검은 비디오 레이어가 아닌 showImage 함수가 띄운 이미지가 캡처된다.

4. 그러니까 rAF는 언제 실행되냐면

VSync₀
└─ rAF #1 ─ showImage(); resolve();  ← 여기서 URL 이동(플레이어 이탈)을 허용하면 위험
   렌더링 파이프라인의 그 과정(Style→ ... →Composite)
VSync₁  ← 썸네일 픽셀이 실제로 보이는 순간
└─ rAF #2 ─ resolve();              ← 여기서 URL 이동 허용 = 안전
   그 과정(Style→ ... →Composite)
VSync₂                              ← 이 시점에는 다른 URL 페이지로 이동 시작

5. 렌더링 파이프라인 순서 다시 정리

# 실제로 일어나는 일
0. VSync₀
이전 프레임 표시 완료
디스플레이가 수직 공백(Vertical Blank)으로 진입. OS/브라우저에 “다음 프레임 그릴 시간” 알림(VSYNC)
1. showImage() 실행 DOM 변경만 기록. 스타일·레이아웃 계산은 아직 안 됨
2. rAF 콜백 실행 (첫 rAF) rAF는 VSync₀ 직후·다음 렌더링 **직전 단계에 실행*
-> resolve()를 여기서 호출하면 이 시점에 아직 렌더링 파이프라인(3-6)이 *안 끝난
상태
3. Style → Layout → Paint 한 프레임 안에서 브라우저가 DOM 변경을 계산·픽셀화
4. Commit 메인 스레드 → 컴포지터 스레드로 레이어 트리 복사
5. Composite / Draw 컴포지터 스레드가 여러 레이어를 합성하고 GPU에 커맨드 제출
6. Buffer swap 예약 GPU가 “다음 VSYNC에 버퍼 교체” 예약
7. VSync₁ 이번에 만든 프레임이 이 순간 패널로 전송돼 사용자에게 보임
-> 여기서야 비로소 showImage() 결과(이미지) 등장
8. rAF 콜백 실행 (두 번째 rAF) VSync₁ 직후. 이제 직전 프레임이 완전히 화면에 반영됐음을 보장
9. Style → Layout → … → VSync₂ 다음 프레임 렌더링 사이클 반복

2줄 요약

requestAnimationFrame의 콜백은 1VSync 늦게 실행되는 게 아니다. 이번 렌더링 사이클에 당장 실행된다.

참고자료:

EOD

20250704

Leave a comment