45장 프로미스
비동기 처리를 위한 콜백 패턴의 단점
1 ) 콜백 헬
45-01
// 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 ..."
}
*/
- 비동기 함수를 호출하면 함수 내부의 비동기로 동작하는 코드가 완료되지 않았다 해도 기다리지 않고 즉시 종료됨.
- 즉 비동기 함수 내부의 비동기로 동작하는 코드는 비동기 함수가 종료된 이후에 완료됨.
- 따라서 비동기 함수 내부의 비동기로 동작하는 코드에서 처리 결과를 외부로 반환하거나 상위 스코프의 변수에 할당하면 기대한 대로 동작하지 않음.
ex
- setTimeout 함수를 호출하면 콜백함수를 호출 스케줄링 한 다음, 타이머 id를 반환하고 즉시 종료됨.
- 즉 비동기 함수인 setTimeout 함수의 콜백함수는 setTimeout 함수가 종료된 이후에 호출됨.
45-02
let g = 0;
// 비동기 함수인 setTimeout 함수는 콜백 함수의 처리 결과를 외부로 반환하거나
// 상위 스코프의 변수에 할당하지 못한다.
setTimeout(() => { g = 100; }, 0);
console.log(g); // 0
- get함수 내부의 onload 이벤트 핸들러가 비동기로 동작함.
- get 함수를 호출하면 GET 요청을 전송하고 onload 이벤트 핸들러를 등록한 다음 undefined를 반환하고 즉시 종료됨.
- 즉 비동기 함수인 get 함수 내부의 onload 이벤트 핸들러는 get 함수가 종료된 이후에 실행됨.
- 따라서 get 함수의 onload 이벤트 핸들러에서 서버의 응답 결과를 반환하거나 상위 스코프의 변수에 할당하면 기대한 대로 동작하지 않음.
- 수정
45-03
// GET 요청을 위한 비동기 함수
const get = url => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();
xhr.onload = () => {
if (xhr.status === 200) {
// ① 서버의 응답을 반환한다.
return JSON.parse(xhr.response);
}
console.error(`${xhr.status} ${xhr.statusText}`);
};
};
// ② id가 1인 post를 취득
const response = get('<https://jsonplaceholder.typicode.com/posts/1>');
console.log(response); // undefined
- 1번은 get함수의 반환문이 아님.
- 비동기 함수는 비동기 처리 결과를 외부에 반환할 수 없고 상위 스코프의 변수에 할당할 수도 없음.
- 따라서 비동기 함수의 처리 결과에 대한 후속 처리는 비동기 함수 내부에서 수행해야함.
- 이때 비동기 함수를 범용적으로 사용하기 위해 비동기 함수에 비동기 처리 결과에 대한 후속 처리를 수행하는 콜백함수를 전달하는 것이 일반적.
- 필요에 따라 비동기 처리가 성공하면 호출될 콜백 함수와 비동기 처리가 실패하면 호출될 콜백 함수를 전달할 수 있음.
45-06
// 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 ..."
}
*/
- 콜백 함수를 통해 비동기 처리 결과에 대한 후속 처리를 수행하는 비동기 함수가 비동기 처리 결과를 가지고 또다시 비동기 함수를 호출 : 콜백 헬
45-07
// GET 요청을 위한 비동기 함수
const get = (url, callback) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();
xhr.onload = () => {
if (xhr.status === 200) {
// 서버의 응답을 콜백 함수에 전달하면서 호출하여 응답에 대한 후속 처리를 한다.
callback(JSON.parse(xhr.response));
} else {
console.error(`${xhr.status} ${xhr.statusText}`);
}
};
};
const url = '<https://jsonplaceholder.typicode.com>';
// id가 1인 post의 userId를 취득
get(`${url}/posts/1`, ({ userId }) => {
console.log(userId); // 1
// post의 userId를 사용하여 user 정보를 취득
get(`${url}/users/${userId}`, userInfo => {
console.log(userInfo); // {id: 1, name: "Leanne Graham", username: "Bret",...}
});
});
2 ) 에러 처리의 한계
- 비동기 처리를 위한 콜백 패턴의 문제점 중에서 가장 심각한 것은 에러 처리가 곤란하다는 것.
45-09
try {
setTimeout(() => { throw new Error('Error!'); }, 1000);
} catch (e) {
// 에러를 캐치하지 못한다
console.error('캐치한 에러', e);
}
- try … catch … finallly 문
try catch finally 문은 에러 처리를 구현하는 방법임. 먼저 try 코드 블록이 실행됨.
이때 try 코드 블록에 포함된 문 중에서 에러가 발생하면 해단 에러는 catch 문의 err 변수에 전달되고
catch 코드 블록이 실행됨. finally 코드 블록은 에러 발생과 상관없이 반드시 한 번 실행됨.
- try에서 호출한 setTimeout 함수는 1초 후에 콜백 함수가 실행되도록 타이머를 설정하고, 이후 콜백 함수는 에러를 발생시킴.
- 하지만 이 에러는 catch 코드 블록에서 캐치되지 않음. 그 이유를 알아보자.
- setTimeout 함수의 콜백함수가 실행될 때 setTImeout 함수는 이미 콜 스택에서 제거된 상태임.
- 이것은 setTimeout 함수의 콜백함수를 호출한 것이 setTimeout 함수가 아니라는 것을 의미.
- setTimeout 함수의 콜백 함수의 호출자(caller)가 setTImeout 함수라면 콜 스택의 현재 실행중인 실행 컨텍스트가 콜백 함수의 실행컨텍스트일 때 실행중인 실행 컨텍스트의 하위 실행 컨텍스트가 setTImeout 함수여야 함.
- 에러는 호출자(caller) 방향으로 전파됨.
- 즉, 콜 스택의 아래 방향으로 전파됨.
프로미스의 생성
- Promise 생성자 함수는 비동기 처리를 수행할 콜백함수를 인수로 전달받는데 이 콜백함수는 resolve와 reject 함수를 인수로 전달받음.
45-10
// 프로미스 생성
const promise = new Promise((resolve, reject) => {
// Promise 함수의 콜백 함수 내부에서 비동기 처리를 수행한다.
if (/* 비동기 처리 성공 */) {
resolve('result');
} else { /* 비동기 처리 실패 */
reject('failure reason');
}
});
- 프로미스는 다음과 같이 현재 비동기 처리가 어떻게 진행되고 있는지를 나타내는 상태 정보를 가짐.
- 생성된 직후의 프로미스는 기본적으로 pending 상태. 이후 비동기 처리가 수행되면 비동기 처리 결과에 따라 다음과 같이 프로미스의 상태가 변경됨.
- 비동기 처리 성공 : resolve 함수를 호출해 프로미스를 fulfilled 상태로 변경
- 비동기 처리 실패 : reject 함수를 호출해 프로미스를 rejected 상태로 변경
- 이처럼 프로미스의 상태는 resolve 또는 reject 함수를 호출하는 것으로 결정됨.
- fulfilled 또는 rejected 상태를 settled 상태라고 함.
- 일단 settled 상태가 되면 더는 다른 상태로 변화할 수 없음.
- 프로미스는 비동기 처리 결과도 상태로 가짐.
- 프로미스는 비동기 처리 상태와 처리 결과를 관리하는 객체.
프로미스의 후속 처리 메서드
- 프로미스의 비동기 처리 상태가 변화하면 이에 따른 후속 처리를 해야함.
- 프로미스가 fulfilled 상태가 되면 처리 결과를 가지고 무언가를 해야하고, rejected 상태가 되면 에러 처리를 해야함.
- 이를 위해 프로미스는 후속 메서드 then, catch, finally를 제공함.
- 프로미스의 비동기 처리 상태가 변화하면 후속 처리 메서드에 인수로 전달한 콜백 함수가 선택적으로 호출됨.
1 ) then
- 첫번째 콜백함수 : 비동기 처리가 성공했을 때 호출되는 성공 처리 콜백 함수
- 두번째 콜백 함수 : 비동기 처리가 실패했을 때 호출되는 실패 처리 콜백 함수
2 ) catch
- 프로미스가 rejected상태인 경우만 호출됨
3 ) finally
- 프로미스의 성공 또는 실패와 상관없이 무조건 한 번 호출됨.
- try, catch ,finally 메서드는 언제나 프로미스를 반환함
프로미스의 에러 처리
- 모든 then메서드 호출 이후에 catch 호출하면 가독성 좋고 명확함.
- 따라서 에러 처리는 then에서 하지 말고 catch에서 하는 것을 권장함.
프로미스 체이닝
- then → catch → then 이런식으로 연속해서 쓸 수 있음.
45-24
const url = '<https://jsonplaceholder.typicode.com>';
// id가 1인 post의 userId를 취득
promiseGet(`${url}/posts/1`)
// 취득한 post의 userId로 user 정보를 취득
.then(({ userId }) => promiseGet(`${url}/users/${userId}`))
.then(userInfo => console.log(userInfo))
.catch(err => console.error(err));
- 콜백 패턴은 가독성이 안좋음.
- async/await 사용해서 동기처럼 처리.
프로미스의 정적 메서드
1 ) Promise.resolve / Promise.reject
- 인수로 전달받은 값을 resolve / reject 하는 프로미스를 생성함.
45-26
// 배열을 resolve하는 프로미스를 생성
const resolvedPromise = Promise.resolve([1, 2, 3]);
resolvedPromise.then(console.log); // [1, 2, 3]
2 ) Promise.all
- 여러 개의 비동기 처리를 모두 병렬 처리할 때 사용
45-30
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);
- 세 개의 비동기 처리를 순차적으로 처리함.
- 앞의 비동기 처리가 완료하면 다음 비동기 처리를 수행함.
- 하지만 위 예제의 경우에는 앞의 비동기 처리 결과를 다음 비동기 처리가 사용하지 않아서 순차적으로 처리할 필요가 없음.
45-31
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);
- 모든 프로미스가 fulfilled 상태가 되면 모든 처리 결과를 배열에 저장해 새로운 프로미스를 반환함.
- 하나라도 rejected 상태가 되면 나머지 프로미스가 fulfilled 상태가 되는 것을 기다리지 않고 즉시 종료함.
45-32
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
3 ) Promise.race
- 가장 먼저 fulfilled 상태가 된 프로미스의 처리 결과를 resolve 하는 새로운 프로미스를 반환함.
45-35
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);
- 하나라도 rejected 상태가 되면 에러를 reject 하는 새로운 프로미스를 즉시 반환함.
4 ) Promise.allSettled
- 전달받은 프로미스가 모두 settled 상태가 되면 처리 결과를 배열로 반환함.
45-37
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}
]
*/
마이크로태스크 큐
- 순서 예측
45-39
setTimeout(() => console.log(1), 0);
Promise.resolve()
.then(() => console.log(2))
.then(() => console.log(3));
- 1 → 2 → 3 (X)
- 2 → 3 → 1 (O)
- 프로미스의 후속 처리 메서드의 콜백함수는 태스크 큐가 아니라 마이크로태스크 큐에 저장됨.
- 마이크로태스크 큐는 태스크 큐보다 우선순위가 높음
fetch
- HTTP 요청을 전송할 URL, HTTP 요청 메서드, HTTP 요청 헤더, 페이로드 등을 설정한 객체를 전달.
- HTTP 응답을 나타내는 Response 객체를 래핑한 Promise 객체를 반환함.
45-40 GET 요청
fetch('<https://jsonplaceholder.typicode.com/todos/1>')
.then(response => console.log(response));
- Response에는 HTTP 응답 몸체를 위한 다양한 메서드를 제공함.
- ex MIME 타입이 application/json인 HTTP 응답 몸체를 얻으려면 json 메서드를 사용함.
- json 메서드는 Response 객체에서 HTTP 응답 몸체(response body)를 취득하여 역직렬화 함.
45-41
fetch('<https://jsonplaceholder.typicode.com/todos/1>')
// response는 HTTP 응답을 나타내는 Response 객체이다.
// json 메서드를 사용하여 Response 객체에서 HTTP 응답 몸체를 취득하여 역직렬화한다.
.then(response => response.json())
// json은 역직렬화된 HTTP 응답 몸체이다.
.then(json => console.log(json));
// {userId: 1, id: 1, title: "delectus aut autem", completed: false}
- 에러 처리에 주의
45-42
const wrongUrl = '<https://jsonplaceholder.typicode.com/XXX/1>';
// 부적절한 URL이 지정되었기 때문에 404 Not Found 에러가 발생한다.
fetch(wrongUrl)
.then(() => console.log('ok'))
.catch(() => console.log('error'));
- fetch 함수가 반환하는 프로미스는 기본적으로 404 Not Found나 500 Internal Server Error와 같은 HTTP 에러가 발생해도 에러를 reject하지 않고 불리언 타입의 ok 상태를 false로 설정한 Response 객체를 resolve 함.
- 오프라인 등의 네트워크 장애나 CORS 에러에 의해 요청이 완료되지 못한 경우에만 프로미스를 reject 함.
- 따라서 반환한 프로미스가 resolve한 불리언 타입의 ok상태를 확인해서 명시적으로 에러를 처리할 필요가 있음.
45-43
const wrongUrl = '<https://jsonplaceholder.typicode.com/XXX/1>';
// 부적절한 URL이 지정되었기 때문에 404 Not Found 에러가 발생한다.
fetch(wrongUrl)
// response는 HTTP 응답을 나타내는 Response 객체다.
.then(response => {
if (!response.ok) throw new Error(response.statusText);
return response.json();
})
.then(todo => console.log(todo))
.catch(err => console.error(err));
- 참고로 axios는 모든 HTTP 에러를 reject 하는 프로미스를 반환함.
- fetch 함수를 통해 HTTP 요청을 전송해보자.
- URL, HTTP 요청 메서드, HTTP 요청 헤더, 페이로드 등을 설정한 객체를 전달
45-44
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' });
}
};
1 ) GET 요청
45-45
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}
2 ) POST 요청
45-46
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}
3 ) PATCH 요청
45-47
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}
4 ) DELETE 요청
45-48
request.delete('<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));
// {}
'JS' 카테고리의 다른 글
모던 자바스크립트 Deep Dive - 에러 처리 (0) | 2024.02.19 |
---|---|
모던 자바스크립트 Deep Dive - async/await (0) | 2024.02.17 |
모던 자바스크립트 Deep Dive - REST API (0) | 2024.02.15 |
모던 자바스크립트 Deep Dive - Ajax (0) | 2024.02.14 |
모던 자바스크립트 Deep Dive - 비동기 프로그래밍 (0) | 2024.02.13 |
댓글