자바스크립트는 효율적인 비동기처리를 위해서 ES6부터 Promise를 도입했습니다.Promise 가 나타난 배경인 'callback hall' 에 대한 설명은 아래 글에서 볼 수 있습니다.
이 글은 프라미스의 기본적인 이해와 이벤트루프에 대한 설명은 건너뛰고 넘어갑니다
Promise 객체는 [[PromiseStatus]], [[PromiseValue]] 두 가지로 이루어 져있습니다.
Promise는 Callback 함수를 인자로 받고, 상태([[PromiseStatus]])와 값([[PromiseValue]])를 포함하는 객체입니다.
new Promise((resolve,reject)=>{})
Promise 의 Callback 함수는 두가지 인자를 받는다.
첫번째 인자는 resolve
메소드로 Promise 객체의 콜백함수가 제대로 수행(fulfilled) 되었을 때 호출되는 메소드입니다.
두번째 인자는 reject
인자로 콜백함수가 거부(rejected) 되었을 때 호출됩니다.
promise의 상태는 resolve 메소드가 호출되면 "fulfilled"가 되고 reject 메소드가 호출되면 "rejected"가 됩니다.
두 상태가 모두 아닐 땐 "pending" 입니다.
[[PromiseValue]]의 값은 resolve,reject 에서 호출되어 인자로 들어가는 값들이 들어값니다.
"fulfilled" , "rejected" 두 상태일때는 각각의 메소드에서 받은 값이 들어가있습니다.
"pending" 일때는 undefined로 초기화되어 있습니다.
이 글에서 가장 다루고 싶었던 것이 마이크로 테스크큐 와 매크로테스크큐 입니다.
setTimeOut
같은 브라우저 메소드를 이용한 비동기를 가능하게 해주는 이벤트 루프에서 저는 tasks queue 라는 것을 배웠습니다.
자바스크립트는 싱글스레드이지만 WebAPI 에서 timer 함수가 실행되고 끝나면 setTimeOut의 콜백함수가 tasks queue 에 쌓이고 이벤트 루프에 의해서 콜스택으로 넘어간다고 알고있었습니다.
하지만!
tasks queue는 사실 두 개였습니다.
그 두 가지 큐는 (macro)task queue
(괄호를 붙인 이유는 통상적으로 task queue 라고도 부르기 때문입니다.) 와 micro task queue
입니다.
이름 그대로 macro task queue 는 macro task 들이 담기는 queue 인 것이고 micro task queue 는 micro task 들이 담기는 queue 입니다.
setTimeout , setInterval , setImmediate, requestAnimationFrame
I/O(click, srcoll ...) , UI 렌더링
process.nextTick , Promise , Object.observe , MutationObserver
콜스택에 있는 모든 함수가 실행 되면서 스택에서 빠져나옵니다.
호출 스택이 비어있으면, 대기 중인 마이크로 테스크가 콜스택으로 하나씩 들어가고 실행됩니다.
호출 스택과 마이크로 스택이 모두 비워지게되면 이벤트 루프는 매크로 태스크 큐의 남은 작업을 콜스택으로 넣어서 실행하게 됩니다.
마이크로 태스크 큐의 task 들은 매크로 테스크 큐 보다 우선 순위가 높다.
이 코드에서 콜스택, 마이크로 테스크 큐 , 매크로 테크스 큐 의 작동을 추적해보면,
첫 번째 줄에서 console.log('Start!')
를 만납니다. 콜스텍에 console.log
가 콜스택에 추가되고 실행된 뒤에 스택을 빠져나갑니다.
두 번째 줄에서 setTimeOut 을 만나고 메소드는 콜스택에 추가됩니다. setTimeOut 이 실행되면 콜백함수가 WebAPI에 추가된다. 타이머 함수가 실행되고 타이머가 완료되면 매크로 테스크 큐로 콜백함수가 다시 넘어간다.
다음 줄인 Promise.resolve() 를 만나서, Promise.resolve()
가 콜스택에 추가되고 Promise.then() 객체를 반환합니다.
then 체이닝에서 res 로는 Promise 값을 받으므로 'Promise!' 가 들어있다.
then의 콜백함수가 마이크로 태스크큐에 추가된다.
마지막으로 전역에서 console.log('End!') 를 읽고 실행한다.
전역의 코드 평가와 실행은 모두 끝났지만 마이크로 태스크큐와 매크로 태스크큐의 태스크들이 남아있다.
매크로 태스크 보다는 대기중인 마이크로 태스크가 더 우선순위에 있기 때문에 res => console.log(res)
가 콜스택으로 옮겨진다.
promise.resovle()
로 이미 resovle 된 값을 넘겨주었기 때문에 매크로 태스크 큐의 res => console.log(res)
는 대기 중
인 상태이다.
콜스택이 모두 비워지고 마이크로 태스크 큐도 모두 비워졌으면, 매크로 태스크 큐를 확인한다.
setTimeOut 의 콜백인 ()=>{ console.log ('TimeOut')}
을 콜스택에 넣고 실행 시킨다.
자바스크립트에서 비동기를 만드는 방법은 Promise 객체만 있는 것은 아니다. ES7 부터 등장한 Async/Await 키워드는 자바스크립트 비동기 코드를 좀 더 동기적
인 코드 처럼 짤 수 있게 만들어 주었다.
하지만 명세의 차이가 있을 뿐이지 Async함수 역시 암묵적으로 Promise 를 반환한다고 해석할 수 있다.
그러나 Async + Await 의 키워드는 약간 다르게 작동한다. await
키워드를 이용하면 awaited 하고 있는 값을 promise가 resolve할 때 까지 기다려서, 비동기 함수를 일시정지 할 수 있다.
우리가 promise의 resolve된 함수를 기다리기 위해서 then()콜백에서 했던 것처럼, 우리는 변수에 promise 값을 할당 할 수 있다!
아래 코드를 보면서 어떤 일이 벌어지는지 같이 알아보자.
먼저 전역 코드의 평가와 실행이 일어난다.
console.log('Before function')
이 콜스택에 들어가서 출력되고 스택을 빠져나온다.
출력
'Before function'
그런 다음 myFunc()
을 호출하게되고 myFunc() 이 콜스택에 쌓이면서 함수안으로 제어권이 넘어가게된다. 함수안의 console.log('In function') 또한 콜스택에 쌓이고 'In function' 을 출력하면서 스택을 빠져나온다.
출력
'Before function'
'In function'
async 함수내에서 두번째 라인에 도달하게 되면, await
키워드를 만나게 됩니다.
await
키워드를 만나면 첫 번째로 일어나는 일은 await 하고있는 함수가 실행이 되는 것입니다.
one
이라는 함수가 콜스택에 쌓이고 one 은 Promise 를 반환하게 됩니다. one() 이 반환하는 Promise 의 콜백 함수는 즉시실행 됩니다. 그래서 setTimeout 이 존재하므로 setTimeOut 까지 실행이 되면 이 setTimeOut 의 콜백함수가 WebAPI로 가서 10초 타이머를 잽니다.
one 이 반환하는 Promise 는 아직 resolve 가 실행되지 않은 Promise 이므로 Pending 상태의 Promise 가 반환되게 됩니다.
출력
'Before function'
'In function'
그런 다음 자바스크립트 엔진은 await
키워드를 만나게 됩니다. await 를 만나면 자바스크립트는 일단 await 를 감싸고 있는 async 함수 실행을 중단하고 마이크로 태스크 큐에 집어 넣습니다.
그렇게 되면 myfunc 에서 빠져나오게되고 현재 콜스택은 비어있으므로 전역에서 다음 코드로 진행되게 됩니다.
출력
'Before function'
'In function'
출력
'Before function'
'In function'
'After function'
콜스택에서 res(one)
메소드가 실행되면 one() 이 반환했던 Promise 객체의 상태가 'fulfilled' 로 바뀌고 값에 'one!' 이 할당됩니다.
Promsie의 상태가 'fulfilled' 로 되자마자 마이크로 태스크 큐에 있던 myFunc()
은 대기 상태가 되서 콜스택이 비워지자마자 콜스택으로 들어갈 준비가 됩니다.
myFunc()
이 콜스택으로 들어가면 이전에 중단됐던 위치에서 다시 실행됩니다.res 변수는 마침내 one이 반환한 promise의 값 'one!' 을 얻습니다.
console.log(res) 가 콜스택에 쌓여서 log 에 찍히고,
console.log('Infunction2') 도 차례로 찍힙니다.
출력
'Before function'
'In function'
'After function'
10초후
'setTimeOut'
'one!'
'Infunction2'
참조
크 이몬 예시까지 아주 꼼꼼하게 작성해주셨네요~~ 잘 보고 갑니다ㅎㅎㅎ