45장 프로미스
45.1 비동기 처리를 위한 콜백 패턴의 단점
45.1.1 콜백 헬(45-01)
- GET 요청 함수 작성해보겠다
// 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 ..."
}
*/
45-02
-
setTimeout 함수의 콜백 함수에서 상위 스코프의 변수에 값을 할당해보겠다
-
기대한 출력을 얻지 못하는 이유는 비동기이기 때문이다
let g = 0;
// 비동기 함수인 setTimeout 함수는 콜백 함수의 처리 결과를 외부로 반환하거나
// 상위 스코프의 변수에 할당하지 못한다.
setTimeout(() => { g = 100; }, 0);
console.log(g); // 0
45-03
-
기대한대로 콘솔 출력이 반환되지 않을것이다. 왜냐하면 onload 이벤트 핸들러가 비동기로 동작하기 때문이다
-
왜냐? get함수가 종료되고 onload 이벤트 핸들러가 실행될거기 때문. 그리고 get함수가 반환값이 없기 때문에 undefinde 반환
-
함수는 반환값을 명시적으로 호출한 다음에 캐치할 수 있으므로 onload를 get 함수가 호출할 수 있다면 가능하지만 get 함수가 호출 못하므로 그럴수도 없다. 따라서 onload 이벤트 핸들러의 반환값은 캐치불가
// 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
45-04
<!DOCTYPE html>
<html>
<body>
<input type="text">
<script>
document.querySelector('input').oninput = function () {
console.log(this.value);
// 이벤트 핸들러에서의 반환은 의미가 없다.
return this.value;
};
</script>
</body>
</html>
45-05
-
상위 스코프의 변수에 할당하면?
-
이또한 의미없다. 위에서 말했듯이 console.log가 종료한 이후에 이벤트 핸들러가 언제나 호출될거기 때문
-
이건 콜 스택이 비어야 이벤트 루프에 의해 테스트 큐의 콜백함수를 콜 스택으로 푸시된다고 앞에서 배운 그것 때문에 가능하지가 않다는것이다
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
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
-
콜백 헬 : 콜백 함수 호출이 중첩되어 복잡도가 높아지는 현상
-
userId를 받아와서 한번더 GET요청을 하는것임
// 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",...}
});
});
45-08
- 콜백 헬의 나쁜 예
get('/step1', a => {
get(`/step2/${a}`, b => {
get(`/step3/${b}`, c => {
get(`/step4/${c}`, d => {
console.log(d);
});
});
});
});
45.1.2 에러 처리의 한계(45-09)
- 가장 심각한 문제는 에러 처리가 곤란하다는 점
try {
setTimeout(() => { throw new Error('Error!'); }, 1000);
} catch (e) {
// 에러를 캐치하지 못한다
console.error('캐치한 에러', e);
}
45.2 프로미스의 생성(45-10)
- ES6에서 도입
// 프로미스 생성
const promise = new Promise((resolve, reject) => {
// Promise 함수의 콜백 함수 내부에서 비동기 처리를 수행한다.
if (/* 비동기 처리 성공 */) {
resolve('result');
} else { /* 비동기 처리 실패 */
reject('failure reason');
}
});
45-11
- 앞의 GET 요청을 프로미스로 다시 구현
// 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');
45-12
-
프로미스는 현재 비동기 처리의 상태 정보를 가짐 pending : 비동기 처리가 아직 수행되지 않은 상태 fulfilled : 비동기 처리가 수행된 상태(성공) rejected : 비동기 처리가 수행된 상태(실패)
-
즉, 프로미스는 비동기 처리 상태와 처리 결과를 관리하는 객체다
-
아래는 pending -> fulfilled 상태로 변화하고, 값은 1을 가진다
// fulfilled된 프로미스
const fulfilled = new Promise(resolve => resolve(1));
45-13
- 아래는 pending -> rejected 상태로 변화한다
// rejected된 프로미스
const rejected = new Promise((_, reject) => reject(new Error('error occurred')));
45.3 프로미스의 후속 처리 메서드
- then, catch, finally를 제공한다
45.3.1 Promise.prototype.then(45-14)
- 첫 인수 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
45.3.2 Promise.prototype.catch(45-15)
- 한개의 콜백 함수를 인수로 전달. rejected 상태인 경우만 콜백 함수 호출
// rejected
new Promise((_, reject) => reject(new Error('rejected')))
.catch(e => console.log(e)); // Error: rejected
45-16
- then(undefined, onRejected)와 동일하게 동작
// rejected
new Promise((_, reject) => reject(new Error('rejected')))
.then(undefined, e => console.log(e)); // Error: rejected
45.3.3 Promise.prototype.finally(45-17)
- 한개의 콜백 함수를 인수로 전달. fulfilled, rejected 상태와 상관없이 무조건 한번 호출
new Promise(() => {})
.finally(() => console.log('finally')); // finally
45-18
- 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!'));
45.4 프로미스의 에러 처리(45-19)
-
에러 처리도 문제없이 할 수 있다
-
에러처리는 catch에서 하는 것을 권장
const wrongUrl = 'https://jsonplaceholder.typicode.com/XXX/1';
// 부적절한 URL이 지정되었기 때문에 에러가 발생한다.
promiseGet(wrongUrl).then(
res => console.log(res),
err => console.error(err)
); // Error: 404
45-20
const wrongUrl = 'https://jsonplaceholder.typicode.com/XXX/1';
// 부적절한 URL이 지정되었기 때문에 에러가 발생한다.
promiseGet(wrongUrl)
.then(res => console.log(res))
.catch(err => console.error(err)); // Error: 404
45-21
const wrongUrl = 'https://jsonplaceholder.typicode.com/XXX/1';
// 부적절한 URL이 지정되었기 때문에 에러가 발생한다.
promiseGet(wrongUrl)
.then(res => console.log(res))
.then(undefined, err => console.error(err)); // Error: 404
45-22
promiseGet('https://jsonplaceholder.typicode.com/todos/1').then(
res => console.xxx(res),
err => console.error(err)
); // 두 번째 콜백 함수는 첫 번째 콜백 함수에서 발생한 에러를 캐치하지 못한다.
45-23
- 에러처리는 catch에서 하는 것을 권장
promiseGet('https://jsonplaceholder.typicode.com/todos/1')
.then(res => console.xxx(res))
.catch(err => console.error(err)); // TypeError: console.xxx is not a function
45.5 프로미스 체이닝(45-24)
- 프로미스 체이닝 : then, catch, finally 후속처리 메서드는 언제나 프로미스를 반환하므로 연속적으로 호출할 수 있는것
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));
45-25
- 그래도 콜백 패턴은 가독성이 좋지 않은데 ES8에서 도입된 async/await을 통해 해결!! 매우 추천한다. 마치 동기처럼 보인다
const url = 'https://jsonplaceholder.typicode.com';
(async () => {
// id가 1인 post의 userId를 취득
const { userId } = await promiseGet(`${url}/posts/1`);
// 취득한 post의 userId로 user 정보를 취득
const userInfo = await promiseGet(`${url}/users/${userId}`);
console.log(userInfo);
})();
45.6 프로미스의 정적 메서드
- 5가지 제공
45.6.1 Promise.resolve / Promise.reject(45-26)
// 배열을 resolve하는 프로미스를 생성
const resolvedPromise = Promise.resolve([1, 2, 3]);
resolvedPromise.then(console.log); // [1, 2, 3]
45-27
- 위 예제는 아래 예제와 동일
const resolvedPromise = new Promise(resolve => resolve([1, 2, 3]));
resolvedPromise.then(console.log); // [1, 2, 3]
45-28
// 에러 객체를 reject하는 프로미스를 생성
const rejectedPromise = Promise.reject(new Error('Error!'));
rejectedPromise.catch(console.log); // Error: Error!
45-29
- 위 예제는 아래 예제와 동일
const rejectedPromise = new Promise((_, reject) => reject(new Error('Error!')));
rejectedPromise.catch(console.log); // Error: Error!
45.6.2 Promise.all(45-30)
-
병렬 처리할 때 사용
-
아래 예제는 3개의 비동기 처리를 순차적으로 처리하므로 세 번째 비동기 처리는 6초 이상이 소요
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);
45-32
- 하나만 rejected 상태가 되면 나머지 프로미스 기다리지 않고 즉시 종료
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
45-33
- 인수가 프로미스 아닌경우 아래처럼 매핑
Promise.all([
1, // => Promise.resolve(1)
2, // => Promise.resolve(2)
3 // => Promise.resolve(3)
])
.then(console.log) // [1, 2, 3]
.catch(console.log);
45-34
- 깃허브 사용자 이름 취득 3개의 비동기 처리를 모두 병렬로 처리하는 예
// 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));
}
};
});
};
const githubIds = ['jeresig', 'ahejlsberg', 'ungmo2'];
Promise.all(githubIds.map(id => promiseGet(`https://api.github.com/users/${id}`)))
// [Promise, Promise, Promise] => Promise [userInfo, userInfo, userInfo]
.then(users => users.map(user => user.name))
// [userInfo, userInfo, userInfo] => Promise ['John Resig', 'Anders Hejlsberg', 'Ungmo Lee']
.then(console.log)
.catch(console.error);
45.6.3 Promise.race(45-35)
- all 메서드처럼 모든 프로미스가 fulfilled 상태가 되는걸 기다리는게 아니라 하나만 fulfilled 상태가 되면 그 프로미스 처리 결과를 resolve하는 새로운 프로미스를 반환한다
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);
45-36
- rejected는 all과 동일하게 처리
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
45.6.4 Promise.allSettled(45-37)
- ES11에 도입되었고, 프로미스가 모두 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}
]
*/
45-38
[
// 프로미스가 fulfilled 상태인 경우
{status: "fulfilled", value: 1},
// 프로미스가 rejected 상태인 경우
{status: "rejected", reason: Error: Error! at <anonymous>:3:60}
]
45.7 마이크로태스크 큐(45-39)
-
마이크로태스크 큐는 후속 처리 메서드(예:then)가 저장된다 태스크 큐는 비동기 함수의 콜백 함수나 이벤트 핸들러가 저장된다
-
예제는 2->3->1의 순으로 출력된다. 마이크로태스크 큐가 태스크 큐보다 우선순위가 높기 때문이다
setTimeout(() => console.log(1), 0);
Promise.resolve()
.then(() => console.log(2))
.then(() => console.log(3));
45.8 fetch(45-40)
-
fetch함수는 XMLHttpRequest 객체와 마찬가지로 HTTP 요청 전송 기능을 제공하는 클라이언트 사이드 Web API다
-
HTTP 응답을 나타내는 Response 객체를 래핑한 Promise 객체를 반환
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => console.log(response));
45-41
- json으로 볼 수도 있다
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
- 에러가 뜰 것 같지만 이것은 ok가 출력된다
const wrongUrl = 'https://jsonplaceholder.typicode.com/XXX/1';
// 부적절한 URL이 지정되었기 때문에 404 Not Found 에러가 발생한다.
fetch(wrongUrl)
.then(() => console.log('ok'))
.catch(() => console.log('error'));
45-43
- reject하지 않고 에러는 response.ok를 false로 설정
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));
45-44
-
참고로 axios는 모든 HTTP 에러를 reject하는 프로미스를 반환해서 catch에서 모든 에러 처리가능하고, fetch보다 다양한 기능을 지원
-
fetch 함수를 통해 HTTP 요청을 전송해보자
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' });
}
};
45-45
- 1.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}
45-46
- 2.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}
45-47
- 3.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}
45-48
-
4.DELETE 요청
-
자세한 내용은 MDN의 Using Fetch 페이지를 참고바람
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));
// {}
댓글남기기