원문 : ⭐️🎀 JavaScript Visualized: Promises & Async/Await
JS 코드를 다루면서 예상대로 동작하지 않은 적이 있나요? 아마 함수가 무작위로 예상되지 않는 시간에 실행되거나 실행이 지연되었을 겁니다. ES6에서 도입된 Promise는 이런 일을 다룰 새롭고 멋진 기능입니다!
수 년전의 고민이 해결되고 잠 못 이루는 밤은 다시 한 번 애니메이션을 만들 시간을 주었습니다. 왜 Promise를 사용해야 하고, 어떻게 동작하며, 어떻게 가장 현대적인 방식으로 사용할 수 있을까요?
만약 자바스크립트 이벤트 루프에 관한 이전 게시물을 읽지 않았다면, 먼저 읽어보는게 도움이 될겁니다. 콜스택, 웹 API, 큐에 대한 기본적인 지식을 바탕으로 이벤트 루프에 대해 다시 한 번 다룰 예정입니다. 하지만 이번에는 흥미로운 추가 기능도 다루겠습니다 🤩
✨♻️ JavaScript Visualized: Event Loop 번역본
만약 Promise가 이미 익숙하다면, 아래 목차만 읽어도 충분합니다.
⚡️ [Promise Syntax](#Promise Syntax)
♻️ [Event Loop: Microtasks and (Macro)tasks](#Event Loop: Microtasks and (Macro)tasks)
자바스크립트를 작성할 때, 우리는 종종 다른 작업에 의존하는 작업을 다루어야합니다. 이미지를 받아서 압축하고, 필터를 적용해서 저장하고 싶다고 가정하겠습니다.📸
먼저 편집하고자 하는 이미지를 가져와야합니다. getImage
함수가 이 일을 해결할 수 있습니다! 이미지가 성공적으로 로드되면 resizeImage
에 해당 값을 전달할 수 있습니다. 이미지 크기가 성공적으로 조정되면 applyFilter
의 이미지 필터에 적용하고자 합니다. 이미지를 압축하고 필터를 추가한 후, 우리는 이미지를 저장하고 사용자에게 모든 동작이 완료되었음을 알리려고 합니다! 🥳
결국 우리는 아래와 같은 것을 하게 될 겁니다.
흠.. 뭔가 보이나요? 별로 좋지 않습니다.. 우리는 결국 이전 콜백 함수에 의존하는 수 많은 중첩 콜백 함수를 갖게 됩니다. 이것을 콜백 지옥이라 부르곤 합니다.
다행히도, Promise가 우리를 이 지옥으로부터 탈출하는걸 도와줍니다! Promise가 무엇인지, 이 상황에서 어떻게 우리를 도울 수 있는지 살펴봅시다! 😃
ES6는 Promise를 도입했습니다. 많은 튜토리얼에서 다음과 같은 내용을 읽을 수 있습니다.
promise는 미래에 resolve되거나, reject될 값의 placeholder입니다.
음.. 이 설명으로는 명확하지 않네요. 사실 이 설명은 이상하고, 모호하고, 예측할 수 없는 마술처럼 느껴집니다. 그러면 Promise가 뭔지 제대로 살펴봅시다
우리는 콜백을 받는 Promise
생성자로 promise를 생성할 수 있습니다. 한 번 해봅시다!
잠깐, 뭐가 반환되었나요?
Promise
는 상태([[PromiseStatus]]
)와 값([[PromiseValue]]
)를 포함하는 객체입니다. 위의 예시에서, [[PromiseStatus]]
의 값이 "pending"
이고 [[PromiseValue]]
의 값이 undefined
임을 볼 수 있습니다.
걱정하지 마세요. 여러분은 이 객체와 상호작용 할 필요도 없고, 심지어 [[PromiseStatus]]
와 [[PromiseValue]]
에 접근할 수도 없습니다! 하지만, 이 속성들의 값은 promise를 사용하는데 중요합니다.
PromiseStatus
의 값은 상태이며, 3가지 중 하나입니다.
fulfilled
: promise가 resolve
(수행) 되었고, 어떤 오류도 일어나지 않았음을 의미합니다. 🥳rejected
: promise가 reject
(거부) 되었고, 무언가 잘못 되었음을 의미합니다.pending
: promise가 아직 resolve, reject되지 않고 pending
(대기) 상태임을 의미합니다. 무슨 얘긴지 이해는 갑니다만, promise는 언제 pending
, rejected
, fulfilled
상태가 될까요? 그리고 왜 그렇 상태가 되는걸까요?
위의 예시에서, 우리는 Promise
생성자에 단순한 함수 ()=>{}
를 전달했습니다. 하지만, 이 콜백 함수는 사실 두 개의 인자를 받습니다. 첫 번째 인자는 resolve
또는 res
라 부르고 이 메소드는 Promise가 resolve(수행) 되어야할 때 호출됩니다. 두 번째 인자는 reject
또는 rej
라 부르고 이 메소드는 Promise가 reject(거부) 되어야할 때, 무언가 잘못 되었을 때 호출됩니다.
resolve
또는 reject
가 호출될 때 로그를 살펴봅시다. 제 예시에서는resolve
를 res
로, reject
를 rej
로 부르겠습니다.
좋습니다! 우리는 마침내 pending
상태와 undefined
값을 제거하는 방법을 알게 되었습니다! promise의 상태는 resolve
메소드가 호출되면 fulfilled
가 되고 reject
메소드가 호출되면 rejected
가 됩니다.
promise의 값([[PromiseValue]]
의 값)은 resolve
또는 reject
에 인자로 전달하는 값입니다.
재미있는 사실은, 저는 Jake Archibald가 이 기사를 교정하도록 했고, 그는 실제로 크롬에 현재 상태가
fulfilled
대신rejected
로 나타나고 있는 버그가 있음을 지적했습니다. Mathias Bynens 덕분에 이 문제는 현재 Canary에서 고쳐졌습니다!
자 그럼, 이제 막연했던 Promise
객체를 제어하는 방법을 조금은 알게 되었습니다. 하지만, Promise는 무엇을 위해 사용될까요?
소개 섹션에서는 이미지를 가져와서 압축하고, 필터를 적용해서 저장하는 예제를 보여드렸습니다. 이 예제는 결국 중첩된 콜백 지옥으로 엉망이 되었습니다.
다행히 Promise가 이 문제를 해결할 수 있습니다. 먼저 전체 코드 블럭을 다시 작성해서 각 함수가 Promise
를 대신 반환하도록 만들어봅시다.
이미지가 잘 로드되었다면 promise가 로드된 이미지를 가지고 resolve(수행) 하도록 합니다! 이미지 로드에 어떤 에러가 있다면, promise가 발생한 오류를 가지고 resolve(수행) 하도록 합니다.
터미널에서 이 동작을 한다면 무슨 일이 일어나는지 살펴봅시다.
좋습니다! promise는 우리가 예상한대로 파싱된 데이터와 함께 반환되었습니다.
하지만.. 이제 어떡하죠? 우리는 promise 객체 전체에 관심있는게 아니라, 데이터의 값만 필요합니다! 운 좋게도 promise의 값을 얻기 위한 내장 메소드가 존재합니다. promise에 우리는 3가지 메소드를 붙일 수 있습니다.
.then()
: promise가 resolve
된 후에 불립니다..catch()
: promise가 reject
된 후에 불립니다..finally()
: promise가 resolve
되든 reject
되든 항상 불립니다. .then()
은 resolve
메소드가 전달하는 값을 받습니다.
.catch()
은 reject
메소드가 전달하는 값을 받습니다.
마침내, 우리는 promise 전체 객체를 다룰 필요 없이 promise를 이용해 resolve된 값을 가지게 되었습니다! 우리는 이제 이 값을 원하는대로 사용할 수 있습니다.
참고로, promise가 항상 resolve되거나, reject된다는 것을 알고 있다면 resolve하거나 reject할 값으로 Promise.resolve
또는Promise.reject
를 사용할 수 있습니다.
여러분은 다음 예제를 종종 보게 될 것입니다.
getImage
예제에서 우리는 콜백을 실행하기 위해 여러개의 콜백을 중첩해야만 했습니다. 다행히, .then
핸들러가 이를 해결할 수 있습니다.
.then
자체의 결과값은 promise 입니다. 이것은 우리가 원하는 많큼 많은 .then
을 연결할 수 있음을 의미합니다. 이전 .then
콜백의 결과는 다음 .then
콜백의 인자로 전달됩니다!
getImage
예제에서, 우리는 많은 .then
콜백을 연결해서 처리된 이미지를 다음 함수로 전달할 수 있습니다. 수 많은 중첩 콜백 대신에 깨끗한 .then
체인을 얻게 됩니다
완벽합니다! 이 구문은 중첩 콜백보다 더 나아보입니다
좋습니다. 우리는 promise를 만드는 방법과 값을 추출하는 방법을 조금은 알게 되었습니다. 스크립트에 코드를 추가해서 다시 실행해보겠습니다.
에엥??
console.log('Start')
가 첫 번째 줄에 있으니, 먼저 Start!
가 기록됩니다. 하지만 두 번째 기록된 값은 promise의 resolve된 값이 아니라 End!
입니다!
그 이후에 promise의 값이 기록되었습니다. 무슨 일이 일어난걸까요?
우리는 마침내 promise의 진정한 힘을 보게 되었습니다!🚀 자바스크립트는 단일 스레드이지만, promise
를 사용하여 비동기 동작을 추가할 수 있습니다!
하지만 잠깐, 우리가 이전에 이러한 것을 본적이 있지 않나요? 🤔 자바스크립트의 이벤트 루프에서 setTimeout
같은 브라우저 고유 메소드를 사용해서 일종의 비동기 동작들을 만들 수 있지 않나요?
맞습니다! 하지만, 사실 이벤트 루프에는 두 종류의 큐(queue)가 존재합니다. (macro)task queue (그냥 task queue라고도 부릅니다.)와 microtask queue입니다. (매크로) 태스크 큐는 (매크로) 태스크를 위해, 마이크로 태스크 큐는 마이크로 태스크를 위해 존재합니다.
그럼 (매크로)태스크 와 마이크로태스크 는 무엇일까요? 여기서 다루는 것보다 몇 가지 더 많은 내용이 있지만, 가장 일반적인 것은 아래에 나와있습니다!
(Macro)task : setTimeout
| setInterval
| setImmediate
Microtask : process.nextTick
| Promise
callback
| queueMicrotask
아, Promise
는 마이크로 태스크입니다! 😃 Promise
가 resolve하고 자신의 then()
, catch()
, finally()
메소드를 호출할 때, 메소드 내의 콜백이 마이크로 태스크에 추가됩니다! 이것은 then()
, catch()
, finally()
내의 메소드가 즉시 실행되는게 아니라, 자바스크립트 코드에 약간의 비동기 동작을 추가함을 의미합니다!
그래서, then()
, catch()
, finally()
콜백은 언제 실행될까요? 이벤트 루프는 태스크에 우선순위를 부여합니다.
현재 콜 스택에 있는 모든 함수가 실행됩니다. 함수가 값을 반환하면 스택에서 빠져나옵니다.
호출 스택이 비어있으면, 대기중인 모든 마이크로 태스크가 콜 스택으로 하나씩 들어가고, 실행됩니다! (마이크로 태스크는 스스로 새로운 마이크로 태스크를 반환해서 효율적으로 무한 마이크로 루프를 생성할 수 있습니다😬)
호출 스택과 마이크로 스택이 모두 비게 되면, 이벤트 루프는 (매크로)태스크 큐에 남아있는 작업이 있는지 확인합니다. 작업이 남아있다면 콜스택으로 들어가서 실행되고, 값을 반환한 후에 빠져나옵니다!
다음을 사용해서 간단한 예시를 살펴 보겠습니다.
Task1
: 코드에서 즉시 호출되어 콜 스택에 바로 추가되는 함수입니다.
Task2
, Task3
, Task4
: 마이크로태스크, 예를 들면 promise의 then
콜백 또는 queueMicrotask
로 추가된 태스크
Task5
, Task6
: (마이크로)태스크, 예를 들면 setTimeout
, setImmediate
의 콜백
먼저, Task1
은 값을 반환하고 콜 스택을 빠져나갑니다. 그리고, 마이크로태스크 큐에 대기하고 있는 작업을 확인합니다. 모든 작업이 콜 스태에 놓이고, 빠져나가면, 엔진은 매크로태스크 큐에 대기하고 있는 작업을 확인합니다. 해당 작업들은 콜 스택에 놓이고, 값을 반환하면 콜 스택을 빠져나갑니다.
좋아좋아, 분홍 상자는 이제 충분합니다. 이제 실제 코드를 봅시다!
이 코드에서 우리는 매크로태스크인 setTimeout
와 마이크로태스크인 promise then()
의 콜백을 가지고 있습니다. 엔진이 setTimeout
함수의 라인에 도달하면 이 코드를 단계별로 실행해서 기록들을 살펴봅시다.
참고로, 다음 예제는
console.log
,setTimeout
,Promise.resolve
와 같은 메서드가 콜스택에 추가되는 것을 보여줍니다. 이것들은 기본 메소드이고 실제로 스택 추적에 나타나지 않습니다. 따라서 디버거를 사용했는데 값이 보이지 않는다고 걱정하지 마세요! 이 예시는 단지 추가적인 boilerplate code 없이 이 컨셉을 쉽게 설명할뿐입니다. 🙂
첫 번째 줄에서, 엔진은 console.log
를 만납니다. 이 메소드는 콜스택에 추가되고, Start!
를 기록한 후에 콜스택을 빠져나간 후 엔진은 계속 진행합니다.
엔진이 setTimeout
을 만나고, 이 메소드는 콜스택에 추가됩니다. setTimeout
는 브라우저의 내장 메소드입니다. 타이머가 완료 될 때까지, 콜백 함수 () => console.log('In timeout')
는 Web API에 추가됩니다. 우리가 타이머에 0
값을 전달했더라도, 콜백은 (마이크로)태스크에 추가 되기 전에 먼저 Web API에 추가됩니다. (setTimeout
은 매크로태스크 입니다!)
엔진은 Promise.resolve()
를 만납니다. Promise.resolve()
는 콜스택에 추가되고, 그 후에 Promise
의 값이 resolve됩니다. 이후 then
의 콜백 함수가 마이크로태스크 큐에 추가됩니다.
엔진은 console.log
를 만납니다. 이 메소드는 콜스택에 즉시 추가되고, End!
를 기록한 후에 콜스택을 빠져나갑니다. 엔진은 계속 진행합니다.
엔진은 이제 콜스택이 비어있음을 확인합니다. 콜스택이 비어 있기 때문에, 엔진은 마이크로태스크 큐에 대기중인 작업이 있는지 확인합니다! 이 경우엔 promise의 then
콜백이 차례를 기다리고 있습니다! 이 콜백은 콜스택에 추가되고, promise의 resolve된 값을 기록합니다. 이 경우에는 Promise!
입니다.
엔진은 콜스택이 비었음을 확인하고, 마이크로태스크 큐를 다시 한 번 확인합니다. 마이크로태스크 큐는 전부 비었습니다.
이제 (매크로)태스크 큐를 확인합니다. setTimeout
의 콜백이 대기하고 있습니다! setTimeout
의 콜백은 콜스택에 추가됩니다. 이 콜백은 console.log
를 반환하고, 이 메소드는 문자열In timeout!
를 기록합니다. setTimeout
은 콜스택을 빠져나갑니다
드디어 완료되었습니다! 🥳 이전에 본 결과물도 예상하지 못한 건 아니었나봅니다!
ES7은 자바스크립트에서 비동기 동작을 추가하고, promise 작업을 더 쉽게 만드는 새로운 방식을 도입했습니다. async
, await
키워드를 도입함으로써, 우리는 암묵적으로 promise를 반환하는 Async
함수를 생성할 수 있습니다. 하지만, 어떻게 그럴 수 있을까요? 😮
우리는 이전에 new Promise (() => {})
, Promise.resolve
또는 Promise.reject
를 입력함으로써 Promise
객체를 사용해서 promise를 명시적으로 만들 수 있다는 걸 확인했습니다.
Promise
객체를 명시적으로 사용하는 것 대신에, 우리는 암묵적으로 객체를 반환하는 비동기 함수를 만들 수 있습니다! 이는 더 이상 Promise 객체를 직접 작성할 필요가 없음을 의미합니다.
async 함수가 암묵적으로 promise를 반환한다는 것은 매우 멋지지만, async
함수의 진정한 힘은 await
키워드를 사용할 때 나타납니다! await
키워드를 사용하면 await
ed 하고 있는 값을 promise가 resolve할 때 까지 기다려서, 비동기 함수를 일시정지 할 수 있습니다. 우리가 promise의 resolve된 함수를 기다리기 위해서 then()
콜백에서 했던 것처럼, 우리는 변수에 promise 값을 할당 할 수 있습니다!
그렇다면, 비동기 함수를 일시정지 할 수 있다? 이게 무슨 뜻일까요?
다음 코드 블록을 실행할 때 어떤 일이 일어나는지 살펴봅시다
흠.. 무슨 일이 일어났죠??
먼저, 엔진이 console.log
를 만납니다. 이 메소드는 콜스택에 추가되고, Before function!
를 기록합니다.
이제, 우리는 async 함수인 myFunc()
를 호출합니다. 그 후 myFunc 함수의 본문이 실행 됩니다. 함수 본문의 첫 번째 라인에서 또 다른 console.log
를 부릅니다. 이 때, 문자열은 In function!
입니다. 이 console.log
는 콜스택에 추가되고, 기록을 남기고, 빠져나옵니다.
함수 본문이 계속 실행되며 두 번째 라인에 도달합니다. 마침내, 우리는 await
키워드를 보게 됩니다! 🎉
맨 처음 일어나는 일은 await된 값이 실행되는 것입니다. 이 경우에는 one
함수 입니다. 이 함수는 콜스택에 추가되고, resolve된 promise를 반환합니다. promise가 resolve되어 값이 반환되면, 엔진은 await
키워드를 만나게 됩니다.
await
키워드를 만나게 되면, async
함수가 일시중지 됩니다.✋🏼 함수 본문의 실행이 일시정지 되고 나머지 함수는 일반 작업 대신 마이크로태스크 에서 실행됩니다!
await
키워드를 만나 myFunc
함수가 일시정지 되었으니, 엔진은 async 함수에서 빠져나와 async 함수가 호출된 실행 컨텍스트에서 코드를 계속 실행합니다. (이 경우 전역 실행 컨텍스트) 🏃🏽♀️
마침내, 전역 실행 컨텍스트에 실행할 작업이 남아있지 않게되었습니다. 이벤트 루프는 마이크로태스크 큐에 남아있는 작업을 확인합니다! async myFunc
함수가 one
의 값을 resolve한 후 대기하고 있습니다! myFunc
은 콜스택에 돌아오고, 이전에 중단됐던 위치에서 다시 실행됩니다.
res
변수는 마침내 one
이 반환한 promise의 값을 얻습니다. 우리는 res
의 값(문자열 One!
)으로 console.log
를 호출합니다. One!
은 콘솔에 기록되고, 호출스택에서 빠져나옵니다. 😊
드이어 모든게 끝났습니다! async
함수와 promise then
의 차이점을 알게되었나요? await
키워드는 async
를 일시정지 시키는 반면에, Promise 본문을 사용했다면 계속 실행되었을겁니다!
흠! 꽤 많은 정보를 다루었네요! 🤯 Promise로 작업하는게 여전히 힘들더라도, 걱정할 필요 없습니다. 개인적으로 자바스크립트로 비동기 작업을 할 때 패턴을 알아차리고, 자신감을 느끼려면 경험이 필요하다고 생각합니다.
하지만 비동기 자바스크립트로 작업할 때 발생하는 "예기치 못한" 또는 "예측할 수 없는" 동작이 이제는 좀 더 의미가 있기를 바랍니다.
끝!!
시각화한 자료 잘 읽었습니다
막연한 개념으로 작업했었는데 지식이 더 늘었네요!