자바스크립트는 비동기 처리를 위해 콜백 패턴을 사용한다
하지만 이런 콜백 패턴 방식은 비동기 처리가 여러 개일 때 가독성이 매우 나쁘고, 에러 처리가 곤란하다
ES6부터 비동기 처리를 위한 패턴으로 프로미스를 도입
비동기 함수 내부의 비동기로 동작하는 코드는 비동기 함수가 종료된 이후에 완료되기 때문에
비동기 함수 내부의 비동기로 동작하는 코드에서 처리 결과를 외부로 반환하거나 상위 스코프의 변수에 할당해도 우리가 의도한 대로 동작하지 않음
따라서 비동기 함수의 처리 결과에 대한 후속 처리를 비동기 함수의 내부에서 수행해야 한다
let number = 0;
setTimeout(()=>{number = 100;},0);
console.log(number); // 100을 원했으나 0이 출력됨
// number=100 할당이 console.log를 마친 뒤에 일어나기 때문에
const get = url => {
const xhr = new XMLHttpRequest();
xhr.open("GET",url);
xhr.send();
xhr.onload = () => {
if (xhr.response===200) return JSON.parse(xhr.response);
console.error(xhr.status,xhr.statusText);
};
};
const response = get('https://jsonplaceholder.typicode.com/posts/1');
console.log(response); // undefined
get 함수는 onload 이벤트에 이벤트 핸들러를 할당하고 종료된다
반환값이 없으므로 undefined를 반환하게 되고
response에는 undefined가 할당된다
이후에 응답이 완료되면 할당한 이벤트 핸들러가 응답 값을 반환하지만 그 값을 이용할 방법이 없다
따라서 비동기 함수 내부에서 후처리를 해주면
const get = (url,callback) => {
const xhr = new XMLHttpRequest();
xhr.open("GET",url);
xhr.send();
xhr.onload = () => {
if (xhr.response===200) {
return callback(JSON.parse(xhr.response));
}
else{
console.error(xhr.status,xhr.statusText);
}
};
};
get('https://jsonplaceholder.typicode.com/posts/1',console.log);
위에는 후처리가 1번이지만 후처리가 여러번 이라면 콜백 함수의 호출이 중첩되면서 매우 복잡해진다
이러한 현상을 콜백 헬 이라고 한다
위 과정을 코드로 작성해보면
//get 함수는 위에 선언된 get과 동일
get('https://jsonplaceholder.typicode.com/posts/1',({userId})=>{
get(`https://jsonplaceholder.typicode.com/users/${userId}`,(userData)=>{
console.log(userData);
});
});
// 후처리가 2번인데도 이렇게 복잡하다
try{
setTimeout(()=>{throw new Error('에러!');},0);
} catch(e) {
console.error(e);
}
try 코드 블록이 실행되고 블록 내부에서 에러가 발생하게 되면 catch문에 변수로 에러가 전달되고 catch 블록 내부가 실행된다
에러는 호출자 방향으로 전파된다 (콜 스택의 아래 방향)
하지만 위 예시에서 에러를 발생시키는 콜백 함수는 테스큐 큐에서 대기하다가 콜 스택이 비었을 때 실행되므로 catch가 불가능
이런 콜백 헬과 에러 처리 문제를 해결하기 위해 ES6에서 프로미스 도입
// 대충 쓰면
const promise = new Promise((resolve,reject)=>{
if (비동기 처리가 성공) resolve(값);
else 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(JSON.parse(xhr.response));
else reject(new Error(xhr.status));
}
});
}
프로미스는 비동기 처리의 상태를 나타내는 상태 정보를 갖는다
fulfilled와 rejected 상태를 settled 상태 라고 함
프로미스는 비동기 처리 상태와 처리 결과를 관리하는 객체
프로미스의 비동기 처리 상태가 변화하면 (pending -> settled)
후속 처리 메서드에 인수로 전달한 콜백 함수가 호출되고
이 때 전달한 콜백 함수에는 프로미스의 처리 결과가 인수로 전달된다
모든 후속 처리 메서드는 프로미스를 반환하고 비동기로 동작
두 개의 콜백 함수를 인수로 전달받는다
new Promise(resolve => resolve('비동기 성공!'))
.then(result=>console.log(result), err => console.error(err));
new Promise((resolve,reject) => reject(new Error('비동기 실패!')))
.then(result=>console.log(result), err => console.error(err));
then 에 인수로 전달한 콜백 함수가 프로미스를 반환하면 걔를 그대로 반환,
값을 반환하면 그 값을 암묵적으로 resolve 또는 reject하여 프로미스를 생성해 반환
한 개의 콜백 함수를 전달받는다
프로미스가 rejected 상태가 되면 콜백 함수를 호출
new Promise((resolve,reject) => reject(new Error('비동기 실패!')))
.catch(err => console.log(err));
한 개의 콜백 함수를 전달받는다
fulfilled,rejected 상관 없이 무조건 한 번 인수로 넘긴 콜백 함수 호출
후속 처리 메서드인 then과 catch로 에러 처리 가능
then의 경우 두 번째 콜백 함수로 처리할 수 있지만 fulfilled 상태가 되어 첫 번째 콜백 함수를 실행하는 도중에 발생한 에러는 캐치가 불가능
-> catch 메서드는 비동기 처리에서 발생한 에러 + then 메서드 내부의 에러까지 캐치
에러 처리는 catch 메서드에서 하자
// 첫 번째 콜백 함수에서 발생한 에러 캐치 불가
promiseGet(url).then(
res => console.정의되지않은메서드(res),
err => console.error(err));
// then 메서드 내부 에러 캐치 가능
promiseGet(url)
.then(res => console.정의되지않은메서드(res))
.catch(err => console.error(err));
후속 처리 메서드인 then,catch,finally 모두 프로미스를 반환하기 때문에 연속으로 호출 가능
= 프로미스 체이닝
프로미스는 콜백 헬이 발생하지는 않지만 결국 콜백 함수는 사용한다
async/await 을 사용하면 프로미스 후속 처리 메서드 없이 동기 방식으로 동작하는 것처럼 프로미스가 처리 결과를 반환하게 할 수 있음
const get = (url,callback) => {
const xhr = new XMLHttpRequest();
xhr.open("GET",url);
xhr.send();
xhr.onload = () => {
if (xhr.response===200) {
return callback(JSON.parse(xhr.response));
}
else{
console.error(xhr.status,xhr.statusText);
}
};
};
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(JSON.parse(xhr.response));
else reject(new Error(xhr.status));
}
});
}
// 콜백 헬 예시
get('https://jsonplaceholder.typicode.com/posts/1',({userId})=>{
get(`https://jsonplaceholder.typicode.com/users/${userId}`,(userData)=>{
console.log(userData);
});
});
// 프로미스를 이용하여 작성하면
promiseGet('https://jsonplaceholder.typicode.com/posts/1')
.then(({userId}) => promiseGet(`https://jsonplaceholder.typicode.com/users/${userId}`))
.then(userData => console.log(userData))
.catch(err => console.error(err));
이미 존재 하는 값을 래핑하여 프로미스를 생성
const resolvedPromise = Promise.resolve([1,2,3]);
resolvedPromise.then(console.log); // [1,2,3]
const rejectedPromise = Promise.reject(new Error('에러!'));
rejectedPromise.catch(console.log); // Error: 에러!
// 위에 코드, 아래 코드 똑같이 동작
const resolvedPromise = new Promise(resolve => resolve([1,2,3]));
resolvedPromise.then(console.log); // [1,2,3
const resolvedPromise = new Promise((resolve,reject) => reject(new Error('에러!')));
rejectedPromise.catch(console.log); // Error: 에러!
const requestData1 = () => {
return new Promise(resolve => setTimeout(()=>resolve(1),3000));
}
const requestData2 = () => {
return new Promise(resolve => setTimeout(()=>resolve(2),2000));
}
const requestData3 = () =>{
return new Promise(resolve => setTimeout(()=>resolve(3),1000));
}
const res = [];
// 6초 뒤에 [1,2,3] 출력
requestData1()
.then((data)=>{
res.push(data);
return requestData2();
})
.then(data=>{
res.push(data);
return requestData3();
})
.then(data=>{
res.push(data);
console.log(res);
})
.catch(console.error);
각각의 requestData 는 서로에게 영향이 없기 때문에 순차적으로 실행할 필요가 없음
모두 병렬로 실행하려면
Promise.all([requestData1(),requestData2(),requestData3()])
.then( arr => console.log(arr)); // 3초 컷
// Promise.resolve()를 통해 요소 각각을 프로미스로 래핑한 뒤 처리
Promise.all([1,2,3])
.then( arr => console.log(arr)); // [1,2,3]
프로미스의 후속 처리 메서드의 콜백 함수는 태스크 큐가 아니라 마이크로태스크 큐에 저장된다
처리 우선 순위 : 마이크로 태스크 큐 > 태스크 큐
setTimeout(()=>console.log(1),0);
Promise.resolve()
.then(()=>console.log(2))
.then(()=>console.log(3));
// 태스크 큐에는 : 1
// 마이크로태스크 큐에는 : 2,3
// 우선순위 대로 2,3,1 출력
fetch(url,http 요청 메서드와 헤더와 페이로드 등을 설정한 객체);
fetch(url); // GET 요청 전송
Response.prototype에는 Response 객체에 있는 HTTP 응답 몸체를 참조할 수 있는 다양한 메서드가 있음
fetch('https://jsonplaceholder.typicode.com/albums/1')
.then(res => res.json()) // Response객체의 json 메서드로 HTTP 응답 몸체를 역직렬화
.then(result => console.log(result)); // 그대로 출력
// {userId: 1, id: 1, title: 'quidem molestiae enim'}
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'
})
}
};