강물둘기 2023. 1. 24. 16:16

* 아래 내용은 이웅모 저자님의  모던 자바스크립트 Deep Dive 책(위키북스)을 정리한 내용입니다.

   저작권에 문제가 된다면 삭제하도록 하겠습니다.

 

 

45.5 프로미스 체이닝(Promise chaining)

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));

만약 후속 처리 메서드의 콜백 함수가 프로미스가 아닌 값을 반환하더라도 그 값을 암묵적으로 프로미스를 생성해 반환한다.

callback hell은 발생하지 않지만, then hell이 발생해 가독성이 좋지는 않다. 이 문제는 ES8에 도입된 async / await을 통해 해결할 수 있다.

 

45.6 프로미스의 정적 메서드

Promise는 5가지 정적 메서드를 제공한다.

 

Promise.resolve / Promise.reject

이미 존재하는 값을 래핑(wrapping) 하여 프로미스를 생성하기 위해 사용한다.

// 배열을 resolve하는 프로미스를 생성
const resolvedPromise = Promise.resolve([1, 2, 3]);
// const resolvedPromise = new Promise(resolve => resolve([1, 2, 3]));

resolvedPromise.then(console.log); // [1, 2, 3]

 

// 에러 객체를 reject하는 프로미스를 생성
const rejectedPromise = Promise.reject(new Error('Error!'));
// const rejectedPromise = new Promise((_, reject) => reject(new Error('Error!')));

rejectedPromise.catch(console.log); // Error: Error!

 

Promise.all

여러 개의 비동기 처리를 모두 병렬(parallel)처리할 때 사용한다.

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);

위 코드는 비동기 처리를 순차적으로 하기 때문에 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));

Promise.all([requestData1(), requestData2(), requestData3()])
  .then(console.log) // [ 1, 2, 3 ] ⇒ 약 3초 소요
  .catch(console.error);

위 예제는 순차적으로 실행할 필요가 없기 때문에 Promise.all 메서드를 사용해서 동시에 처리하면 약 3초가 소요된다.

 

프로미스를 요소로 갖는 이터러블을 인수로 전달받기 때문에 배열(혹은 이터러블)로 넣어줘야 한다.

모든 프로미스가 fulfilled 상태가 되면 모든 처리 결과를 배열에 저장해 새로운 프로미스를 반환한다.

배열의 순서는 처리 시간과 상관없이 넣은 순서대로 유지된다.

 

만약 하나라도 rejected 상태가 되면 나머지 프로미스 상태변화를 기다리지 않고 즉시 종료한다.

Promise.all([
  new Promise(resolve => setTimeout(() => resolve(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

 

Promise.race

Promise.all 과 비슷하게 여러 프로미스가 담긴 이터러블을 인수로 받는다. 그 중에서 가장 먼저 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);

 

Promise.race 역시 하나라도 rejected 상태가 되면 에러를 reject하는 프로미스를 즉시 반환한다.

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

 

Promise.allSettled

ES11에서 추가된 Promise.allSettled 메서드는 프로미스를 요소로 갖는 이터러블을 인수로 받아 모든 프로미스가 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.7 마이크로 태스크 큐(Micro task queue)

setTimeout(() => console.log(1), 0);

Promise.resolve()
  .then(() => console.log(2))
  .then(() => console.log(3));

위 코드는 1 - 2 - 3 순서로 출력될 것 같지만, 2 - 3 - 1 의 순서로 출력된다.

그 이유는 프로미스의 후속 처리 메서드(then,catch,finally)의 콜백 함수는 태스크 큐가 아니라 마이크로 태스크 큐에 저장되기 때문이다.

 

일반적인 비동기함수의 콜백 함수나 이벤트 핸들러는 태스크 큐에 일시 저장되고, 

프로미스의 후속 처리 메서드(then,catch,finally)의 콜백 함수는 마이크로 태스크 큐에 일시 저장된다.

 

마이크로 태스크 큐는 태스크 큐보다 우선순위가 높기 때문에

마이크로 태스크 큐에 저장되어 있는 프로미스의 후속 처리 메서드의 콜백 함수가 먼저 실행이 되고,

그 이후에 태스크 큐에 저장되어 있던 비동기 함수의 콜백 함수가 실행이 된다.

 

 

45.8 Fetch

Fetch 함수는 XMLHttprequest 객체와 마찬가지로 HTTP 요청 전송 기능을 제공하는 클라이언트 사이드 Web API이다.

Fetch 함수는 프로미스를 지원하기 때문에 콜백 패턴의 단점에서 자유롭다.

Fetch 함수는 HTTP 응답을 나타내는 response 객체를 결과값으로 가지는 Promise 객체를 반환한다.

const promise = fetch(url [,options])

Response 객체

 

Response.prototype.json 메서드를 사용하면 Response 객체에서 HTTP 응답 몸체(response.body)를 취득하여 역직렬화 한다.

fetch('https://jsonplaceholder.typicode.com/todos/1')
  // response는 HTTP 응답을 나타내는 Response 객체이다.
  .then(response => response.json())
  // json은 역직렬화된 HTTP 응답 몸체이다.
  .then(json => console.log(json));
  // {userId: 1, id: 1, title: "delectus aut autem", completed: false}

 

fetch 함수를 사용할 때 에러 처리에 주의해야 한다.

const wrongUrl = 'https://jsonplaceholder.typicode.com/XXX/1';

// 부적절한 URL이 지정되었기 때문에 404 Not Found 에러가 발생한다.
fetch(wrongUrl)
  .then(() => console.log('ok'))
  .catch(() => console.log('error'));

위 코드처럼 에러 처리를 하면 ok가 출력된다.

 

fetch 함수는 네트워크 장애나 CORS 에러에 의해 요청이 완료되지 못한 경우에만 프로미스를 reject 한다.

* CORS(Cross-origin resource sharing) 에러 : 교차 출처 리소스 공유 정책 위반

https://evan-moon.github.io/2020/05/21/about-cors/

 

CORS는 왜 이렇게 우리를 힘들게 하는걸까?

이번 포스팅에서는 웹 개발자라면 한번쯤은 얻어맞아 봤을 법한 정책에 대한 이야기를 해보려고 한다. 사실 웹 개발을 하다보면 CORS 정책 위반으로 인해 에러가 발생하는 상황은 굉장히 흔해서

evan-moon.github.io

 

그 이외에 404 Not found나 500 Internal Server Error와 같은 HTTP 에러가 발생해도 reject하지 않고 불리언 타입의 ok 상태를 false로 설정한 Response 객체를 resolve 한다.

 

따라서 fetch 함수를 사용할 때는 ok상태를 확인해 명시적으로 에러를 처리할 필요가 있다.

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하는 프로미스를 반환한다.

 

 

 

Reference

- 이웅모 ,  모던 자바스크립트 Deep Dive , 위키북스 , 2020 

- https://evan-moon.github.io/2020/05/21/about-cors/