JS의 비동기 처리 위한 패턴으로 콜백함수를 사용했음,
그러나 콜백 지옥으로 인해 가독성이 떨어지고, 에러 처리가 곤란하다는 문제점이 발생.
const get = url => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();
xhr.onload = () => {
if (xhr.status === 200){
return JSON.parse(xhr.response) // 서버 응답 반환
} else {
console.error(`${xhr.status} ${xhr.statusText}`)
}
}
}
get(`~~~~`);
해당 get 함수가 응답 결과를 반환하는 과정을 살펴볼 필요가 있다.
get 함수는 일단 비동기 함수.
1. xhr.open, xhr.send 까지 실행되어도, 브라우저는 요청을 비동기적으로 서버로 보냄.
2. 그순간 Get 함수는 이미 실행을 끝내고 반환(return)
3. 나중에 서버에서 응답이 오면, xhr.onload 콜백이 실행
따라서 console.log는 찍히지만, get함수 자체는 아무것도 반환 X
즉,
xhr.onload = () => {
return JSON.parse(xhr.response); // ❌ get() 함수 호출자가 이 값을 못 받음
}
onload 안에서 return을 써도, 함수의 반환값이 되지 않는다는 뜻.
get 함수 실행시점
get을 호출 -> xhr.open, xhr.send가 실행
xhr.onload = () => {...} 이 이벤트 핸들러로 등록만 진행
아직 서버 응답이 안왔기에 핸들러 실행 X
Call Stack 처리
JS는 싱글 스레드라서 한줄씩 호출 스택에 넣고 실행하고 뺌
console.log는 동기 처리라 바로 스텍에 들어와서 실행
그러나 xhr.onload는 비동기라 스택에 안들어오고 나중에 실행할 함수로만 등록
서버 응답 도착시
브라우저, load 이벤트발생
xhr.onload 콜백이 Task Queue에 들어감
Event Loop가 호출 스택이 빌때까지 기다렸다가 콜백을 스택에 올려 실행
// 1. post 정보 가져오기
get(`${url}/posts/1`, ({ userId }) => {
console.log(userId); // 1
// 2. post 안의 userId로 user 정보 가져오기
get(`${url}/users/${userId}`, userInfo => {
console.log(userInfo);
// {id: 1, name: "Leanne Graham", username: "Bret", ...}
});
});
이런식으로 첫번재의 get 요청이 끝나야 userId를 알수 있으므로
두번째 POST 요청은 비동기처리임에도 get 요청이 끝날때까지 기다리게 된다.
결국 콜백 함수 호출이 중첩되어 복잡도가 높아지는데, 이를 "Callback Hell" 이라고 한다.
또한 에러처리 부분에서도 한계점을 보여주는데,
이를 해결하는 방법이 바로 Promise
new 연산자 + Promise 생성자 함수 조합으로 생성.
Promise 생성자 함수는 비동기처리를 수행할 resolve 와 reject 함수를 Parameter로 받음.
const promise = new Promise((resolve, reject) => {
// 여기 안에서 비동기 작업 수행
if (/* 비동기 처리 성공 */) {
resolve('result'); // 성공 시 호출(fulfilled)
} else {
reject('failure reason'); // 실패 시 호출(rejected)
}
});
따라서 위의 get 함수를 Promise를 이용해서 바꾸어보면
const get = url => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();
xhr.onload = () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.response));
} else {
reject(new Error(`${xhr.status} ${xhr.statusText}`));
}
};
});
};
get('~~~~')
.then(data => console.log(data))
.catch(err => console.error(err));
생성 직후 프로미스는 기본적으로 Pending.
이 비동기 처리가 성공하면 resolve 함수 호출하여 프로미스를 fulfilled 상태로 변경
반면 비동기 처리가 실패하면 reject 함수를 호출하여 프로미스를 rejected 상태로 변경
이런식으로 비동기 처리가 성공하면 [[PromiseState]]: "fulfilled"
로 변경된다.
에러면 rejected.
프로미스의 상태가 변화하면 이에 맞게 후속처리를 해야한다.
fufilled 가 되면 처리 결과를 갖고 무언가를 해야하고,
rejected 상태가 되면 처리 결과(에러)를 갖고 에러 처리를 해야한다.
then은 두개의 콜백 함수를 인수로 전달 받는다.
catch는 한개의 콜백함수만 인수로 전달받는데, 프로미스가 rejected 상태인 경우만 호출된다.
new Promise(resolve => resolve('fulfilled'))
.then(v => console.log(v))
.catch(e => console.error(e));
// 출력: fulfilled
new Promise((_, reject) => reject(new Error('rejected')))
.then(v => console.log(v))
.catch(e => console.error(e));
// 출력: Error: rejected
따라서 콜백에서 번거로웠던 에러처리를 then ... catch를 활용하여 더 간단하게 처리 가능하다.
const url = 'https://jsonplaceholder.typicode.com';
// id가 1인 post의 userId를 획득
promiseGet(`${url}/posts/1`)
// 그 userId를 이용해 user 정보 요청
.then(({ userId }) => promiseGet(`${url}/users/${userId}`))
// 최종적으로 user 정보 출력
.then(userInfo => console.log(userInfo))
// 에러 발생 시 처리
.catch(err => console.error(err));
then -> then -> catch 순으로 후속처리 메서드를 호출,
여러개의 비동기 처리를 모두 병렬로 처리시 사용
const requestData1 = () => new Promise(r => setTimeout(() => r(1), 3000));
const requestData2 = () => new Promise(r => setTimeout(() => r(2), 2000));
const requestData3 = () => new Promise(r => setTimeout(() => r(3), 1000));
Promise.all([requestData1(), requestData2(), requestData3()])
.then(res => {
console.log(res); // [1, 2, 3] → 약 3초 소요
})
.catch(console.error);
3개의 프로미스를 병렬로 시작, 총 3초가 걸린다.
결과 배열 순서는 시작 순서를 보장한다.
Promise 를 활용한 콜백패턴이 가독성이 좋지 않다보니 등장한 개념
async function runParallel() {
const p1 = requestData1();
const p2 = requestData2();
const p3 = requestData3();
const res = await Promise.all([p1, p2, p3]);
console.log(res); // [1, 2, 3] → 약 3초
}
runParallel();
위와 동일한 기능을 한다.
all 처럼 이터러블을 인수로 받는데, all과 다른점은 가장 먼저 끝난 프로미스의 결과만 반환한다.
이름처럼 race 1등만 반환
Promise.race([
new Promise(r => setTimeout(() => r(1), 3000)),
new Promise(r => setTimeout(() => r(2), 2000)),
new Promise(r => setTimeout(() => r(3), 1000))
]).then(console.log); // 3 (약 1초 뒤)
setTimeout(() => console.log(1), 0);
Promise.resolve()
.then(() => console.log(2))
.then(() => console.log(3));
JS에서 Queue는 두종류인데
근데 이벤트 루프의 우선순위가
1. Call Stack이 비면
2. 마이크로 태스크 큐를 먼저 전부 비운다
3. 그 다음 태스크 큐에서 1개 가져와서 실행, 다시 2번으로 돌아가 반복
따라서 마이크로 태스크큐에 들어간 프로미스가 먼저 실행되고, 그다음 태스크큐로 이동하기에
위 출력은 2 -> 3 -> 1 이 되는 것.
fetch는 XMLHttpRequest 객체와 마찬가지로
HTTP 요청 전송 기능을 제공하는 클라이언트 사이드 Web API
이 fetch 함수가 Response 객체를 래핑한 Promise 객체를 반환
단, 404 Found Err, 500 Internal Server Err 같은 HTTP 에러가 발생해도,
에러를 reject 하지 않고, 이 Response 객체를 resolve 한다는 것 기억하자.
오프라인 등 네트워크 장애나 CORS 에러에 의해 요청이 미완료 된 경우에만 프로미스를 reject