반응형
JavaScript의 이벤트 루프
예제를 이용해 아래 문제를 풀어봅시다.
각각 1밀리 초와 2밀리 초 후에 실행되는 두 개의 타이머를 설정했습니다.
setTimeout(()=> console.info('A'), 1)
setTimeout(()=> console.info('B'), 2)
console.info('C')
콘솔엔 어떤 결과가 출력될까요?
A, B, C일까요?
JavaScript 프로그래머라면, A, B, C가 아니라 C, A, B가 된다는 사실을 알고 있을 것입니다.
JavaScript나 TypeScript를 사용해 본 적이 없는 사람이라면 이 결과가 직관적이지 않고 이상해 보일 것입니다.
사실 동작 원리는 간단합니다.
JavaScript의 동시성 모델이 C언어의 sleep이나 Java 같은 다른 스레드 기반 작업 스케줄링 언어와는 다르기 때문에 이런 현상이 발생합니다.
거시적으로 보면 JavaScript VM은 아래처럼 동시성을 흉내 냅니다.
- 메인 JavaScript 스레드는 XMLHTTPRequest(AJAX 요청), setTimeout(잠자기), readFile(디스크에서 파일 읽기) 등의 네이티브 비동기 API를 호출합니다. 이들 API는 JavaScript 플랫폼에서 제공하며, 직접 만들 수 없습니다.
- 네이티브 비동기 API를 호출한 이후에 다시 메인 스레드로 제어가 반환되며, 아무 일도 없었던 것처럼 코드를 계속 실행합니다.
- 비동기 작업이 완료되면 플랫폼은 태스크(task)를 이벤트 큐에 추가합니다. 각 스레드가 자신만의 큐를 가지고 있으며 이를 이용해 비동기 연산 결과를 메인 스레드로 전달합니다. 태스크엔 호출 자체와 관련한 메타 정보 일부와 메인 스레드와 연결된 콜백 함수의 참조가 들어있습니다.
- 메인 스레드의 콜 스택이 비면 플랫폼은 이벤트 큐에 남아 있는 태스크가 있는지 확인합니다. 대기 중인 태스크가 있으면 플랫폼은 그 태스크를 실행합니다. 이때 함수 호출이 일어나며 제어는 메인 스레드 함수로 반환됩니다. 함수 호출이 끝나고 콜 스택이 다시 비면 플랫폼은 다시 기다리는 태스크가 있는지 이벤트 큐에서 확인합니다. 콜 스택과 이벤트 큐가 모두 비고, 모든 비동기 네이티브 API 호출이 완료될 때까지 이 과정을 반복합니다.
위의 사실을 기억하고 다시 setTimeout 예제로 돌아가보겠습니다.
예제에선 아래와 같은 일이 일어납니다.
- setTimeout을 호출하면 개발자가 건넨 콜백 참조와 1을 인수로 네이티브 타임아웃 API를 호출합니다.
- setTimeout을 다시 호출하면 두 번째 콜백 참조와 2를 인수로 네이티브 타임아웃 API를 다시 호출합니다.
- C를 콘솔에 출력합니다.
- 백그라운드에서 1밀리 초가 지난 다음 JavaScript 플랫폼이 태스크(task)를 이벤트 큐에 추가하여, 첫 번째 setTimeout에서 지정한 시간이 만료되었고 콜백을 호출할 수 있음을 알립니다.
- 다시 1밀리 초가 지난 다음 플랫폼이 두 번째 setTimeout의 콜백을 호출할 수 있도록 두 번째 태스크(task)를 이벤트 큐에 추가합니다.
- 콜 스택이 비었으므로 3번 과정을 완료한 플랫폼은 이벤트 큐에 태스크(task)가 있는지 확인합니다. 4, 5번 과정 중 하나라도 완료되었다면 태스크가 존재할 것입니다. 발견한 각 태스크에서 관련 콜백 함수를 호출합니다.
- 설정한 두 타임아웃이 모두 지났고 이벤트 큐와 콜 스택이 모두 비었다면 프로그램은 종료됩니다.
이러한 방식으로 A, B, C가 아니라 C, A, B가 출력됩니다.
반응형
'👶 TypeScript' 카테고리의 다른 글
백엔드 프레임워크 (0) | 2023.01.25 |
---|---|
비동기 스트림 (0) | 2023.01.24 |
콜백(callback) 사용하기 (0) | 2023.01.24 |
async와 await (0) | 2023.01.23 |
비동기 프로그래밍, 동시성과 병렬성 (0) | 2023.01.23 |