반응형
이벤트 루프는 자바스크립트의 비동기 처리 메커니즘으로, 싱글 스레드 환경에서도 비동기 작업(예: 타이머, 네트워크 요청 등)을 처리할 수 있게 도와주는 비동기 실행 모델이다.
자바스크립트는 기본적으로 한 번에 하나의 작업만 처리할 수 있는 싱글 스레드 언어지만, 이벤트 루프 덕분에 비동기 처리가 가능해진다.
동작 원리
1. 콜 스택 (Call Stack)
- 실행 중인 함수들이 쌓이는 스택이다.
- 가장 위에 있는 함수가 실행되며, 실행이 끝나면 스택에서 제거 된다.
2. 웹 API / 백그라운드 작업
- 브라우저나 Node.js 런타임이 제공하는 기능 (예: setTimeout, fetch, 이벤트 리스너)이 실행된다.
- 비동기 작업은 콜 스택에서 바로 실행되지 않고, 백그라운드로 넘겨진다.
3. 태스크 큐(Task Queue) / 콜백 큐
- 비동기 작업이 완료되면, 해당 콜백 함수가 이 큐에 들어가게 된다.
- 이벤트 루프가 콜 스택이 비었을 때 큐에서 작업을 하나씩 꺼내 실행한다.
4. 마이크로태스크 큐(Microtask Queue)
- Promise.then() 이나 process.nextTick() 같은 작업들이 들어가는 큐이다.
- 일반 태스크 큐보다 우선순위가 높아서 먼저 실행되게 된다.
간단한 예시 코드
console.log('Start');
setTimeout(() => {
console.log('Timeout callback');
}, 0);
Promise.resolve().then(() => {
console.log('Promise resolved');
});
console.log('End');
- 실행 순서
Start
End
Promise resolved
Timeout callback
- 설명
- console.log(’Start’)와 console.log(’End’)는 동기적으로 실행 (콜 스택)
- Promise는 마이크로태스크 큐로 이동, 이벤트 루프가 바로 실행
- setTimeout은 태스크 큐에 들어가고, 이벤트 루프가 다음 틱에 실행
이벤트 루프의 한 사이클(Tick)의 의미
이벤트 루프는 끊임없이 반복하면서 다음 순서를 따르게 된다.
- 콜 스택(Call Stack) 처리
- 현재 실행 중인 동기 코드를 모두 처리해 콜 스택이 완전히 비워질 때까지 실행
- 마이크로태스크 큐(Microtask Queue) 처리
- 콜 스택이 비워진 후, 마이크로태스크 큐에 있는 작업들(Promise의 then, catch, finally 등)을 모두 실행
- 태스크 큐(Task Queue) 처리
- 마이크로태스크 큐도 비워졌다면, 이제 이벤트 큐(태스크 큐)에서 작업(예: setTimeout, setInterval)을 꺼내 콜 스택으로 넘긴다.
이 과정을 한 번 완료하면 한 번의 틱(Tick)이 끝난다. 그리고 이 작업이 완료되면 이벤트 루프는 다시 처음으로 돌아가 새로운 틱을 시작해 동일한 과정을 반복한다.
setTimeout의 다음 틱은 언제 실행되나?
setTimeout()으로 예약된 콜백은 최소한 다음과 같은 조건이 충족되어야 실행된다.
- 명시된 시간이 지나야 한다.(예: setTimeout(fn,0)은 최소한 0ms 이후 실행 준비)
- 현재 콜 스택이 모두 비워져야 한다.
- 마이크로태스크 큐에 있는 작업이 모두 처리되어야 한다.
즉 다음 틱이란
- 현재 실행 중인 동기 코드(콜 스택)가 모두 끝난 이후,
- 예약된 비동기 작업이 실행될 수 있는 다음 이벤트 루프 사이클을 말한다.
이벤트 루프 내부 구조
Node.js 이벤트 루프의 구조는 크게 6단계로 나눌 수 있다. 이 이벤트 루프는 libuv라는 C++ 기반의 라이브러리로 구현되어 있다.
이벤트 루프의 6가지 단계
- timers
- setTimeout(), setInterval() 의 콜백이 실행된다.
- 설정된 시간이 지난 후, 해당 콜백이 큐에 들어간다.
- pending callbacks
- 일부 비동기 작업(예: 네트워크 요청)에 대한 콜백이 실행된다.
- idle, prepare(내부 작업)
- 내부적으로 시스템이 준비해야 하는 작업들을 처리(거의 신경 쓸 필요 없음)
- poll
- I/O 이벤트가 발생할 때까지 대기하고, 발생한 이벤트의 콜백 실행
- 예: 파일 읽기, 네트워크 요청 등
- 여기서 큐가 비어 있고, 타이머가 준비되지 않았다면 여기서 대기할 수도 있음
- check
- setImmediate() 로 등록된 콜백이 실행됨
- setTimeout() 보다 빠르게 실행될 수 있음
- close callbacks
- 소켓이나 핸들 같은 리소스가 닫힐 때 실행됨 (예: socket.on(’close’, …) )
우선 순위 정리 (Queue 순서)
- process.nextTick() → 최우선 처리 (이벤트 루프 외부에서 실행됨)
- Microtasks Queue → Promise 콜백 처리
- Event Loop Phase → 위 6단계 순서대로 실행
- setImmediate() → 특정 시점에 바로 실행하고 싶을 때 사용
- setTimeout() / setInterval() → 타이머 만료 후 실행
이벤트 루프와 멀티 스레딩 (libuv의 역할)
Node.js는 싱글 스레드 기반이지만, 내부적으로는 libuv라는 비동기 I/O 라이브러리를 사용해 멀티스레딩을 지원한다.
- 파일 시스템 작업, 네트워크 요청, 암호화 작업 등을 백그라운드 스레드 풀에서 처리
- 처리 결과가 완료되면 해당 콜백이 이벤트 루프의 큐에 들어가 실행됨
const crypto = require('crypto');
console.log('Start');
// CPU 집약적인 작업
crypto.pbkdf2('password', 'salt', 100000, 64, 'sha512', () => {
console.log('Hash calculated.');
});
console.log('End');
- 이 작업은 libuv의 스레드 풀에서 실행되므로, 메인 스레드는 막히지 않고 다른 작업을 계속 처리할 수 있음
심화 개념
1. process.nextTick() vs Promise vs setTimeout()
- process.nextTick() → Node.js에서 사용되며, 가장 높은 우선순위로, 마이크로태스크보다 먼저 실행.
- Promise.then() → 마이크로태스크로 실행돼서, 태스크 큐보다 먼저 실행
- setTimeout() → 태스크 큐에서 실행되며, 위 두 개보다 우선 순위가 낮음
예시 코드
console.log('Start');
process.nextTick(() => console.log('Next Tick'));
Promise.resolve().then(() => console.log('Promise'));
setTimeout(() => console.log('Timeout'), 0);
console.log('End');
출력 순서
Start
End
Next Tick
Promise
Timeout
2. I/O 작업의 이벤트 루프 처리
파일 시스템, 네트워크 요청 같은 I/O 작업도 이벤트 루프에서 처리되지만, 그 과정에서 내부적으로 libuv라는 라이브러리가 동작한다.
- 예를 들어, 파일 읽기 작업은 별도의 스레드 풀에서 비동기적으로 처리되고, 완료되면 이벤트 루프의 태스크 큐에 콜백이 등록된다.
3. Blocking vs Non-Blocking 코드
- Blocking code란 동기적으로 동작하는 코드로, 현재 작업이 끝나야 다음 작업이 시작된다.
- 이벤트 루프가 해당 작업이 끝날 때까지 멈춰 있으므로(block), 다른 요청을 처리할 수 없다.
- 동기 작업이 너무 오래 걸리면 이벤트 루프가 막혀버려 전체 애플리케이션이 멈추는 문제가 발생할 수 있음.
- 예를 들어 while(true) {} 같은 무한 루프는 콜 스택을 점유한 채 이벤트 루프가 돌아갈 수 없게 만든다.
- 따라서 웹 서버 같은 경우, 여러 사용자의 요청을 동시에 처리해야 되기 때문에, Non-blocking I/O가 필수적이다.
4. Timers Phase의 오해
- setTimeout(fn, 0)이 정확히 0ms 후에 실행된다는 보장은 없다.
- 최소 대기 시간이 0ms 일 뿐, 이벤트 루프의 상태에 따라 실행 타이밍이 밀릴 수 있다.
5. Garbage Collection과 이벤트 루프
- 자바스크립트의 가비지 컬렉션도 이벤트 루프 틱 사이사이에 실행되는데, 이 과정에서 짧게나마 앱이 멈출 수 있음. 왠만 하면 거의 느껴지지 않음.
공부하면서 정리한 내용입니다. 모든 지적 감사히 받겠습니다 :)
반응형
'프로그래밍 언어 > JavaScript' 카테고리의 다른 글
[js] forEach와 비동기 작업(Transaction API error: Transaction already closed: Could not perform operation 발생!) (1) | 2024.05.31 |
---|---|
[JS] var, let, const의 차이를 알아보자 (0) | 2024.03.01 |
[javascript] 자바스크립트 SyntaxError: Unexpected token o in JSON at position 1 에러 (0) | 2023.10.17 |
[JS] 자바스크립트 reduce()를 알아보자 (0) | 2023.07.28 |
[JS] import와 require의 차이 (0) | 2023.05.15 |