뒤늦게 정리하는 Promise

Jeongkuk Seo
sjk5766
Published in
12 min readSep 24, 2018

--

사실 자바스크립트의 Promise는 접한지 조금 되었는데, 더 오래 기억할 겸, 정리해본다.

1. 비동기와 callback hell

자바스크립트에서 비 동기 함수를 호출할 땐, 보통 callback 함수를 등록하고 callback 함수 내부에서 작업을 한다. 문제는 개발을 하다보면 함수 내부에서 또 다른 함수들을 호출하는 경우가 있다. 아래 예제는 callbackHell 이라는 함수가 내부에서 비 동기 함수 3개를 호출하는 코드로, 실제 작업코드가 들어간다고 하면 더 보기 힘들고 복잡해 질 것이다. 이러한 현상을 콜백-헬 (callback hell)이라 한다.

2. Promise

자바스크립트 callback hell을 해결하기 위해 ECMA Script 2015 (ES 6)에서부터 Promise가 도입되었다. Promise를 사용하면, callback에 의한 스파게티 소스를 보다 단순하고 직관적으로 만들 수 있다. 한 가지 명심할 건, Promise가 코드를 동기적으로 만들수 있게 하더라도 내부적으로는 비 동기로 동작한다. Promise는 아래와 같이 new 연산자를 이용해 Promise 객체를 생성해 사용하게 된다.

Promise 객체에는 성공 시 사용되는 resolve, 에러 발생 시 사용되는 reject, 함수 호출 시 등록되는 콜백함수를 then 키워드를 사용할 수 있다. 아래 예제를 분석해보자. 4번 line에는 promiseFun 이라는 함수를 선언하는데 5번 line에서 new 연산자로 Promise 객체를 리턴하고 있다. Promise 함수에서 성공 시에는 resolve 메소드로 값을 전달하고, 실패 시에는 reject 메소드로 값을 전달한다. 12번 line에서는 Promise 객체를 리턴하는 함수에 대한 처리부이다. resolve로 응답한 경우, then 메소드에서 데이터를 전달받아 처리하며, reject로 응답이 된 경우 catch 메소드에서 에러를 처리한다.

catch 메소드는 promise.then(undefined, onRejected)를 단순히 랩핑한 메서드이므로 다음과 같이 작성해도 무방하다. (무방하지만 이렇게 쓰지마세용)

위에서 .catch 대신 promise.then(undefined, onRejected) 을 사용할 수 있는데 왜 쓰지 말라고 했을까? 잠깐 샛길이긴 한데 아래를 보자. promiseFun1 함수를 호출하는 부분이다. 위 코드는 then에서 에러를 처리하고 아래 코드는 catch 메소드를 처리한다. Promise 함수가 reject할 경우는 두 방법 모두 정상적으로 처리한다. 하지만 then 메소드 내부에서 에러가 발생 할 경우, catch는 에러를 정상적으로 잡지만 then에서 처리할 경우, 에러를 잡지 못해 내가 원하는 에러 핸들링을 정상적으로 못 할 수 있다.

3. Promise 생성 방법 return new Promise vs Promise.resolve

Promise를 생성하는 방법에는 크게 두 가지 방법이 존재한다. Promise.resolve() 와 return new Promise 를 사용하는 방법이다. 아래에서 두 방법의 사용법을 보자. 동일한 기능을 하되 Promise를 선언하는 방법이 다르다.

어떤 글에서 Promise.resolve는 return new Promise의 sugar라고 표현했다. sugar란, 기존 문법을 좀 더 쉽게 쓰기 위한 별칭(alias), 방법을 말한다. 그 글에 의하면 Promise.resolve와 return new Promise는 동일하다. 과연 그럴까.

문제는 Promise 내부에서 비동기 함수가 호출되면 Promise.resolve의 경우 값을 잃어버린다. 두 방식으로 파일을 읽어 데이터를 resolve하도록 코드를 만들었다. fun1은 Promise.resolve로 Promise를 선언하고 내부에서 비 동기 함수인 readFile 함수를 호출하고 데이터를 resolve 한다. fun2는 return new Promise 방법으로 선언하였다.

결과는 아래와 같다. Promise.resolve로 호출할 경우 데이터가 undefined로 뜬다. 이유는..모르겠다. (알면 제발 알려주세요 ㅠ_ㅠ)

[centos@ip-172–31–19–210 promise]$ node test.js
undefined // fun1 의 결과
<Buffer 31 32 33 0a> // fun2 의 결과

따라서 나는 Promise 를 return new Promise 구문으로 쓸 생각이다. 이 방법에도 자바스크립트 문법에 따라 입맛에 맞게 작성할 수 있다. 아래 asynPromiseFun, asynPromiseFun1, asynPromiseFun2 함수는 return new Promise로 객체를 반환하며 문법의 차이만 있을 뿐 기능은 동일하다. 무엇을 선택할 건가? 나는 일단,, 귀찮은건 시르니 arrow 문법으로 가자. 두 번째와 세 번째 인데, 두 번째 문법은 함수의 매개변수가 많을 때, 개발 툴 (atom, visual code.. etc) 에디터 상의 소스가 비교적 길어지지 않을 것 같다. 세 번째 문법은 내부 들여쓰기가 한 단계 줄어드니 나름의 깔끔함이 있고, 히히 모르겠다 ^^!

4. Promise 상태

Promise는 다음과 같이 3가지 상태를 가진다.

Fulfilled : 성공(resolve)했을 때의 상태로, onFulfilled가 호출된다.
Rejected : 실패(reject)했을 때의 상태로, onRejected가 호출된다.
Pending : 성공도 실패도 아닌 상태로, Promise 객체가 생성된 초기 상태

처음 Promise 객체가 생성되면 Pending 상태로 있다가 resolve, reject 메소드가 호출되면 Fullfilled, Rejected 상태로 변경되어 onFulfilled, onRejected 메소드를 호출하게 된다.

아래 코드를 보자. Promise 객체만 만들고, resolve, reject 메소드를 호출하지 않았다.

결과는 보면 promise 객체가 pending 상태라는 것을 확인할 수 있다.

[centos@ip-172–31–19–210 promise]$ node test.js
Promise { <pending> }

5. Promise Chain

개발을 하다보면 함수 A호출이 끝나면 함수 B를 호출해야 하는 상황이 생긴다. 이럴 땐 Promise Chain을 사용하면 된다.

인자가 없는 함수 호출

인자가 있는 함수 호출

함수에서 다음 함수로 인자를 전달하는 경우

참고로 위의 13 ~ 16번 line의 함수 호출부는 아래와 같이도 쓸수 있다.

Promise Chain 에러 핸들링

일련의 함수 호출 순서를 Promise Chain으로 등록하였다. 헌데 개발하다 보니, A,B 함수의 예외처리는 이렇게 하고, C,D 함수의 예외처리는 저렇게 하고.. 이럴 수 있다. 아래 코드를 분석해보자. promiseFun1 ~ 4의 함수를 선언하고, 에러 처리 과정을 보기 위해 promiseFun2, promiseFun4 함수에서는 reject를 호출하여 일부러 에러를 발생시켰다. catch 메소드가 22, 26번 line에서 두 번 보인다. 예상하겠지만 첫 번째 catch 메소드 위에서 에러가 발생(reject)하면 첫 번째 catch로 그 이후 then에서 에러가 발생(reject)하면 두 번 째 catch 메소드에서 처리한다.

위 코드의 흐름을 그림으로 정리해보자.

6. Promise.all / Promise.race

Promise.all 은 Promise 객체를 배열로 입력받고, 모든 객체의 상태가 Fulfilled 상태가 되면 등록한 .then 메소드가 호출된다. 배열로 전달 된 Promise 객체들은 차례대로 실행되지 않고 동시에 실행된다. 아래 코드를 보자. asyncTime 함수는 내부적으로 setTimeout 함수를 호출한다. 인자 값으로 전달된 ms만큼 기다렸다가 resolve를 호출한다. 9번 line에서는 Promise.all 메소드를 호출하며 배열로 Promise 객체를 전달하고 있다.

만약, Promise.all에 전달 된 Promise 객체들이 동기적으로 실행된다면 최소 3초는 필요할 것이다. (12,13번 line에서 delay 값을 1초, 2초를 주고있음) 결과를 보면 동시에 실행되는 것을 확인할 수 있다.

[centos@ip-172–31–19–210 promise]$ node test.js
time: 2002.036ms

Promise.race는 Promise.all 과 달리 전달된 Promise 객체 배열 중 하나만 완료되면 then 메소드를 호출한다. 위 소스의 9번 line의 all을 race로 변경한 뒤 실행 시킨 결과이다.

[centos@ip-172–31–19–210 promise]$ node test.js
time: 12.260ms

위에서는 Promise가 무엇인지, 어떻게 생성하고 오류는 어떻게 처리하는지를 다뤘습니다. 아래 내용은 길진 않지만 Promise에 대해 좀 더 추가적인 알아야 할 것들을 정리했습니다.

비 동기로 처리되는 Promise

Promise는 비 동기 콜백-헬을 완화시켜 주는 기법으로, Promise 코드 자체가 동기적으로 동작하진 않습니다. 아래 코드는 Promise가 비 동기적으로 동작함을 보이기 위한 테스트 코드 입니다. 순서를 예측해보세용. 5번 line에 middle이 출력 될 테고, promiseFun 내부의 data가 출력될지 11번 line의 finish가 먼저 출력될까요

결과는 아래와 같습니다. promise의 then 함수는 동기적으로 처리되지 않고 비 동기적으로 처리되는 걸 알 수 있습니다.

[centos@ip-172–31–19–210 promise]$ node test.js
middle
finish
11

then, catch는 새로운 Promise 객체를 반환합니다.

아래 코드를 봅시다. 1~9번 line은 테스트를 위한 코드로 이렇게 쓰진 않지만 분명 동작하는 코드입니다. 1~3번 line은 Promise 객체를 생성하여 변수에 담고 4~6번 line은 then 메소드를 생성하고 변수에 담고 7~9번 lne은 catch 메소드를 변수에 담습니다. 각 변수를 cosole.log로 출력하면 각자 Promise 객체를 생성한 걸 확인할 수 있습니다. 15,16 line에서는 Promise 객체가 같은지 확인하는 코드로 서로 다른 걸로 보아, 1~3번 line에서 생성한 Promise 객체가 아니라 then과 catch가 새로운 Promise 객체를 생성하는 것을 확인할 수 있습니다.

Promise 마지막

어떤 해외 블로그에서 다음과 같은 질문이 나왔습니다. 아래 4개의 차이점을 알고 있느냐고 ㅎ.ㅎ;;

Q: What is the difference between these four promises?doSomething().then(function () {
return doSomethingElse();
});
doSomething().then(function () {
doSomethingElse();
});
doSomething().then(doSomethingElse());
doSomething().then(doSomethingElse);

아래 4개의 Promise 동작을 알기 위해 아래와 같이 테스트를 해보았습니다. 1~7번 line은 Promise를 리턴하는 함수 두 개를 정의 했습니다. 우선 9~19번 line의 차이를 설명하면 Promise Chain을 썻는데 다음 then 함수로 넘겨 주기 위해서는 return을 해줘야 합니다. return을 하지 않으면 then 함수에서는 undefined를 전달 받기 때문에 원하는 결과를 얻지 못할 수 있습니다. 다음은 21~22 line 인데, 차이는 then 함수의 인자에 함수호출의 결과를 전달하느냐, 함수 자체를 전달하느냐의 차이입니다. 살펴보면 then 함수의 인자는 함수로 사용됩니다. 따라서 21번 line에서는 doSomething에 대한 결과 10을 받고, 22번 line에서는 의도한대로 doSomethingElse의 resovle값인 20을 정상적으로 처리합니다.

--

--