실무에서는 여러개의 promise 객체를 다뤄야할 일도 종종 발생한다. 

데이터를 바로 출력하지 않고 배열에 저장해두었다가 모든 데이터가 저장되고 나서 배열을 출력해보도록 하자.

async function getEmployee(id) {
  const response = await fetch('https://learn.codeit.kr/api/employees/${id}');
  const data =  await response.json();
  return data;
};

for (let i = 1; i < 11; i++) {
  const employee = await getEmployee(i); //함수 바깥에서 값을 받아오려면 await문을 써야한다.
}

(for문) 함수 바깥에서 값을 받아오려면 await 문을 써야한다. 하지만 await이 있으면 getEmployee 함수가 완전히 끝날 때까지 기다린 후에 다음줄로 넘어가기 때문에 리퀘스트를 보내고 파싱하는 작업을 순차적으로 하게된다. 이럴 때 promise.all 메소드를 활용할 수 있다. 

Promise.all은 여러 promise를 동시에 기다릴때 사용한다.

async function getEmployee(id) {
  const response = await fetch('https://learn.codeit.kr/api/employees/${id}');
  const data =  await response.json();
  return data;
};
//promises 배열에 추가되는 것은 promise 객체이다.
const promises = []; //일단 이렇게 promises라는 배열을 만들고

for (let i = 1; i < 11; i++) {
  promises.push(getEmployee(i)); //getEmployee의 결과를 await하지않고 바로 배열에 추가
}

이렇게 하면 await을 하지 않기 때문에 리퀘스트를 거의 병렬적으로 보내게된다. (promises 배열에 추가되는 것은 promise 객체이다.)

async function getEmployee(id) {
  const response = await fetch('https://learn.codeit.kr/api/employees/${id}');
  const data =  await response.json();
  return data;
};

const promises = []; 

for (let i = 1; i < 11; i++) {
  promises.push(getEmployee(i));
}

Promise.all(promises); //추가

Promise.all(promises); 를 풀어서 작성해보자면 이렇게 된다.

Promise.all([p1,     p2,     p3,     p4,     p5,     ...]);

처음에는 배열에 있는 promise들이 모두 pending 상태일 것이다.

pending 상태

Promise.all 메소드도 promise를 리턴하는데 처음에는 pending 상태이다가 아규먼트로 전달된 promise들이 모두 fulfilled 상태가 되면 Promise.all도 fulfilled 상태가 되고, 

각 promise의 성공 결과값들로 이루어진 배열을 성공 결과값으로 갖게된다. 지금 쓰고 있는 예시의 경우 직원 데이터로 이루어진 배열이 성공 결과값이 될것이다.

반대로 아규먼트로 전달한 promise 중 하나라도 rejected 상태가 되면 Promise.all도 rejected 상태가 되고 rejected된 promise의 오류를 결과값으로 갖게된다.


Promise.all도 결국 promise 객체를 리턴하기 때문에 await이나 then 메소드를 이용할 수 있다.

async function getEmployee(id) {
  const response = await fetch('https://learn.codeit.kr/api/employees/${id}');
  const data =  await response.json();
  return data;
};

const promises = []; 

for (let i = 1; i < 11; i++) {
  promises.push(getEmployee(i));
}

const employees = await Promise.all(promises); //await 이용
console.log(employees);

await을 이용해서 성공 결과값, 즉 직원 배열을 가져왔다. 위 코드를 실행해보면 직원 코드가 잘 출력된다.


Promise.all도 try catch문을 이용해서 오류를 처리할 수도 있다.

async function getEmployee(id) {
  const response = await fetch(`https://learn.codeit.kr/api/employees/${id}`);
  const data = await response.json();
  return data;
}

const promises = [];

for (let i = 1; i < 11; i++) {
  promises.push(getEmployee(i));
}

let employees; //try문 바깥에서 선언

try {
  employees = await Promise.all(promises);
} catch (error) {
  console.log(error);
}

console.log(employees);

getEmployee 변수를 try문 바깥에서도 사용할 수 있도록 try문 바깥에서 선언했다. 그러면 Promise.all 부분에서 오류가 나도 try catch문이 오류를 잡아준다.

promise를 활용하는 비동기 작업 여러개를 병렬적으로 처리하고 싶을 땐 Promise.all 메소드를 사용하면 된다. (여러 비동기 작업을 한꺼번에 처리)

then 메소드와 프로미스 체인을 사용할 때는 catch와 finally 메소드를 사용할 수 있다.

fetch('https://learn.codeit.kr/api/employees')
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.log('Error!')) //코드 추가
  .finally(() => console.log('Finished')); //코드 추가
  
console.log('Task 2');
  
console.log('Task 3');

//위 코드는 아래 코드와 동일하다.
//export async function printEmployees() {
//  try {
//    const response = await fetch('https://learn.codeit.kr/api/employees');
//    const data = await response.json();
//    console.log(data); 
//  } catch (error) {
//    console.log('Error!');
//  } finally {
//    console.log('Finished');
//  }
//};

//printEmployees();

//console.log('Taks 2');
//console.log('Taks 3');

catch 메소드는 프로미스 체인에서 발생하는 오류를 잡아서 콜백을 실행해주고, finally 메소드는 프로미스 체인에서 어떤 일이 발생하든 끝에 콜백을 실행해준다. 코드를 실행해보면

Task 2
Task 3
[  
  {
    id: 1,
    name: 'Alice',
    email: 'alice@codeitmall.kr',
    department: 'engineering'
  },
...
]
Finished

Task 2와 3이 먼저 출력되고, 직원 데이터가 출력된 다음에 Finished 가 출력된 후 종료된다.

만약 주소가 잘못된 주소라면?

fetch('https://learn.codeit.krrrrrrrrrrrrrr/api/employees') //잘못된 주소 입력
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.log('Error!')) 
  .finally(() => console.log('Finished')); 
  
console.log('Task 2');
  
console.log('Task 3');
Task 2
Task 3
Error!
Finished

출력되는 순서는 이전과 똑같은데 이전에 출력됐던 직원 데이터 대신 Error!가 출력된다.


1. fetch 함수와 then, catch, finally 메소드는 모두 promise를 리턴한다.

2. 그리고 fetch 함수가 실행되는 동안 프로미스 체인 아래에 있는 코드를 실행한다.

3. fetch를 할 때 잘못된 주소를 적었기 때문에 오류가 발생할 것이다. 그래서 첫번째 promise는 rejected 상태가 되고, 결과값으로 오류를 갖게 된다.

4. then 메소드는 앞선 promise가 fulfilled 상태가 되야 등록된 콜백을 실행하는데, rejected 상태이기 때문에 아무 콜백도 실행하지 않는다. (뒤에 있는 promise는 이전 promise와 똑같은 상태와 결과값을 갖게된다.)

그러면 다음 then에 연결되어 있는 promise도 당연히 rejected가 된다.

5. catch 메소드는 이전 promise 상태가 rejected 상태가 되면 promise의 결과값을 가지고 콜백을 실행하는데 then의 반대라고 생각하면 된다. 
6. catch문의 콜백을 실행하고(Error! 출력) 

6-1 콜백이 promise를 리턴하면 catch 메소드가 리턴하는 promise도 동일한 상태와 결과값을 갖게 되고
6-2 콜백이 일반값을 리턴하면 catch 메소드가 리턴하는 promise는 fulfilled 상태가 되고 리턴 값을 결과값으로 갖게된다. (지금은 console.log를 리턴하고 있는데 console.log는 undefined를 리턴한다.)
7. promise가 fulfilled상태가 되고 undefined를 결과값으로 갖게 된다.

catch에서 오류를 이미 처리했기 때문에 콜백이 잘 실행되면 catch가 리턴하는 promise는 rejected가 아닌 fulfilled 상태를 갖는다.

8. 마지막으로 finally 메소드는 앞선 promise의 결과가 결정(fullfilled 혹은 rejected 상태)되면 등록된 콜백을 실행한다. 그래서 오류가 발생하든 안하든 항상 finally 콜백은 실행된다. 

fianlly 메소드의 등록된 콜백 실행

9. 그리고 이번에도 finally가 리턴한 promise는 fulfilled 상태가 되고, undefined를 결과값으로 갖게된다.

‼️결론

  • promise 객체의 then, catch, finally 메소드를 연결해서 프로미스 체인을 만들 수 있다. 
  • then 안에는 앞에 작업이 성공했을 때 실행할 콜백을 등록하면 된다.
  • catch 안에는 앞서는 프로미스 체인 어딘가에서 오류가 났을 때 실행할 콜백을 등록하면 된다.
  • 오류가 어디서 나든 rejected 상태의 promise가 계속 전파되는 원리 때문에 catch 메소드가 오류를 처리할 수 있다.
  • 그래서 catch 메소드는 보통 then 이후에 사용한다.
  • finally는 앞 선 promise의 결과가 어떻든 해결이 되면(더 이상 pending 상태가 아니면) 안에 있는 콜백을 실행한다. 
  • 프로미스 체인의 결과와 상관 없이 항상 실행되어야 하는 코드가 있다면 finally에 등록한다.
  • 프로미스 체인 어디에서든 결과를 기다리는 도중에는 이후에 있는 코드를 실행하고, 결과가 나오면 다시 체인으로 돌아오기 때문에 코드를 비동기적으로 처리할 수 있다.

'코린이 개념잡기 > 비동기 자바스크립트' 카테고리의 다른 글

리퀘스트: fetch 기본 문법  (0) 2024.12.25
Promise.all()  (1) 2024.12.25
.then() 메소드  (0) 2024.12.25
promise rejected 상태 이해하기  (0) 2024.12.25
오류 처리하기 (try catch)  (1) 2024.12.23

자바스크립트에서 비동기 코드를 잘 활용하려면 async, await과 then 메소드 둘 다 알고 있는 것이 좋다. 

//main.js
async function printEmployees() {
  const response = await fetch('https://learn.codeit.kr/api/employees');
  const data = await response.json();
  console.log(data);
}

오류처리는 하지 않는 printEmployees 함수를 then 문법으로 바꿔본다면,

const dataPromise = fetch('https://learn.codeit.kr/api/employees')
					  .then((response) => response.json());
dataPromise.then((data) => console.log(data));

이렇게 바꿀 수 있다. 살펴보자면,

1. fetch는 promise를 리턴한다. (1번째 줄)
 2. then은 promise 객체의 메소드이기 때문에 뒤에 이어서 쓸 수 있다.(2번째 줄)
3. 그리고 then메소드 자체도 promise를 리턴한다. (console.log(dataPromise)를 입력해보고 실행해보면 promise pending 상태가 나온다.)
4. 그래서 dataPromise 뒤에서 then 메소드를 붙일 수 있다. (3번째 줄)

then 메소드는 앞 선 비도기 작업이 완료되면 등록된 콜백을 실행해준다. 

1. 리스폰스가 돌아오면 (response) => response.json() 콜백을 실행하고
2. response.json이 완료되면 (data) => console.log(data) 콜백을 실행한다.

이렇게 해서 실행해봐도 직원 정보가 잘 출력되는 결과를 확인 할 수 있다. 위 코드를 조금 더 간결하게 쓸 수 도 있는데,

fetch('https://learn.codeit.kr/api/employees')
  .then((response) => response.json())
  .then((data) => console.log(data));

이렇게 작성할 수도 있다. then은 promise를 리턴하기 때문에 then 뒤에 바로 then을 또 붙일 수 있는 것이다. 

이렇게 promise 뒤에 메소드를 계속 연결해서 쓰는것을 프로미스 체인(Promise Chain)이라고 한다.

작동방식을 세세하게 살펴보자.

1. fetch 함수는 URL로 리퀘스트를 보내고 곧바로 promise를 리턴한다.
2. 그리고 이어서 then 메소드 들도 promise를 리턴한다.

3. 이 때 안에 있는 콜백을 실행하는건 아니고 일단 pending 상태의 promise를 리턴하는 것이다.
4. 기다리다가 리퀘스트가 완료되서 리스폰스가 돌아오면 첫번째 promise는 fulfilled 상태가 된다.

5. promise가 fulfilled 상태가 되면 then 메소드에 등록된 콜백이 실행된다.
6. 그리고 이 때 promise의 결과값을 콜백의 첫번째 파라미터로 전달한다. (그래서 리스폰스가 콜백으로 전달)

리스폰스가 콜백으로 전달

6-1. 만약 콜백이 promise를 리턴하면 then 메소드가 리턴한 promise도 동일한 상태와 결과값을 갖게 된다.

6-1예시

6-2. 콜백이 promise가 아닌 평범한 값을 리턴하면 then 메소드가 리턴한 promise는 fulfilled 상태가 되고, 리턴값을 결과값으로            갖게 된다.

6-2 예시

7. 지금의 경우에는 response.json을 리턴하니까 response.json이 fulfilled 상태가 되면 
8. then 메소드가 리턴한 promise도 fulfilled 상태가 된다.

9. fulfilled 상태가 되면 다음 then 메소드에 등록된 콜백이 실행되는데 파싱된 데이터가 첫번째 파라미터로 전달된다.

파싱된 데이터가 파라미터로 전달!

10. 이 콜백은 데이터를 출력하고 console.log를 리턴하는데 console.log는 undefined를 리턴하기 때문에 결국 콜백은 undefined를 리턴한다.

11. 그래서 마지막 promise는 fulfilled 상태가 되고, undefined 값을 결과로 갖게 된다.

결과

이 promise에 무언가를 이어서 하지 않을거기 때문에 마지막 promise에 대해서는 신경쓰지 않아도 된다.


프로미스 체인(Promise Chain)의 중요한점

  • 비동기 작업을 기다리는 동안에는 다른 코드를 먼저 실행한다.
fetch('https://learn.codeit.kr/api/employees')
  .then((response) => response.json())
  .then((data) => console.log(data));
  
console.log('Task 2'); //코드 추가
  
console.log('Task 3'); //코드 추가

예를 들어 이렇게 코드를 추가해 보면 결과는 Task 2와 3이 먼저 출력되고 직원 데이터가 그 뒤를 이어 출력된다. 

실행 순서를 되짚어보자면 fetch 함수가 promise를 리턴하고, 이 promise가 pending 상태인 동안 console.log('Task 2')와 console.log('Task 3')을 실행한다. 그리고 조금 기다린 후 fulfilled 상태가 되면 리스폰스를 가지고 첫번째 then 메소드의 콜백을 실행한다. 그러면 이 콜백은 다시 promise를 리턴하고, 이 promise가 fulfilled가 될때까지 나머지 코드를 먼저 실행하는데 이번엔 더 이상 실행할 코드가 없기 때문에 잠시 기다렸다가 fulfilled 상태가 되면 promise의 결과값, 파싱 결과로 두번째 then 메소드의 콜백을 실행한다. 그러면 이제 직원 데이터가 실행되고 프로그램이 종료된다.

+ Recent posts