티스토리 뷰

반응형

이벤트 루프와 비동기 처리 + 가비지 컬렉션과의 연관

 

 

자바스크립트는 싱글 스레드 기반이다. 

싱글 스레드이기 때문에 한번에 한가지 작업만 실행할 수 있다.

하지만 그렇다고 한번에 한가지 작업만 하게되면? 문제가 많을것이다. (무한 대기가 생기겠지?)

그렇기 때문에 브라우저나 노드 환경에서 여러 작업을 동시에 처리할 수 있도록 설계되어있는데, 이 핵심이 이벤트 루프다.

 

 

이벤트 루프란?

자바스크립트의 비동기 처리를 가능하게하는 매커니즘이다. (비동기 작업 HTTP요청이나 setTimeout 등..)

비동기 작업은 나중에 실행되도록 예약하고, 이벤트 루프는 준비된 작업을 실행할 수 있도록 한다.

이때 어떤 순서대로 실행을 시킬까?

우리가 여기서 알아야하는 기본적인 개념은 '콜스택, 태스트큐, 마이크로태스크큐' 이다. (WebAPIs 도..)

 

 

콜스택 (Call Stack)

자바스크립트 엔진이 실행할 코드를 담아두는 스택 자료구조로 선입 후출 방식(LIFO, Last In First Out)이다. 즉, 가장 마지막에 들어온 코드가 먼저 실행된다는 의미이다. 

동기적인 작업(함수 호출, 연산 등)은 콜스택에 쌓이고 선입 후출로 실행되며, 완료 시 스택에서 제거된다.

모든 동기 코드가 스택에 쌓였다가 나오는 것은 아니다, 스택에 쌓였다가 바로 빠지는 경우도 있기 때문에 선입 후출 방식을 체감하기 위해서는 중첩 호출처럼 쌓이는 구조일때 가능하다.

 

 

예시)

console.log('start');
console.log('end');

 

이 경우에는 선입 후출 구조를 관찰할만큼의 중첩이 없는 각각 한줄짜리 실행이기에 스택에 쌓이는 시간이 부족하므로 하나 실행 → 끝  → 다음줄 실행 으로 이루어진다.

 

 

예시)

function a() {
  console.log('a');
  b();
}

function b() {
  console.log('b');
  c();
}

function c() {
  console.log('c');
}

a();

 

호출 흐름

  1. a() 호출 → 스택: [a]
  2. b() 호출 → 스택: [a, b]
  3. c() 호출 → 스택: [a, b, c]
  4. c() 실행 끝 → pop → [a, b]
  5. b() 실행 끝 → pop → [a]
  6. a() 실행 끝 → pop → []

 

태스크 큐 (Task Queue)

비동기 작업의 콜백 함수가 담기는 곳으로 큐 자료구조로 선입 선출 방식(FIFO, First In First Out)이다.

먼저 들어온 코드가 먼저 실행된다.

태스크큐의 비동기 작업의 콜백 함수란 setTimeout, setInterval과 같이 타이머 콜백이나 DOM 이벤트의 비동기 콜백 등이 여기에 해당한다.

콜 스택이 완전히 비어있게 되면 이벤트 루프가 태스크 큐에서 작업을 가져와 콜 스택에 넣는 방식이다.

 

→ WebAPIs, NodeJsAPIs는 타이머(setTimeout), 네트워크요청(fetch), 이벤트 리스너(addEventListener)는 브라우저(또는 Node.js런타임)의 백그라운드에서 실행되고 완료된 콜백을 태스크 큐로 전달한다. 

이벤트 루프가 콜 스택이 비어있는 시점에 해당 콜백을 꺼내 콜스택으로 보낸다. (콜스택에 push)

 

 

마이크로태스크 큐 (Microtask Queue)

Promise의 then이나 catch (promise의 콜백), MutationObserver의 콜백이 담기는 곳으로 큐 자료구조로 선입 선출(FIFO, First In First Out) 방식이다. 먼저 들어온 코드가 먼저 실행된다.

마이크로태스크큐의 우선순위는 태스크 큐보다 높아서 콜 스택이 완전히 비어있게 되면 이벤트 루프가 마이크로태스크 큐를 먼저 확인해서 작업을 가져와 콜 스택에 넣는다. 

 

→ 사실 위처럼 보이는게 맞지만, 정확한 개념으로는 틀렸다고 볼 수 있다. (mdn)

  • When a new iteration of the event loop begins, the runtime executes the next task from the task queue. Further tasks and tasks added to the queue after the start of the iteration will not run until the next iteration.

이벤트 루프의 틱은 태스크 큐를 먼저 확인하여 하나를 실행하고 (macrotask 처리) 

이후 콜스택이 비면, 마이크로태스크큐를 모두 처리한다. (microtask 처리) 

 

즉, 실행순서는 아래와 같다.

콜스택이 비었는지 체크 (체크 후 실행) → 태스크큐에서 작업을 체크, 작업이 있으면 1개 꺼내 콜스택에 넣고 실행 → 실행 중에 콜스택이 채워졌다가 다시 비게되면 → 마이크로태스크큐에서 작업을 체크, 작업이 있으면 전부 실행 → 필요할 경우 렌더링 → 콜 스택 비어있는지 체크부터 다시 반복(다음 루프)

 

 

예제)

태스크큐가 먼저 실행되는데 어째서 setTimeout(fn, 0)이 뒤에 출력되는지에 대한 예시

(이벤트 루프의 한 틱안에서 마이크로태스크큐가 전부 처리되기 때문에, 우선적으로 보이는 현상)

console.log('script start');

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

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

queueMicrotask(() => {
  console.log('queueMicrotask (Microtask)');
});

console.log('script end');

 

출력

  • script start              // Call Stack
  • script end                // Call Stack
  • promise 1 (Microtask)     // Microtask Queue
  • promise 2 (Microtask)     // Microtask Queue
  • queueMicrotask (Microtask)// Microtask Queue
  • setTimeout (Macrotask)    // Macrotask Queue

 

설명

이벤트 루프 틱의 실행 순서에서 큐의 순서는 태스크 큐 > 마이크로 태스크큐이다.

하지만 setTimeout(fn, 0)을 호출하면, 브라우저의 WebAPIs의 타이머가 실행되고, 그 콜백이 태스크큐로 들어가기까지 최소지연시간(1ms~4ms)이 존재한다. 즉, 0ms로 지정해도 무조건 '다음 이벤트 루프 틱 이후'에 일어나게 된다. (0.5ms와 같이 더 빨라져도 구조상 다음 이벤트 루프 틱 이후)

따라서 위 예시의 출력의 이벤트 루프 틱 최초 1회에 실행되는건 queueMicrotask(microtask) 까지이다. 그리고 나서 다시 이벤트 루프 틱이 돌고, 2번째 틱에서 setTimeout이 출력된다.

 

출력만으로 충분히 마이크로태스크큐의 우선순위가 높다고 오해할수있지만, 이벤트 루프의 정확한 실행 순서는 태스크큐 > 마이크로 태스크큐 이다.

 

 

 

가비지 컬렉션과의 연관

이벤트 루프와 가비지 컬렉션은 서로 독립적으로 작동하지만 메모리 관리와 실행 흐름의 관점에서 간접적인 연관이 있다.

가비지 컬렉션을 간단히 말하면, 사용하지 않는 메모리를 해제하여 메모리 누수를 방지하는데 이때 메모리 해제의 기준은 도달 가능성으로 참조되지 않는 객체를 메모리에서 제거하는 역할을 한다.

 

이벤트 루프에서 처리되는 비동기 작업은 객체와 데이터(콜백함수, 타이머, 네크워크 응답 등)를 메모리에 유지해야한다.

가비지 컬렉션은 아래와 같은 상황에서 중요한 역할을 한다.

 

1. 비동기 작업 중 메모리 관리

이벤트 루프에서 비동기 작업의 콜백이 실행되기 전까지는 해당 작업과 관련된 객체가 메모리에 유지된다.

setTimeout의 콜백이 대기 상태일때, 관련 객체는 도달 가능한 상태로 판단되어 가비지 컬렉션 대상이 되지 않는다.

 

 

2. 작업 완료 후 메모리 해제

비동기 작업이 완료되고, 콜백이 실행된 후 관련 데이터가 더 이상 필요 없어졌을 경우, 가비지 컬렉션이 불필요한 객체를 제거하여 메모리를 확보한다.

 

동작 설명을 해보면

setTimeout이 실행 > 콜백이 태스크 큐에 대기 - 이때는 가비지 컬렉션 대상이 아니어서 메모리에 유지된다.

> 콜스택이 비어있어서 이벤트 루프가 마이크로 태스크 확인, 이것도 비어있어서 태크스 큐 확인 > setTimeout의 콜백이 이벤트 루프에 의해 콜스택으로 이동 > 콜백 실행 - 가비지 컬렉션 대상이 되어 메모리에서 해제된다.

 

 

3. 이벤트 루프 - 가비지 컬렉션 서로에게 미치는 영향

이벤트 루프 > 가비지 컬렉션에 미치는 영향

이벤트 루프는 콜백의 생명주기를 관리하며 작업 완료 후에는 메모리에서 해제될 수 있도록 준비한다. 만약 이벤트 루프가 차단되면 (blocking task 발생) 가비지 컬렉션이 실행되는 타이밍이 지연될 수 있다. 이는 메모리 관리 효율성을 떨어트릴수 있다.

 

가비지 컬렉션 > 이벤트 루프에 미치는 영향

가비지 컬렉션은 자주 실행되지 않고, 주로 메모리가 부족할 때 실행되는데 가비지 컬렉션이 실행 중에는 자바스크립트의 싱글 스레드 모델에 따라 실행 중인 작업이 일시적으로 중단 될 수 있다.

즉, 자바스크립트 엔진은 가비지 컬렉션 중 '일시적으로 모든 작업을 중단' (stop-the-world 문제)하게 되면 이로 인해 이벤트 루프가 잠시 멈추게 되고 성능에 영향을 줄 수 있다.

 

따라서 최적화를 잘 하기 위해서는

이벤트 루프는 불필요한 비동기작업을 줄이고 이벤트 루프가 효율적으로 동작하도록 해야하며, 가비지 컬렉션은 전역 객체나 참조 순환을 피하여 참조를 관리하는 다양한 방식(weakMap, WeakSet..)을 고려해야한다.

 

 

반응형
최근에 올라온 글
최근에 달린 댓글
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
Total
Today
Yesterday