Languages/JavaScript

[HUFS/GnuVil] #24 프로미스

성중 2022. 11. 29. 15:09

전통적인 콜백 패턴은 콜백 헬로 인해 가독성이 나쁘고 비동기 처리 중 에러 처리가 곤란하며 여러 비동기 처리를 한 번에 하는 것에도 한계가 있다. ES6에서는 새로운 패턴으로 프로미스(Promise)를 도입해 콜백 패턴의 단점을 보완하고 비동기 처리 시점을 명확하게 표현한다

 

// 프로미스 생성
const promise = new Promise((resolve, reject) => {
  // Promise 함수의 콜백 함수 내부에서 비동기 처리를 수행한다.
  if (/* 비동기 처리 성공 */) {
    resolve('result');
  } else { /* 비동기 처리 실패 */
    reject('failure reason');
  }
});

Promise 생성자 함수가 인수로 전달받은 콜백 함수 내부에서 비동기 처리를 수행한다

 

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

API 통신에서의 사용 예시는 위와 같다

 

비동기 처리 진행에 따라 프로미스가 가지는 상태(state) 정보
프로미스의 상태는 resolve 또는 reject 함수를 호출하는 것으로 결정

프로미스의 비동기 처리 상태가 변화의 후속 처리 메서드then / catch / finally가 제공된다

* 태스크 큐보다 우선순위가 높은 마이크로 태스크 큐에서 동작

 

// 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

then을 사용해 프로미스의 fulfilled/rejected 상태에 호출할 1/2번째 콜백 함수를 지정할 수 있다

 

// rejected
new Promise((_, reject) => reject(new Error('rejected')))
  .catch(e => console.log(e)); // Error: rejected

catch의 콜백 함수는 프로미스의 rejected 상태에만 호출된다

 

new Promise(() => {})
  .finally(() => console.log('finally')); // finally

finally의 콜백 함수는 프로미스의 fulfilled/rejected 상태와 관계없이 무조건 한 번 호출된다

 

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

후속 처리 메서드는 언제나 프로미스를 반환하므로 연속적으로 호출하는 프로미스 체이닝이 가능하다

 

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

// 에러 객체를 reject하는 프로미스를 생성
const rejectedPromise = Promise.reject(new Error('Error!'));
rejectedPromise.catch(console.log); // Error: Error!

resolve/reject 메서드는 임의로 이미 존재하는 값을 래핑하여 프로미스로 활용할 때 사용한다

 

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

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

all 메서드는 여러 개의 비동기 처리를 모두 병렬 처리할 때 사용한다

 

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

비슷한 원리로 race 메서드는 여러 개의 비동기 처리를 병렬 처리할 때 가장 먼저 fulfilled 상태가 되는 프로미스의 처리 결과를 resolve하는 새로운 프로미스를 반환한다

* 하나라도 먼저 rejected 상태가 되면 에러를 reject하는 새로운 프로미스를 즉시 반환

 

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}
]
*/

allSettled 메서드는 all 메서드에서 더 나아가 모든 병렬 처리 결과를 배열로 반환한다

 

fetch 함수HTTP 요청 전송 기능을 제공하는 클라이언트 사이드 Web API로, URL과 HTTP 요청 메서드, 헤더, 페이로드 등을 전달받아 HTTP 응답을 나타내는 Response 객체를 래핑한 Promise 객체를 반환한다

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}

후속 처리 메서드 then을 통해 프로미스가 resolve한 Response 객체를 전달받을 수 있다

 

Response 객체

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

Response 객체의 프로퍼티와 catch 메서드로 에러 처리도 가능하다

 

 내용은 위키북스의 '모던 자바스크립트 Deep Dive' 바탕으로 작성되었습니다.