45장 프로미스
비동기 처리를 위한 콜백 패턴의 단점
콜백 헬
// GET 요청을 위한 비동기 함수
const get = url => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();
xhr.onload = () => {
if (xhr.status === 200) {
// 서버의 응답을 콘솔에 출력한다.
console.log(JSON.parse(xhr.response));
} else {
console.error(`${xhr.status} ${xhr.statusText}`);
}
};
};
// id가 1인 post를 취득
get('https://jsonplaceholder.typicode.com/posts/1');
/*
{
"userId": 1,
"id": 1,
"title": "sunt aut facere ...",
"body": "quia et suscipit ..."
}
*/
get 함수는 비동기 함수이다
→ 비동기 함수 내부의 비동기로 동작하는 코드가 완료되지 않아도 즉시 종료된다.
⇒ 코드 처리 결과를 외부에 반환하거나 변수에 할당할 수가 없다.
get 함수 내부의 onload 이벤트 핸들러가 비동기로 동작한다.
get함수는 GET요청 전송하고 onload 이벤트 핸들러 등록한다음에 바로 undefined를 반환한다. 이후 GET요청이 도착하면 onload 이벤트가 실행되는 것.
let todos;
// GET 요청을 위한 비동기 함수
const get = url => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();
xhr.onload = () => {
if (xhr.status === 200) {
// ① 서버의 응답을 상위 스코프의 변수에 할당한다.
todos = JSON.parse(xhr.response);
} else {
console.error(`${xhr.status} ${xhr.statusText}`);
}
};
};
// id가 1인 post를 취득
get('https://jsonplaceholder.typicode.com/posts/1');
console.log(todos); // ② undefined
xhr 요청 → console.log → todos에 xhr.response저장
왜 이렇게 되냐면, load 이벤트가 발생하면 테스크 큐에 저장되어 대기하다가 콜스택이 비면 이벤트 루프에 의해서 콜 스택으로 푸시되어 실행된다.
→ console.log가 끝난다음에야 콜 스택으로 푸쉬된다.
이것들을 해결하기 위해서 결과 처리를 비동기함수에 콜백으로 넘겨주는 방식이 생겼다.
// GET 요청을 위한 비동기 함수
const get = (url, successCallback, failureCallback) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();
xhr.onload = () => {
if (xhr.status === 200) {
// 서버의 응답을 콜백 함수에 인수로 전달하면서 호출하여 응답에 대한 후속 처리를 한다.
successCallback(JSON.parse(xhr.response));
} else {
// 에러 정보를 콜백 함수에 인수로 전달하면서 호출하여 에러 처리를 한다.
failureCallback(xhr.status);
}
};
};
// id가 1인 post를 취득
// 서버의 응답에 대한 후속 처리를 위한 콜백 함수를 비동기 함수인 get에 전달해야 한다.
get('https://jsonplaceholder.typicode.com/posts/1', console.log, console.error);
/*
{
"userId": 1,
"id": 1,
"title": "sunt aut facere ...",
"body": "quia et suscipit ..."
}
*/
그런데 만약 비동기 함수를 넘겨준다면 또 그 비동기 함수에 대한 결과 처리를 콜백으로 넘겨줘야하는 콜백 함수 호출이 중첩되어서 복잡도가 높아지는 문제가 발생한다. 이를 콜백 헬이라고 한다.
에러 처리의 한계
콜백패턴의 또 다른 문제점으로는 에러 처리가 곤란하다는 것이다.
try {
setTimeout(() => { throw new Error('Error!'); }, 1000);
} catch (e) {
// 에러를 캐치하지 못한다
console.error('캐치한 에러', e);
}
setTimeout 함수는 비동기함수이므로 콜백함수가 호출되는 것을 기다리는 것이 않고 즉시 종료되어 콜 스택에서 제거된다.
즉 이후 등록된 콜백 함수를 호출하는 것은 setTimeout 함수가 아니게 된다.
에러는 호출자 방향으로 전파되는데, 콜백함수를 호출한 것이 setTimeout 함수가 아니가 되므로 에러를 캐치할 수 없다.
프로미스의 생성
- Promise 생성자 함수를 new 붙여서 호출하면 프로미스 객체 생성됨
- 프로미스 객체는 표준 빌트인 객체이다.
- Promise 생성자 함수는 비동기 처리를 수행할 콜백함수를 인수로 전달받는데, resolve와 reject함수를 인수로 전달받는다.
// 프로미스 생성
const promise = new Promise((resolve, reject) => {
// Promise 함수의 콜백 함수 내부에서 비동기 처리를 수행한다.
if (/* 비동기 처리 성공 */) {
resolve('result');
} else { /* 비동기 처리 실패 */
reject('failure reason');
}
});
비동기 처리가 성공하면 resolve 함수를 호출하고, 실패하면 reject 함수를 호출
// GET 요청을 위한 비동기 함수
const promiseGet = url => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();
xhr.onload = () => {
if (xhr.status === 200) {
// 성공적으로 응답을 전달받으면 resolve 함수를 호출한다.
resolve(JSON.parse(xhr.response));
} else {
// 에러 처리를 위해 reject 함수를 호출한다.
reject(new Error(xhr.status));
}
};
});
};
// promiseGet 함수는 프로미스를 반환한다.
promiseGet('https://jsonplaceholder.typicode.com/posts/1');
프로미스는 다음과 같은 상태 정보를 갖는다.
생성된 직후에는 pending상태를 가지고,, 이후 처리 결과에 따라서 프로미스의 상태가 변경된다.
- 비동기 처리 성공 : resolve 함수를 호출해서 프로미스를 fulfilled 상태로 변경
- 비동기 처리 실패 : reject 함수를 호출해서 프로미스를 rejected 상태로 변경
즉, 프로미스 상태는 resolve 또는 reject 함수를 호출하는 것으로 결정된다.
pending과 반대 의미로 비동기 처리가 수행된 상태를 settled 라고하고 이는 fulfilled, rejected 모두 포함된다.
pending → settled 로의 상태변화는 가능해도 반대는 안된다.
프로미스의 후속 처리 메서드
프로미스의 후속 결과 처리를 위한 후속 메서드 then, catch, finally가 있다.
모든 후속 처리 메서드는 프로미스를 반환하며, 비동기로 동작한다.
Promise.prototype.then
then은 2개의 콜백함수를 인수로 전달 받음
- 첫번째 콜백함수는 프로미스가 fulfilled 상태가 되면 호출. 처리 결과를 인수로 전달한다.
- 두번재 콜백함수는 rejected 상태가 되면 호출. 에러(처리결과)를 인수로 전달
// fulfilled
new Promise(resolve => resolve('fulfilled'))
.then(v => console.log(v), e => console.error(e)); // fulfilled
// rejected
new Promise((_, reject) => reject(new Error('rejected')))
.then(v => console.log(v), e => console.error(e)); // Error: rejected
Promise.prototype.catch
한개의 콜백함수를 인수로 전달받는다.
프로미스가 rejected 상태인 경우에만 호출
// rejected
new Promise((_, reject) => reject(new Error('rejected')))
.catch(e => console.log(e)); // Error: rejected
Promise.prototype.finally
1개의 콜백함수를 인수로 전달받음.
프로미스의 성공 실패 상관없이 무조건 한번 호출된다.
프로미스 상태와 상관없이 공통적으로 처리할 내용있을 때 사용한다.
new Promise(() => {})
.finally(() => console.log('finally')); // finally
then,catch,finally를 get에 사용한다면 이렇게 사용
const promiseGet = url => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();
xhr.onload = () => {
if (xhr.status === 200) {
// 성공적으로 응답을 전달받으면 resolve 함수를 호출한다.
resolve(JSON.parse(xhr.response));
} else {
// 에러 처리를 위해 reject 함수를 호출한다.
reject(new Error(xhr.status));
}
};
});
};
// promiseGet 함수는 프로미스를 반환한다.
promiseGet('https://jsonplaceholder.typicode.com/posts/1')
.then(res => console.log(res))
.catch(err => console.error(err))
.finally(() => console.log('Bye!'));
프로미스의 에러 처리
프로미스를 사용하면 콜백패턴보다 에러처리를 쉽게 할 수 있다.
then의 두번째 콜백함수로 처리
const wrongUrl = 'https://jsonplaceholder.typicode.com/XXX/1';
// 부적절한 URL이 지정되었기 때문에 에러가 발생한다.
promiseGet(wrongUrl).then(
res => console.log(res),
err => console.error(err)
); // Error: 404
catch 사용해서 error 처리
const wrongUrl = 'https://jsonplaceholder.typicode.com/XXX/1';
// 부적절한 URL이 지정되었기 때문에 에러가 발생한다.
promiseGet(wrongUrl)
.then(res => console.log(res))
.catch(err => console.error(err)); // Error: 404
then의 2번째 콜백함수를 통한 에러 캐치는 첫번째 콜백함수에서 발생한 속에러? 를 캐치하지 못하고 코드가 복잡지니까 catch를 사용하는 편이 좋다.
프로미스 체이닝
후속 처리 메서드 | 콜백 함수의 인수 | 후속 처리 메서드의 반환값 |
---|---|---|
then | promiseGet 함수가 반환한 프로미스가 resolve한 값(id가 1인 post) | 콜백 함수가 반환한 프로미스 |
then | 첫 번째 then 메서드가 반환한 프로미스가 resolve한 값(post의 userId로 취득한 user 정보) | 콜백 함수가 반환한 값(undefined)을 resolve한 프로미스 |
catch | promiseGet 함수 또는 앞선 후속 처리 메서드가 반환한 프로미스가 reject한 값 | 콜백 함수가 반환한 값(undefined)을 resolve한 프로미스 |
후속 처리 메서드는 기본적으로 콜백함수가 반환한 프로미스를 반환하기 때문에 프로미스 체이닝이 가능하다. | ||
만약 프로미스 아닌 값 반환해도 암묵적으로 resolve or reject해서 프로미스 생성해서 반환한다. |
프로미스는 체이닝을 통해서 비동기 처리 결과를 전달받아서 후속처리를 해서 콜백 헬이 발생하지 않는다. 그렇다고 콜백 패턴을 사용하지 않는다는 것은 아님.
프로미스의 정적 메서드
Promise도 정적메서드들을 가지고 있다.
Promise.resolve / Promise.reject
이미 존재하는 값을 래핑하여 프로미스 생성하기 위해 사용
Promise.resolve
는 인수로 전달 받은 값을 resolve하는 프로미스를 생성
// 배열을 resolve하는 프로미스를 생성
const resolvedPromise = Promise.resolve([1, 2, 3]);
resolvedPromise.then(console.log); // [1, 2, 3]
// 밑과 같은 효과
const resolvedPromise = new Promise(resolve => resolve([1, 2, 3]));
resolvedPromise.then(console.log); // [1, 2, 3]
Promise.reject
는 인수로 전달 받은 값을 reject 하는 프로미스 생성
// 에러 객체를 reject하는 프로미스를 생성
const rejectedPromise = Promise.reject(new Error('Error!'));
rejectedPromise.catch(console.log); // Error: Error!
// 동문
const rejectedPromise = new Promise((_, reject) => reject(new Error('Error!')));
rejectedPromise.catch(console.log); // Error: Error!
Promise.all ⭐️
여러 개의 비동기 처리를 모두 병렬 처리할 때 사용한다.
const requestData1 = () => new Promise(resolve => setTimeout(() => resolve(1), 3000));
const requestData2 = () => new Promise(resolve => setTimeout(() => resolve(2), 2000));
const requestData3 = () => new Promise(resolve => setTimeout(() => resolve(3), 1000));
// 세 개의 비동기 처리를 순차적으로 처리
const res = [];
requestData1()
.then(data => {
res.push(data);
return requestData2();
})
.then(data => {
res.push(data);
return requestData3();
})
.then(data => {
res.push(data);
console.log(res); // [1, 2, 3] ⇒ 약 6초 소요
})
.catch(console.error);
3개의 비동기 처리가 1 → 2→ 3 순차적으로 실행된다.
but, 3개 처리 간에 연관성이 없어서 모두 병렬로 처리해도 된다.
⇒ 이럴 때 Pormise.all
메서드를 사용할 수 있다.
const requestData1 = () => new Promise(resolve => setTimeout(() => resolve(1), 3000));
const requestData2 = () => new Promise(resolve => setTimeout(() => resolve(2), 2000));
const requestData3 = () => new Promise(resolve => setTimeout(() => resolve(3), 1000));
Promise.all([requestData1(), requestData2(), requestData3()])
.then(console.log) // [ 1, 2, 3 ] ⇒ 약 3초 소요
.catch(console.error);
- Promise.all 메서드는 프로미스를 요소로 갖는 이터러블을 인수로 전달 받음.
- Promise.all 메서드는 모든 프로미스가 모두
fulfilled
상태가 되면 종료. → 가장 늦게 처리되는 프로미스보다 조금 더 오래걸림. - fulfilled 상태가 되면 resolve된 처리 결과를 배열에 저장해 새로운 프로미스를 반환한다. 프로미스의 순서대로 배열에 저장 (처리순서 X)
만약 배열의 프로미스가 하나라도 rejected상태가 되면 나머지 프로미스가 fulfilled 되는 것을 기다리지 않고 즉시 종료함.
Promise.all([
new Promise((_, reject) => setTimeout(() => reject(new Error('Error 1')), 3000)),
new Promise((_, reject) => setTimeout(() => reject(new Error('Error 2')), 2000)),
new Promise((_, reject) => setTimeout(() => reject(new Error('Error 3')), 1000))
])
.then(console.log)
.catch(console.log); // Error: Error 3
만약 인수로 전달받은 요소가 이터러블이 아니면 Promise.resolve
메서드 이용해서 프로미스로 래핑한다.
Promise.all([
new Promise((_, reject) => setTimeout(() => reject(new Error('Error 1')), 3000)),
new Promise((_, reject) => setTimeout(() => reject(new Error('Error 2')), 2000)),
new Promise((_, reject) => setTimeout(() => reject(new Error('Error 3')), 1000))
])
.then(console.log)
.catch(console.log); // Error: Error 3
Promise.race
Promise.all
메서드처럼 프로미스를 요소로 갖는 이터러블을 인수로 전달 받는데, 가장 먼저 fulfilled
상태가 된 프로미스의 처리 결과를 resolve
하는 새로운 프로미스 반환. 말 그대로 race!
Promise.race([
new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3
])
.then(console.log) // 3
.catch(console.log);
하나라도 rejceted 상태가 되면 에러를 reject하는 새로운 프로미스 즉시 반환
Promise.race([
new Promise((_, reject) => setTimeout(() => reject(new Error('Error 1')), 3000)),
new Promise((_, reject) => setTimeout(() => reject(new Error('Error 2')), 2000)),
new Promise((_, reject) => setTimeout(() => reject(new Error('Error 3')), 1000))
])
.then(console.log)
.catch(console.log); // Error: Error 3
Promise.allSettled
마찬가지로 프로미스 배열 전달 받아서, 프로미스가 모두 settled
상태가 되면 처리 결과를 배열로 반환.
즉, fulfilled
이든 rejected
이던 간에 배열로 반환
Promise.allSettled([
new Promise(resolve => setTimeout(() => resolve(1), 2000)),
new Promise((_, reject) => setTimeout(() => reject(new Error('Error!')), 1000))
]).then(console.log);
/*
[
{status: "fulfilled", value: 1},
{status: "rejected", reason: Error: Error! at <anonymous>:3:54}
]
*/
마이크로태스크 큐
setTimeout(() => console.log(1), 0);
Promise.resolve()
.then(() => console.log(2))
.then(() => console.log(3));
// 2 3 1
1 → 2 → 3 순서가 아니라 2 → 3 → 1 순서로 출력된다.
왜 why?
프로미스의 후속처리 메서드(then)의 콜백함수는 태스크 큐가 아니라 마이크로태스크 큐에 저장된다.
프로미스의 후속 처리 메서드의 콜백함수는 마이크로태스크 큐에 저장되고 그 외의 비동기 함수의 콜백 함수나 이벤트 핸들러는 태스크 큐에 저장된다.
마이크로태스크 큐가 태스크 큐보다 우선 순위 높음!
즉, 콜 스택 비어 → 마이크로태스크 큐 / 마이크로태스크 큐도 비어 → 태스크 큐
챗 Gpt 피셜.. 끄덕
- 우선순위: Promise와 같은 마이크로태스크를 사용하는 작업은 UI 업데이트와 관련하여 더 높은 우선순위를 가지는 경우가 많습니다. 따라서 마이크로태스크 큐를 별도로 두어 해당 작업들을 빠르게 처리하고, 더 빈번한 UI 업데이트를 할 수 있도록 합니다
- 효율성: 일반적으로 마이크로태스크 큐에 들어오는 작업은 적지만, 빈번하게 발생하는 경우가 많습니다. 따라서 이러한 빠르게 처리해야 할 작업들을 별도의 큐로 분리하여 효율적으로 관리합니다.
- 데드락 방지: 일반적인 태스크 큐의 작업이 무한 루프에 빠지는 경우, 마이크로태스크 큐를 사용하여 해당 문제를 해결할 수 있습니다. 마이크로태스크 큐는 태스크 큐보다 우선순위가 높으므로, 무한 루프에 빠지는 작업보다 우선적으로 처리됩니다.
fetch
XMLHttpRequest 객체와 마찬가지로 HTTP 요청 전송 기능을 제공하는 클라이언트 사이드 Web API라고 한다. XMLHttpRequest 객체보다 사용법이 간단하고 프로미스를 지원해서 콜백 패턴의 단점에서 자유롭다.
fetch 함수는 http 응답인 reponse 객체를 래핑한 Promise객체를 반환한다.
const request = {
get(url) {
return fetch(url);
},
post(url, payload) {
return fetch(url, {
method: 'POST',
headers: { 'content-Type': 'application/json' },
body: JSON.stringify(payload)
});
},
patch(url, payload) {
return fetch(url, {
method: 'PATCH',
headers: { 'content-Type': 'application/json' },
body: JSON.stringify(payload)
});
},
delete(url) {
return fetch(url, { method: 'DELETE' });
}
};
get 요청
request.get('https://jsonplaceholder.typicode.com/todos/1')
.then(response => {
if (!response.ok) throw new Error(response.statusText);
return response.json();
})
.then(todos => console.log(todos))
.catch(err => console.error(err));
// {userId: 1, id: 1, title: "delectus aut autem", completed: false}
post 요청
request.post('https://jsonplaceholder.typicode.com/todos', {
userId: 1,
title: 'JavaScript',
completed: false
}).then(response => {
if (!response.ok) throw new Error(response.statusText);
return response.json();
})
.then(todos => console.log(todos))
.catch(err => console.error(err));
// {userId: 1, title: "JavaScript", completed: false, id: 201}
patch 요청
request.patch('https://jsonplaceholder.typicode.com/todos/1', {
completed: true
}).then(response => {
if (!response.ok) throw new Error(response.statusText);
return response.json();
})
.then(todos => console.log(todos))
.catch(err => console.error(err));
// {userId: 1, id: 1, title: "delectus aut autem", completed: true}