MDN 문서 내용을 정리해봤다. Promise 사용 이유 말고 오로지 사용법에 대해서만 알아볼 것이다.
Promise란 무엇인가?
Promise는 비동기 함수를 다루기 위한 목적으로 만들어진 객체로, 일반 객체와는 다르게 상태 정보를 갖는다. 아래는 MDN 문서의 그림이다.
위 그림과 같이 Promise는 네 가지 상태 정보를 가지며 각 상태의 의미는 아래와 같다.
1. pending
: 비동기 처리가 아직 수행되지 않은 상태
: resolve 또는 reject 함수가 아직 호출되지 않은 상태
2. fulfilled
: 비동기 처리가 수행된 상태(성공)
: resolve 함수가 호출된 상태
3. rejected
: 비동기 처리가 수행된 상태(실패)
: reject 함수가 호출된 상태
4. settled
: 비동기 처리가 수행된 상태(성공 또는 실패)
: resolve 또는 reject 함수가 호출된 상태
Promise() 생성자
// 구문
new Promise(executor)
// 예시
const promise = new Promise((resolve, reject) => {
...
// 비동기 함수 수행
...
resolve('성공!')
...
...
reject('실패...')
});
- executor는 실함함수다. 매개변수로 resolve, reject 함수를 가진다.
- executor 내부에서 resolve를 만나면 resolve 내부의 값을 저장하고, reject를 만나면 reject 내부의 값을 저장한다. resolve 값은 .then에서 받을 수 있고, reject 값은 .catch에서 받을 수 있다.
- 따라서 if문으로 resolve, reject를 나누는 것이 일반적이다.
위에서 볼드체로 처리한 문장이 사실상 프로미스의 모든것이라고 할 수 있다. 아주 간단한 예제를 하나 보자. 'then'에 대해서는 곧 자세히 다룰텐데 우선은 promise의 저장값을 이어 받는 메소드로 알고있자.
// 1초 뒤 '실행'을 콘솔에 출력하고 'Hello World'를 promise에 저장
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('실행');
resolve('Hello World!');
}, 1000);
})
아주 간단한 코드다. 위 코드를 실행시켜보면 1초 뒤에 '실행'이라고 콘솔에 출력될 것이다. 그리고 출력 직후에 resolve를 만나 promise 내부에 'Hello World!'가 저장된다. 이렇게 resolve를 통해 내부에 저장된 값은 .then 메소드로 이어받을 수 있다. 아래 코드를 이어서 쳐본 뒤 실행시켜보자.
// res에는 promise의 resolve 값이 들어간다
promise.then((res) => {
console.log(res);
})
결과가 어떻게 될까? 콘솔에는 'Hello World!'가 출력될 것이다. 이유는 아까 설명했듯이 Promise에서 resolve 값이 'Hello Wrold!'였기 때문이다.
resolve를 써봤으니 reject도 써보자. 위 코드에서 resolve를 reject로 수정하기만 하면 된다. 어떤점이 다른지 잘 봐보자.
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('실행');
reject('Hello World!'); // resolve -> reject로 바뀜
}, 1000);
})
// then -> catch로 바뀜
promise.catch((err) => {
console.log(err);
})
출력 결과는 아까 코드와 동일하다. 차이는 resolve가 reject로 바뀌었고, then이 catch로 바뀌었다는 점이다.
그런데 이렇게만 보면 Promise는 굉장히 쓸모없는 객체같다. 이걸 어떻게 활용할 수 있을까? 다른건 다 제쳐두고, resolve와 reject를 모두 사용할 방법은 없을까? 방법이 있다! Promise 객체를 함수의 반환값으로 사용하면 가능하다. 아래 코드를 보자.
// 짝수 or 홀수
const even_odd = function(n) {
return new Promise((resolve, reject) => {
if(n % 2 == 0){
resolve(`${n}은 짝수`);
}
else{
reject(`${n}은 홀수`);
}
})
}
even_odd(58).then(res => {console.log(res)});
even_odd(19).catch(err => {console.log(err)});
even_odd 함수는 n을 인자로 받고, Promise 객체를 반환하는 함수다. 반환하는 Promise 객체의 내부를 살펴보면 짝수면 resolve로 받고, 홀수면 reject로 받고 있다. 따라서 짝수면 then으로 resolve 값을 받을 수 있고, 홀수면 catch로 reject 값을 받을 수 있다. 이렇게 보통 Promise 객체는 함수의 반환값으로 많이 활용된다. 함수가 받는 인자값을 Promise 내부에서 활용하면 if문으로 resolve와 reject를 나눌 수 있기 때문이다.
그런데 위 함수는 좋은 예시가 아니다. 이 글의 시작 부분에서 Promise의 생성 목적은 비동기 함수를 다루기 위해서라고 했었다. 따라서 일반적으로 Promise의 콜벡함수 내부에서는 비동기 처리 작업을 수행한다. 이때 비동기가 성공하면 resolve 함수를 호출하고, 이때 프로미스는 'fulfilled' 상태가 된다. 반대로 비동기 처리가 실패하면 reject 함수를 호출하고 이때 프로미스는 'rejected' 상태가 된다.
어떤 좋은 예시가 있을까... Promise의 콜벡함수 내부에서 특정 서버에 요청을 보내고 응답 코드가 200 ~ 300대라면 resolve 함수를 호출하고, 그 외에는 reject 함수를 호출하는 경우가 있겠다.
then, catch, finally
1) then
// 구문
p.then(onFulfilled, onRejected);
p.then(function(value) {
// 이행
}, function(reason) {
// 거부
});
then 또한 Promise 객체로 내부에 값을 저장한다. 즉, then 내부에서 저장하고 있는 값은 then으로 다시 이어받을 수 있다. 위 코드에서 p는 Promise 객체를 말하며, 일반적으로 then의 인자로 onFulfilled 함수만을 받는다. onFulfilled 함수가 받는 인자(value)가 바로 p의 저장값이다. value는 상황에 따라 다른 값을 가지게 되는데, 각 경우는 아래와 같다.
- p가 값을 반환할 경우, value는 p의 반환값이다.
const p = Promise.resolve(1);
p // 1
.then(res => res + 1) // 1 -> 2
.then(res => { console.log(res) }); // 2
- p가 값을 반환하지 않으면, value는 'undefined' 이다.
const p = Promise.resolve(1);
p // 1
.then(res => {}) // 1 -> undefined
.then(res => { console.log(res) }); // undefined
- p에서 오류가 발생할 경우, reason이 오류를 이어 받는다. 일반적으로 Promise의 오류는 catch에서 받는다.
const p = Promise.resolve();
p
.then(() => {
// reject를 반환
throw new Error('으악!');
})
.then(() => {
console.log('실행되지 않는 코드');
}, error => {
console.error('onRejected 함수가 실행됨: ' + error.message);
});
2) catch
// 구문
p.catch(onRejected);
p.catch(function(reason) {
// rejection
});
catch는 then과 유사하다. then과의 차이는 Promise의 reject 값을 받는다는 점이다. catch도 Promise 객체이기 때문에 내부에 값을 저장하고 있으며 다음 then에서 이어받을 수 있다.
- Promise 체이닝에서 오류가 발생할 경우, reason이 오류가 발생한 Promise 객체의 오류를 받는다.
const p = Promise.resolve();
p
.then(() => {
console.log('실행되는 코드')
})
.then(() => {
throw new Error('으악!');
})
.then(() => {
console.log('실행되지 않는 코드');
})
.catch((error) => {
console.error('onRejected 함수가 실행됨: ' + error.message)
})
- catch의 반환값을 다음 then에서 받을 수 있다.
const p = Promise.resolve();
p
.then(() => {
throw new Error('으악!');
})
.then(() => {
console.log('실행되지 않는 코드');
})
.catch((error) => {
return 'catch 반환값';
})
.then((res) => {
console.log(res);
})
3) finally
p.finally(onFinally);
p.finally(function() {
// settled (fulfilled or rejected)
});
finally는 Promise가 실행 완료되고 무조건 실행되는 메소드다. Promise가 이행되었는지 거부되었는지 판단할 수 없기 때문에 콜벡함수는 어떤 인자도 전달받지 않는다.
const p = Promise.resolve();
p
.then(() => {
throw new Error('으악!');
})
.then(() => {
console.log('실행되지 않는 코드');
})
.catch((error) => {
console.error('onRejected 함수가 실행됨: ' + error.message)
})
.finally(() => {
console.log('Promise 실행완료');
})
예제 및 분석
1) Promise를 활용한 비동기
const p = new Promise(resolve => {
console.log('Promise 내부 resolve 전');
resolve('resolve 값');
})
const continue_p = p.then((res) => {
console.log(res);
console.log('첫번째 then');
})
.then(console.log('두번째 then(콜백함수 아님)'));
console.log('\n다른 작업 수행\n');
continue_p.then(() => {
console.log('세번째 then')
})
/** 결과 **
Promise 내부 resolve 전
두번째 then(콜백함수 아님)
다른 작업 수행
resolve 값
첫번째 then
세번째 then
*/
- continue_p를 통해 Promise 체이닝이 분리되어 진행되고 있다. 즉, Promise를 사용하면 하고 싶은 작업을 분리해서 진행할 수 있다. 만약에 Promise 내부에 비동기 함수 결과값을 저장한다면, Promise를 통해 비동기 함수를 동기적으로 실행할 수 있게 된다.
- 출력 순서를 보면 'Promise 내부 resolve 전' 값이 먼저 출력되고 있다. 이는 Promise 생성자의 executor 함수는 동기적으로 실행되는 것을 의미한다. 정확하게 말하면 Promise 생성까지는 동기적으로 실행된다. 생성된 Promise 이후 체이닝부터 비동기로 실행된다.
- '두번째 then'을 주목하자. 만약에 then의 인자가 콜백 함수가 아니면 그 부분은 동기적으로 실행된다. then 내부가 함수일때만 잡큐(job queue)로 넘어가고, 함수가 아니면 바로 실행된다. 출력 결과에서 '두번째 then'이 왜 두번째로 출력됐는지 생각해보면 이해할 수 있다. 그런데 then 안에 콜백함수를 넣지 않으면 promise를 쓸 이유가 없기 때문에 꼭 콜백함수를 넣자.
2) 동기 vs 비동기
See the Pen Untitled by 이찬 (@vexkruqa-the-typescripter) on CodePen.
MDN 예제 코드를 그대로 가져와봤다. 동기와 비동기의 차이를 알 수 있는 코드다. 버튼을 클릭하면서 동기와 비동기의 차이를 느껴보자. Promise 생성까지는 동기적으로 실행된다. 이유는 첫 번째 예제와 동일하다.
3) 비동기끼리의 우선순위
console.log('start')
setTimeout(function() {
console.log('setTimeout')
}, 0)
Promise.resolve(console.log('in promise executor'))
.then(() => {
console.log('promise chaining1')
})
.then(() => {
console.log('promise chaining2')
})
console.log('end')
/** 결과 **
start
in promise executor
end
promise chaining1
promise chaining2
setTimeout
*/
- 동기적 실행 부분 : 'start', 'in promise executor', 'end' 이므로 이들은 순차적으로 출력된다.
- 비동기적 실행 부분 : 'setTimeout', 'promise chaining1', 'promise chaining2'
- 비동기함수는 백그라운드를 거쳐 큐에서 대기하는데, setTimeout 함수는 Task queue에서 대기하고 Promise는 Microtask queue에서 대기한다. Micro Task queue의 우선순위가 더 높아서 Promise가 먼저 실행된다. 참고로 Microtask queue는 Job queue라고도 한다.
추가 메소드
1. Promise.resolve()
// 구문
Promise.resolve(value);
// 예제
Promise.resolve(3).then(res => { console.log(res) });
- 무조건 fulFilled 상태의 Promise를 생성하며, 내부 저장값은 value다.
2. Promise.reject()
// 구문
Promise.resolve(reason);
// 예제
Promise.reject(3).catch(err => { console.log(err) });
- 무조건 Rejected 상태의 Promise를 생성하며, 내부 저장값은 reason이다.
3. Promise.race()
// 구문
Promise.race(iterable);
// 예제
const promise1 = new Promise((resolve, reject) => {
setTimeout(resolve, 500, 'promise1');
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'promise2');
});
const promise3 = new Promise((resolve, reject) => {
setTimeout(reject, 50, 'promise3');
})
Promise.race([promise1, promise2, promise3])
.then((value) => {
console.log(value);
})
.catch((value) => {
console.log(value);
})
- 가장 먼저 완료된 Promise의 결과를 저장한다.
- 가장 먼저 완료된게 reject 라면, cath문으로. value는 가장 먼저 완료된 reject 값.
- 가장 먼저 완료된게 resolve 라면, then문으로. value는 가장 먼저 완료된 resolve 값.
- iteralbe이 비었으면 오류
4. Promise.allSettled()
// 구문
Promise.allSettled(iterable);
// 예제
const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('foo')
}, 2000);
})
const promises = [promise1, promise2];
Promise.allSettled(promises)
.then((results) => {
results.forEach(promise => {
if(promise.status === 'fulfilled')
console.log(`status: ${promise.status}, value: ${promise.value}`);
else if(promise.status === 'rejected')
console.log(`status: ${promise.status}, reason: ${promise.reason}`);
})
})
/** 결과
status: fulfilled, value: 3
status: rejected, reason: foo
반환값 형태
[
{ status: 'fulfilled', value: 3 },
{ status: 'rejected', reason: 'foo' }
]
*/
- iterable에는 Array와 같은 순회 가능한 객체가 들어간다.
- iterable 내의 모든 promise 결과를 반환한다.
- 반환값은 코드 주석 참고. 각 promise의 상태와 값이 객체 형태로 저장되어 있다.
5. Promise.all()
// 구문
Promise.all(iterable);
// 예제
const promise1 = Promise.resolve(3);
const promise2 = Promise.resolve(1);
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 3000, 'foo');
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values)
})
/**결과**
[ 3, 1, 'foo' ]
*/
- iterable 내의 모든 promise가 resolve면 한번에 다 실행후 결과를 배열에 담아 반환
- iterable 내의 promise 중 하나라도 reject면 catch문에서 첫번째 reject의 결과를 반환
- iterable이 비었으면 오류
6. Promise.any()
// 구문
Promise.any(iterable);
//예제
const promise1 = Promise.reject(5);
const promise2 = new Promise((resolve) => setTimeout(resolve, 1000, 'quick'));
const promise3 = new Promise((resolve) => setTimeout(resolve, 2000, 'slow'));
const promises = [promise1, promise2, promise3];
Promise.any(promises).then((value) => console.log(value));
/** 결과 **
quick
*/
- iterable 내의 promise 중에 가장 먼저 fulfilled 되는 것을 반환
- promise 중에 reject가 있어도 무시한다
- iterable이 비었으면 오류
<참조>
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
'JavaScript' 카테고리의 다른 글
[ JavaScript ] Rest parameter, Spread Syntax, 구조 분해 할당 (0) | 2022.04.30 |
---|---|
[ JavaScript ] async/await (0) | 2022.04.29 |
[ JavaScript ] 함수와 클로저(Closure) ... 커링을 곁들인 (0) | 2022.04.23 |
[ JavaScript ] 실행 컨텍스트 (0) | 2022.04.23 |
[ JavaScript ] 스코프와 호이스팅(feat. var let const) (1) | 2022.04.22 |