본문 바로가기
WEB/JS

(js)Promise만들기(2) - 구상

by jackWillow 2024. 1. 27.

←(js)Promise만들기(1) - 전체코드

TMI

더보기

저는 Promise를 이벤트 기반으로 설계했습니다.

프로미스의 핵심은 언제 끝날지 알 수 없는 비동기 처리를 순서대로 연결하는 것인데,

그럼 비동기 처리가 완료됐다는 것을 어떻게 알리지? ==> 아! 이벤트를 발생시키자!

라는 생각으로 이어졌던 것입니다.

 

다 만들고 나서야 이벤트가 아니어도 충분히 만들 수 있다는 것을 알았지만, 이벤트도 나름의 장점을 가지고 있습니다.

비동기 처리가 완료됐을 때 실행해야할 로직를 이행(resolve)과 거부(reject)로 구분해 저장해야하는데,

이벤트는 fulfill과 reject 이름으로 리스너에 저장하면 알아서 처리해줘서 그 부분에 신경을 덜 쓸 수 있기 때문입니다.

(실제로는 IE에 CustomEvent와 EventTarget이 없는바람에 직접 구현해야했습니다.)

처음 머릿속으로 그린 이미지

용어 설명

Promise란 무엇인지, pending이 뭔지 같은 지루한 얘기는 하지 않겠습니다.

하지만, [Promise만들기(2) - 구상]에서 사용할 용어 설명만 잠깐하겠습니다.

 

  • initPromise : new Promise로 가장 처음 만든 프로미스
  • nextPromise : .then으로 initPromise 다음으로 실행되야 할 프로미스
  • executor : initPromise의 인자로 들어가는 함수
  • resolve콜백, reject콜백 : executor의 인자로 들어가는 콜백들.
  • .then : 실제 then 메소드. 다음 프로미스를 만들어 반환하는 메소드
  • thenCallback(T.C) : .then메소드의 인자로 들어가는 함수들 총칭.
  • nextResolve(N.R) : nextPromise의 이행함수(resolve).
  • 상태(state) : 프로미스 상태. pending, fulfilled, rejected
  • 결과(result) : 상태와 함께 프로미스에 저장된 값. 이행과 확정에 따라 값이 변할 수 있다.
  • 이행(resolve) : resolve콜백과 reject콜백을 합친 의미.
  • 확정(settle) : 이행과 비슷해 보이지만, 프로미스가 완전히 고정된 상태. fulfilled 혹은 rejected된 상태.

 

다들 아시겠지만, 이행은 그렇다쳐도 확정은 뭔데? 라고 생각하시는 분들이 있을 수도 있습니다.(저도 몰랐습니다.)

간단히  설명하자면, resolve콜백의 인자로 프로미스가 들어가는 상황을 생각하시면 됩니다.

 

initPromise에서 executor가 실행되고, 그 안에서 resolve콜백이 실행되는데,

프로미스가 아닌 다른 타입이면 그냥 result에 저장하면 되지만, 프로미스가 들어오면 프로미스가 확정될 때까지 기다려야 합니다.

resolve콜백이 실행된것을 "이행(resolve)"이라고 하고, 프로미스가 이행되었다면, 해당 프로미스는 더 이상 다른 이행에(resolve콜백이나 reject콜백) 영향을 받지 않아야 합니다. 프로미스가 이행된 후에 그 상태는 보통 fulfilled나 rejected이지만, 위와 같이 resolve콜백에 프로미스가 들어오면 그것이 확정되기를 기다리는 동안 pending일 수도 있습니다.

반면, "확정(settle)"은 이행 후에 실행되고, 프로미스가 fulfilled 또는 rejected 상태인 완전히 고정된 상태입니다.

(참고: States and Fates)

 


기본 구상

프로미스가 동기적으로 처리가 되려면, initPromise에서 먼저 이행되고 nextPromise의 이행이 동작해야합니다.

그러려면 nextPromise를 만들 때, 자신의 이행을 동작시킬 스위치도 하나 만들어서 initPromise가 이행되면 누르도록 만들어야 할 것입니다.

저는 이것을 중간에 Event를 둬서 매개역할을 하도록 만들었습니다.

그래서 제 프로미스를 간단하게 설명하면, initPromise가 이행되고 이벤트를 발생시키면 Event가 스위치를 누르는 구조입니다.


전체 구상

1. 프로미스 하나만 있을 때

기호 설명: 그림에서 프로미스는 사각형, 이벤트는 삼각형으로 표현했습니다. 메소드는 이름으로 표현했으며 호출되었을 때 적고, 완료되면 그 위에 동그라미 표시를 했습니다. 그리고 화살표는 진행흐름을 나타냅니다.

 

가장 먼저 initPromise 하나를 만들었을 때의 동작입니다.

new Promise를 하면, 프로미스 생성 => executor => resolve(이행) => settle(확정) 의 순서로 진행됩니다.

resolve콜백에 프로미스가 들어오면 달라지겠지만, 그 경우는 .then메소드를 먼저하고 보는게 더 쉽기 때문에 나중에 하도록 하겠습니다.

state와 result값은 마지막 프로미스가 확정되는 순간 결정됩니다.

 

2. nextPromise가 있을 때

하지만 nextPromise가 있으면 조금 달라집니다.

initPromise 생성 => .then 호출 => Event에 스위치 저장 => nextPromise 생성 및 반환 => initPromise의 executor 호출 => 그 외 로직...

 

어? executor는 initPromise 생성자 함수의 매개변수인데 이렇게 뒤에서 호출된다는게 말이되나?

저도 이 부분을 가장 오랫동안 고민했습니다.

executor 안에서 resolve가 호출되고 바로 확정이 진행되면서 이벤트도 보낼텐데...

그렇다면 executor가 호출되는 순간 이미 nextPromise가 존재한다는 사실을 알고 있어야 하는데 도저히 그 방법이 떠오르지 않았습니다.

하루종일 고민하고 야근을 자처하며 밤늦게까지 고민하다가 결국 해결 못했는데, 집으로 돌아오는 지하철에서 떠올라 혼자 속으로 기쁨을 억눌렀던 기억이 납니다.

제가 떠올린 방법은 자바스크립트 이벤트 루프를 사용하는 것입니다.

출처: https://medium.com/sessionstack-blog/how-does-javascript-actually-work-part-1-b0bacc073cf

이 그림 아마 다들 기억하실 겁니다.

setTimeout 같은 비동기 함수는 백그라운드 어딘가에서 처리하다가 완료되면 콜백큐로 들어가서 콜스택이 비면 콜스택에 들어가 실행된다는 자바스크립트 초반에 공부하는 이벤트 루프입니다.

여기서 주목할 점은 콜백큐에 있는 것은 콜스택이 비어야만 들어간다는 점입니다.

그렇다면 executor를 setTimeout 0초에 넣으면, 실행하자마자 콜백큐에 들어갈테고 .then 메소드를 포함한 전체 코드가 실행된 후에 콜스택으로 들어와 executor를 실행할 수 있습니다.

 

nextPromise가 생성된 후의 진행은 위 그림과 같습니다.

executor => resolve => settle => event => switch => thenCallback => nextPromise의 resolve => settle

 

  1. executor 안에서 비동기 처리가 완료되면 resolve(이행)함수가 그 값을 인자로 받아 실행합니다. (위 코드에서는 'hello')
  2. resolve에 들어온 값을 settle에 넘겨주면 프로미스 상태와 함께 결과값으로 저장하고, 그 값을 담아 이벤트를 보냅니다.
  3. Event에서는 저장하고 있던 스위치를 실행시켜 그 안의 thenCallback(T.C)를 실행 시킵니다.
  4. thenCallback에서 받은 값은(위 코드에선 'hello world!') nextPromise의 resolve(N.R)로 넘겨서 실행시킵니다. 여기서 N.R을 nextResolve.call(nextPromise, result) 처럼 nextPromise와 묶어서 실행시켜서 nextPromise의 resolve를 직접 실행시키는 효과를 줍니다.
  5. 그 후 settle까지 실행하면 nextPromise를 확정시키면서 완료됩니다.

그 후 nextPromise의 executor가 실행되지만 빈 함수만 있으므로 아무런 영향도 없습니다.(initPromise의 executor와 실행 순서가 바뀔 수 있지만 상관 없습니다.)

 

3. thenCallback에서 프로미스를 리턴할 때

만약 thenCallback에서 프로미스를 반환한다면 nextResolve로 넘기지 않고, 스위치 내부에 다른 분기를 만들어야 합니다.

위 그림은 thenCallback에서 막 프로미스를 반환한 상황입니다.

여기서 T.CPromise에서 확정된 값을 nextPromise의 resolve로 보내야 합니다. 즉, T.CPromise와 nextPromise를 연결해야 합니다.

프로미스와 프로미스를 연결하는 방법은 nextPromise를 생성했을 때와 거의 동일합니다.

 

스위치 내부에서 반환받은 T.CPromise에 .then메소드를 호출해 T.CPromise의 이벤트에 스위치를 저장합니다.

이 때 T.CPromise의 .then메소드에 thenCallback으로 nextPromise의 resolve를 넣습니다.

그러면 T.CPromise의 settle에서 넘겨준 상태와 결과값을 스위치에서 thenCallback 대신 받을 수 있으므로 연결 완료입니다.

 

4. resolve콜백으로 프로미스가 들어올 때

이쯤되면 제가 resolve콜백의 프로미스를 어떻게 처리할지 이미 알고 계시리라고 생각합니다.

resolve콜백에서 받은 resolvePromise의 .then메소드를 호출해 스위치를 만듭니다.

이번에는 thenCallback으로 initPromise의 resolve를 넣어서 두 프로미스를 연결하면 끝입니다.

 

다음은 코드 설명으로 넘어가겠습니다.

 

→(js)Promise만들기(3) - CustomEvent, EventTarget

 

참고: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

 

Promise - JavaScript | MDN

The Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.

developer.mozilla.org

참고: https://github.com/domenic/promises-unwrapping/blob/master/docs/states-and-fates.md

 

반응형