프로필사진
움짤로 보는 자바스크립트 동작 원리(5) - Promises & Async/Await

2020. 5. 25. 18:21🔴 FE/JavaScript

300x250

원문 (영어) : https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke#syntax
저자 : Lydia Hallie

👩🏻‍💻Thanks to Lydia for this amazing article about Javascript!


🖍 Introduction

자바스크립트로 코드 작성을 할 때, 우리는 흔히 다른 작업들에 의존하는 작업들을 처리해야하는 경우가 있다.
이미지를 받아서 압축하고, 필터를 입히고, 저장해야한다고 가정을 해보자.

제일 먼저 해야하는 일은 편집할 이미지를 받는 것이다.
getImage 함수로 이를 실행할 수 있다.
이미지가 성공적으로 로트된 후에 이를 resizeimage 함수로 전달할 수 있다.
이미지가 사이즈 조정이 된 후, 우리는 applyFilter 함수에서 해당 이미지에 필터를 적용하고자 한다.
이미지가 압축되고 필터가 추가된 후, 우리는 이미지를 저장하고 사용자에게 성공적으로 처리했다는 것을 알리려고 한다.

우리는 이러한 코드를 작성할 수 있다 :

흠.. 이 코드는 괜찮지만 좋지는 않다.
함수가 함수를 포함하고, 함수가 함수를 또 포함하고.. 이전의 콜백 함수에 의존하는 함수들로 구성되어 있다.
주로 이러한 코드는 'callback hell'이라고 부르는데, 포함된 콜백 함수들로 되어있어서 코드를 읽기 힘들기 때문이다!

다행히도, 우리는 promises라는 것의 도움을 받을 수 있다!
promise가 뭔지, 어떻게 이러한 상황을 해결해 줄 수 있는지 살펴보자


🖍 Promise Syntax

ES6는 Promise라는 것을 발표했다.
많은 튜토리얼에 따르면 이런 것들을 찾아볼 수 있다 :

"promise는 값에 대한 placeholder이고, 그 값은 미래에 언젠간 resolve 혹은 reject 될 수 있다."

이러한 설명은 저자에게 명확하게 들리지 않았다.
오히려 promise는 이상하고, 모호하며, 예측할 수 없는 조각이라고 생각이 들게끔 만들었다.

promise가 정말로 무엇인지 알아보자

우리는 콜백을 받는 Promise 생성자를 통해 promise를 생성할 수 있다.
한번 만들어보자

잠시만.. 방금 우리가 리턴받은게 뭐지?

Promise는 상태([[PromiseStatus]]) 와 ([[PromiseValue]])을 가지고 있는 객체이다.
위의 예제에서 우리는 [[PromiseStatus]]의 값이 "pending"이라고 뜨는 것을 볼 수 있다.
그리고 해당 promise의 값은 undefined이다.

걱정하지 마라, 당신은 이 객체와 교류할 필요가 없다.
그리고 당신은 [[PromiseStatus]]와 [[PromiseValue]]에 접근조차도 할 수 없다!
하지만 이 속성들의 값들은 promises로 작업을 할 때 중요하다.


PromiseStatus의 값(=상태)은 아래 3가지 값들 중 하나가 될 수 있다 :

  - ✅fulfilled : 해당 promise는 resolve 되었다. 모든 것들은 잘 진행되었으며, promise 안에 에러가 발생하지 않았다.
  - ❌rejected : 해당 promise는 reject 되었다. 뭔가 잘못됐다..
  - ⏳pending : 해당 promise는 resolve 나 reject 되지 않았다(아직), pending 상태인 것이다.

언제 promise가 pending, fulfilled, rejected 상태가 될까?

위의 예제에서는 우리는 단순히 () => {} 를 통해 콜백 함수를 Promise 생성자에게 넘겨주었다.
하지만 이러한 콜백 함수는 사실 두개의 인자를 받는다.
첫번째 인자의 값은 resolve 또는 res라고 부른다. 이는 Promise가 resolve해야할 때 호출되는 메소드이다.
두번째 인자의 값은 reject 또는 rej라고 부르는데, Promise가 reject해야할 때 호출되는 메소드이다.

resolve 또는 reject 메소드를 호출했을 때 로그를 찍어보자!
여기서는 resolve 메소드는 res, reject 메소드는 rej라고 부를 것이다.

좋아! 우리는 'pending' 상태와 undefined 값을 어떻게 피해야하는지 알았다!
만약 우리가 resolve 메소드를 호출하면 promise의 status는 "fulfilled'될 것이고,
rejected 메소드를 호출하면 "rejected"가 될 것이다.

promise의 value([[PromiseValue]]의 값)는 resolved나 rejected 메소드에 전달하는 인자값이다.


우리는 이제 모호했던 Promise 객체를 다루는 법을 조금 알아보았다.
이것이 무엇에 이용될까?

소개부분에 저자는 이미지를 다루는 예제를 보여줬다.
그리고 그 예제의 코드는 nested callback mass로 구성되었다.

운 좋게도 Promise로 이러한 문제를 해결할 수 있다!
먼저, 전체 코드 블록을 다시 작성하자. 그러면 함수 각자 Promise를 리턴할 것이다.

만약에 이미지가 로드되고 작업에 문제가 없었다면, 로드된 이미지로 promise를 resolve하자!
만약 파일을 로딩하는 데에 문제가 생겼다면 해당 에러로 promise를 reject하자.

터미널에서 돌려보면 이러하다 :

좋다! 예상대로 promise는 파싱된 데이터 값과 함께 리턴되었다.

하지만 우리는 해당 promise 객체 전체에 관심이 없다. 우리는 그저 데이터의 값만 신경쓰면 된다.
다행히도 그 안엔 built-in methods가 있어서 promise의 값을 얻을 수 있다.
promise에 우리는 3가지 메소드들을 사용할 수 있다 :

- .then() : promise가 resolve 된 후에 호출됨
- .catch() : promise가 reject 된 후에 호출됨
- .finally() : 항상 호출됨 (resolve 또는 reject)

.then() 메소드는 resolve 메소드에 전달된 값을 받는다.

.catch() 메소드는 reject 메소드에 전달된 값을 받는다.

드디어 우리는 promise 객체 전체를 가지고있지 않고서도 promise에 의해 resolve된 값을 받게 되었다!
우리는 이 값으로 우리가 원하는 작업을 할 수 있다.


참고로 당신이 promise는 항상 resolve 되거나 reject 된다는 것을 알고있다면,
Promise.resolve 나 Promise.reject을 사용해서 원하는 값으로 promise를 reject/resolve 할 수 있다.

아래 예제에서 이러한 문법을 자주 보게 될 것이다.


getImage 예제에서 우리는 여러개의 겹쳐진 콜백들을 볼 수 있었다. 다행히도 .then 핸들러는 이 문제를 해결해준다!

.then 자신의 결과값이 바로 promise의 값이다.
즉, 우리는 .then을 원하는 만큼 여러개 체인처럼 연결하여 쓸 수 있다는 것이다 : 
직전의 then 콜백의 결과는 그 다음 then 콜백에게 인자로 전달된다.

이 문법은 그전에 있었던 곂쳐진 콜백들보다 훨씬 보기 좋다!


🖍 Microtasks and (Macro)tasks

자, 우리는 promise를 생성하고 값을 빼내는지 어느정도 알았다.
스크립트에 코드를 더 추가해서 실행해보자.

잠시만..?

먼저 "Start!"가 로그에 찍혔다.
'console.log('Start!')는 맨 처음 라인에 있기에 이는 예상했던 일이었다.

하지만 두번째로 로그에 찍힌 값은 "End!"이다. 이는 resolve된 promise의 값은 아니다.
"End!"가 찍히고 나서야 promise의 값이 로그에 찍혔다.
무슨일이 일어나는 것일까?

우리는 드디어 promise의 리얼 파워를 보게되었다!
자바스크립트는 싱글쓰레드이긴 하지만 우리는 Promise를 통해 비동기적인 작업을 추가할 수 있다!


그 전 시리즈를 봤다면 뭔가 익숙한 내용일 것이다.

https://kkangdda.tistory.com/73

 

움짤로 보는 자바스크립트 동작 원리(1) - Event Loop

원문 (영어) : https://dev.to/lydiahallie/javascript-visualized-event-loop-3dif 저자 : Lydia Hallie 👩🏻‍💻Thanks to Lydia for this amazing article about Javascript! 자바스크립트는 단일 쓰레드로..

kkangdda.tistory.com

여기에서 우리는 setTimeout과 같은 브라우저의 네이티브 기능을 통해 비동기적인 처리를 할 수 있다는 것을 알았다.

👉🏻맞는 말이다. 하지만 Event Loop에는 사실 2가지 queue가 존재한다 :
(macro)task queue (=task queue) & microtask queue

(macro)task queue는 (macro)task를 위해, microtask는 microtask를 위해 동작한다.

그렇다면 (macro)task와 microtask는 무엇일까?
비록 더 많은 부분들을 설명해야하지만, 아래의 표를 보면 기본적인 정보를 알 수 있다.

(macro)task setTimeout | setInterval | setImmediate
microtask process.nextTick | Promise callback | queueMicrotask

우리는 microtask 리스트에서 Promise를 찾을 수 있다!
Promise가 resolve되고 then(), catch() 또는 finally()를 호출하면 해당 메소드의 콜백은 microtask queue에 추가된다!
이는 즉, then(), catch() 또는 finally() 메소드의 콜백은 즉시 실행되지 않는다는 것을 뜻한다.
이렇게 하여 자바스크립트 코드에 비동기처리를 할 수 있게 만들어준다.

그렇다면 then(), catch(), finally() 콜백은 언제 실행될까?
event loop은 서로 다른 우선순위를 작업들에게 분배해준다 :

1. call stack안에 있는 모든 함수들은 실행된다. 그들이 값을 리턴했을 때 stack에서 빠져나온다.
2. call stack이 비었을 때 줄을 서있었던 microtask들이 callstack으로 하나씩 들어가고 실행된다! (microtask들은 새로운 microtask를 리턴할 수 있어서 무한의 microtask loop를 효율적으로 생성할 수 있다)
3. 만약 call tack과 microtask queue가 비었으면 event loop는 (macro)task queue에 남은 task가 있는지 체크한다.
남은 task는 callstack으로 들어가서 실행되고 빠져나온다!


그렇다면 간단한 예제 하나를 보자.

- Task1 : call stack에 즉시 추가되는 함수이다. 예로 들자면 코드 안에서 즉시 호출했을 때 일어난다.
- Task2, Task3, Task4 : microtasks(예시로 promise에서 then의 콜백)이나 queueMicrotask에 추가된 항목이다.
- Task5, Task6 : (macro)task이다. 예를 들면 setTimeout이나 setImmediate 콜백이 있다.

첫째로 Task1이 값을 리턴하고 call stack에서 빠져나간다. 그 다음, 엔진은 microtask queue에 작업들이 있는지 체크한다.
모든 작업들이 call stack에 들어가고 마지막으로 빠져나가면, 엔진은 (macro)task queue에 작업이 남아있는지 확인한다.
만약 있다면 call stack에 들어가고 값을 리턴한 후 빠져나갈 것이다.

실제 코드를 보면 이렇다 :

이 코드 안에서 우리는 macro task인 setTimeout과 microtask인 promise then()의 콜백을 가지고 있다. 
이 코드를 단계별로 실행시켜서 어떤 로그가 찍는지 보자.


 

잠깐 추가 설명 :
아래 예제들에서 저자는 console.log, setTimeout, Promise.resolve와 같은 메소드를 call stack에 추가할 것이다.
이들은 내부적인 메소드이고 사실상 stack traces에서 잡히지 않는다.
따라서 debugger에서 찾을 수 없어도 걱정하지 마라!

맨 처음 라인에서 엔진은 console.log()를 마주친다.
이는 "Start!"라고 값이 콘솔에 찍히고 나서 call stack에 추가된다.
해당 메소드는 call stack에서 빠져나가고 엔진은 계속해서 실행된다.

그 다음 엔진은 call stack으로 들어간 setTimeout메소드를 맞닥뜨린다.
setTimeout 메소드는 브라우저의 native 메소드이다.
타이머가 끝날때까지 그것의 콜백 함수인 ()=>console.log('In timeout')은되 Web API에 추가된다.

비록 우리는 타이머에 0이라는 값을 넣었지만, 콜백은 먼저 Web API에 들어갈 것이고, 그 다음엔 (macro)task queue에 추가될 것이다. (setTimeout은 macro task이다!)


그 다음, 엔진은 Promise.resolve()메소드와 마주친다.
Promise.resolve()메소드는 call stack에 추가되고, 그 다음엔 "Promise!"라는 값으로 resolve된다.
해당 메소드의 then 콜백 함수가 microtask queue에 추가된 것이다.


그리고 나서, 엔진은 console.log()메소드를 마주한다.
이는 call stack 즉시 추가되는데, 추가되고 나서는 "End!"라는 값을 로그로 찍는다.
그리고 call stack에서 나오고 엔진은 계속해서 돌아간다.

엔진은 callstack이 비어있는 것을 발견한다.
call stack이 비어있기 때문에 엔진은 microtask queue에 작업들이 있는지 체크한다.
그리고 거기서 promise then의 콜백이 자신의 차례를 기다리고 있는 것을 찾아낸다!
그것은 call stack으로 들어가고, promise의 resolve된 값들을 로그로 찍는다. (="Promise!" 문자열값)

이제 엔진은 call stack이 비어진 것을 보고 microtask queue를 다시 한번 체크할 것이다.
하지만 이제 microtask queue에는 아무것도 남아있지 않다.

이제 (macro)task queue를 체크할 차례이다!
setTimeout 콜백이 거기에서 자신의 차례를 기다리고 있다.
setTimeout 콜백은 call stack으로 들어간다.
콜백 함수는 console.log 메소드를 리턴해서 "In timeout!"이라는 문자열을 로그로 찍는다.
그러고 나서 setTimeout 콜백은 call stack에서 빠져나온다.

드디어 끝났다! 이제 이 코드들의 결과들이 낯설게 느껴지지 않을 것이다.


🖍 Async / Await

ES7은 자바스크립트의 새로운 비동기 동작 방식을 발표했고, promises를 더욱더 쉽게 활용할 수 있게 되었다!
async와 aswait 키워드로 우리는 promise를 포함하여 리턴하는 비동기 동작들을 생성할 수 있다.

하지만 어떻게 만들어야 할까?

전 예제에서는 우리는 Promise 객체를 사용하여 promise를 명시적으로 만들어냈다.
new Promise( () => {} ), Promise.resolve, Promise.reject 를 사용하면 됐었다.

명시적으로 Promise 객체를 사용하는 대신, 우리는 객체를 포함해서 리턴하는 비동기 함수를 생성할 수 있다.
즉, 우리는 이제 Promise 객체를 쓸 필요가 없다는 뜻이다.

async function이 암시적으로 promise를 리턴하는 것은 굉장한 사실이지만,
async function의 진정한 힘은 await 키워드를 사용했을때 나타난다!
await 키워드를 사용해서 우리는 await된 값이 resolve된 promise를 리턴할 때까지 비동기 함수를  지연시킬 수 있다.
만약 전에 then() 콜백을 썼었던 것처럼 resolve된 promise의 값을 받고 싶다면,
우리는 해당 변수들을 await된 promise의 값에 할당하면 된다.

그렇다면, 비동기 함수를 지연시킨다는 것이 무슨 뜻일까?

아래의 코드가 어떻게 돌아가는지 살펴보자

흠.. 무슨일이 일어난 것일까?


먼저, 엔진은 console.log를 마주친다.
이는 call stack에 들어가고, "Before function!"이 로그에 찍힌다.

그 다음, 우리는 myFunc()이라는 비동기 함수를 호출한다.
이를 호출하면 myFunc 함수의 body부분이 동작한다.
첫번째 라인에서는 또 다른 console.log를 호출하는데, 동시에 "In function!"의 문자열도 함께 동작한다.
console.log는 call stack에 추가되고, 로그를 찍는 다음, call stack에서 빠져나온다.

해당 함수의 body부분은 계속해서 실행되고, 두번째 라인이 실행되어 await 키워드까지 온다!

제일 처음으로 발생하는 일은 await된 값이 실행되는 것이다.(여기서는 함수 one을 가리킨다)
이는 call stack 안으로 들어가고, resolve된 promise를 리턴한다.
promise가 resolve되고 one이 값을 리턴할 때, 엔진은 await 키워드를 마주한다.

await 키워드와 마주치면, 비동기 함수는 지연된다.
함수의 body의 실행이 멈추고, 다른 비동기 함수가 microtask안에서 동작하게 된다.

비동기 함수 'myFunc'가 await 키워드와 마주쳐서 지연될 때,
엔진은 비동기 함수에서 나와 실행 컨텍스트에 있는 코드를 계속해서 실행한다.
이 실행 컨텍스트는 비동기 함수가 호출되었던 컨텍스트이며, 지금과 같은 경우에는 global 실행 컨텍스트를 칭한다!

드디어 glocal 실행 컨텍스트에 더이상 작업해야 하는 일이 남아있지 않게 되었다!
event loop은 microtask가 남아있는지 체크하고, 하나를 발견한다!
비동기 함수 'myFunc'이 one을 resolve한 뒤 차례를 기다리고 있었던 것이다.
myFunc은 call stack으로 다시 들어가고, 전에 남겼던 코드를 계속해서 실행한다.

res 변수는 one이 리턴한 resolve된 promise값을 전달받는다.
우리는 console.log을 res의 값과 함께 호출하고, "One!"이라는 문자열이 로그에 찍힌다.
이는 콘솔에 로그를 찍은 후, call stack에서 빠져나온다.

모든 작업이 끝이 났다.
promise의 then과 async funcion의 차이점을 이해했는가?
await 키워드는 비동기 함수를 지연시키는 반면
만약 우리가 then을 사용한다면 Promise의 body가 지연되지 않고 계속해서 실행될 것이다.

300x250