JS 웹 개발(4)

깨진알·2023년 12월 19일

JavaScript

목록 보기
20/21

비동기 실행과 Promise 객체(2)

1. catch 메소드

fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.text())
  .catch((error) => { console.log(error); })
  // .then(undefined, (error) => { console.log(error); })
  .then((result) => { console.log(result); });

Promise 객체가 rejected 상태가 되면 실행할 콜백을 등록하는 메소드이다. catch 메소드는 사실 then 메소드의 첫 번째 인자로 undefined를 넣은 것과 같다. undefined가 어떻게 동작하는지 설명하도록 하겠다.

일단 fetch 함수의 작업이 실패해서 Promise-A 객체가 rejected 상태가 되면, 첫 번째 then 메소드의 두 번째 콜백이 실행되어야 한다. 하지만 지금 첫 번째 then 메소드에는 두 번째 콜백이 없기 때문에 아무 콜백도 실행되지 않는다. 이때 Promise-B 객체가 Promise-A와 똑같은 rejected 상태가 되고, 동일한 작업 실패 정보를 갖게 된다.

이제 rejected 상태가 된 Promise-B에 붙은 then 메소드에는 두 번째 콜백이 있기 때문에 이 두 번째 콜백이 실행된다. 즉, catch 메소드의 콜백이 실행되는 것이다.

// Internet Disconnected

fetch('https://jsonplaceholder.typicode.com/users') // Promise-A
  .then((response) => response.text()) // Promise-B
  .then(undefined, (error) => { console.log(error); }) // Promise-C
  .then((result) => { console.log(`Quiz: ${result}`); }); // Promise-D 

이 코드를 실행하면 최종적으로 무엇이 출력될 것 같은가?

undefined가 출력된다. catch 메소드 안의 콜백이 실행되었을 때 아무 값도 리턴하지 않았다. 자바스크립트에서는 아무 값도 리턴하지 않은 경우에는 undefined를 리턴한 것으로 간주한다. catch 메소드가 리턴한 Promise 객체는 fulfilled 상태가 되면서, undefined를 작업 성공 결과로 가지게 되는 것이다.


2. catch 메소드는 마지막에 쓴다.

fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.text())
  .then((result) => {
    console.log(result);
    throw new Error('test');
  })
  .catch((error) => { console.log(error); });

catch 메소드를 Promise Chain 가장 마지막에 붙임으로써, 중간에 에러가 발생해서 어느 Promise 객체가 rejected 상태가 되더라도 항상 대처가 가능하다.


2-1. catch 메소드를 여러 개 쓰는 경우

catch 메소드를 마지막뿐만 아니라 Promise Chain 중간중간에 쓰는 경우도 존재한다. 만약 중간에 에러가 발생해도 catch 메소드가 그 대안을 뒤로 넘겨줄 수 있으면 catch 메소드를 중간에 써도 된다.

fetch('https://friendbook.com/my/newsfeeds')
  .then((response) => response.json()) // -- A
  .then((result) => { // -- B
    const feeds = result;
    // 피드 데이터 가공...
    return processedFeeds; 
  })
  .catch((error) => { // -- C
    // 미리 저장해둔 일반 뉴스를 보여주기  
    const storedGeneralNews = getStoredGeneralNews();
    return storedGeneralNews;
  })
  .then((result) => { /* 화면에 표시 */ }) // -- D
  .catch((error) => { /* 에러 로깅 */ }); // -- E

어떤 SNS 웹 사이트에서 나에게 최적화된 뉴스피드(newsfeed)를 보여주는 코드라고 가정해 보자. 만약 Server로부터 뉴스피드가 잘 조회되면 현재 코드에서 A, B, D줄에 있는 콜백들이 잘 실행되고, 사용자에게 뉴스피드가 잘 표시된다. 하지만 만약 fetch 작업이 실패하게 되면 어떻게 될까?

fetch 함수의 작업일 실패하면 C줄에 콜백이 실행된다. 사용자가 매번 뉴스피드를 볼 때마다, 나중에 오프라인 상태가 될 때를 대비해서 모든 사람이 공통적으로 볼 수 있는, 텍스트로만 이루어진 최근 일반 뉴스 데이터를 갱신해서 웹 브라우저에 저장한다고 가정해 보자. C줄의 콜백은 바로 이렇게 저장해둔 일반 뉴스 데이터를 그대로 가져오는 기능을 한다. 이렇게 되면 인터넷이 안 되는 상황에서도 나만을 위한 최적화된 뉴스피드는 못 보지만 일반적인 세상 뉴스는 사용자가 볼 수 있게 된다.

이렇게 비록 에러가 발생했다고 해도 만약 실패한 작업 대신 다른 방법을 통해서 작업을 정상적으로 끝마칠 수 있는 상황이라면 catch 메소드를 중간에 사용하기도 한다. (코드에서 getStoredGeneralNews가 해당된다.)


3. finally 메소드

fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.text())
  .then((result) => { console.log(result); })
  .catch((error) => { console.log(error); })
  .finally(() => { console.log('exit'); });

Promise 객체가 어떤 상태(fulfilled 또는 rejected)가 되던 '항상' 실행하고 싶은 콜백을 등록하고 싶을 때 사용되는 메소드이다.


4. Promise 객체는 왜 등장했을까?

fetch('https://first.com', (response) => {
  // Do Something
  fetch('https://second.com', (response) => {
    // Do Something
    fetch('https;//third.com', (response) => {
      // Do Something
      fetch('https;//fourth.com', (response) => {
        // Do Something
      });
    });
  });
});

위 코드를 보면 fetch 함수 안의 콜백에 fetch 함수가 있고 그 함수의 콜백 안에 fetch 함수가 계속 반복된다. 이런 코드는 가독성이 떨어지는 문제가 있다. 이런 현상을 콜백 지옥 또는 콜백 헬(callback hell)이라고 한다. 또는 지옥의 피라미드(Pyramid of Doom)라고도 한다.

fetch('https://first.com')
  .then((response) => {
    // Do Something 
    return fetch('https://second.com');
  })
  .then((response) => {
    // Do Something 
    return fetch('https://third.com');
  })
  .then((response) => { 
    // Do Something 
    return fetch('https://third.com');
  });

하지만 fetch 함수는 Promise 객체를 리턴하기 때문에 이런 식으로 Promise Chaining을 해서 좀 더 깔끔한 코드로 여러 비동기 작업을 순차적으로 처리할 수 있다. 이뿐만 아니라 기존에 콜백을 직접 넣는 방식에 비해 Promise 객체의 문법은 비동기 작업에 관한 좀 더 세밀한 개념들이 반영되어 있다. 이전의 방식에서는 콜백에 필요한 인자를 넣어주고 실행하면 되는 단순한 방식이었다면, Promise 객체 문법에는 pending, fulfilled, rejected 상태, 작업 성공 결과 및 작업 실패 정보(이유), then, catch, finally 메소드 등과 같은 비동기 작업에 관한 보다 정교한 설계가 문법 자체에 반영되어 있다는 것을 알 수 있다.

즉 Promisl 객체라는 개념은 callback hell 문제를 해결하고, 비동기 작업 처리에 관한 좀 더 세밀한 처리를 자바스크립트 문법 단에서 해결하기 위해 등장했고, ES6(=ES2015)에 추가되었다.


5. 직접 만들어보는 Promise 객체

// Promise('executor 함수')
// resolve : Promise 객체를 fulfilled로 만들수 있는 함수와 연결
// reject : Promise 객체를 rejected로 만들수 있는 함수와 연결
const p = new Promise((resolve, reject) => {
  setTimeout(() => { resolve('success'); }, 2000); // 2초 후 fulfilled 상태가 된다.
  setTimeout(() => { reject(new Error('fail')); }, 2000); // 2초 후 rejected 상태가 된다.
});

p.then((result) => { console.log(result); }); // fulfilled
p.catch((error) => { console.log(error); }); // rejected

6. Promisify

보통 전통적인 형식의 비동기 실행 함수를 사용하는 코드를, Promise 기반의 코드로 변환하기 위해 Promise 객체를 직접 만드는 경우가 많다.

(1) setTimeout 예시

function wait(text, milliseconds) {
  setTimeout(() => text, milliseconds);
}

wait 함수는 특정 밀리세컨즈만큼 시간이 지난 후에 text 파라미터로 전달받은 값을 리턴하는 함수이다.

function wait(text, milliseconds) {
  setTimeout(() => text, milliseconds);
}

fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.text())
  .then((result) => wait(`${result} by Codeit`, 2000)) // 2초 후에 리스폰스의 내용 뒤에 'by Codeit' 추가하고 리턴
  .then((result) => { console.log(result); });

기존 코드에 두 번째 then 메소드를 추가하고, 그 안에서 wait 함수를 호출했다.

하지만 response의 내용과 by Codeit이 출력되지 않고 undefined가 출력된다. 이유는 wait 함수에 있다.

wait 함수는 내부에서 setTimeout 함수를 호출한다. 그리고 setTimeout 함수의 첫 번째 파라미터로 들어간 콜백이 2초 후에 text를 리턴한다. 이 때 헷갈리면 안 되는 것은 wait 함수가 두 번째 then 메소드 안의 콜백에서 실행될 때, wait 함수는 setTimeout 함수를 실행할 뿐 아무것도 리턴하지 않는다는 사실이다. setTimeout 함수 안의 콜백이 2초 후에 리턴하는 text는 wait 함수의 리턴값이 아니다.

따라서 세 번째 then 메소드의 콜백으로 undefined가 넘어가고, 그래서 undefined가 출력되게 된다. setTimeout은 비동기 실행되는 함수이다. Promise Chaining 안에서 이렇게 비동기 실행되는 함수를 바로 사용하면, 나중에 실행되는 부분의 리턴값(text)를 Promise Chaining에서 사용할 수 없게 된다.

function wait(text, milliseconds) {
  const p = new Promise((resolve, reject) => {
    setTimeout(() => { resolve(text); }, 2000);
  });
  return p;
}

fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.text())
  .then((result) => wait(`${result} by Codeit`, 2000)) // 2초 후에 리스폰스의 내용 뒤에 'by Codeit' 추가하고 리턴
  .then((result) => { console.log(result); });

wait 함수 안에서 Promise 객체를 직접 생성했고, executor 함수 안에서 setTimeout 함수를 호출했다. 그리고 setTimeout 함수 안의 콜백에서 resolve 함수를 호출하는데 이 때 아규먼트로 text를 넣었다. 그렇다면 Promise 객체 p는 2초 후에 fulfilled 상태가 되고, 작업 성공 결과는 파라미터 text 값이 된다. wait 함수가 Promise 객체 p를 리턴하게 된다.

(2) 콜백 헬(callback hell)과 Promise

자바스크립트가 실행되는 환경에는 웹 브라우저뿐만 아니라 Node.js라고 하는 것도 있다. 이 Node.js는 자바스크립트를 Server에서도 실행할 수 있게 해주는 또 다른 '자바스크립트 실행 환경'이다. 이 Node.js에서는 브라우저에서와는 또 다른 비동기 함수들이 제공된다.

fs.readFile('file1.txt', 'utf8', (error, data) => {
  if (err) {
    console.log(err);
  } else {
    console.log(data);
  }
});

Node.js에는 위와 같이 특정 파일의 내용을 읽기 위해 사용되는 readFile이라는 비동기 실행 메소드가 있다. fs는 readFile 메소드를 가진 객체로, 파일에 관한 기능들을 갖고 있다. readFile 메소드는 첫 번째 파라미터로 파일의 이름, 두 번째 파라미터로 파일 해석 기준(인코딩 기준), 세 번째 파라미터로 콜백을 받는다. readFile 함수는 파일을 읽다가 에러가 발생하면 콜백의 첫 번째 파라미터(error)에, 해당 에러 객체를 전달하고 콜백을 실행한다. 만약 파일을 정상적으로 다 읽었으면 콜백의 두 번째 파라미터(data)에, 읽어들인 파일 내용을 전달하고 콜백을 실행한다.

readFile 메소드도 콜백을 파라미터에 바로 넣는 비동기 실행 함수라는 점에서 setTimeout 함수, addEventListener 메소드와 비슷하다. 하지만 콜벡 헬(callback hell)이라는 문제가 있다. file1.txt 파일의 내용을 출력하고 file2.txt, file3.txt라는 파일의 내용을 또 출력해야 한다고 가정해보자.

fs.readFile('file1.txt', 'utf8', (error1, data1) => {
  if (error1) {
    console.log(error1);
  } else {
    console.log(data1);
    fs.readFile('file2.txt', 'utf8', (error2, data2) => {
      if (error2) {
        console.log(error2);
      } else {
        console.log(data2);
        fs.readFile('file3.txt', 'utf8', (error3, data3) => {
          if (error3) {
            console.log(error3);
          } else {
            console.log(data3);
          }
        });
      }
    });
  }
});

코드의 가독성이 떨어지게 된다. 콜백을 바로 파라미터에 집어넣는 전통적인 형식의 비동기 실행 함수들인 이런 문제를 갖고 있다. 순차적으로 비동기 실행 함수들을 실행하려고 하면 콜백 안에 또 콜백이 있고, 그 안에 또 콜백이 있는 콜백 헬(콜백 지옥, callback hell) 현상을 초래하게 된다. 이런 문제를 해결하는 방법이 Promisify이다.

function readFile_promisified(filename) {
  const p = new Promise((resolve, reject) => {
    fs.readFile(filename, 'utf8', (error, data) => {
      if (error) {
        reject(error); // 에러 발생 시 -> rejected
      } else {
        resolve(data); // 파일 내용 읽기 완료 -> fulfilled
      }
    });
  });
  return p;
}

작업을 수행하다가 에러가 나면 readFile 함수의 콜백에서 reject 함수를 호출하고, 파일의 내용을 정상적으로 다 읽었을 때는 resolve 함수를 호출한다는 사실이다. 그리고 reject 함수의 파라미터에는 error 객체를, resolve 함수의 파라미터에는 파일의 내용인 data를 전달했다. 각각 Promise 객체의 작업 실패 정보 또는 작업 성공 결과가 된다.

readFile_promisified('file1.txt')
  .then((data) => { console.log(data); return readFile_promisified('file2.txt'); })
  .then((data) => { console.log(data); return readFile_promisified('file3.txt'); })
  .then((data) => { console.log(data); })
  .catch((error) => { console.log(error); });

readFile 메소드를 Promisify해서 만들면 다음과 같이 깔끔하게 작성할 수 있다.

(3) Promisify를 하면 안 되는 함수들도 있다.

콜백을 여러 번 실행하는 함수들(setInterval, addEventListener 등)인 경우에는 Promisify를 하면 안 된다. 왜냐하면 Promise 객체는 한번 pending 상태에서 fulfilled 또는 rejected 상태가 되고나면 그 뒤로는 그 상태와 결과가 바뀌지 않기 때문이다.

const box = document.getElementById('test');
let count = 0;

function addEventListener_promisified(obj, eventName) { // 이런 Promisify는 하지 마세요
  const p = new Promise((resolve, reject) => {
    obj.addEventListener(eventName, () => { // addEventListener 메소드
      count += 1;
      resolve(count);
    });
  });
  return p;
}

addEventListener_promisified(box, 'click').then((eventCount) => { console.log(eventCount); });

마지막 addEventListener을 보면 함수의 아규먼트로 DOM 객체 box와 문자열 'click'을 넣어서 box 객체가 클릭 이벤드에 반응하도록 했다. 하지만 이 코드를 실행하고 box를 클릭해보면 처음에 1이 출력되고 나서 다음 count 값들은 출력되지 않는다. 왜냐하면 pending 상태에 있던 Promise 객체(p 객체)가 한번 fulfilled 상태 또는 rejected 상태가 되고 나면 Promise 객체의 상태 및 결과가 고정되어 그 뒤로는 바뀌지 않기 때문이다.

따라서 resolve(count)라고 하는 코드가 box 버튼을 클릭할 때마다 여러 번 실행된다고 해도 p 객체가 갖고 있는 상태와 결과는 변하지 않는다. 그래서 then 메소드 안의 콜백도 처음 클릭했을 때 딱 한번 실행되고 끝나는 것이다.


7. 이미 상태가 결정된 Promise 객체

(1) 이미 상태가 결정된 Promise 객체 만들기

1. fulfilled 상태의 Promise 객체 만들기

const p = Promise.resolve('success');

Promise의 resolve라는 메소드를 사용하면 바로 fulfilled 상태의 Promise 객체를 만들 수 있다. 위와 같이 쓰면 fulfilled 상태이면서, 작업 성공 결과로 문자열 'success'를 가진 Promise 객체를 만들 수 있다.

2. rejected 상태의 Promise 객체 만들기

const p = Promise.reject(new Error('fail'));

Promise의 reject라는 메소드를 사용하면 바로 rejected 상태의 Promise 객체를 만들 수 있다. 위와 같이 쓰면 rejected 상태이면서, 작업 실패 정보로, fail이라는 메시지를 가진 Error 객체를 가진 Promise 객체를 만들 수 있다.

const p = new Promise((resolve, reject) => {

});

Promise 객체를 직접 생성하는 방법으로 new 생성자와 executor 함수를 사용하는 것 말고도 resolve 메소드나, reject 메소드를 사용하는 방법도 기억해야 한다.

// 1
const p = Promise.resolve('success');
p.then((result) => { console.log(result); }, (error) => { console.log(error); });

// 2
const p = Promise.reject(new Error('fail'));
p.then((result) => { console.log(result); }, (error) => { console.log(error); });

1번에서는 첫 번째 콜백이 실행되어서 작업 성공 결과인 문자열 success가 출력되고 2번에서는 두 번째 콜백이 실행되어서 작업 실패 정보인 Error 객체의 내용이 출력된다.

어떤 비동기 작업을 처리할 필요가 있다면, new 생성자와 executor 함수를 사용해서 Promise 객체를 만들어야 하지만, 그렇지 않고 바로 상태가 이미 결정된 Promise 객체를 만들고 싶을 때는 이 resolve 또는 reject 메소드를 사용해야 한다.

// 1
function doSomething(a, b) {
    //~~
  if (problem) {
    throw new Error('Failed due to..'));
  } else {
    return fetch('https://~');
  }
}

// 2
function doSomething(a, b) {
  // ~~
  if (problem) {
    return Promise.reject(new Error('Failed due to..'));
  } else {
    return fetch('https://~');
  }
}

1번처럼 문제(problem이 falsy인 경우)가 없는 경우에만 fetch 함수를 호출해서 Promise 객체를 리턴하는 함수가 있을 때, 문제가 발생하는 경우에는 바로 Error 객체를 throw해 버리고 있다. 2번처럼 문제가 존재하는 경우에도 에러를 바로 throw하는 게 아니라, 생성한 에러를 Promise 객체의 작업 실패 정보로 설정해서, 그 Promise 객체를 리턴하는 것으로 바뀌었다.

(2) Promise 객체의 작업 성공 결과 또는 작업 실패 정보

간혹 Promise 객체를 공부할 때, Promise 객체가 pending 상태일 때 미리 then 메소드가 붙어있어야만 나중에 이 Promise 객체가 fulfilled 상태 또는 rejected 상태가 되었을 때 그 결과(작업 성공 결과 또는 작업 실패 정보)를 콜백의 파라미터로 받을 수 있고, 이미 fulfilled 상태 또는 rejected 상태가 된 Promise 객체의 경우에는 then 메소드를 붙여도 그 결과를 콜백에서 받지 못한다고 오해할 수 있다.

하지만 방금 resolve, reject 메소드에서도 봤듯이 이미 fulfilled 또는 rejected 상태가 결정된 Promise 객체라도 then 메소드를 붙이면, 콜백에서 해당 작업 성공 결과 또는 작업 실패 정보를 받아올 수 있다. 시점과는 전혀 상관이 없는 개념인 것이다. 어느 시점이든, 몇 번이든 then 메소드를 붙여서 해당 결과를 가져올 수 있다.

const p = new Promise((resolve, reject) => {
  setTimeout(() => { resolve('success'); }, 2000); // 2초 후에 fulfilled 상태가 됨
});

p.then((result) => { console.log(result); }); // Promise 객체가 pending 상태일 때 콜백 등록
setTimeout(() => { p.then((result) => { console.log(result); }); }, 5000); // Promise 객체가 fulfilled 상태가 되고 나서 콜백 등록 

Promise가 pending 상태일 때 등록한 콜백이든, fulfilled 상태가 된 후에 등록한 콜백이든 잘 실행되는 것을 알 수 있다. 이렇게 어느 시점이든, 몇 번의 then 메소드를 붙이든 상관없이, pending 상태만 아니라면 항상 then 메소드로 Promise 객체의 결과를 추출할 수 있다.

Promise 객체는 항상 결과를 줄 수 있는 공급자(Provider)이고 그것의 then 메소드는 그 결과를 소비하는 콜백인 소비자(Consumer)를 설정하는 메소드라는 사실을 잘 기억하면 좋다.


8. 여러 Promise 객체를 다루는 방법

(1) all 메소드

// 1번 직원 정보
const p1 = fetch('https://learn.codeit.kr/api/members/1').then((res) => res.json());
// 2번 직원 정보
const p2 = fetch('https://learn.codeit.kr/api/members/2').then((res) => res.json());
// 3번 직원 정보
const p3 = fetch('https://learn.codeit.kr/api/members/3').then((res) => res.json());

Promise
  .all([p1, p2, p3])
  .then((results) => {
    console.log(results); // Array : [1번 직원 정보, 2번 직원 정보, 3번 직원 정보]
  });

all 메소드도 then 메소드처럼 새로운 Promise 객체를 리턴하지만, all 메소드는 아규먼트로 들어온 배열 안에 있는 모든 Promise 객체가 pending 상태에서 fulfilled 상태가 될 때까지 기다린다. 그리고 모든 Promise 객체들이 fulfilled 상태가 되면, all 메소드가 리턴했던 Promise 객체는 fulfilled 상태가 되고, 각 Promise 객체의 작업 성공 결과들로 이루어진 배열을, 그 작업 성공 결과로 갖게 된다.

all 메소드가 리턴한 Promise 객체는, 각 개별 Promise 객체의 작업 성공 결과로 이루어진 배열을 자신의 작업 성공 결과로 갖는다는 것을 알 수 있다. 이렇게 all 메소드는 여러 Promise 객체의 작업 성공 결과를 기다렸다가 모두 한 번에 취합하기 위해서 사용한다.

// 1번 직원 정보
const p1 = fetch('https://learn.codeit.kr/api/members/1').then((res) => res.json());
// 2번 직원 정보
const p2 = fetch('https://learn.codeit.kr/api/members/2').then((res) => res.json());
// 3번 직원 정보
const p3 = fetch('https://learnnnnnn.codeit.kr/api/members/3').then((res) => res.json());

Promise
  .all([p1, p2, p3])
  .then((results) => {
    console.log(results); // Array : [1번 직원 정보, 2번 직원 정보, 3번 직원 정보]
  });

p3가 rejected 상태가 되면, all 메소드가 리턴한 Promise 객체는 p3 객체처럼 rejected 상태가 되고 동일한 작업 실패 정보를 갖게 된다. 이렇게 all 메소드는 하나의 Promise 객체라도 rejected 상태가 되면, 전체 작업이 실패한 것으로 간주해야 할때 사용한다.

// 1번 직원 정보
const p1 = fetch('https://learn.codeit.kr/api/members/1').then((res) => res.json());
// 2번 직원 정보
const p2 = fetch('https://learn.codeit.kr/api/members/2').then((res) => res.json());
// 3번 직원 정보
const p3 = fetch('https://learnnnnnn.codeit.kr/api/members/3').then((res) => res.json());

Promise
  .all([p1, p2, p3])
  .then((results) => {
    console.log(results); // Array : [1번 직원 정보, 2번 직원 정보, 3번 직원 정보]
  })
  .catch((error) => {
    console.log(error);
  });

Promise 객체가 하나라도 rejected 상태가 되는 경우에 대비하기 위해서는 catch 메소드를 붙여주면 된다.

(2) race 메소드

race 메소드도 all 메소드와 마찬가지로 여러 Promise 객체들이 있는 배열을 아규먼트로 받는다. 그리고 race 메소드도 all 메소드처럼 Promise 객체를 리턴하지만, 적용 원리가 다르다. race 메소드가 리턴한 Promise 객체는 아규먼트로 들어온 배열의 여러 Promise 객체들 중에서 가장 먼저 fulfilled 상태 또는 rejected 상태가 된 Promise 객체와 동일한 상태와 결과를 갖게 된다.

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve('Success'), 1000);
});
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error('fail')), 2000);
});
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error('fail2')), 4000);
});

Promise
  .race([p1, p2, p3])
  .then((result) => {
    console.log(result); // hello 출력
  })
  .catch((value) => {
    console.log(value);
  });

p1 객체가 가장 먼저 1초 후에 fulfilled 상태가 된다. race 메소드가 리턴한 Promise 객체는 이 중에서 가장 빨리 상태가 결정된 pa 객체와 동일한 상태와 결과를 갖는다. 말그대로 race 메소드는 여러 Promise 객체들을 레이스(경쟁)시켜서 가장 빨리 상태가 결정된 Promise 객체를 선택하는 메소드이다.

all 메소드나 race 메소드 말고 allSettled, any라는 메소드도 있다. 이것들도 all, race 메소드처럼 Promise 객체 배열을 아규먼트로 받고 Promise 객체를 리턴한다. 각 메소드가 리턴한 Promise 객체가 A라고 가정하고 설명하도록 하겠다.

  • allSettled 메소드

배열 내의 모든 Promise 객체가 fulfilled 또는 rejected 상태가 되기까지 기다리고, pending 상태의 Promise 객체가 하나도 없게 되면, A의 상태값은 fulfilled 상태가 되고 그 작업 성공 결과로, 하나의 배열을 갖게 된다. 이 배열에는 아규먼트로 받았던 배열 내의 각 Promise 객체의
(1) 최종 상태를 status 프로퍼티,
(2) 그 작업 성공 결과는 value 프로퍼티,
(3) 그 작업 실패 정보는 reason 프로퍼티
에 담은 객체들이 요소로 존재한다.

[
   {status: "fulfilled", value: 1},
   {status: "fulfilled", value: 2},
   {status: "fulfilled", value: 3},
   {status: "rejected",  reason: Error: an error}
]

fulfilled 상태와 rejected 상태를 묶어서 setteld 상태라고 한다.

  • any 메소드

여러 Promise 객체들 중에서 가장 먼저 fulfilled 상태가 된 Promise 객체의 상태와 결과가 A에도 똑같이 반영된다. 만약 모든 Promise 객체가 rejected 상태가 되어버리면 AggregateError라고 하는 에러를 작업 실패 정보로 갖고 rejected 상태가 된다. any라는 단어의 뜻처럼 배열 속의 Promise 객체 중 단 하나라도 fulfilled 상태가 되면 되는 것이다.


9. axios

오늘날 개발 실무에서는 fetch 함수말고도 Ajax 통신을 할 수 있는 방법이 있다. 바로 axios라고 하는 외부 패키지를 사용하는 것이다.

axios
  .get('https://jsonplaceholder.typicode.com/users')
  .then((response) => {
    console.log(response);
  })
  .catch((error) => {
    console.log(error);
  });

axios 패키지에서 제공하는 axios 객체를 사용해서 GET request를 보내고 그 response를 받는 코드이다. axios.get 부분만 fetch로 바꾸면 사용법이 비슷하다. 사실 axios 객체에서 request를 보내는 많은 메소드들이 fetch 함수처럼 Promise 객체를 리턴한다. axios 객체에는 fetch 함수에는 없는 몇 가지 기능 및 장점들이 있다.

  • 모든 request, response에 대한 공통 설정 및 공통된 전처리 함수 삽입 가능
  • serialization, deserialization을 자동으로 수행
  • 특정 request에 대해 얼마나 오랫동안 response가 오지 않으면 request를 취소할지 설정 가능(request timeout)
  • 업로드 시 진행 상태 정보를 얻을 수 있음
  • request 취소 기능 지원

axios가 이렇게 fetch에 비해 다양한 기능을 지원하는 것은 맞지만 단점도 있다. 바로 별도의 다운로드가 필요한 패키지라는 것이다. fetch 함수는 웹 브라우저에서 바로 지원되는 함수이기 때문에 별도로 패키지를 다운로드받지 않아도 되지만, axios는 별도로 패키지를 다운로드해줘야 한다.

그래서 axios에서 제공하는 추가 기능이 필요한 경우에는 axios를 쓰고, 그런 기능이 필요하지 않고 별도의 패키지 다운로드를 원하지 않는 경우에는 fetch 함수를 사용한다.

profile
프론트엔드 지식으로 가득찰 때까지

0개의 댓글