이벤트 루프와 렌더링

woogi·2025년 4월 23일
post-thumbnail

개요

이 블로그 글은 Jake Archibald: Event Loop - JSConf.Asia 강의를 보고 정리한 내용입니다.

다음 두 코드를 비교해 보겠습니다.

코드1

document.body.appendChild(el)
el.style.display = 'none'

코드2

el.style.display = 'none'
document.body.appendChild(el)

코드 1을 보면 "요소를 먼저 추가한 다음 숨기니, 사용자가 잠깐 요소를 보게 되지 않을까?" 하는 의문이 들 수 있습니다.
하지만 실제로는 두 코드 모두 동일하게 작동합니다. 이것이 가능한 이유는 바로 이벤트 루프 덕분입니다.

이벤트 루프와 메인 스레드

웹 페이지에는 메인 스레드가 존재하며, 여기서 자바스크립트 실행과 DOM 렌더링이 모두 이루어집니다. 자바스크립트는 싱글 스레드 언어이므로 한 번에 하나의 작업만 실행할 수 있습니다.

이벤트 루프는 작업을 조율하는 역할을 합니다. 특정 작업을 태스크 큐에 할당하면 이벤트 루프가 이를 처리합니다:

settimout(func1,1000)
settimout(func1,1000)

이벤트 루프는 실행 중이던 작업이 끝나면, 태스크 큐에 있는 작업(위 코드)을 가져와 실행합니다.

브라우저 렌더링 과정

브라우저의 렌더링 과정은 다음과 같은 단계로 이루어집니다:

1. JavaScript 실행 - 이벤트 핸들러, 타이머 콜백 등의 실행
2. 스타일 계산 - CSS 규칙 적용
3. 레이아웃 계산 - 요소 크기와 위치 계산
4. 페인트 - 픽셀을 화면에 그리기
5. 컴포지팅 - 레이어 합성

이 중요한 사실은 JavaScript 코드가 모두 실행된 후에만 렌더링이 일어난다는 것입니다. 따라서 앞서 본 두 코드는 모두 동일한 결과를 보여줍니다.

무한 루프로 브라우저 괴롭히기 코드

button.addEventListner("click",event =>{
while(true)
}

이 코드는 무한 루프로 인해 JavaScript 실행이 끝나지 않기 때문에 브라우저가 멈춥니다.

비동기 함수로 브라우저 괴롭히기 코드

function loop(){
setTimeout(loop,0)
}
button.addEventListner("click",event =>{
loop()
}

반면, 이 코드는 브라우저를 멈추지 않습니다. 왜냐하면 setTimeout은 작업을 태스크 큐에 넣고 현재 실행 중인 작업을 마치기 때문입니다. 이벤트 루프는 한 작업을 실행한 후 렌더링 단계를 거친 다음, 다음 작업을 실행합니다.

애니메이션 최적화: requestAnimationFrame vs setTimeout

애니메이션을 구현할 때 두 가지 방법을 비교해 보겠습니다.

콜백 애니메이션 코드

function callback(){
	moveBoxFoward();
  	requestAnimationFrame(callback);
}
callback()

리퀘스트 애니메이션 코드

function callback(){
	moveBoxFoward();
  	setTimeOut(callback);
}
callback()

빨라서 좋은거 아닌가요

setTimeout을 사용한 코드는 콜백이 더 자주 발생하여 애니메이션 속도가 빠르게 보일 수 있습니다. 하지만 이로 인해 다음과 같은 문제가 발생합니다.

  • 렌더링 엔진의 주기와 관계없이 진행되어 티어링 현상 발생

  • 불필요한 계산 및 프레임 드롭 발생

반면 requestAnimationFrame

  • 화면 주사율에 맞게 최적화된 애니메이션 구현 가능
  • 브라우저의 렌더링 사이클과 정확히 동기화
  • 백그라운드 탭에서 불필요한 실행을 방지하여 자원 절약

마이크로태스크와 매크로태스크

function loop(){
Promise.resolve.then(loop)
}
button.addEventListner("click",event =>{
loop()

과거에는 이러한 코드가 여러 개의 개별 이벤트(버블링 포함)를 생성했습니다. 이를 효율적으로 처리하기 위해 MutationObserver마이크로태스크 큐가 도입됩니다.

마이크로태스크로 브라우저 괴롭히기

function loop() {
  Promise.resolve().then(loop);
}

button.addEventListener("click", event => {
  loop()
})
  • 위 코드는 같은 비동기 함수임에도 불구하고 위(비동기 함수로 브라우저 괴롭히기 코드)예제와 달리 브라우저의 움직임이 멈춥니다.
  • 이는 마이크로 테스트 큐매크로 테스크 큐의 차이에서 발생하는 문제입니다.

마이크로 태스크들은 실행하면서 새로운 마이크로 태스크를 큐에 추가할 수도 있다. 새롭게 추가된 마이크로 태스크도 큐가 빌 때까지 계속해서 실행된다.

반대로, 이벤트 루프는 매크로 태스크 큐에 있는 것을 실행시키기 시작할 때 있는 매크로 태스크만 실행시킨다. 매크로 태스크가 추가한 매크로 태스크는 다음 이벤트 루프가 실행될 때까지 실행되지 않는다

  • 위 비동기 함수로 브라우저 괴롭히기 코드 예시는 루프가 매크로 테스크 큐에 삽입이 되어 다음 이벤트 루프까지 실행을 시키지 않지만

  • 마이크로 테스트 큐에 삽입된 요소들은 큐가 빌 때 까지 계속해서 실행합니다.

이러한 차이점으로 인해 같은 비동기 처리임에도 불구하고 두 코드가 다른 결과가 나옴을 알 수 있습니다.

참고 자료

이벤트 루프와 태스크 큐 (마이크로 태스크, 매크로 태스크) 벨로그
https://velog.io/@yejineee/%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84%EC%99%80-%ED%83%9C%EC%8A%A4%ED%81%AC-%ED%81%90-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%ED%83%9C%EC%8A%A4%ED%81%AC-%EB%A7%A4%ED%81%AC%EB%A1%9C-%ED%83%9C%EC%8A%A4%ED%81%AC-g6f0joxx

Jake Archibald: Event Loop - JSConf.Asia 유튜브 영상
https://www.youtube.com/watch?v=cCOL7MC4Pl0

관련 강의를 정리해놓은 블로그
https://www.andreaverlicchi.eu/blog/jake-archibald-in-the-loop-jsconf-asia-talk-transposed/

profile
https://www.hellowook.com

0개의 댓글