비동기 실행과 Promise 객체

LEE GYUHO·2023년 10월 15일
0

📌 비동기 실행과 Promise 객체

  • 📘fetch 함수와 비동기 실행
    비동기 실행: 한번 시작된 작업이 완료되기 전에 바로 다음 코드로 실행이 넘어가고 나중에 콜백이 실행됨으로써 작업이 마무리 되는 것
console.log('Start!');

fetch('https://www.google.com')
  .then((response) => response.text())
  .then((result) => { console.log(result); });

console.log('End'); 

이 코드에는 2개의 콜백이 있다.
(1) (response) ⇒ response.text()
(2) (result) ⇒ { console.log(result); }
fetch 함수가 리퀘스트를 보내고, 서버의 리스폰스를 받게 되면 그때서야 이 콜백들 이 순서대로 실행된다.

  1. console.log('Start');
  2. fetch 함수(리퀘스트 보내기 및 콜백 등록)
  3. console.log('End');
  4. 리스폰스가 오면 2. 에서 then 메소드로 등록해뒀던 콜백 실행

End가 response의 내용보다 먼저 출력된 이유는 fetch 함수에 비동기 실행이 되는 부분이 있기 때문이다.

  • 📘'비동기 실행'이라는 건 왜 존재하는 걸까?
    보통 '비동기 실행'이 '동기 실행'에 비해, 동일한 작업을 더 빠른 시간 내에 처리할 수 있기 때문이다.

  • 📘알아야하는 비동기 실행 함수

    • setTimeout 함수
      특정 함수의 실행을 원하는 시간만큼 뒤로 미루기 위해 사용하는 함수

      console.log('a');
       setTimeout(() => { console.log('b'); }, 2000);
       console.log('c');
      // 이 코드에서 setTimeout함수는 첫 번째 파라미터에 있는 
      // () => { console.log('b'); } 이 콜백 함수의 실행을 
      // 두 번째 파라미터에 적힌 2000밀리세컨즈(2초) 뒤로 미룬다.

      이 코드를 실행하면 a와 c가 먼저 출력되고 약 2초 뒤에 b가 출력된다.

    • setInterval 함수
      특정 콜백을 일정한 시간 간격으로 실행하도록 등록하는 함수

      console.log('a');
       setInterval(() => { console.log('b'); }, 2000);
       console.log('c');
      // b를 출력하는 콜백이 2초 간격으로 계속 실행된다.

      이 코드를 실행하면 a와 c가 먼저 출력되고 2초 간격으로 b가 계속 출력된다.

    • addEventListener 메소드
      addEventListener 메소드는 DOM 객체의 메소드이다.

      클릭과 같은 특정 이벤트가 발생했을 때 실행할 콜백을 등록하는 addEventListener 메소드도 비동기 실행과 관련이 있다. 파라미터로 전달된 콜백이 당장 실행되는 것이 아니라, 나중에 특정 조건(클릭 이벤트 발생)이 만족될 때(마다) 실행되기 때문이다.

    • 위의 함수, 메소드들과 fetch의 차이점

      setTimeout(콜백, 시간) 
       setInterval(콜백, 시간)
       addEventListener(이벤트 이름, 콜백)
      
      fetch('https://www.google.com')
      .then((response) => response.text()) // fetch 함수가 리턴하는 객체의 then 메소드를 사용해서 콜백을 등록
      .then((result) => { console.log(result); });

      왜 fetch 함수만 사용하는 형식이 다른 걸까?
      그건 바로 fetch 함수는, 좀 더 새로운 방식으로 비동기 실행을 지원하는 자바스크립트 문법과 연관이 있기 때문이다.

      fetch 함수는 Promise 객체라는 것을 리턴하고, 이 Promise 객체는 비동기 실행을 지원하는 또 다른 종류의 문법에 해당한다.

  • 📘Promise 객체
    promise 객체는 어떤 작업에 관한 '상태 정보'를 가지고 있는 객체

    request를 보내서 response를 받는 작업등의 성공하거나 실패한 결과가 fetch함수가 리턴하는 promise 객체에 저장된다.

    promise 객체는 3가지 중 하나의 상태를 가진다.

    • pending: 진행중
    • fulfilled: 성공
    • rejected: 실패
console.log('Start!');

fetch('https://www.google.com') // promise 객체를 리턴한다.
  .then((response) => response.text()) //then은 promise 객체의 메소드이다.
  .then((result) => { console.log(result); });

console.log('End'); 

처음 fetch 함수가 리턴한 promise 객체는 pending 상태이다.
그 후 response를 정상적으로 받으면 fulfilled 상태가 된다.
문제가 생겨 response를 받지 못한다면 rejected 상태가 된다.

만약 작업이 성공해서 promise 객체가 fulfilled 상태가 되면 promise 객체는 그 작업의 성공 결과도 갖는다. 여기서는 response가 작업 성공 결과에 해당한다.

promise 객체가 rejected 상태가 되면 작업의 실패 이유에 관한 정보를 갖게 되는데 이것을 작업 실패 정보라고 한다.

  • 📘.then
    then 메소드는 promise 객체가 pending 상태에서 fulfilled 상태가 될 때 실행할 콜백을 등록하는 메소드이다.

  • 📘Promise Chaining
    promise 객체에 then 메소드를 연속적으로 붙이는 것을 말한다.

console.log('Start!');

fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.text())
  .then((result) => {
    const users = JSON.parse(result);
    return users[0];
  })
  .then((user) => {
    console.log(user);
    const { address } = user;
    return address;
  })
  .then((address) => {
    console.log(address);
    const { geo } = address;
    return geo;
  }).then((geo) => {
    console.log(geo);
    const { lat } = geo;
    return lat;
  }).then((lat) => {
    console.log(lat);
  })
  
  console.log('End'); 
  //user 객체에서 address 프로퍼티를 추출하고 
  //address 객체에서 geo라는 프로퍼티를 추출하고
  //마지막으로 geo 객체에서 lat이라는 프로퍼티를 추출하고 있다.
  //lat은 위도 경도 할 때 경도라는 뜻이다.
    • 여러개의 promise 객체가 있는 것 처럼 표현하는 이유?
      then 메소드는 새로운 promise 객체를 리턴한다. 위의 예제에 있는 각각의 then 메소드들은 각각 별개의 promise 객체를 리턴한다.
    • then 메소드가 리턴한 promise 객체는 처음에는 pending 상태이다.
      나중에 then 메소드로 등록한 콜백이 실행되고 콜백에서 어떤 값을 리턴하면 then 메소드가 리턴했던 promise 객체가 영향을 받는다. 이때 콜백에서 어떤 값을 리턴하냐에 따라서 받는 영향이 달라진다.
      • 콜백 안에서 promise 객체를 리턴하는 경우
        콜백에서 promise 객체를 리턴하면 then 메소드가 리턴했던 promise 객체는 콜백이 리턴한 promise 객체와 동일한 상태와 결과를 갖게 된다.
      • promise 객체가 아닌 값을 리턴하는 경우
        만약 콜백에서 promise 객체 말고 단순 숫자나 문자열, 일반 객체등을 리턴하면 then 메소드가 리턴했던 promise 객체는 fulfilled 상태가 되고 콜백의 리턴값을 작업 성공 결과로 갖게 된다.
  • 📘Promise Chaining이 필요한 경우

console.log('Start!');

fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.text())
  .then((result) => {
    const users = JSON.parse(result);
    return users[0];
  })
  .then((user) => {
    console.log(user);
    const { address } = user;
    return address;
  })
  .then((address) => {
    console.log(address);
    const { geo } = address;
    return geo;
  }).then((geo) => {
    console.log(geo);
    const { lat } = geo;
    return lat;
  }).then((lat) => {
    console.log(lat);
  })
  
  console.log('End'); 

위 코드는 이렇게 바꿀 수 있다.

console.log('Start!');

fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.text())
  .then((result) => {
	const users = JSON.parse(result);
    return users[0];
    console.log(user);
    const { address } = user;
    console.log(address);
    const { geo } = address;
    console.log(geo);
    const { lat } = geo;
    console.log(lat);
  });
    
console.log('End'); 

Promise Chaining은 비동기 작업을 순차적으로 처리하기 위해서 사용한다.
Promise Chaining을 하면 순차적으로 처리해야 할 비동기 작업의 수가 아무리 많아도 계속해서 then 메소드를 붙여나감으로써 깔끔한 코드를 작성할 수 있다.

console.log('Start!');

fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.text())
  .then((result) => {
	const users = JSON.parse(result);
    const { id } = users[0];
    fetch(`https://jsonplaceholder.typicode.com/posts?userId=${id}`)
      .then((response) => response.text())
      .then((posts) => {
        console.log(posts);
      });
   });
    
console.log('End');

const { id } = users[0]; 여기에서 첫번째 사용자의 id값을 구한다.
posts라는 path로 request를 보낸다.
query부분에 ?userId=${id} 이것을 추가해서 방금 구한 id값을 넣었다.
이렇게 request를 요청하면 해당 아이디를 가진 사용자가 작성한 여러 글을 볼 수 있다.


위 코드는 이렇게 바꿀 수 있다.

console.log('Start!');

fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.text())
  .then((result) => {
	const users = JSON.parse(result);
    const { id } = users[0];
    return fetch(`https://jsonplaceholder.typicode.com/posts?userId=${id}`)
   })
   .then((response) => response.text())
   .then((posts) => {
     console.log(posts);
   });
    
console.log('End');

콜백에서 promise 객체를 리턴하면 then 메소드가 리턴했던 promise 객체가 콜백이 리턴한 promise 객체와 동일한 상태와 결과를 가진다는 규칙 때문에 가능하다.

  • 📘rejected 상태가 되면 실행할 콜백
fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.text(), (error) => { console.log(error); })
  .then((result) => { console.log(result); });

// (response) => response.text() fulfilled 상태가 되면 실행할 콜백
// (error) => { console.log(error); } rejected 상태가 되면 실행할 콜백
// Promise 객체가 rejected 상태가 되면 해당 콜백에는 작업 실패의 정보가 파라미터로 넘어온다.
    • 아무 값도 리턴하지 않을 때 undefined를 리턴한 것으로 보고 fulfilled 상태가 되고, 작업 성공 결과로 undefined를 갖게 된다.
    • 아무런 콜백도 실행되지 않을 때 호출된 then 메소드를 갖고 있는 이전 Promise 객체와 동일한 상태와 결과를 갖게 된다.
    • 콜백에서 에러가 발생했을 때 rejected 상태가 되고, 해당 에러 객체를 작업 실패 정보로 갖게 된다.
  • 📘catch 메소드

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

//.catch((error) => { console.log(error); }) 이것은
//.then(undefined, (error) => { console.log(error); }) 이것과 같다.
//실무에서는 굳이 이렇게 사용하지는 않고 catch를 사용한다.
    • 에러에 잘 대응하기 위해 catch 메소드는 마지막에 쓴다.
  • 📘finally 메소드
    Promise 객체가 fulfilled 상태이거나 rejected 상태인 것에 상관없이 항상 실행하고 싶은 콜백이 있는 경우 finally 메소드로 등록

    Promise Chain에서 작업이 어떻게 진행되든, 최종적으로 항상 실행해야 할 코드는 finally 메소드의 콜백 안에 써주면 된다

    catch보다 뒤에 사용한다.

fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.text())
  .catch((error) => { console.log(error); })
  .then((result) => { console.log(result); })
  .finally(() => { console.log('exit'); });
  • 📘Promise 객체 만들기
const p = new Promise((resolve, reject) => {
  setTimeout(() => { resolve('success'); }, 2000);
  //setTimeout(() => { reject(new Error('fail')); }, 2000);
  //이렇게도 만들 수있다. 
});
//p라는 Promise 객체가 2초 후에 fulfilled 상태가 되며 resolve 함수 안에 있는
//success라는 문자열이 작업 성공 결과가 된다.
p.then((result) => { console.log(result); });
//결과 확인을 위해 이 코드를 실행해보면 2초 뒤에 success가 출력된다.
//p.catch((error) => { console.log(error); });
//2초 뒤에 Error: fail이 출력된다.
  • 📘Promisify
// function wait(text, milliseconds) {
//   setTimeout(() => text, milliseconds);
// }
//위 처럼 작성하면 리턴값이 없기 때문에 아래처럼 작성해줘야 한다.
function wait(text, milliseconds) {
  const p = new Promise((resolve, reject) => {
    setTimeout(() => { resolve(text); }, 2000);
  });
  return p;
}

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);
      }
    });
  }
});
//이 코드는 가독성이 떨어지기 때문에 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;
}
    • Promisify를 하면 안 되는 함수
      • 기존의 비동기 실행 함수들 중에서도 그 콜백을 한번만 실행하는 것들(setTimeout, readFile 등)만 Promisify해서 사용해도 된다.
      • 콜백을 여러 번 실행하는 함수들(setInterval, addEventListener 등)인 경우에는 이렇게 Promisify하면 안 된다.
        ( Promise 객체는 한번 pending 상태에서 fulfilled 또는 rejected 상태가 되고나면 그 뒤로는 그 상태와 결과가 바뀌지 않기 때문이다.)
  • 📘이미 상태가 결정된 Promise 객체

    아예 처음부터 바로 fulfilled 상태이거나 rejected 상태인 Promise 객체를 만들기

    • fulfilled 상태의 Promise 객체 만들기
      const p = Promise.resolve('success');
    • rejected 상태의 Promise 객체 만들기
      const p = Promise.reject(new Error('fail'));
  • 📘Promise의 메소드

    • all 메소드
      all 메소드도 then 메소드처럼 새로운 Promise 객체를 리턴한다.

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

      all 메소드는 하나의 Promise 객체라도 rejected 상태가 되면, 전체 작업이 실패한 것으로 간주해야 할 때 사용합니다.

      Promise 객체가 하나라도 rejected 상태가 되는 경우에 대비하려면 catch를 사용해주면 된다.

      // 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번 직원 정보]
        });
    • 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 상태가 되고, 그 작업 성공 결과로 문자열 Success를 가지게 되는데요.
      p2는 2초 후에, p3는 4초 후에 rejected 상태가 된다.
      race 메소드가 리턴한 Promise 객체는 이 중에서 가장 빨리 상태 정보가 결정된 p1 객체와 동일한 상태와 결과를 가진다.

      const p1 = new Promise((resolve, reject) => {
        setTimeout(() => resolve('Success'), 6000);
      });
      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보다 p2가 먼저 상태가 결정된다.
      그럼 결국 race 메소드가 리턴한 Promise 객체는 p2처럼 rejected 상태가 되고 동일한 작업 실패 정보를 갖게 된다.

    • allSettled 메소드
      배열 내의 모든 Promise 객체가 fulfilled 또는 rejected 상태가 되기까지 기다리고, pending 상태의 Promise 객체가 하나도 없게 되면, A의 상태값은 fulfilled 상태가 되고 그 작업 성공 결과로, 하나의 배열을 갖게 됩니다.

      이 배열에는 아규먼트로 받았던 배열 내의 각 promise 객체의

      (1) 최종 상태를 status 프로퍼티,
      (2) 그 작업 성공 결과는 value 프로퍼티,
      (3) 그 작업 실패 정보는 reason 프로퍼티

      에 담은 객체들이 요소로 존재한다.

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

      [
        {status: "fulfilled", value: 1},
        {status: "fulfilled", value: 2},
        {status: "fulfilled", value: 3},
        {status: "rejected",  reason: Error: an error}
      ]
    • any 메소드
      여러 Promise 객체들 중에서 가장 먼저 fulfilled 상태가 된 Promise 객체의 상태와 결과가 A에도 똑같이 반영된다.
      만약 모든 Promise 객체가 rejected 상태가 되어버리면 AggregateError라고 하는 에러를 작업 실패 정보로 갖고 rejected 상태가 된다.
      any라는 단어의 뜻처럼 배열 속의 Promise 객체 중 단 하나라도 fulfilled 상태가 되면 되는 것이다.

📌 async와 await

  • 📘async란? await이란?
// fetch('https://jsonplaceholder.typicode.com/users')
//   .then((response) => response.text())
//   .then((result) => { console.log(result); });

ansync function fetchAndPrint() {
  const response = await fetch('https://jsonplaceholder.typicode.com/users');
  const result = await response.text();
  console.log(result);
}

fetchAndPrint();

//async는 asynchronous의 줄임말로 비동기를 의미한다.
//함수 안에 비동기적으로 실행 될 부분이 있다는 것을 말한다.
//await은 뒤에 코드를 실행하고 그 코드가 리턴하는  Promise 객체를 기다려준다.
//해당 Promise 객체가 fulfilled 상태나 rejected 상태가 될 때 까지 기다린다.
//Promise 객체가 fulfilled 상태가 되면 그 작업 성공 결과를 추출해서 리턴한다.
//첫번째 await은 fetch('https://jsonplaceholder.typicode.com/users') 이것이 
//리턴하는 Promise 객체가 fulfilled 상태가 될 때 까지 기다린다.
//그리고 fulfilled 상태가 되면 그 작업 성공 결과인 response 객체를 추출해서 return 한다.
//await은 async함수 안에서만 사용 가능하다.
  • 📘async/await 구문의 실행 원리
ansync function fetchAndPrint() {
  console.log(2);
  const response = await fetch('https://jsonplaceholder.typicode.com/users');
  console.log(7);
  const result = await response.text();
  console.log(result);
}

console.log(1);
fetchAndPrint();
console.log(3);
console.log(4);
console.log(5);
console.log(6);

//결과는 1 2 3 4 5 6 7 순서대로 출력된다.
//await을 만나는 순간 함수 바깥으로 나가서 함수 바깥에 있는 코드를 실행하고 돌아오기 때문이다.

//async 함수 안의 코드가 실행되다가 await을 만나면, 일단 await 뒤의 코드가 실행되고, 
//코드의 실행 흐름이 async 함수 바깥으로 나가서 나머지 코드를 다 실행합니다. 
//물론 함수 바깥에 더 이상 실행할 코드가 없을 수도 있습니다. 
//어느 경우든 그 이후로는, await 뒤에 있던 Promise 객체가 fulfilled 상태가 되기를 기다립니다. 
//그리고 기다리던 Promise 객체가 fulfilled 상태가 되면 await이 Promise 객체의 작업 성공 결과를 리턴하는 겁니다.
  • 📘catch문과 finally문
ansync function fetchAndPrint() {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');
    const result = await response.text();
    console.log(result);
  } catch (error) {
    console.log(error);
  } finally {
    console.log('exit');
  }
}

fetchAndPrint();
  • 📘async 함수
    async 함수는 Promise 객체를 항상 리턴한다.

    async 함수는 그 안에서 리턴하는 값에 따라 그에 맞는 Promise 객체를 리턴한다.

ansync function fetchAndPrint() {
  return 3;
}

fetchAndPrint();

// async 함수가 붙어있는 fetchAndPrint 함수는 숫자 3을 작업 성공 결과로 가진
// fulfilled 상태의 Promise 객체를 리턴한다.
    • 어떤 값을 리턴하는 경우

      • Promise 객체를 리턴하는 경우
        async 함수 안에서 Promise 객체를 리턴하는 경우에는 해당 Promise 객체와 동일한 상태와 작업 성공 결과(또는 작업 실패 정보)를 가진 Promise 객체를 리턴한다.(그냥 해당 Promise 객체를 리턴한다고 봐도 괜찮다.)

      • Promise 객체 이외의 값을 리턴하는 경우
        async 함수 내부에서 Promise 객체 이외에 숫자나 문자열, 일반 객체 등을 리턴하는 경우에는, fulfilled 상태이면서, 리턴된 값을 작업 성공 결과로 가진 Promise 객체를 리턴한다.

    • 아무 값도 리턴하지 않는 경우

      • fulfilled 상태이면서, undefined를 작업 성공 결과로 가진 Promise 객체를 리턴한다.
    • async 함수 내부에서 에러가 발생했을 때

      • rejected 상태이면서, 해당 에러 객체를 작업 실패 정보로 가진 Promise 객체가 리턴된다.
profile
누구나 같은 팀으로 되길 바라는 개발자가 되자

0개의 댓글