4 minute read

프로미스(2)

프로미스의 에러 처리

앞선 포스팅에서 살펴보았듯이 비동기 처리를 위한 콜백 패턴은 에러 처리가 곤란하다는 문제가 있다. 프로미스는 catch메서드를 사용해 에러를 문제없이 처리할 수 있다.

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

// 부적절한 URL이 지정되었기 때문에 에러가 발생한다.
promiseGet(wrongUrl)
  .then(res => console.log(res))
  .catch(err => console.error(err)); // Error: 404

then 메서드의 두 번째 콜백 함수로 처리할 수도 있다.

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

// 부적절한 URL이 지정되었기 때문에 에러가 발생한다.
promiseGet(wrongUrl).then(
  res => console.log(res),
  err => console.error(err)
); // Error: 404

하지만 then메서드의 두 번째 콜백 함수는 첫 번째 콜백 함수에서 발생한 에러를 캐치하지 못하고 코드가 복잡해서 가독성이 좋지 않다.

promiseGet('https://jsonplaceholder.typicode.com/todos/1').then(
  res => console.xxx(res),
  err => console.error(err)
); // 두 번째 콜백 함수는 첫 번째 콜백 함수에서 발생한 에러를 캐치하지 못한다.

따라서 catch 메서드를 모둔 then 메서드 호출이 끝난 이후에 호출하면 비동기 처리에서 발생한 에러뿐만 아니라 then 메서드 내부에서 발생한 에러까지 모두 캐치할 수 있다.

promiseGet('https://jsonplaceholder.typicode.com/todos/1')
  .then(res => console.xxx(res))
  .catch(err => console.error(err)); // TypeError: console.xxx is not a function

이렇게 작성하는 것이 가독성이 좋고 명확하다. 에러처리는 then 메서드에서 하지 말고 catch메서드에서 하도록 하자.


프로미스 체이닝

지난 포스트에서 구현했던 콜백 지옥 코드를 프로미스를 사용해 다시 구현해보자.

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

위 예제에서 them → then → catch 순으로 후속 처리 메서드를 호출했다. 이처럼 후속처리 메서드 then, catch, finally 후속 처리 메서드는 언제나 프로미스를 반환하므로 연속적으로 호출할 수 있다. 이를 프로미스 체이닝이라한다.

후속 처리 메서드 콜백 함수의 인수 후속 처리 메서드의 반환 값
then promiseGet 함수가 반환한 프로미스가 resolve한 값(id가 1인 post) 콜백 함수가 반환한 프로미스
then 첫 번째 then메서드가 반환한 프로미스가 resolve한 값(post의 userId로 취득한 user 정보) 콜백 함수가 반환한 값(undefined)을 resolve한 프로미스
catch promiseGet 함수 또는 앞선 후속 처리 메서드가 반환한 프로미스가 reject한 값 콜백 함수가 반환한 값(undefined)을 resolve한 프로미스

프로미스는 프로미스 체이닝을 통해 비동기 처리 결과를 전달받아 후속 처리를 하므로 비동기 처리를 위한 콜백 패턴에서 발생하던 콜백 지옥이 발생하지 않는다. 다만 프로미스도 콜백 패턴을 사용하므로 콜백 함수를 사용하지 않는 것은 아니다.

콜백 패턴은 가독성이 좋지 않다. 이 문제는 async/await를 통해 해결할 수 있다. 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);
})();

자세한 async/await 내용은 다음에 다시 알아보자.

Promise.all

Promise는 주로 생성자 함수로 사용되지만 함수도 객체이므로 메서드를 가질 수 있다. 그 중 Promise.all 메서드는 여러 개의 비동기 처리를 모두 병렬 처리할 때 사용한다.

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

위 예제는 세 개의 비동기 처리를 순차적으로 처리한다. 즉 앞선 비동기 처리가 완료하면 다음 비동기 처리를 수행한다. 따라서 첫 번째 비동기 처리에 3초, 두 번째 비동기 처리에 2초, 세 번째 비동기 처리에 1초가 소요돼 총 6초 이상이 소요된다. 그런데 위 세 개의 비동기 처리는 서로 의존하지 않고 개별적으로 수행된다. 즉, 앞선 비동기 처리 결과를 다음 비동기 처리가 사용하지 않는다. 따라서 위 예제의 경우 세 개의 비동기 처리를 순차적으로 처리할 필요가 없다.


Promise.all 메서드는 여러 개의 비동기 처리를 모두 병렬처리 할 때 사용한다. 이를 이용해 위 예제를 병렬로 처리해보자.

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 메서드는 프로미스를 요소로 갖는 배열 등의 이터러블을 인수로 전달받는다. 그리고 전달받은 모든 프로미스가 모두 fulfilled상태가 되면 모든 처리 결과를 배열에 저장해 새로운 프로미스를 반환한다.

위 예제의 경우 Promise.all 메서드는 3개의 프로미스를 요소로 갖는 배열을 전달받았다. 각 프로미스는 다음과 같이 동작한다.

  • 첫 번째 프로미스는 3초 후에 1을 resolve한다.
  • 두 번째 프로미스는 2초 후에 2을 resolve한다.
  • 세 번째 프로미스는 1초 후에 3을 resolve한다.

모든 프로미스가 fullfiled 상태가 되면 resolve된 처리 결과를 모두 배열에 저장해 새로운 프로미스를 반환한다. 이때 첫 번재 프로미스가 가장 나중에 fulfilled 상태가 돼도 Promise.all 메서드는 첫 번째 프로미스가 resolve한 처리 결과부터 차례대로 배열에 저장해 그 배열을 resolve하는 새로운 프로미스를 반환한다. 즉 처리 순서가 보장된다.

Promise.all 메서드는 인수로 전달받은 배열의 프로미스가 하나라도 rejected 상태가 되면 나머지 프로미스가 fullfilled 상태가 되는 것을 기다리지 않고 즉시 종료한다.

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