브라우저 렌더링 & 이벤트 루프 & RequestAnimationFrame API

Tori·2024년 11월 19일

JavaScript

목록 보기
5/7
post-thumbnail

mdn 브라우저는 어떻게 동작하는가 참고
mdn Critical Rendering Path(중요 렌더링 경로) 참고
제주코딩베이스캠프 - 브라우저는 어떻게 화면을 렌더링할까? 참고
poiemaweb 이벤트 루프 참고
Inpa Dev rAF 참고






브라우저의 렌더링, 이벤트 루프, 그리고 RequestAnimationFrame API는 웹 애플리케이션의 성능과 사용자 경험에 중요한 역할을 한다.

각각의 개념을 자세히 알아보려고 합니다.



브라우저 렌더링

브라우저 렌더링은 HTML, CSS, JavaScript 등의 웹 페이지 요소를 화면에 표시하는 과정을 말한다. 이 과정은 여러 단계로 나뉘어 있다.



1. HTML 파싱 -> DOM 트리 생성

브라우저가 html 파일을 렌더링 하는 경우를 살펴보면 사용자가 브라우저 주소창에 어떤 주소를 입력(URL을 입력하거나, 링크를 클릭, 폼(form)을 제출)하는 등의 동작을 통해 요청을 보낼 때 마다 발생한다.

그 주소에 있는 서버가 약속되어있는 HTML 파일을 브라우저로 전송을 해준다.

이때 전송받는 HTML 코드는 8bit의 데이터 형태로 전송된다. (byte stream 이라고 한다.)

브라우저는 전송받은 바이트를 문자로 변환하게 된다.
그리고 가지고 있는 토큰(브라우저에 저장되어있는 HTML의 시작, 종료태그, 속성, 속성값 등, 약속된 여러 가지 값들을 의미)과 비교를해서 해당 문자가 HTML 코드인지 확인을 하게 된다. (이러한 토큰을 통해서 문자가 HTML 코드인지 아닌지 확인하는 과정을 토큰화한다고 한다.)

이 토큰화 과정을 통해 노드가 생성되는데 노드는 DOM Tree를 이루는 구조의 한 단위라고 보면 된다. (html, head, body, ...)

작은 노드들이 모여서 하나의 큰 트리 구조를 이루게 되는데 이 트리가 바로 DOM Tree 이다.

  • 브라우저는 HTML 문서를 파싱하여 DOM(Document Object Model) 트리를 생성한다.
  • DOM은 문서의 구조를 나타내는 객체이다.
  • 브라우저가 DOM Tree를 생성할 때 link, img와같은 태그를 만나게되면 해당 태그 안에 명시되어있는 리소스를 다운받게 된다.
  • 애니메이션 및 기타 작업 스크립트를 수행
  • 브라우저는 script를 만나게 되면 DOM Tree 생성을 중단하고 script 태그 안에 들어있는 자바스크립트 코드를 해석한다. (defer, async 스크립트를 이용해서 문제를 해결할 수 있다.)



2. CSS 파싱 -> CSSOM 트리 생성

HTML 파싱과정과 매우 유사하다. 브라우저로부터 전달받은 CSS 바이트 파일을 문자로 해석하게 되고, 가지고 있는 토큰과 비교해서 노드를 발행한다. 그리고 이런 노드들이 모여서 CSSOM 트리를 생성하게 된다.

  • CSS 파일과 스타일 태그를 파싱하여 CSSOM(CSS Object Model) 트리를 생성한다.
  • CSSOM은 스타일 규칙을 나타내는 객체이다.



3. Render Tree 생성

  • DOM과 CSSOM을 결합하여 Render Tree를 생성한다.
  • Render Tree는 화면에 실제로 표시될 요소들만 포함하며, 각 요소의 스타일 정보도 포함된다.



4. Layout (Reflow)

  • Render Tree를 기반으로 각 요소의 위치와 크기를 계산한다. (Render Tree 배치)
  • 이 과정을 레이아웃 또는 리플로우라고 한다.
  • css 속성 중 display: none과 같은 속성이 적용된 요소는 Render Tree안에 포함되지 않는다. (웹 접근성을 위해서 텍스트를 숨겨야 되는 경우에 display: none과 같은 속성을 적용해버리면 렌더트리 안에 해당 요소가 들어가있지 않기 때문에 화면을 읽어주는 스크린 리더같은 속성이 인식할 수 없는 문제가 발생하게 된다.)

Layout 속성

potion, left, top, width, heigth, margin, padding, display, float, overflow, font-family, font-size, text-align, vertical-align, ...등



5. Paint (Repaint)

  • 레이아웃이 완료되면, 브라우저는 각 요소를 픽셀로 변환하여 화면에 그린다. 이 과정을 페인팅이라고 한다.
  • 렌더 트리를 따라서 페인트 기록이 생성되는데 이 페인트 기록에는 요소를 렌더링하는 순서, 지금까지의 정보를 바탕으로 한 페이지를 여러 개의 레이어로 나눈다음 그 위에 text, color, img, shadow 등 모든 시각적인 부분을 그리는 작업이 진행된다.

Paint 속성

background, box-shadow, border-radius, border-style, color, outline, text-decoration, ...등



6. Composite

  • 복잡한 레이아웃의 경우, 여러 레이어로 나누어 그린 후 최종적으로 각 레이어들을 합성하여 화면에 표시한다.
    - 앞서 Paint 단계에서 만들어둔 여러가지 레이어를 스크린에 픽셀로 표현하게 된다.
    - 나누어져있던 레이어들을 하나로 합성해서 페이지를 완성하게 된다.



Reflow & Repaint

CSS를 수정해서 화면에 보여지는 레이아웃이 변경될 때 브라우저는 Render Tree를 다시 생성하게 된다.

그리고 Layout 아웃 단계의 바로 뒤인 Paint 단계도 다시 실행된다.
Composite 단계도 다시 실행되고 최종적으로 화면이 사용자에게 보여지게 된다.

그렇기 때문에 브라우저는 레이아웃을 중간에 변경하는 것은 꽤 부담이 되는 작업이다.
그리고 이러한 현상을 리플로우 현상이라고 한다.

리플로우와 리페인트가 반드시 순차적으로 동시에 실행되는 것은 아니다.
레이아웃의 영향이 없는 변경은 리플로우 없이 리페인트만 실행된다.

transform, opacity는 리플로우, 리페인트 생략 - GPU가 관여할 수 있는 속성

transform(animation 작업을 많이 하게 됨), opacity는 DOM 트리를 변경하지 않도록 설계가 되어있다.





이벤트 루프 (Event Loop)

이벤트 루프는 자바스크립트의 비동기 프로그래밍을 처리하는 중요한 매커니즘으로 자바스크립트는 싱글 스레드로 동작하지만, 비동기 작업을 처리하기 위해 이벤트 루프를 사용하여 작업을 관리한다.

Call Stack 내에서 현재 실행중인 task가 있는지 그리고 Task Queue에 task가 있는지 반복하여 확인한다. 만약 Call Stack이 비어있다면 Task Queue 내의 task가 Call Stack으로 이동하고 실행된다.

이벤트 루프의 작동 방식과 메모리 구조에 대해 알아볼것이다.


이벤트 루프의 기본 개념

JavaScript의 실행 환경은 다음과 같은 주요 구성 요소로 이루어져 있다.

Call Stack (호출 스택)

  • 현재 시행 중인 함수의 호출을 추적한다.
  • 함수가 호출되면 스택에 추가되고, 실행이 끝나면 스택에서 제거된다.

Heap (힙)

  • 객체와 같은 동적 메모리 할당을 위한 공간이다.

Task Queue(Event Queue, 작업 큐)

  • 비동기 작업(ex) setTimeout, I/O 작업 등)이 완료되면 해당 작업이 대기하는 큐다.

Microtask Queue (마이크로 작업 큐)

  • Promise의 then 또는 catch와 같은 마이크로 작업이 대기하는 큐다.
  • 마이크로 작업은 일반 작업보다 우선적으로 실행된다.




이벤트 루프의 작동 방식

이벤트 루프의 작동 방식은 다음과 같다.

1. Call Stack이 비어있을 때

: 이벤트 루프는 Task Queue와 Microtask Queue를 확인한다.

2. Microtask Queue 우선 처리

: Microtask Queue에 작업이 있으면, 해당 작업을 모두 처리한다. 이 과정은 Microtask Queue가 비어있을 때까지 반복된다.


3. Task Queue 처리

: Microtask Queue가 비어있으면, Task Queue에서 대기 중인 작업을 하나 꺼내 Call Stack에서 실행한다.


4. 반복

: 이 과정을 계속 반복하여 비동기 작업을 처리한다.



메모리 구조

JavaScript의 메모리 구조는 다음과 같이 요악할 수 있다.

Call Stack

: 함수 호출과 실행을 관리하는 스택 구조, LIFO(Last In, First Out) 방식으로 작동한다.


Heap

: 동적으로 할당된 메모리 공간으로, 객체와 배열 등의 데이터가 저장된다.


Task Queue

: 비동기 작업이 완료되면 해당 작업이 대기하는 큐, 일반적으로 이벤트 리스너와 같은 작업이 포함된다.


Microtask Queue

: Promise와 같은 마이크로 작업이 대기하는 큐, Microtask Queue는 Task Queue보다 우선적으로 처리된다.



이벤트 루프의 작동 방식을 보여주는 간단한 예시코드

console.log('Start');

setTimeout(() => {
  console.log('Timeout 1');
}, 0);

Promise.resolve().then(() => {
  console.log('Promise 1');
});

setTimeout(() => {
  console.log('Timeout 2');
}, 0);

Promise.resolve().then(() => {
  console.log('Promise 2');
});

console.log('End');

// Start
// End
// Promise 1
// Promise 2
// Timeout 1
// Timeout 2

설명

  1. console.log('Start')와 console.log('End')는 호출 스택에서 즉시 실행된다.
  2. setTimeout은 Task Queue에 작업을 추가한다.
  3. Promise.resolve().then()은 Microtask Queue에 작업을 추가한다.
  4. Microtask Queue의 작업이 먼저 실행되므로 Promise 1과 Promise 2가 먼저 출력된다.
  5. 그 후 Task Queue의 작업이 실행되어 Timeout 1과 Timeout 2가 출력된다.

이와 같이 자바스크립트의 이벤트 루프는 비동기 작업을 효율적으로 처리하는 데 중요한 역할을 한다.





RequestAnimationFrame API

들어가기 전에 자바스크립트에서 웹 애니메이션을 처리하는 방법에 대해서 알아보자.

자바스크립트 웹 애니메이션

웹페이지의 애니메이션 구현방법으로 CSS의 animation, transition, transform 속성을 통해 구현하는 방법이 있지만 사용자와의 상호작용을 구현하게 하기 위해 자바스크립트와 함께 사용하여 스타일을 변화하는 경우가 많다.

간단하고 규칙적인 애니메이션은 CSS를 사용하여 요소의 좌표값, 스타일 크기 변화등을 처리하고, 세밀한 조작이 필요한 애니메이션은 자바스크립트로 스타일 속성을 변경시키는 편이다.
자바스크립트로 스타일 속성을 변화시키는 방법은 CSS보다 성능이 좋지 않다고 한다.

자바스크립트와의 상호 협력이 필요할 경우 이를 위한 최적화 기법이 존재하는데 애니메이션 관련 최적화 API인 requestAnimationFrame() 에 대한 사용법과 원리를 알아보려고 한다.



초당 60 프레임

보통 사람의 눈은 1초에 60번 장면이 넘어가야 부드럽다고 느낀다. 그래서 기기들은 시각적인 효과를 위해 초당 60번 화면을 그리도록 설계된다.

  • frame: 각각의 장면
  • frame rate or frame per second(fps): 특정 시간 내에 보여지는 frame 갯수



타이머 함수를 이용한 애니메이션 스크립팅

자바스크립트로 일정 시간마다 코드를 반복 호출하는 대표적인 방법으로 타이머 함수인 setInterval()setTimeout을 이용해 60프레임에 맞춰 코드를 짤 수 있다.

const animation = () => {
  // 애니메이션 업데이트 로직
}

// 1초에 60번 무한 반복
setInterval(animation, 1000/60);
const animation = () => {
  // 애니메이션 업데이트 로직
  
  // 함수 내부에서 다시 setTimeout을 호출하여 반복
  setTimeout(animation, 1000/60);
}

setTimeout(animation, 1000/60)



타이머 함수의 문제점

setInterval(), setTimeout()의 문제점은 주어진 시간내에 동작할 뿐 프레임 시작 시간에 맞춰 실행되는 것이 보장되지 않는다.

1초에 60번 실행되려면 16ms 간격으로 프레임 단위가 진행되어야 하는데 브라우저에서 다른 작업을 수행중일 경우 콜백 코드 부분이 제때 실행되지 못할 수 있다.

자바스크립트 실행에 의해 reflow가 일어나서 브라우저의 렌더링 단계인 reflow-repaint-composite 과정이 일어나게 되면, 프레임이 생성되지 못하고 누락되어 프레임이 깎이는 현상이 나타난다. -> 자바스크립트의 콜 스택이 싱글 스레드이기 때문

프레임이 깎이는 것은 프레임 드랍이 일어나 화면이 버벅이고, 이러한 지연 발생 문제때문에 대안으로 나온것이 requestAnimationFrame(rAF)이다.



requestAnimationFrame

: requestAnimationFrame API는 웹 브라우저에서 애니메이션을 부드럽고 효율적으로 구현하기 위해 제공되는 메서드이다. 이 API는 브라우저의 렌더링 주기(자세히 알아보기)에 맞춰 애니메이션을 업데이트 할 수 있도록 도와준다.



1. 개념

requestAnimationFrame 메서드는 브라우저에게 수행하기를 원하는 애니메이션을 알리고 다음 리페인트 바로 전에 브라우저가 애니메이션을 업데이트할 지정된 함수를 호출하도록 요청한다.

이 메서드는 리페인트 이전에 호출할 인수로 콜백을 받는다.

브라우저는 최적의 시간에 애니메이션을 실행하여 CPU와 GPU의 부하를 줄이고, 배터리 수명을 연장하며, 부드러운 화면 전환을 제공한다.

  • 브라우저에게 애니메이션을 수행하고 싶다고 알려주는 방법
  • 다음 repaint가 발생하기 전에 호출될 콜백을 예약



2. 주요 특징

  1. 브라우저의 리페인트(repaint) 주기에 맞춰 실행된다.
  2. 일반적으로 60fps(초당 60프레임)로 동작한다. (디스플레이 주사율을 따른다.)
  3. 비활성 탭이나 hidden 상태일 때는 실행을 중지하여 성능을 최적화한다.
  4. setTimeout / setInterval보다 부드러운 애니메이션을 구현할 수 있다.



3. 사용법

requestAnimationFrame(줄여서 rAF)을 사용하는 기본적인 방법은 다음과 같다.

function animate(timestamp) {
  // 애니메이션 업데이트 로직
  // ex) 요소의 위치 변경, 회전 등
  
  // 특정한 조건에서 requestAnimationFrame 중지, 콜백 종료
  if(특정한 조건) {
    cancelAnimationFrame();
  }
  
  // 다음 애니메이션 프레임 요청
  requestAnimationFrame(animate);
}

// 애니메이션 시작
requestAnimationFrame(animate);



requestAnimationFrame(rAF)의 실행 단계

rAF는 브라우저의 렌더링 단계에서 처리된다.

이는 HTML Living Standard 스펙에서 확인할 수 있다.

  • rAF 콜백은 microtask queue나 macro task queue에서 실행되지 않는다.
  • 대신 브라우저의 렌더링 단계에서 실행된다.
  • rAF 콜백은 브라우저의 렌더링 파이프라인의 일부로 실행된다. 이는 성능과 애니메이션의 부드러움을 최적화하기 위한 설계이다.

브라우저 렌더링 파이프라인에서의 위치

rAF 콜백의 실행 순서는 다음과 같다.

JavaScript -> requestAnimationFrame -> Style -> Layout -> Paint -> Composite


예제 코드로 확인

console.log('1');

setTimeout(() => {
    console.log('2');
}, 0);

Promise.resolve().then(() => {
    console.log('3');
});

requestAnimationFrame(() => {
    console.log('4');
});

console.log('5');

// 출력 순서:
// 1
// 5
// 3 (마이크로태스크)
// 2 (매크로태스크)
// 4 (다음 렌더링 단계에서 실행)



브라우저 렌더링 프로세스

Jake Archibald의 브라우저 이벤트 루프 설명을 참고하면, 브라우저의 렌더링 프로세스는 다음과 같다.

1. Task (macro task) 실행

2. microtask queue 실행

3. 렌더링 기회 확인

렌더링이 필요한 경우

  1. rAF 콜백 실행
  2. style 계산
  3. layout 계산
  4. paint



구체적 실행 순서를 확인하는 예시

Promise.resolve().then(() => console.log('micro task'));

setTimeout(() => console.log('macro task'), 0);

requestAnimationFrame(() => {
    console.log('animation frame');
    
    // rAF 내부의 마이크로태스크
    Promise.resolve().then(() => {
        console.log('micro task inside rAF');
    });
});

// 실행 순서:
// 1. micro task
// 2. macro task
// 3. animation frame
// 4. micro task inside rAF




profile
🌿

0개의 댓글