비동기 처리를 위한 콜백 패턴의 단점
- 비동기 함수 : 함수 내부에 비동기로 동작하는 코드를 포함한 함수
- 비동기 함수 내부의 비동기로 동작하는 코드는 비동기 함수가 종료된 이후 완료
- 비동기 함수 내부의 비동기로 동작하는 코드에서 처리 결과를 외부로 반환하거나 상위 스코프의 변수에 할당하면 기대한 대로 동작 X
let todos;
// GET 요청을 위한 비동기 함수
const get = url =>{
const xhr = new XHLHttpRequest();
xhr.open('GET',url);
xhr.send();
xhr.onload=()=>{
if(xhr.stats === 200){
// 1. 서버의 응답을 상위 스코프의 변수에 할당
todos = JSON.parse(xhr.response);
}
}
}
get('url');
console.log(todos); // undefined
- get 함수 실행 컨텍스트 생성 → 실행 컨텍스트 스택 (콜 스택)에 푸시 → xhr.onload 이벤트 핸들러 프로퍼티에 이벤트 핸들러 바인딩
- get 함수 종료 → 실행 컨텍스트 스택에서 팝
- console.log 실행 컨텍스트 생성 및 실행 컨텍스트 스택에 푸시 (console.log 호출 직전에 load 이벤트가 발생했더라도 xhr.onload 이벤트 핸들러 프로퍼티에 바인딩한 이벤트 핸들러는 console.log보다 먼저 실행 X)
- 서버로부터 응답이 도착하면 xhr 객체에서 load 이벤트가 발생하는데 즉시 실행되는 것이 아니라 load 이벤트가 발생하면 일단 태스크 큐에 저장되어 대기하다가 콜 스택이 비면 이벤트 루프에 의해 콜스택으로 푸시되어 실행
- xhr.onload 이벤트 핸들러가 실행되는 시점에는 콜 스택이 빈 상태여야 하므로 console.log는 이미 종료된 이후
- 비동기 함수는 처리 결과를 외부에 반환 X, 상위 스코프에 할당 X
- 서버의 응답 등 비동기 함수의 처리 결과에 대한 후속 처리는 비동기 함수 내부에서 수행해야함
- 이때 비동기 함수를 범용적으로 사용하기 위해 비동기 함수에 비동기 처리 결과에 대한 후속처리를 수행하는 콜백 함수를 전달하는 것이 일반적
// GET 요청을 위한 비동기 함수
const get = (url, successCallback, failureCallback) => {
const xhr = new XMLHttpRequest();
xhr.open();
xhr.send("GET", url);
xhr.onload = () => {
if (xhr.status === 200) {
successCallback(JSON.parse(xhr.response));
} else {
failureCallback(xhr.status);
}
};
};
// 서버의 응답에 대한 후속처리를 위한 콜백함수를 비동기 함수인 get에 전달
get("url", console.log, console.error);
- 콜백 헬 : 콜백 함수를 통해 비동기 처리 결과에 대한 후속 처리를 수행하는 비동기 함수가 비동기 처리 결과를 가지고 또 다시 비동기 함수를 호출해야 하는 경우 콜백 함수 호출이 중첩되고 복잡도가 높아지는 현상
에러 처리의 한계
try {
setTimeout(() => {throw new Error("Error!"); }, 1000);
} catch (error) {
// 에러 캐치 못함
console.error("캐치한 에러 :", error);
}
- 에러는 호출자 방향으로 전파되지만 setTimeout 함수의 콜백 함수를 호출한 것은 setTimeout 함수가 아니므로 (콜백함수가 실행될 때 setTimeout 함수는 이미 콜스택에서 제거된 상태) catch 블록에서 캐치되지 X
프로미스의 생성
- Promise 는 호스트 객체가 아닌 표준 빌트인 객체로 new 연산자와 함께 호출하여 객체 생성
- 비동기 처리를 수행할 콜백 함수를 인수로 전달받는데 resolve 와 reject 함수를 인수로 전달 받음
const promise = new Promise((resovle, reject) => {
// Promise 함수의 콜백 함수 내부에서 비동기 처리를 수행
if('비동기 처리 성공') {
resolve('result');
} else { // 비동기 처리 실패
reject('failure Error');
}
})
| 프로미스의 상태 정보 | 의미 | 상태 변경 조건 |
| pending | 비동기 처리가 아직 수행되지 않은 상태 | 프로미스가 생성된 직후 기본 상태 |
| fulfilled | 비동기 처리가 수행된 상태 (성공) | resolve 함수 호출 |
| rejected | 비동기 처리가 수행된 상태 (실패) | reject 함수 호출 |
- 프로미스 생성 직후 : pending 상태
- 비동기 처리 성공 : resolve 함수를 호출해 프로미스를 fulfilled 상태로 변경
- 비동기 처리 실패 : reject 함수를 호출해 프로미스를 rejected 상태로 변경
- fulfilled 도 아니고 rejected 도 아니고 pending 도 아닌 상태 : settled (더 이상 다른 상태로 변화 X)

- PromiseState : 비동기 처리 상태 정보
- PromiseValue : 비동기 처리 결과 정보
프로미스의 후속 처리 메서드
- 프로미스의 비동기 처리 상태가 변화하면 후속 처리 메서드에 인수로 전달한 콜백 함수가 선택적으로 호출됨
- 후속 처리 메서드의 콜백 함수에 프로미스의 처리 결과가 인수로 전달됨
- 모든 후속 처리 메서드는 프로미스를 반환하며 비동기로 동작
1. Promise.prototype.then
- 첫 번째 콜백 함수 : 프로미스가 fulfilled 상태(resolve 함수가 호출된 상태)가 되면 호출됨. 이 때 콜백 함수는 프로미스의 비동기 처리 결과를 인수로 전달받음
- 두 번째 콜백 함수 : 프로미스가 rejected 상태(reject 함수가 호출된 상태)가 되면 호출됨. 이 때 콜백 함수는 프로미스의 에러를 인수로 전달받음
new Promise((resolve) => resolve("fulfilled"))
.then((v) => console.log(v),(e) => console.error(e),); // fulfilled
new Promise((_, reject) => reject(new Error("rejected")))
.then((v) => console.log(v),(e) => console.error(e),); // Error: rejected
2. Promise.prototype.catch
- 프로미스가 rejected 상태인 경우만 호출됨
- then 과 동일하게 동작하여 언제나 프로미스 객체를 반환
new Promise((_, reject) => reject(new Error("rejected")))
.catch((e) => console.log(e)); // Error: rejected
new Promise((_, reject) => reject(new Error("rejected")))
.then(undefined, (e) => console.log(e)); // Error: rejected
3. Promise.prototype.finally
- 프로미스의 성공 (fulfilled) 또는 실패 (rejected)와 상관없이 무조건 한 번 호출
- 프로미스의 상태와 상관없이 공통적으로 수행해야 할 처리 내용이 있을 때 유용
- 언제나 프로미스 객체를 반환
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(데이터 받을 url)
.then((res) => console.log(res))
.catch((err) => console.log(err))
.finally(() => console.log("Bye !"));
프로미스의 에러 처리
- 비동기 처리에서 발생한 에러는 then 메서드의 두 번째 콜백 함수 또는 catch 로 처리 가능
- 가독성과 then 메서드 내부에서 발생한 에러 처리를 위해 then 메서드 보다 catch 메서드를 사용
const wrongURL = "https://wrrrrrooooooong.com";
// then 메서드로 에러 처리
promiseGET(wrongURL).then(
(res) => console.log(res),
(err) => console.log(err),
); // Error : 404
// catch 메서드로 에러 처리
promiseGET(wrongURL)
.then((res) => console.log(res))
.catch((err) => console.log(err)); // Error : 404
프로미스 체이닝
- 프로미스 후속 처리 메서드는 언제나 프로미스를 반환하므로 순차적으로 호출 가능
- 프로미스는 프로미스 체이닝을 통해 비동기 처리 결과를 전달받아 후속처리를 하므로 비동기 처리를 위한 콜백 패턴에서 발생하던 콜백 헬 발생 X
프로미스의 정적 메서드
1. Promise.resolve/ Promise.reject
- 이미 존재하는 값을 래핑하여 프로미스를 생성하기 위해 사용
- Promise.resolve 메서드는 인수로 전달받은 값을 resolve 하는 프로미스를 생성
const resolvedPromise = Promise.resolve([1, 2, 3]);
// const resolvedPromise = new Promise((resolve) => resolve([1, 2, 3]));
resolvedPromise.then(console.log); // [ 1, 2, 3 ]
- Promise.reject 메서드는 인수로 전달받은 값을 reject 하는 프로미스를 생성
const rejectedPromise = Promise.reject(new Error("Error !"));
// const rejectedPromise = new Promise((_, reject) => reject(new Error("Error !")));
rejectedPromise.then(console.log); // Error: Error !
2. Promise.all
- 프로미스를 요소로 갖는 배열 등의 이터러블을 인수로 전달 받음
- 여러 개의 비동기 처리를 모두 병렬 처리할 때 사용
- 인수로 전달받은 배열의 모든 프로미스가 모두 fulfilled 상태가 되면 종료
- 하나라도 rejected 상태가 되면 나머지 프로미스가 fulfilled 상태가 되는 것을 기다리지 않고 즉시 종료
- 처리 순서 보장
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.log);
3. Promise.race
- 프로미스를 요소로 갖는 배열 등의 이터러블을 인수로 전달 받음
- Promise.all 메서드처럼 모든 프로미스가 fulfilled 상태가 되는 것을 기다리는 것이 아니라 가장 먼저 fulfilled 상태가 된 프로미스의 처리 결과를 resolve 하는 새로운 프로미스 반환
- 하나라도 rejected 상태가 되면 나머지 프로미스가 fulfilled 상태가 되는 것을 기다리지 않고 즉시 종료
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.race([requestData1(), requestData2(), requestData3()])
.then(console.log)// 3
.catch(console.log);
4. Promise.allSettled
- 프로미스를 요소로 갖는 배열 등의 이터러블을 인수로 전달 받음
- 전달받은 프로미스가 모두 settled 상태(비동기 처리가 수행된 상태, 즉 fulfilled 또는 rejected 상태)가 되면 처리 결과를 배열로 반환
- 반환된 배열에는 결과와 상관없이 모든 프로미스들의 처리 결과가 담겨있음
- 프로미스가 fulfilled 인 경우 status 와 프로미스 처리 결과인 value 프로퍼티를 가짐
- 프로미스가 rejected인 경우 status 와 에러를 나타내는 reason 프로퍼티를 가짐
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 !
}
]
*/
마이크로태스크 큐
- 프로미스의 후속 처리 메서드의 콜백 함수는 태스크 큐가 아니라 마이크로태스크 큐에 저장
- 마이크로태스크 큐에는 프로미스의 후속 처리 메서드의 콜백 함수가 일시 저장
- 콜백함수나 이벤트 핸들러를 일시 저장한다는 점에서 태스크 큐와 동일하지만 마이크로태스크 큐는 태스크 큐 보다 우선순위가 높음
- 이벤트 루프는 콜 스택이 비면 먼저 마이크로태스크 큐에서 대기하고 있는 함수를 가져와 실행하고 마이크로태스크 큐가 비면 태스크 큐에서 대기하고 있는 함수를 가져와 실행
fetch
- HTTP 요청 전송 기능을 제공하는 클라이언트 사이드 Web API
- XMLHttpRequest 객체보다 사용법이 간단하고 프로미스를 지원하기 때문에 비동기 처리를 위한 콜백 패턴의 단점에서 자유로움
- HTTP 응답을 나타내는 Response 객체를 래핑한 Promise 객체를 반환
const promise = fetch(url, [, options])
- fetch 함수가 반환한 프로미스가 래핑하고 있는 MIME 타입이 application/json 인 HTTP 응답 몸체를 취득하려면 Response.prototype.json 메서드를 사용 (Response 객체에서 HTTP 응답 몸체를 취득하여 역직렬화)
- fetch 함수가 반환하는 프로미스는 기본적으로 404, 500 같은 HTTP 에러가 발생해도 에러를 reject 하지 않고 불리언 타입의 ok 상태를 false 로 설정한 Response 객체를 resolve (오프라인 등의 네트워크 장애나 CORS 에러에 의해 요청이 완료되지 못한 경우에만 프로미스를 reject)
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(url)
.then((result) => result.json())
.then((data) => console.log(data))
.catch((err) => console.log(err))
// POST
request
.post(url, { obj })
.then((result) => result.json())
.then((data) => console.log(data))
.catch((err) => console.error(err));
// PATCH
request
.patch(url, { obj })
.then((result) => result.json())
.then((data) => console.log(data))
.catch((err) => console.error(err));
// DELETE
request
.delete(url, { obj })
.then((result) => result.json())
.then((data) => console.log(data))
.catch((err) => console.error(err));'Javascript' 카테고리의 다른 글
| [모던 자바스크립트 Deep Dive] 43. 에러 처리 (1) | 2023.12.17 |
|---|---|
| [모던 자바스크립트 Deep Dive] 42. 제너레이터와 async/await (1) | 2023.12.17 |
| [모던 자바스크립트 Deep Dive] 40. REST API (0) | 2023.12.11 |
| [모던 자바스크립트 Deep Dive] 39. Ajax, JSON, XMLHttpRequest (0) | 2023.12.11 |
| [모던 자바스크립트 Deep Dive] 38. 비동기 프로그래밍 (0) | 2023.12.07 |