[ 번역 ] 자바스크립트 시각화 : 비동기 처리

DD·2021년 4월 6일
8

번역

목록 보기
7/10
post-thumbnail

원문 : ⭐️🎀 JavaScript Visualized: Promises & Async/Await

JS 코드를 다루면서 예상대로 동작하지 않은 적이 있나요? 아마 함수가 무작위로 예상되지 않는 시간에 실행되거나 실행이 지연되었을 겁니다. ES6에서 도입된 Promise는 이런 일을 다룰 새롭고 멋진 기능입니다!
수 년전의 고민이 해결되고 잠 못 이루는 밤은 다시 한 번 애니메이션을 만들 시간을 주었습니다. Promise를 사용해야 하고, 어떻게 동작하며, 어떻게 가장 현대적인 방식으로 사용할 수 있을까요?

만약 자바스크립트 이벤트 루프에 관한 이전 게시물을 읽지 않았다면, 먼저 읽어보는게 도움이 될겁니다. 콜스택, 웹 API, 큐에 대한 기본적인 지식을 바탕으로 이벤트 루프에 대해 다시 한 번 다룰 예정입니다. 하지만 이번에는 흥미로운 추가 기능도 다루겠습니다 🤩

✨♻️ JavaScript Visualized: Event Loop 번역본

만약 Promise가 이미 익숙하다면, 아래 목차만 읽어도 충분합니다.

  • 🥳 Introduction

  • ⚡️ [Promise Syntax](#Promise Syntax)

  • ♻️ [Event Loop: Microtasks and (Macro)tasks](#Event Loop: Microtasks and (Macro)tasks)

  • 🚀 Async/Await





Introduction

자바스크립트를 작성할 때, 우리는 종종 다른 작업에 의존하는 작업을 다루어야합니다. 이미지를 받아서 압축하고, 필터를 적용해서 저장하고 싶다고 가정하겠습니다.📸

먼저 편집하고자 하는 이미지를 가져와야합니다. getImage 함수가 이 일을 해결할 수 있습니다! 이미지가 성공적으로 로드되면 resizeImage에 해당 값을 전달할 수 있습니다. 이미지 크기가 성공적으로 조정되면 applyFilter 의 이미지 필터에 적용하고자 합니다. 이미지를 압축하고 필터를 추가한 후, 우리는 이미지를 저장하고 사용자에게 모든 동작이 완료되었음을 알리려고 합니다! 🥳

결국 우리는 아래와 같은 것을 하게 될 겁니다.

흠.. 뭔가 보이나요? 별로 좋지 않습니다.. 우리는 결국 이전 콜백 함수에 의존하는 수 많은 중첩 콜백 함수를 갖게 됩니다. 이것을 콜백 지옥이라 부르곤 합니다.

다행히도, Promise가 우리를 이 지옥으로부터 탈출하는걸 도와줍니다! Promise가 무엇인지, 이 상황에서 어떻게 우리를 도울 수 있는지 살펴봅시다! 😃



Promise Syntax

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가 호출될 때 로그를 살펴봅시다. 제 예시에서는resolveres로, rejectrej로 부르겠습니다.

좋습니다! 우리는 마침내 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 체인을 얻게 됩니다

완벽합니다! 이 구문은 중첩 콜백보다 더 나아보입니다



Microtasks and (Macro)tasks

좋습니다. 우리는 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() 콜백은 언제 실행될까요? 이벤트 루프는 태스크에 우선순위를 부여합니다.

  1. 현재 콜 스택에 있는 모든 함수가 실행됩니다. 함수가 값을 반환하면 스택에서 빠져나옵니다.

  2. 호출 스택이 비어있으면, 대기중인 모든 마이크로 태스크가 콜 스택으로 하나씩 들어가고, 실행됩니다! (마이크로 태스크는 스스로 새로운 마이크로 태스크를 반환해서 효율적으로 무한 마이크로 루프를 생성할 수 있습니다😬)

  3. 호출 스택과 마이크로 스택이 모두 비게 되면, 이벤트 루프는 (매크로)태스크 큐에 남아있는 작업이 있는지 확인합니다. 작업이 남아있다면 콜스택으로 들어가서 실행되고, 값을 반환한 후에 빠져나옵니다!



다음을 사용해서 간단한 예시를 살펴 보겠습니다.

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은 콜스택을 빠져나갑니다

드디어 완료되었습니다! 🥳 이전에 본 결과물도 예상하지 못한 건 아니었나봅니다!



Async/Await

ES7은 자바스크립트에서 비동기 동작을 추가하고, promise 작업을 더 쉽게 만드는 새로운 방식을 도입했습니다. async, await 키워드를 도입함으로써, 우리는 암묵적으로 promise를 반환하는 Async 함수를 생성할 수 있습니다. 하지만, 어떻게 그럴 수 있을까요? 😮

우리는 이전에 new Promise (() => {}), Promise.resolve 또는 Promise.reject를 입력함으로써 Promise 객체를 사용해서 promise를 명시적으로 만들 수 있다는 걸 확인했습니다.

Promise 객체를 명시적으로 사용하는 것 대신에, 우리는 암묵적으로 객체를 반환하는 비동기 함수를 만들 수 있습니다! 이는 더 이상 Promise 객체를 직접 작성할 필요가 없음을 의미합니다.

async 함수가 암묵적으로 promise를 반환한다는 것은 매우 멋지지만, async 함수의 진정한 힘은 await 키워드를 사용할 때 나타납니다! await 키워드를 사용하면 awaited 하고 있는 값을 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로 작업하는게 여전히 힘들더라도, 걱정할 필요 없습니다. 개인적으로 자바스크립트로 비동기 작업을 할 때 패턴을 알아차리고, 자신감을 느끼려면 경험이 필요하다고 생각합니다.

하지만 비동기 자바스크립트로 작업할 때 발생하는 "예기치 못한" 또는 "예측할 수 없는" 동작이 이제는 좀 더 의미가 있기를 바랍니다.

끝!!

profile
기억보단 기록을 / TIL 전용 => https://velog.io/@jjuny546

2개의 댓글

comment-user-thumbnail
2021년 4월 24일

시각화한 자료 잘 읽었습니다
막연한 개념으로 작업했었는데 지식이 더 늘었네요!

답글 달기
comment-user-thumbnail
2022년 2월 17일

감사합니다.

답글 달기