
mdn 브라우저는 어떻게 동작하는가 참고
mdn Critical Rendering Path(중요 렌더링 경로) 참고
제주코딩베이스캠프 - 브라우저는 어떻게 화면을 렌더링할까? 참고
poiemaweb 이벤트 루프 참고
Inpa Dev rAF 참고
브라우저의 렌더링, 이벤트 루프, 그리고 RequestAnimationFrame API는 웹 애플리케이션의 성능과 사용자 경험에 중요한 역할을 한다.
각각의 개념을 자세히 알아보려고 합니다.
브라우저 렌더링은 HTML, CSS, JavaScript 등의 웹 페이지 요소를 화면에 표시하는 과정을 말한다. 이 과정은 여러 단계로 나뉘어 있다.
브라우저가 html 파일을 렌더링 하는 경우를 살펴보면 사용자가 브라우저 주소창에 어떤 주소를 입력(URL을 입력하거나, 링크를 클릭, 폼(form)을 제출)하는 등의 동작을 통해 요청을 보낼 때 마다 발생한다.
그 주소에 있는 서버가 약속되어있는 HTML 파일을 브라우저로 전송을 해준다.
이때 전송받는 HTML 코드는 8bit의 데이터 형태로 전송된다. (byte stream 이라고 한다.)
브라우저는 전송받은 바이트를 문자로 변환하게 된다.
그리고 가지고 있는 토큰(브라우저에 저장되어있는 HTML의 시작, 종료태그, 속성, 속성값 등, 약속된 여러 가지 값들을 의미)과 비교를해서 해당 문자가 HTML 코드인지 확인을 하게 된다. (이러한 토큰을 통해서 문자가 HTML 코드인지 아닌지 확인하는 과정을 토큰화한다고 한다.)이 토큰화 과정을 통해 노드가 생성되는데 노드는 DOM Tree를 이루는 구조의 한 단위라고 보면 된다. (html, head, body, ...)
작은 노드들이 모여서 하나의 큰 트리 구조를 이루게 되는데 이 트리가 바로 DOM Tree 이다.
HTML 파싱과정과 매우 유사하다. 브라우저로부터 전달받은 CSS 바이트 파일을 문자로 해석하게 되고, 가지고 있는 토큰과 비교해서 노드를 발행한다. 그리고 이런 노드들이 모여서 CSSOM 트리를 생성하게 된다.
레이아웃 또는 리플로우라고 한다.potion, left, top, width, heigth, margin, padding, display, float, overflow, font-family, font-size, text-align, vertical-align, ...등
페인팅이라고 한다.background, box-shadow, border-radius, border-style, color, outline, text-decoration, ...등
CSS를 수정해서 화면에 보여지는 레이아웃이 변경될 때 브라우저는 Render Tree를 다시 생성하게 된다.
그리고 Layout 아웃 단계의 바로 뒤인 Paint 단계도 다시 실행된다.
Composite 단계도 다시 실행되고 최종적으로 화면이 사용자에게 보여지게 된다.
그렇기 때문에 브라우저는 레이아웃을 중간에 변경하는 것은 꽤 부담이 되는 작업이다.
그리고 이러한 현상을 리플로우 현상이라고 한다.
리플로우와 리페인트가 반드시 순차적으로 동시에 실행되는 것은 아니다.
레이아웃의 영향이 없는 변경은 리플로우 없이 리페인트만 실행된다.
transform(animation 작업을 많이 하게 됨), opacity는 DOM 트리를 변경하지 않도록 설계가 되어있다.
이벤트 루프는 자바스크립트의 비동기 프로그래밍을 처리하는 중요한 매커니즘으로 자바스크립트는 싱글 스레드로 동작하지만, 비동기 작업을 처리하기 위해 이벤트 루프를 사용하여 작업을 관리한다.
Call Stack 내에서 현재 실행중인 task가 있는지 그리고 Task Queue에 task가 있는지 반복하여 확인한다. 만약 Call Stack이 비어있다면 Task Queue 내의 task가 Call Stack으로 이동하고 실행된다.
이벤트 루프의 작동 방식과 메모리 구조에 대해 알아볼것이다.
JavaScript의 실행 환경은 다음과 같은 주요 구성 요소로 이루어져 있다.
이벤트 루프의 작동 방식은 다음과 같다.
: 이벤트 루프는 Task Queue와 Microtask Queue를 확인한다.
: Microtask Queue에 작업이 있으면, 해당 작업을 모두 처리한다. 이 과정은 Microtask Queue가 비어있을 때까지 반복된다.
: Microtask Queue가 비어있으면, Task Queue에서 대기 중인 작업을 하나 꺼내 Call Stack에서 실행한다.
: 이 과정을 계속 반복하여 비동기 작업을 처리한다.
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
이와 같이 자바스크립트의 이벤트 루프는 비동기 작업을 효율적으로 처리하는 데 중요한 역할을 한다.
들어가기 전에 자바스크립트에서 웹 애니메이션을 처리하는 방법에 대해서 알아보자.
웹페이지의 애니메이션 구현방법으로 CSS의 animation, transition, transform 속성을 통해 구현하는 방법이 있지만 사용자와의 상호작용을 구현하게 하기 위해 자바스크립트와 함께 사용하여 스타일을 변화하는 경우가 많다.
간단하고 규칙적인 애니메이션은 CSS를 사용하여 요소의 좌표값, 스타일 크기 변화등을 처리하고, 세밀한 조작이 필요한 애니메이션은 자바스크립트로 스타일 속성을 변경시키는 편이다.
자바스크립트로 스타일 속성을 변화시키는 방법은 CSS보다 성능이 좋지 않다고 한다.
자바스크립트와의 상호 협력이 필요할 경우 이를 위한 최적화 기법이 존재하는데 애니메이션 관련 최적화 API인 requestAnimationFrame() 에 대한 사용법과 원리를 알아보려고 한다.
보통 사람의 눈은 1초에 60번 장면이 넘어가야 부드럽다고 느낀다. 그래서 기기들은 시각적인 효과를 위해 초당 60번 화면을 그리도록 설계된다.
자바스크립트로 일정 시간마다 코드를 반복 호출하는 대표적인 방법으로 타이머 함수인 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 API는 웹 브라우저에서 애니메이션을 부드럽고 효율적으로 구현하기 위해 제공되는 메서드이다. 이 API는 브라우저의 렌더링 주기(자세히 알아보기)에 맞춰 애니메이션을 업데이트 할 수 있도록 도와준다.
requestAnimationFrame 메서드는 브라우저에게 수행하기를 원하는 애니메이션을 알리고 다음 리페인트 바로 전에 브라우저가 애니메이션을 업데이트할 지정된 함수를 호출하도록 요청한다.
이 메서드는 리페인트 이전에 호출할 인수로 콜백을 받는다.
브라우저는 최적의 시간에 애니메이션을 실행하여 CPU와 GPU의 부하를 줄이고, 배터리 수명을 연장하며, 부드러운 화면 전환을 제공한다.
requestAnimationFrame(줄여서 rAF)을 사용하는 기본적인 방법은 다음과 같다.
function animate(timestamp) {
// 애니메이션 업데이트 로직
// ex) 요소의 위치 변경, 회전 등
// 특정한 조건에서 requestAnimationFrame 중지, 콜백 종료
if(특정한 조건) {
cancelAnimationFrame();
}
// 다음 애니메이션 프레임 요청
requestAnimationFrame(animate);
}
// 애니메이션 시작
requestAnimationFrame(animate);
rAF는 브라우저의 렌더링 단계에서 처리된다.
이는 HTML Living Standard 스펙에서 확인할 수 있다.
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의 브라우저 이벤트 루프 설명을 참고하면, 브라우저의 렌더링 프로세스는 다음과 같다.
렌더링이 필요한 경우
- rAF 콜백 실행
- style 계산
- layout 계산
- 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