전 포스트 에 이은 글이다.
이 포스트의 예제 코드는 ES6 으로 작성되었습니다.
뒤돌아보기
전 포스트들에서 비동기의 대략적 흐름과 Promise 의 기본 동작에 대해 다루다가 잠깐 언급한 내용이 있다. Timer 와 Promise 를 비교하면서 Timer 함수보다 Promise 가 더 우선권이 있다고 했었다.
실제로 Promise 다른 javascript 일반적인 비동기 수행보다 앞선 비동기적 우선권을 가진다. 이것에 대해 이해하려면 HTML Living Standard 에 새로 추가된 Micro Task 에 대해 좀 더 알 필요가 있다.
Task
와 MicroTask
에 대해 자세히 알아보자
Task
Event Loop 는 하나 혹은 그 이상의 Task Queue 라는 부르는 Task 가 순서대로 정렬된 List 를 가진다.
Task 는 다음과 같은 여러 작업들의 모음이다.
- 스크립트 실행
- 이벤트
- HTML 파싱
- 콜백
- Fetch, Ajax
- DOM 조작
javascript 를 실행하는 것도 태스크, HTML 파싱, Timer, DOM 조작 등이 전부 Task 이다.
javascript 가 코드 블럭을 수행하면 call stack 에 함수 호출을 쌓으며 실행해나가는 Task 를 수행하고, 도중 Ajax, DOM 조작을 만나면 Task Queue 에 넣고 계속 루프 작업을 진행하게 된다. 타이머를 만나면 바로 Task Queue 에 추가되지 않고 지정된 시간 후 Task Queue 에 추가된다.
삽입된 Task 는 다음 Event Loop, 혹은 지정된 시간, 이벤트 트리거에 의해 다시 수행된다.
비동기와 Promise #1 에서 다뤘듯 특별할게 없는 동작이다.
Micro Task
Micro Task 는 새로운 Task 로서 기존의 Task 에 영향을 받지 않고 Async 로 빠르게 수행되는 Task 들이다.
Micro Task 는 현재 실행중인 Task 의 실행이 종료된 뒤 바로 다음에 일어날 일들이 쌓이는 곳이다.
일반적인 구현으로는 각 Task 가 끝나거나, Event Loop 의 시작과 끝에서 체크된다. 이 작업을 표준 문서에서는 Micro Task checkpoint 라고 정의하고 있다.
Task 의 종료와 루프의 시작과 끝에서 수행되기에 일반적인 Task 의 실행이 다음 루프에서 처리되는 것보다 우선권이 있다.
HTML 스펙의 Micro Task checkpoint - perform a microtask checkpoint 의 설명을 대략 요약하면 다음과 같다.
중간에 여러 개념들이 등장하지만, Micro Task 에 초점을 맞춰 요약해보면 다음과 같은 흐름이다.
Micro Task checkpoint
수행- 핸들링 : 이벤트 루프의 Micro Task 큐가 비어 있으면 완료 단계로
- Event Loop 의 Micro Task 큐 대기열에서 가장 오래된 Micro Task 를 선택
- Event Loop 의 현재 실행중인 작업 을 3번 단계에서 선택한 작업으로 설정
- 실행 : 선택한 Task 를 실행.
- Event Loop 의 현재 실행중인 작업을 null 로 설정
- 위의 단계에서 실행 된 Micro Task 를 큐에서 제거하고 Micro Task 큐 처리 단계 (2번 단계) 로
- 완료 :
Micro Task checkpoint
완료
아래는 위의 흐름을 개념적 코드로 표현해보았다. (실제 구현이 이렇다는건 절대 아니다)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function performMicroTaskCheckPoint(eventLoop) {
while(eventLoop.microCheckPointFlag) { if(eventLoop.microTaskQueue.length < 1) { eventLoop.microCheckPointFlag = false; break; } const microTask = eventLoop.microTaskQueue.shift(); eventLoop.setCurrentRunngingTask(microTask); eventLoop.executeCurrentTask(); eventLoop.setCurrentRunngingTask(null); } }
|
여기서 알 수 있는건 Micro Task 큐가 비어있지 않다면 Task 가 비어있을 때까지 무한히 핸들링 -> 실행 단계를 반복하도록 되어 있다는 점이다.
만일 Micro Task 에서 다른 MicroTask 를 등록하는 작업을 반복하면 다음의 이벤트 루프는 수행되지 못할 수도 있다는 뜻이다.
Micro Task 과다 중첩 예제
테스트 코드로 알아보자. 사용할 Micro Task 는 이 시리즈에서 한창 다루는 Promise 를 사용한 예제이다.
먼저 사용할 함수 두개를 만들자
1 2 3 4 5 6 7 8 9 10 11 12 13
|
const doIncrementChain = promise => { return promise.then(val => { console.log('Promise value', val); return Promise.resolve(++val) }); };
const putImmidiateTimer = fn => { setTimeout(fn, 0); };
|
위 두 함수를 사용해서 예제 코드는 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| putImmidiateTimer(_=> console.log('I am Timer!'));
let promise = new Promise(resolve => { console.group('promise start~'); return resolve(1); });
let loopCount = 100000; while(loopCount--) promise = doIncrementChain(promise);
promise.then(_=> console.groupEnd('promise executed!'));
|
이 코드는 위의 스펙대로 모든 Promise 가 추가한 MicroTask 를 전부 소비하고 난 뒤에야 타이머 작업이 시작된다.
혹 사양이 낮은 PC나 환경에 따라서는 PC가 멈추거나 오류를 낼 수 있다.(NodeJS 의 경우 아마 1000 번의 Micro Task 큐 작업이 한계라고 알고 있다)
이 블로그를 작성중인 작업 컴퓨터의 사양이 좀 낮은 관계로 10번만 수행시켰다. (Chrome 브라우저 콘솔)
실행 결과를 보았듯이 Timer 작업은 앞선 Micro Task 인 Promise 에 밀려 제일 나중에 실행된다.
루프 카운트를 10000 으로 늘려도 결과는 같다.(다만 과하게 늘릴 경우 수행이 늦어지거나 엔진 다운이 있을 수 있다.)
결론
비동기 및 Promise 포스팅이 이걸로 끝났다.
비동기에 대해서는 여기 써놓은 내용 이상으로 다룰 내용이 너무 깊고 많다. 노오력이 부족한 관계로 새로운 사실을 알게 될 때마다 포스트를 수정해나갈 생각이다.
참고