js는 동기 방식과 비동기 방식 실행을 지원한다. 어떠한 구조, 어떠한 방식으로 구성되어있길래 비동기 방식을 처리할 수 있는지 자세하게 알아보자.
잘못된 부분이나 부족한 부분은 댓글로 알려주시면 감사하겠습니다!
자바스크립트 엔진
자바스크립트 엔진은 자바스크립트 코드를 실행하는 프로그램 또는 인터프리터이다. 구글의 V8과 애플의 webkit이 유명하며 이 글에서는 V8을 기반으로 설명할 것이다. js엔진에는 Heap과 call stack이 있다. heap에는 변수의 내용이 주로 저장되고, call stack에는 실행되는 함수들(정확히는 실행 문맥)이 쌓인다. 싱글 스레드 방식으로 사용되어 한번에 하나의 일만 가능하다.
브라우저 구조
자바스크립트 엔진은 한번에 하나의 일만 처리 가능한데, 어떻게 브라우저는 한번에 많은 일을 처리하는가? 브라우저에서 js를 실행할 때, js엔진만 있는것이 아니기 때문이다. js엔진을 제외하고도 webapis, callback queue, eventloop가 멀티 스레드로 작동하고 있다.
js엔진은 싱글 스레드이기 때문에 하나의 일만 처리 가능하다. 이것이 무슨 현상을 초래하는가? js로 10초가 걸리는 굉장히 많은 작업을 하게된다면, 그 작업이 끝날 동안에(call stack이 빌 때까지) 브라우저는 어떠한 반응도 하지 않는다. 버튼을 클릭하지도, 텍스트를 드래그하지도 못하는 것이다. 브라우저의 main thread가 rendering과 js코드 실행을 둘다 하기 때문이다.
js 코드를 실행할 때 setTimeout, setInterval, ajax(xmlhttprequest), 이벤트리스터, promise등은 call stack에서 실행되지 않고 web api로 실행을 넘겨준다. 그리고 해당 작업이 처리되면 callback queue로 callback작업이 enqueue된다. event loop는 call stack과 callback queue를 감시하며 call stack이 비었고 callback queue에 작업이 남았다면, callback queue에서 작업 하나를 call stack으로 옮긴다.
callback queue는 (macro)task queue와 microtask queue로 구분할 수 있다.
macrotask queue | setTimeout, setInterval, ajax(xmlhttprequest), event dispatch등이 들어간다. 이벤트 루프는 macrotask queue에 있는 task를 하나 빼서 실행하고 다음 루프로 넘어간다. |
microtask queue | promise, mutation observer의 콜백 등이 들어간다. 이벤트 루프는 microtask queue가 빌 때까지 task를 처리 후 다음 루프로 넘어간다. call stack이 비면 macrotask보다 먼저 처리된다. |
animationframe queue | requestAnimationFrame으로 등록한 callback함수들이 들어간다. repaint직전에 queue에 있는 task들을 전부 처리한다. |
예제들
예제 - 1
동작 순서
0. test.js는 하나의 task로 처리된다. console.log("c"), setTimeout(()=>console.log("b"), 1000), console.log("a")가 순서대로 call stack에 담긴다(실행 문맥).
1. console.log("a")가 call stack에 쌓이고 실행된다.
2. setTimeout이 call stack에 쌓이고 실행된다. 이때 web api에 timer가 작동한다.
3. console.log("c")가 call stack에 쌓이고 실행된다.
4. web api의 timer가 지정된 1000ms가 지나면, callback인 console.log("b")가 macrotask queue에 enque된다.
5. 이벤트 루퍼가 macrotask queue에 있는 console.log("b")를 call stack으로 옮긴다.
6. call stack에 있는 console.log("b")가 실행된다.
예제 - 2
동작 순서
0. test.js는 하나의 task로 처리된다. console.log("c"), setTimeout(()=>console.log("b"), 0), console.log("a")가 순서대로 call stack에 담긴다(실행 문맥).
1. console.log("a")가 call stack에 쌓이고 실행된다.
2. setTimeout이 call stack에 쌓이고 실행된다. 이때 web api에 timer가 작동한다. 시간이 0ms로 주어졌으므로 console.log("b")는 macrotask queue에 enque된다.
3. (다른 유튜브 영상이나 블로그를 보면 이때 call stack에 아무것도 없는것 처럼 보여진다. 하지만 전체 js코드가 하나의 실행 문맥을 가지므로 call stack에는 console.log("c")가 남아있는 상태이다. 그래서 macrotask queue까지의 실행 순서가 오지 않는다.) console.log("c")가 call stack에 쌓이고 실행된다.
4. 이벤트 루퍼가 macrotask queue에 있는 console.log("b")를 call stack으로 옮긴다.
5. call stack에 있는 console.log("b")가 실행된다.
예제 - 3
동작 순서
0. 스크립트는 하나의 task로 처리된다. console.log(Promise.resolve('abc'))부터 let job1 = new ...까지 순서대로 call stack에 담긴다.
1. Promise안의 console.log('in promise')가 실행된다.
2. job1.then이 위에서 resolve된 data와 콜백 함수가 microtask queue에 쌍힌다.
3. console.log('a')가 실행된다.
4. promise가 실행되고 then의 콜백 함수가 microtask queue에 쌓인다.
5. promise가 실행되고 catch의 콜백 함수가 microtask queue에 쌓인다.
6. console.log('c')가 실행된다.
7. console.log(Promise.resolve('abc'))가 실행된다. promise객체가 출력된다.
8. microtask queue에 있던 세개의 콜백 함수가 실행된다.
예제 - 4
동작 순서
0. 스크립트는 하나의 task로 처리된다. console.log('after'), ff1(), console.log('before')까지 순서대로 call stack에 담긴다.
1. console.log('before')가 실행된다.
2. ff1의 실행 문맥이 call stack에 쌓인다. console.log('in func')가 실행된다.
3. ff2가 실행되어 'ff2'를 a에 반환하고 ff1의 남은 부분은 microtask에 옮겨진다. await키워드를 만나면 js엔진은 실행을 지연하기 때문이다.
4. console.log('after')가 실행된다.
5. 이벤트 루프에 의해 microtask에서 ff1의 남은 부분이 call stack에 옮겨진다. console.log(a)가 실행된다.
참고: https://www.youtube.com/watch?v=wcxWlyps4Vg
https://www.youtube.com/watch?v=YpQTeIqjC4o&t=631s
-
'web > javascript' 카테고리의 다른 글
JavaScript Obfuscator (자바스크립트 난독화) (0) | 2024.09.03 |
---|---|
javascript set(집합) (2) | 2023.10.14 |
Objects의 시간복잡도 (0) | 2023.10.01 |
youtube html에 삽입하기(youtube iframe_api) (0) | 2023.08.31 |
JS Promise.all & Promise.race (0) | 2023.02.21 |