자바스크립트 웹 개발 기본기

코드잇의 자바스크립트 웹 개발 기본기 강의를 듣고 정리한 내용입니다.
문제가 될 시 삭제하겠습니다.

자바스크립트 웹 개발 기본기

3. 비동기 실행과 Promise 객체

01. fetch 함수와 비동기 실행

  • then 메소드로 등록했던 callback 들이 response가 오면 출력됨
  • then 메소드로 등록만하고, 그 아래 코드가 실행됨
  • 비동기 실행 : 시작된 작업이 완료되기 전에 다음 코드를 실행하는 것
  • 동기 실행 : 시작한 작업을 다 처리하고 나서야 다음코드를 실행하는 것
  1. console.log('Start');
  2. fetch 함수(리퀘스트 보내기 및 콜백 등록)
  3. console.log('End');
  4. 리스폰스가 오면 2. 에서 then 메소드로 등록해뒀던 콜백 실행
console.log('Start!');

fetch('https://jsonplaceholder.typicode.com/users')
    .then((response) => response.text())  // (response) => { return response.text() } 와 같은 의미이고, arrow function 으로 사용됨
    .then((result) => {console.log(result); });

console.log('End!');

03. 알아야하는 비동기 실행 함수들

1. setTimeout(콜백, 시간) : 특정 함수의 실행을 원하는 시간 만큼 뒤로 미루는 함수

console.log('a');
setTimeout(() => { console.log('b'); }, 2000);
console.log('c');

2. setInterval(콜백, 시간) : 특정 콜백을 특정 시간 간격으로 실행함

console.log('a');
setInterval(() => { console.log('b'); }, 2000);
console.log('c');

3. addEventListener(이벤트 이름, 콜백)

파라미터로 전달된 콜백이 당장 실행되는 것이 아닐, 특정 조건이 만족될 때마다 실행 됨
(1) 해당 DOM 객체의 onclick 속성에 그 함수를 설정하거나,

...

btn.onclick = function (e) { // 해당 이벤트 객체가 파라미터 e로 넘어옵니다.
  console.log('Hello Codeit!');
};

// 또는 arrow function 형식으로 이렇게 나타낼 수도 있습니다. 
btn.onclick = (e) => {
  console.log('Hello Codeit!');
};

...
(2) 해당 DOM 객체의 addEventListener 메소드의 파라미터로 전달하면 됩니다.
...

btn.addEventListener('click', function (e) { // 해당 이벤트 객체가 파라미터 e로 넘어옵니다.
  console.log('Hello Codeit!');
});

// 또는 arrow function 형식으로 이렇게 나타낼 수도 있습니다.
btn.addEventListener('click', (e) => {
  console.log('Hello Codeit!');
});

...

04. fetch 함수는 Promise 객체를 리턴합니다.

  • promise : 작업에 관한 상태정보를 가지고 있는 객체
    • pending : 작업이 진행중
    • fulfilled : 작업 성공, 작업 성공 결과를 받음
    • rejected : 작업 실패 , 작업 실패 결과를 받음

05. fetch 함수를 사용한 코드 다시 해석하기

  • then 메소드는 promise 객체의 메소드임
  • fetch 함수는 promise 객체를 return 하고, .then 메소드를 사용하면 fullfilled 상태가 되었을 때 실행할 콜백을 등록
console.log('Start!');

fetch('https://jsonplaceholder.typicode.com/users')
    .then((response) => response.text())  // (response) => { return response.text() } 와 같은 의미이고, arrow function 으로 사용됨
    .then((result) => {console.log(result); });

console.log('End!');

06. promise chaining이란?

  • .then 메소드는 각각 별개의 promise 객체를 return 함

07. text, json 메소드도 Promise 객체를 return 해요

  1. text 메소드
    • fetch 함수로 response를 잘 받으면, repsonse 객체의 text 메소드는, fulfilled 상태 이면서 response의 body에 있는 내용을 string 타입으로 변환한 값을 작업 성공결과로 가진 promise 객체를 return 함
    • 이때 작업 성공 결과는 string 타입이고, 그 값이 만약 JSON 데이터라면 JSON 객체의 parse 메소드로 Deserialize를 해줘야 함
      • JSON.parse(result);
  2. json 메소드
    • fetch 함수로 response를 잘 받으면, repsonse 객체의 json 메소드는, fulfilled 상태 이면서 response의 body에 있는 JSON 데이터를 자바스크립트 객체로 Deserialize 해서 생겨난 객체를 '작업 성공 결과'로 가진 Promise 객체를 return 함
    • then 메소드가 리턴했던 Promise 객체(A)는 그 콜백에서 리턴한 Promise 객체(B)와 동일한 상태와 결과를 갖게 된다.
    • text, json 메소드가 Promise 객체를 리턴하는 메소드임

08. Promise Chaining이 필요한 경우

  • 비동기 작업을 순차적으로 처리하기 위해서 Promise Chaining 을 함
  • response 가 들어있는 정보를 가지고 해당 사용자가 작성한 id를 조회함
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');

09. 신입 사원 정보 반영하기

fetch('https://learn.codeit.kr/api/interviews/summer')
  .then((response) => response.json())
  .then((interviewResult) => {
    const { interviewees } = interviewResult;
    const newMembers = interviewees.filter((interviewee) => interviewee.result === 'pass');
    return newMembers;
  })
  .then((newMembers) => fetch('https://learn.codeit.kr/api/members', {
    method: 'POST',
    body: JSON.stringify(newMembers),
  }))
  .then((response) => { 
    if (response.status === 200) {
      return fetch('https://learn.codeit.kr/api/members');
    } else {
      throw new Error('New members not added');
    }
  })
  .then((response) => response.json())
  .then((members) => {
    console.log(`총 직원 수: ${members.length}`);
    console.log(members);
  });

10. rejected 상태 일 경우

  • then 메소드의 두번째 파라미터로 넣으면 rejected 시 실행됨
  • rejected 시 작업 실패 정보가 넘어옴
fetch('https://jsonplaceholder.typicode.com/users'}
    .then((response) => response.text(), (error) => {console.log(error); })
    .then((result) => { console.log(result); });

11. then 메소드 완벽하게 이해하기

Promise 객체를 배울 때는 then 메소드에 관해서만 확실히 알면 딱히 어려운 내용이 없습니다. 이번 노트에서는 Promise의 then 메소드에 관한 규칙을 제대로 깊이있게 배워봅시다.

잠깐 이 코드를 보세요.

const successCallback = function () { };
const errorCallback = function () { };

fetch('https://jsonplaceholder.typicode.com/users')// Promise-A
  .then(successCallback, errorCallback);// Promise-B

이때까지 배운 내용을 바탕으로 이 코드를 해석해봅시다. 일단, 이 코드에서

(1) fetch 메소드가 리턴하는 Promise 객체를 Promise-A 객체라고 하고,
(2) then 메소드가 리턴하는 Promise 객체를 Promise-B 객체라고 해봅시다.

그리고 fetch 함수의 작업이 성공하는 경우와 실패하는 경우로 나누어서 생각해보겠습니다.

  1. fetch 함수의 작업이 성공해서 Promise-A 객체가 fulfilled 상태가 된 경우 : then 메소드 안의 "첫 번째" 콜백인 successCallback이 실행됩니다.
  2. fetch 함수의 작업이 실패해서 Promise-A 객체가 rejected 상태가 된 경우 : then 메소드 안의 "두 번째" 콜백인 errorCallback이 실행됩니다.

자, 여기서 중요한 점은 Promise-B는, 실행된 successCallback 또는 errorCallback에서 무엇을 리턴하느냐에 따라

  • 상태(fulfilled or rejected)와
  • 결과(작업 성공 결과 or 작업 실패 정보)가

결정된다는 점입니다.

이번 노트에서는 then 메소드가 리턴한 Promise 객체가, 콜백이 리턴하는 값에 따라 어떻게 달라지는지 경우를 나누어서 다뤄볼 겁니다. 이전 영상에서 배운 내용도 있고, 새롭게 배우는 내용도 있으니까 집중해서 잘 읽어보세요.

1. 실행된 콜백이 어떤 값을 리턴하는 경우

successCallback이 실행되든, errorCallback이 실행되든, 실행된 콜백에서 어떤 값을 리턴하는 경우입니다. 이때 그 값의 종류에 따라

  • Promise 객체인 경우와
  • Promise 객체 이외의 경우,

이 2가지 경우로 다시 나눌 수 있습니다.

(1) Promise 객체를 리턴하는 경우

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

위 코드에서 (response) ⇒ response.json() 이 콜백은 Promise 객체를 리턴하는 코드입니다. response 객체의 json 메소드가 Promise 객체를 리턴한다는 사실은 이전 노트에서 배웠죠? 이렇게 콜백에서 Promise 객체를 리턴하는 경우에는 그 콜백을 등록한 then 메소드가 리턴했던 Promise 객체가 콜백이 리턴한 Promise 객체의 상태와 결과를 똑같이 따라 갖게 됩니다. 즉, 위 코드의 첫 번째 then 메소드가 리턴했던 Promise 객체는, response 객체의 json 메소드가 리턴한 Promise 객체가 추후에 갖게 되는 상태와 결과를 그대로 따라서 갖게 된다는 뜻입니다.

좀 더 편하게 기억하기 위해서는 그냥 콜백에서 리턴하는 Promise 객체를 then 메소드가 그대로 리턴한다고 생각하셔도 됩니다. 그럼 이제 그 다음부터는 콜백에서 리턴한 Promise 객체로부터 다시 Promise Chain이 쭉 이어져 나간다고 보면 되죠.

(2) Promise 객체 이외의 값을 리턴하는 경우

콜백이 꼭 Promise 객체만을 리턴하는 것은 아니겠죠? 그냥 단순한 숫자, 문자열, 일반 객체 등을 리턴할 수도 있는데요. 이런 경우에 then 메소드가 리턴했던 Promise 객체는 fulfilled 상태가 되고 작업 성공 결과로 그 값을 갖게 됩니다.

// Internet Disconnected

fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.json(), (error) => 'Try again!')
  .then((result) => { console.log(result) });

예를 들어, 지금 인터넷이 안 되는 상황에서 이 코드를 실행했다고 해봅시다. 그럼 fetch 함수의 작업이 실패해서 두 번째 콜백인 (error) ⇒ 'Try again! 이 실행되겠죠? 두 번째 콜백은 'Try again!'이라는 문자열을 리턴하고 있는데요. 이렇게 하면 해당 콜백을 등록한 then 메소드가 리턴했던 Promise가 fulfilled 상태가 되고, 그 작업 성공 결과로 'Try again' 문자열을 갖게 됩니다.

자, 이때까지는 이전 영상들에서 모두 배운 내용들입니다. 아래부터는 조금 색다른 규칙들이 등장합니다. 집중해서 읽어봅시다.

2. 실행된 콜백이 아무 값도 리턴하지 않는 경우

// Internet Disconnected

fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.json(), (error) => { alert('Try again!'); })
  .then((result) => { console.log(result) });

방금 전과 같은 상황에서 콜백이 무언가를 리턴하는 게 아니라 이 코드에서처럼 단순히 alert 함수만 실행하고 끝난다고 해봅시다. 그럼 결과적으로 이 콜백은 아무런 값도 리턴하지 않은 것과 같은데요. 자바스크립트에서는 함수가 아무것도 리턴하지 않으면 undefined를 리턴한 것으로 간주됩니다. 따라서 방금 전 1. (2) 규칙에 따라 then 메소드가 리턴했던 Promise 객체는 fulfilled 상태가 되고, 그 작업 성공 결과로 undefined를 갖게 됩니다.

3. 실행된 콜백 내부에서 에러가 발생했을 때

콜백이 실행되다가 에러가 발생하는 경우가 있습니다. 예를 들어

fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => {
        ...
        add(1, 2);// ReferenceError 발생
        ...
  });

이렇게 정의하지도 않은 함수를 콜백에서 사용해서 에러가 발생하거나

fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => {
        ...
        throw new Error('failed');
        ...
  });

특정 경우에 인위적으로 throw 문을 써서 에러를 발생시키는 경우도 있을 겁니다.

이렇게 콜백이 실행되다가 에러가 발생한다면, then 메소드가 리턴했던 Promise 객체는 어떻게 될까요? 이 경우에는 Promise 객체가 rejected 상태가 되고, 작업 실패 정보로 해당 에러 객체를 갖게 됩니다. 잠깐 아래의 코드를 개발자 도구에서 실행해보겠습니다.

const promise = fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => { throw new Error('test'); });

promise 를 입력하여 then 메소드가 리턴한 Promise 객체의 내부를 살펴보면 이렇게 생겼는데요.

https://bakey-api.codeit.kr/api/files/resource?root=static&seqId=4374&directory=Untitled.png&name=Untitled.png

지금 [[PromiseState]]는 Promise 객체의 상태를, [[PromiseResult]]는 Promise 객체의 결과(작업 성공 결과 또는 작업 실패 정보)를 나타내는 내부 슬롯입니다.(내부 슬롯이란 자바스크립트 실행 엔진에서 내부적으로 관리하는 속성이라고 생각하시면 됩니다. 지금 당장 알아야할 내용은 아니니 Promise 객체에 집중합시다)
자세히 보면 현재 Promise 객체가 rejected 상태이고, 발생한 Error 객체를 그 작업 실패 정보로 갖고 있다는 것을 알 수 있습니다. 이렇게 콜백 실행 중에 에러가 발생하면, then 메소드가 리턴한 Promise 객체는 rejected 상태가 되고, 그 작업 실패 정보로 해당 Error 객체를 갖게 된다는 점, 잘 기억하세요!

4. 아무런 콜백도 실행되지 않을 때

// Internet Disconnected

fetch('https://www.google.com')// Promise-1
  .then((response) => response.text())// Promise-2
  .then((result) => { console.log(result) }, (error) => { alert(error) });

then 메소드의 아무런 콜백도 실행되지 않는 경우가 있습니다. 지금 인터넷을 끊고 나서 위 코드를 실행했다고 합시다. 그럼 fetch 함수가 리턴한 Promise-1 객체는 rejected 상태가 되기 때문에, 첫 번째 then 메소드의 두 번재 콜백이 실행되어야 합니다. 그런데 지금 두 번째 콜백이 없죠? 이런 경우에는 아무런 콜백도 실행되지 않는데요. 이런 경우에 then 메소드가 리턴한 Promise-2 객체는 어떻게 될까요? 이런 경우에 then 메소드가 리턴했던 Promise-2 객체는, 이전 Promise 객체와 동일한 상태와 결과를 갖게 됩니다. 그러니까 지금 Promise-2 객체는 Promise-1 객체처럼 rejected 상태가 되고, 똑같은 작업 실패 정보를 갖게 됩니다.

그럼 rejected 상태가 된 Promise-2의 then 메소드에는 이제 두 번째 콜백이 존재하기 때문에 그 두 번째 콜백이 실행됩니다. 이렇게 아무런 콜백도 실행되지 않는 경우에는 그 이전 Promise 객체의 상태와 결과가 그대로 이어진다는 사실, 잘 기억하세요.

자, 이때까지 Promise 객체의 then 메소드가 리턴한 Promise 객체의 상태가, then 메소드 안의 콜백이 리턴하는 값에 따라 무슨 상태와 결과를 갖게 되는지 배웠는데요. 사실 위의 내용을 이해하지 못해도, Promise 객체를 당장 사용하는 데는 문제가 없을 수도 있습니다. 하지만 나중에 Promise 객체를 사용하는 코드에서 문제가 생기거나 고난이도의 코드를 작성해야 할 때는 이런 기본적인 규칙을 모르면 내가 무엇을 잘못했는지조차 알 수 없게 됩니다. 따라서 이번에 배울 때 제대로 배우고 넘어갑시다.

위의 내용을 이해될 때까지 반복해서 읽어보세요. 그래야 다음에 나오는 내용들을 잘 이해할 수 있습니다.

14. catch 메소드

  • catch 메소드는 then 메소드를 약간 변형시킨 것에 불과함
    • .then(undefined, (error) ⇒ {console.log(error); }) 와 동일하다고 생각하면 됨
fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.json())
    .catch((error) => { console.log(error); })
  .then((result) => { console.log(result) });

15. catch 메소드 이해하기

// Internet Disconnected

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

이전 영상에서는 다음과 같은 catch 메소드를 봤습니다. 그런데 어떻게 fetch 함수에서 발생한 에러가 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(result); });// Promise-D

catch 메소드는 사실 then 메소드의 첫 번째 인자로 undefined을 넣은 것과 같다고 했죠? 그래서 catch 메소드를 then 메소드로 변환해봤습니다. 이 코드에서 fetch 함수와 각각의 then 메소드가 리턴하는 Promise 객체를 순서대로 Promise-A, B, C, D라고 합시다. 그리고 각각의 Promise의 상태가 어떻게 변하는지 살펴봅시다.

일단 fetch 함수의 작업이 실패해서 Promise-A 객체가 rejected 상태가 되면, 첫 번째 then 메소드의 두 번째 콜백이 실행되어야 합니다. 하지만 지금 첫 번째 then 메소드에는 두 번째 콜백이 없기 때문에 아무 콜백도 실행되지 않는데요. 이런 경우에는 어떻게 된다고 했죠? Promise-B 객체가 Promise-A와 똑같은 rejected 상태가 되고, 동일한 작업 실패 정보를 갖게 됩니다! 혹시 기억이 안 나면 'then 메소드 완벽하게 이해하기' 노트에서 4. 아무런 콜백도 실행되지 않을 때 부분을 보고 와주세요.

그럼 이제 rejected 상태가 된 Promise-B에 붙은 then 메소드에는 두 번째 콜백이 있기 때문에 이 두 번째 콜백이 실행됩니다. 즉, catch 메소드의 콜백이 실행되는 거죠. 어떻게 fetch 함수의 에러가 catch 메소드의 콜백에까지 전달될 수 있는지 이제 아시겠죠? then 메소드의 작동 원리만 잘 기억하고 있다면 딱히 어려운 내용은 아닙니다.

자, 그럼 보너스 문제를 하나 드릴게요. 이 코드를 실행하면 최종적으로 무엇이 출력될까요?

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

방금 전과 동일한 코드이고, 대신 마지막 console.log에 Quiz:라는 단어가 붙어있습니다. 이 단어 옆의 result로 무엇이 출력되는지가 문제인데요. 코드를 실행해보면

https://bakey-api.codeit.kr/api/files/resource?root=static&seqId=4378&directory=Untitled.png&name=Untitled.png

undefined가 출력되네요. 왜 그런 걸까요?

catch 메소드는 사실 then 메소드라고 했으니까 이것을 기억하면 이해할 수 있습니다. 지금 catch 메소드 안의 콜백이 실행되었을 때 무슨 값을 리턴했나요? 아무 값도 리턴하지 않았는데요. 이렇게 자바스크립트에서는 아무 값도 리턴하지 않은 경우에는 undefined를 리턴한 것으로 간주한다고 했었죠? 따라서 이전에 배운 것처럼 catch 메소드가 리턴한 Promise 객체는 fulfilled 상태가 되면서, undefined를 작업 성공 결과로 가지게 되는 겁니다. 그래서 그 뒤의 then 메소드의 콜백의 파라미터로 undefined가 넘어가서 undefined가 출력된 겁니다.

이렇게 결국 catch 메소드도 then 메소드의 실행 원리를 정확히 알아야 잘 해석할 수 있습니다.

16. 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); });

19. finally 메소드

  • 항상 실행하고 싶은 callback 이 있는 경우에 사용
  • 보통 catch 메소드 바로 뒤에 사용

 

22. promise 객체는 왜 등장했을까 ?

  • setTimeout 함수나, addEventlistener 메소드 처럼 직접 파라미터에 콜백을 전달하는 형식을 사용하면 콜백 헬(callback hell) 이라고 하는 문제를 일으킬 수도 있기 때문
  • 여러 비동기 작업을 순차적으로 수행해야 할 때 아래처럼 복잡해짐
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 함수는 Promise 객체를 리턴하기 때문에 여러 비동기 작업을 순차적으로 좀더 세밀하게 처리할 수 있다.
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');
  });

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

const p = new Promise((resolve, reject) => {
    setTimeout(() => { resolve('success'); }, 2000);  // 2초 후에 promise 객체가 fullfilled 상태가 된다는 뜻
});

p.then((result) => { console.log(result); }); // 작업 성공 결과가 출력됨
const p = new Promise((resolve, reject) => {
    setTimeout(() => { reject new Error('fail')); }, 2000); // 2초 후에 promise 객체가 rejected 상태가 된다는 뜻
});

p.catch((error) => { console.log(error); }); // 에러 결과가 출력됨

25. promisify

이전 영상에서는 직접 Promise 객체를 만드는 방법을 배웠습니다. 그럼 언제 이런 식으로 Promise 객체를 직접 만들게 되는 걸까요? 다양한 경우들이 있지만, 전통적인 형식의 비동기 실행 함수를 사용하는 코드를, Promise 기반의 코드로 변환하기 위해 Promise 객체를 직접 만드는 경우가 많습니다. 각각의 예시를 통해 이게 무슨 말인지 이해해봅시다.

1. setTimeout 함수 예시

예를 들어 이런 wait이라는 함수가 있다고 합시다.

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

wait 함수는 특정 밀리세컨즈만큼 시간이 지난 후에 text 파라미터로 전달받은 값을 리턴하는 함수입니다. 지금 보이는 setTimeout 함수는 이전에 '알아야하는 비동기 실행 함수들' 노트에서 배웠었죠? 이 wait 함수를 Promise Chaining 코드에서 사용해볼게요.

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

바로 이 Promise Chaining 코드에 wait 함수를 추가해볼 건데요. 이렇게 써보겠습니다.

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 함수를 호출했습니다. 이렇게 쓰면 2초 후에 리스폰스의 내용 뒤에 by Codeit이라는 문구를 붙여서 출력될 것 같은데요. 정말 그렇게 되는지 확인해봅시다.

코드를 실행해보면, 리스폰스의 내용과 by Codeit이 출력되지 않았습니다. 그 대신 undefined가 출력되었는데요.

왜 그런 걸까요?
그 이유는 바로 wait 함수에 있습니다.

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

이 wait 함수는 내부에서 setTimeout 함수를 호출합니다. 그리고 setTimeout 함수의 첫 번째 파라미터로 들어간 콜백이 2초 후에 text를 리턴하죠. 그런데 여기서 혼동하면 안 되는 것은 wait 함수가

...
  .then((result) => { return wait(`${result} by Codeit`, 2000); })
...

이 두 번째 then 메소드 안의 콜백에서 실행될 때,

wait 함수는 setTimeout 함수를 실행할 뿐 아무것도 리턴하지 않는다는 사실입니다.
setTimeout 함수 안의 콜백이 2초 후에 리턴하는 text는, wait 함수의 리턴값이 아닙니다.

이 사실에 유의해야 하는데요. wait 함수는 단지 setTimeout 함수를 실행하고 아무것도 리턴하지 않는 함수일 뿐입니다. 그리고 자바스크립트에서는 이전에 배운대로 함수에서 아무것도 리턴하지 않으면 undefined를 리턴하는 것으로 간주하기 때문에 wait 함수의 리턴값은 undefined입니다.

따라서 세 번째 then 메소드의 콜백으로 undefined가 넘어가고, 그래서 위 이미지에서 보이는 것처럼 undefined가 출력된 겁니다.

setTimeout은 비동기 실행되는 함수인데요. Promise Chaining 안에서 이렇게 비동기 실행되는 함수를 바로 사용하면, 나중에 실행되는 부분의 리턴값(여기서는 text)를 Promise Chain에서 사용할 수 없게 됩니다.

이 문제를 해결하려면 이전 영상에서 배웠던 Promise 객체를 직접 생성하는 방법을 사용하면 됩니다. wait 함수를 이렇게 수정해볼게요.

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

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

자, 이 상태에서 코드를 다시 실행해보면

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

 

이번에는 약 2초 후에 리스폰스의 내용이 잘 출력되고, 리스폰스의 내용 맨 마지막에는 by Codeit이라는 문구가 잘 붙어서 출력되는 것을 알 수 있습니다.

방금처럼 기존의 비동기 실행 함수(여기서는 setTimeout)의 콜백이 리턴하는 값을 Promise Chain에서 사용하고 싶다면, 해당 함수를 감싸서 Promise 객체를 직접 생성하는 코드를 작성해야 합니다. 그리고 그 Promise 객체를 리턴해야 방금처럼 Promise Chain에서 해당 리턴값을 받아서 사용할 수 있습니다.

이렇게 전통적인 형식의 비동기 실행 함수를 Promise 객체로 감싸서 그 Promise 객체를 리턴하는 형식으로 만드는 작업을 Promisify(프로미스화하다)라고 하는데요. 앞으로도 이 Promisify라는 용어를 사용하겠습니다. 계속 내용을 읽어봅시다.

2. 콜백 헬(callback hell)과 Promise

이번에는 Promisify의 또 다른 예시를 보겠습니다. 그런데 이번에는 브라우저가 아니라 조금 다른 환경에서의 코드를 볼 건데요. 바로 Node.js라고 하는 환경입니다. 오늘날 자바스크립트가 실행되는 환경에는 웹 브라우저뿐만 아니라 Node.js라고 하는 것도 있습니다. 이 Node.js는 오늘날 자바스크립트를 서버에서도 실행할 수 있게 해주는 또 다른 '자바스크립트 실행 환경'인데요. 이 Node.js에서는 브라우저에서와는 또 다른 비동기 함수들이 제공됩니다. (Node.js가 뭔지 더 궁금하신 분들은 이 영상을 참고하세요.)

Node.js에는 다음과 같이 특정 파일의 내용을 읽기 위해 사용되는 readFile이라는 비동기 실행 메소드가 있습니다.

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

여기서 fs는 readFile 메소드를 가진 객체로, 파일에 관한 기능들을 갖고 있습니다. 일단 여기서 당장 중요한 내용은 아니니까 readFile 메소드에만 집중합시다. readFile 메소드는 첫 번째 파라미터로 파일의 이름, 두 번째 파라미터로 파일 해석 기준(인코딩 기준), 세 번째 파라미터로 콜백을 받는데요. readFile 함수는 파일을 읽다가 에러가 발생하면 콜백의 첫 번째 파라미터(error)에, 해당 에러 객체를 전달하고 콜백을 실행합니다. 만약 파일을 정상적으로 다 읽었으면 콜백의 두 번째 파라미터(data)에, 읽어들인 파일 내용을 전달하고 콜백을 실행하는데요.

이 readFile 메소드도, 콜백을 파라미터에 바로 넣는 비동기 실행 함수라는 점에서 setTimeout 함수, addEventListener 메소드와 비슷합니다. 그런데 이런 형식의 함수(또는 메소드)들은 한 가지 단점이 있다고 했었죠?(참고) 그건 바로 콜백 헬(callback hell) 문제입니다. 예를 들어, 위 코드에서 이제 file1.txt 파일의 내용을 출력하고 나서 그 다음에 file2.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);
      }
    });
  }
});

이렇게 코드를 쓰면 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) 현상을 초래하게 된다는 겁니다.

실제로 실무에서 개발을 하다 보면 이런 콜백 헬이 아주 큰 문제가 됩니다. 그런데 이런 함수들은 Promise 객체를 리턴하는 것도 아니고 애초에 이런 형식으로 정의되어 있기 때문에 문제를 해결하기가 어려워 보이는데요. 이 문제에 대한 대표적인 해결책이 바로 우리가 배운 Promisify입니다.

지금 이 readFile 메소드를 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_promisified라는 이름의 함수를 정의했는데요. 지금 함수 안에서는 Promise 객체를 직접 생성하고 있습니다.
그리고 Promise 객체가 생성될 때 실행되는 executor 함수 안에서는 fs 객체의 readFile 메소를 호출했습니다.

여기서 중요한 것은 작업을 수행하다가 에러가 나면 readFile 함수의 콜백에서

...                         (error, data) => {
  if (error) {
    reject(error);// 에러 발생 시 -> rejected
  } else {
    resolve(data);// 파일 내용 읽기 완료 -> fulfilled
  }
}

reject 함수를 호출하고, 파일의 내용을 정상적으로 다 읽었을 때는 resolve 함수를 호출한다는 사실입니다. 그리고 reject 함수의 파라미터에는 error 객체를, resolve 함수의 파라미터에는 파일의 내용인 data를 전달했는데요. 이 각각은, 생성된 Promise 객체의 작업 실패 정보 또는 작업 성공 결과가 되겠죠?

이제 readFile 메소드를 Promisify해서 만든 readFile_promisified 함수를 사용해서 위의 콜백 헬 코드에서 작성했던 내용을 똑같이 작성해봅시다.

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_promisified 함수는 Promise 객체를 리턴하기 때문에 이렇게 자유롭게 Promise Chain 안에서 사용할 수 있습니다.

이렇게 원하는 경우에는 전통적인 형식의 비동기 실행 함수를 Promisify해서 콜백 헬을 방지하고, 가독성 높은 코드를 작성할 수 있습니다.

3. Promisify를 하면 안 되는 함수들도 있습니다.

이제 기존의 전통적인 형식의 비동기 실행 함수도 원하는 경우에는 Promisify해서 콜백 헬을 방지할 수 있다는 것을 알게 되었습니다. 하지만 전통적인 형식의 비동기 실행 함수라고 해서 모두 Promisify해서 사용해도 되는 것은 아닙니다.

기존의 비동기 실행 함수들 중에서도 그 콜백을 한번만 실행하는 것들(setTimeout, readFile 등)만 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_promisified 함수는 DOM 객체의 addEventListener 메소드를 Promisify한 함수인데요.

지금 Promise 객체가 생성될 때 실행되는 executor 함수 안에서는, DOM 객체에 어떤 이벤트가 발생할 때, 실행할 콜백을 등록하고 있습니다.
특정 이벤트가 발생할 때마다 count라고 하는 변수의 값을 1씩 늘려서 resolve 함수의 파라미터로 전달해서 실행하도록 하는 내용이 들어있는데요.

마지막 코드를 보면,

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

이렇게 addEventListener_promisified 함수의 아규먼트로 DOM 객체 box와 문자열 'click'을 넣어서 box 객체가 클릭 이벤트에 반응하도록 했습니다.
(HTML 코드는 생략된 상태입니다.)

하지만 이 코드를 실행하고 box를 클릭해보면
처음에 1이 딱 출력되고 나서 그 다음 count 값들은 출력되지 않습니다.

왜냐하면 pending 상태에 있던 Promise 객체(여기서는 p 객체)가 한번 fulfilled 상태 또는 rejected 상태가 되고 나면
Promise 객체의 상태 및 결과가 고정되어 그 뒤로는 바뀌지 않기 때문입니다.

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

이렇게 콜백이 여러 번 실행되어야하는 비동기 실행 함수인 경우에는 Promisify를 하면 안 됩니다. Promisify를 하고 싶은 경우라도, 콜백이 딱 한 번 실행되는 함수인 경우에만 해야한다는 사실, 잘 기억하세요!

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

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

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

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

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

3. 어느 시점이든, 몇번이든 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 상태가 되고 나서 콜백 등록

28. 여러 promise 객체를 다루는 방법

  1. all 메소드
  • 배열 안에 있는 모든 Promise 객체가 pending 상태에서 fulfilled 상태가 될 때까지 기다림.
  • 그리고 모든 Promise 객체들이 fulfilled 상태가 되면, all 메소드가 리턴했던 Promise 객체는 fulfilled 상태가 되고,
    각 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://learn.codeit.kr/api/members/3').then((res) => res.json());

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

2. race 메소드

  • race 메소드가 리턴한 Promise 객체는 아규먼트로 들어온 배열의 여러 Promise 객체들 중에서 가장 먼저 fulfilled 상태 또는 rejected 상태가 된 Promise 객체와 동일한 상태와 결과를 갖게 됨
  • 아래 코드에서는 p1 객체가 결과가 됨
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);
  });

29. axios

axios 객체에서 리퀘스트를 보내는 많은 메소드들이 fetch 함수처럼 Promise 객체를 리턴함
그래서 fetch 함수의 사용법과 비슷한 점이 많지만, axios 객체에는 fetch 함수에는 없는 다음과 같은 몇 가지 기능 및 장점들이 있음

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

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

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

4. async/await을 활용

01. async/await 란?

  • async : 비동기, 함수 안에 비동기적으로 실행될 부분이 있다는 것을 의미, promise 객체를 return 하는 부분이 있다는 것을 의미
    • await : ~을 기다리다. promise 객체를 return 하는 것을 기다려줌 ,fullfilled, rejected 상태가 될때까지 기다리고 다음을 실행함
async function fetchAndPrint() {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');
    const result = await response.text();
    console.log(result);
}

fetchAndPrint();

02. async/await 구문의 실행 원리

  • await 키워드는 async 안에서만 사용할 수 있음
  • async 함수 안의 코드가 실행되다가 await을 만나면, 일단 await 뒤의 코드가 실행되고, 코드의 실행 흐름이 async 함수 바깥으로 나가서 나머지 코드를 다 실행함
  • await 뒤에 있던 Promise 객체가 fulfilled 상태가 되기를 기다림. 그리고 기다리던 Promise 객체가 fulfilled 상태가 되면 await이 Promise 객체의 작업 성공 결과를 리턴
async 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);

/* 위의 함수와 같다고 할 수 있음 
function fetchAndPrint() {
  console.log(2);
  fetch('https://jsonplaceholder.typicode.com/users')
    .then((response) => {
      console.log(7);
      return response.text();
    })
    .then((result) => { console.log(result); });
}
*/
// 결과
1
2
3
4
5
6
undefined // console 창에서 마지막에 return 값이 없으면 undefiend 나온 것 
7
[리스폰스의 내용]

06. catch 문과 finally 문

  • catch : try문이 실패하면 실행함
  • finally : 성공/ 실패와 상관없이 무조건 실행함
async 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();

09. async 함수는 Promise 객체를 리턴합니다.

  • async 함수는 항상 promise 객체를 return 함
  • 작업 성공 결과를 3으로 가진 promise 객체를 return 함
async function fetchAndPrint() {
    return 3;
}

fetchAndPrint();
  • 아래 구문도 promise 객체를 return 함
async function fetchAndPrint() {
    return fetch('https://jsonplaceholder.typicode.com/users')
                    .then((response) => response.text());
}

fetchAndPrint();

11. async 함수 안의 async 함수

  • async 함수는 항상 promise 객체를 return 하는 원리를 이용해서 async 안에 async 함수를 사용해서 같은 promise 객체를 return 하도록 할 수 있음
  • 함수 앞에 await을 붙여서 사용하면 됨
const applyPrivacyRule = async function (users) {
    const resultWithRuleApplied = users.map((user) => {
        const keys = Object.keys(user);
        const userWithoutPrivateInfo = {};
        keys.forEach((key) => {
            if (key !== 'address' && key !== 'phone') {
                userWithoutPrivateInfo[key] = user[key];
        }
    });
    return userWithoutPrivateInfo;
});

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

async function getUsers() {
    try {
        const response = await fetch('https://jsonplaceholder.typicode.com/users');
        const result = await response.text();
        const users = JSON.parse(result);
        const resultWithPrivacyRuleApplied = await applyPrivacyRule(users);
        return resultWithPrivacyRuleApplied; 
    } catch (error) {
        console.log(error);
    } finally {
        console.log('exit');
    }
}

getUsers().then((result) => { console.log(result); });

12. async를 붙이는 위치

// 1) Function Declaration
async function example1(a, b) {
  return a + b;
}

// 2-1) Function Expression(Named)
const example2_1= async function add(a, b) {
  return a + b;
};

// 2-2) Function Expression(Anonymous)
const example2_2 = async function(a, b) {
  return a + b;
};

// 3-1) Arrow Function
const example3_1 = async (a, b) => {
  return a + b;
};

// 3-2) Arrow Function(shortened)
const example3_2 = async (a, b) => a + b;

// 즉시 실행 함수
(async function print(sentence) {
  console.log(sentence);
  return sentence;
}('I love JavaScript!'));

(async function (a, b) {
  return a + b;
}(1, 2));

(async (a, b) => {
  return a + b; 
})(1, 2);

(async (a, b) => a + b)(1, 2);

14. async 함수를 작성할 때 주의해야할 성능 문제(심화)

  • 아래처럼 작성하는 경우 순서대로 각 URL 에 request를 보내고, 그 response의 내용을 출력하는 함수인데, await 문이 있기 때문에 Promise 객체가 fulfilled 상태가 될때까지는 URL에 대한 작업들이 시작될 수 없음
  • 순차적인 처리를 해야하는 경우라면 아래의 코드를 사용하는게 맞겠지만, 모든 response 내용이 잘 출력되기만 하면되고, 그 순서는 상관없는 경우라면 다르게 사용해야 한다.
async function getResponses(urls) {
  for(const url of urls){
    const response = await fetch(url);
    console.log(await response.text());
  }
}
  • 아래처럼 url 에 request를 보내고 response를 받는 코드를, 별도의 즉시 실행되는 async 함수로 감싸줌
async function fetchUrls(urls){
  for(const url of urls){
    (async () => { // 추가된 부분!
      const response = await fetch(url);
      console.log(await response.text());
    })(); // 추가된 부분!
  }
}

댓글

Designed by JB FACTORY