본문 바로가기
Front-end/Vanilla JS

JS 근본 공부 - Promise

by devraphy 2020. 11. 6.

1. Promise란 무엇인가?

- Promise라는 단어의 뜻처럼, 코드가 동작하는 순서를 약속하는 약속의 객체라고 표현할 수 있다. 

- JavaScript에서 제공되는 Object(=객체) 중 하나로, 콜백을 사용하지 않고 비동기식 처리를 구현하는 방법이다. 

 

a) 어떻게 사용되는가? 

- 요즘은 맛집을 가보면 promise가 사용되는 것을 볼 수 있다. 대기손님이 많을 경우, 번호표를 나눠주는 것이 아니라 웨이팅 리스트에 자신의 정보를 등록하고 자리가 나면 시스템에 의해 자동으로 문자를 받는 것을 경험한 적이 있을 것이다.

 

→ 영업시간동안(정해진 시간동안) 자리가 비워지면 대기순서대로 알림이 가는 기능을 promise로 구현한 것이다.  

 

- 인터넷 강의에서도 promise가 사용되는 것을 볼 수 있다. 기간제로 또는 기수로 운영되는 인터넷 강의 같은 경우, 이미 강의가 진행중이라면 다음 기수에 참가할 수 있도록 waiting list 또는 pre-registration 등록을 통해 기존의 강의가 종료되고 새로운 강의가 열렸을 때, 자동으로 문자알림이나 이메일 등을 받을 수 있는 시스템이 바로 promise의 한 예다. 

 

→ 강의가 종료되고 새로운 강의를 모집하기 시작할 때(정해진 시간), 미리 등록해 놓은 수강생에게 문자알림이나 이메일을 보낸다는 기능을 promise로 구현한 것이다. 

 

 


2. Promise 사용방법 

- Promise를 사용할 때 신경써야할 2가지 요소가 있다. 

 

1. state(상태): 프로세스가 operation을 수행중인지, 기능수행을 성공했는지 실패했는지 등의 상태
- Pending(수행중)
- Fulfilled(수행 완료)
- Rejected(수행 실패)

 

2. Producer vs Consumer: 정보를 제공하는 producer와 정보를 제공받는 consumer의 입장차이를 이해해야 한다.

 

 

a) Promise 만들기

- Promise는 class로 존재하기 때문에,  new 생성자를 이용해 object를 만든다.

- Promise의 생성자를 보면 executor라는 callback 함수를 들어 있는데, 이 executor에는 resolve(성공시 결과값을 전달)와 reject(실패시 에러를 전달)라는 callback함수다.

 

 

const promise = new Promise((resolve, reject) => {
  console.log("doing something...");
});

- promise 안에서는 heavy한 일들이 수행된다. 네트워크에서 데이터를 받아오거나 크기가 큰 파일을 읽어오는 과정은 시간이 오래 걸리므로 동기적으로 처리하기 불편하기 때문이다. 그러므로 이렇게 시간이 오래걸리는 과정들은 비동기적으로 처리하는 것이 좋다.

 

 

[출력결과]

 

- promise 객체를 생성한 후 사용하거나 호출하지 않았음에도 promise 내부의 executor라는 callback함수가 바로 실행되는 것을 확인할 수 있다. 여기서 promise의 중요한 특징 한가지를 알 수 있는데, promise는 생성과 동시에 실행된다는 것이다. 그러므로 사용자의 입력이나 요청과 같이 특정 조건에서 필요한 기능이 아닌 바로 실행이 필요한 기능을 작성할 때만 사용된다.

 

- 중요한 특징이기에, 불필요한 네트워크 통신을 만들지 않도록 잊지 말자.

 

 

b) resolve

- executor 내부에 존재하는 callback함수로, executor가 성공적으로 수행을 완료한 경우 결과 값을 전달한다.

 

 

[resolve 예시]

const promise = new Promise((resolve, reject) => {
    console.log("doing something...");
    setTimeout(() => {
        resolve('Raphael');
    }, 2000);
});


// 2. Consumer
promise.then((value) => { // value라는 parameter는 resolve가 전달하는 값이다.
    console.log(value);
})

- promise가 잘 작동되어 executor가 잘 수행되면 'Raphael'이라는 문자열을 출력하도록 작성하였다. 

- Consumer는 promise의 결과값을 전달받아 사용하는 관점을 의미하고 실제로 사용자의 관점이기도 하다. 

- promise의 결과값은 then, catch, finally를 이용해 전달받을 수 있다. 

 

 

[출력결과]

 

 

c) reject

- executor 내부에 존재하는 callback함수로, executor에서 에러가 발생하거나 수행을 실패한 경우 결과값을 전달한다. 

- reject는 보통 error Object를 통해 에러 메세지를 전달한다.  

 

 

[reject 예시]

const promise = new Promise((resolve, reject) => {
  console.log("doing something...");
  setTimeout(() => {
    reject(new Error("no network"));
  }, 2000);
});


promise.then((value) => {
  console.log(value);
});

 

 

[출력결과]

- 원하던 대로 실패한 경우 reject가 잘 작동한 것일까?  Uncaught의 의미가 무엇일까? 무엇이 잡히지 않았다는 얘기일까?

- 여기서 한가지 간과한 요소가 존재한다. 

- 바로 customer에서 성공한 경우만 처리하고, 실패한 경우를 처리하지 않았다 것이다. 어떻게 처리해야 할까?

 

 

[customer에서 실패경우 처리예시]

promise
  .then((value) => {
    // value - resolve가 전달하는 값 (성공 케이스)
    console.log(value);
  })
  .catch((error) => {
    // error - reject가 전달하는 값 (실패 케이스)
    console.log(error);
  });

 

 

[출력결과]

- 아까와는 다르게 깔끔하게 console에 출력되는 것을 확인할 수 있다. 

 

 

d) then은 뭐고 catch는 뭘까?

 

[then API 문서]

- 위의 문서에 적힌 설명을 읽어보면 onfulfilled(성공)인 경우 value를 paramter로 사용하고, onrejected(실패)인 경우 reason을 사용한다고 나와 있다. 맨 마지막에 @returns 부분을 읽어보면 실행된 callback이 완료될 경우, 그 결과값을 갖고 있는 promise를 반환한다는 것을 알 수 있다.

 

- 하지만, 위의 예시에서는 reason을 parameter로 사용하지 않았기 때문에 실패케이스를 처리하기 위해서 catch를 사용한 것이다. 

 

 

[catch API 문서]

- @param onrejected 부분의 설명을 읽어보면, catch는 promise가 실패한 경우 실행되는 callback이라고 명시되어 있다. 

- 그리고 여기서 말하는 callback은 catch함수의 매개변수로서 작성된 callback함수를 의미한다. 

- 이와 같은 이유로 위에서 보았던 promise - reject의 예시처럼 catch를 사용하게 된것이다.

 

 

[처리과정 설명]

promise
  .then((value) => {
    // value - resolve가 전달하는 값 (성공 케이스)
    console.log(value);
  })
  .catch((error) => {
    // error - reject가 전달하는 값 (실패 케이스)
    console.log(error);
  });

- promise가 선언과 동시에 실행이 되고, 그 결과값을 customer가 전달받게 된다. 

- promise의 결과값을 then을 통해 반환 받은 후 성공케이스의 경우 value로 처리, 실패한 경우에는 error로 처리되는 것이다. 

 

* chaining - .(점)연산자를 활용하여 기능을 묶어서 처리하는 것을 chaining이라고 한다. 

 

e) finally는 무엇일까?

- promise의 결과를 처리하는 방식 중 하나로, 새롭게 추가된 요소이다. 

- promise의 성공 또는 실패여부와는 상관없이 무조건 호출되는 callback이다. 

- promise의 결과여부와는 상관없이 항상 실행되는 기능을 작성하고 싶을 때 finally를 사용한다.

 

promise
  .then((value) => {
    // value - resolve가 전달하는 값 (성공 케이스)
    console.log(value);
  })
  .catch((error) => {
    // error - reject가 전달하는 값 (실패 케이스)
    console.log(error);
  })
  .finally(() => {
    // promise의 결과와는 상관없이 무조건 실행되는 callback
    console.log("finally!");
  });

 

 

[출력결과]

 


3. Promise chaining

 

[예시]

const fetchNumber = new Promise((resolve, reject) => {
  setTimeout(() => resolve(1), 1000);
});

fetchNumber
  .then((num) => num * 2)
  .then((num) => num * 3)
  .then((num) => {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(num - 1), 1000);
    });
  })
  .then((num) => console.log(num));

 

- 위의 코드는 다음과 같은 순서로 동작한다. 

1. fetchNumber라는 promise가 실행된다.

2. 성공케이스의 경우 1초 뒤에 1이라는 숫자를 반환한다.

3. customer에게 반환된 숫자는 3가지의 연산이 진행된다. 

4. 1 * 2 

5. 2 * 3

6. 새로운 promise를 생성한 뒤 성공케이스로 6 - 1의 연산 결과값이 1초 뒤에 반환되도록 한다. 

7. 최종적으로 계산된 값(5)이 출력된다.  

 

 

[출력결과]

- 마지막에 5가 출력된 것을 확인할 수 있다. 

- 이처럼 다른 비동기적 요소들을 묶어서 처리할 수 있다. 이것을 promise chaining이라고 한다. 

 

 


4. Error Handling - 오류를 잘 처리하는 방법 

- promise를 사용할 때에는 성공/실패여부에 따른 결과를 잘 처리하는 것이 굉장히 중요하다. 

- 그러므로 다음 예제를 통해 오류를 잘 처리하는 방법을 알아보자. 

 

 

[예시 - producer]

const getHen = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve("🐔"), 1000);
  });

const getEgg = (hen) =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(`${hen} => 🥚`), 1000);
  });

const cook = (egg) =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(`${egg} => 🍳`), 1000);
  });

- 위의 코드처럼 총 3가지의 promise를 반환하는 함수이 있다. 

- 이를 바탕으로 다음과 같은 customer를 작성한다. 

 

 

 

[예시 - customer]

getHen()
  .then((hen) => getEgg(hen))
  .then((egg) => cook(egg))
  .then((meal) => console.log(meal));

- 위에서 작성한 코드가 총 3초가 지난 후 잘 출력이 되는지 확인해보자. 

 

 

 

[출력결과] 

- 기대한 대로 잘 출력되는 것을 확인할 수 있다. 

 

 

[꿀팁]

- 함수를 작성할 때 반환값을 받아서 이를 다른 함수의 매개변수로 바로 사용하는 경우, 다음과 같이 생략할 수 있다.  

getHen().then(getEgg).then(cook).then(console.log);

 

a) producer의 두번째 함수인 getEgg()에서 오류가 발생한 경우, 어떻게 처리해야 할까?

// PRODUCER
const getHen = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve("🐔"), 1000);
  });

const getEgg = (hen) =>
  new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error(`error! ${hen} => 🥚`)), 1000);
  });

const cook = (egg) =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(`${egg} => 🍳`), 1000);
  });
  
  //CUSTOMER
  getHen().then(getEgg).then(cook).then(console.log).catch(console.log);

 

- 위의 코드에서 처럼, 가장 마지막 부분에서 에러처리를 한다.

- 위에서부터 에러가 타고 내려오기 때문에 가능한 것이다. 

 

 

b) 에러가 난 경우, 즉각적으로 대처할 수 있도록 코드를 짤 수 있을까?

- 에러가 나는 것을 대비해서 즉각적으로 대처하는 방법은, customer 부분에서 에러가 날 수 있는 부분마다 처리과정을 작성하는 것이다. 아래의 코드 예시와 출력결과를 살펴보자. 

getHen()
  .then(getEgg)
  .catch((error) => {
    return "🍔";
  })
  .then(cook)
  .then(console.log)
  .catch(console.log);

- 위의 코드처럼, 과정 중간에서 발생된 에러를 즉각 처리하여 온전하게 최종 결과까지 도달하는 것을 볼 수 있다. 

 


5. 콜백지옥을 천국으로 만들기 

- 이전 포스팅에서 callback hell에 대해 설명한 적이 있다. 

- 그때 사용했던 callback 무더기의 소스코드를 promise를 사용해 깔끔한 코드로 개선시켜보자. 

 

 

[콜백지옥 소스코드]

class UserStorage {
  loginUser(id, password, onSuccess, onError) {
    // 로그인 검증 함수
    // 로그인 성공시 onSuccess 콜백 함수 호출
    // 로그인 실패시 또는 에러발생시 onError 콜백 함수 호출
    setTimeout(() => {
      if (
        // 로그인 유효성 검증을 간단한 코드로 구현
        // 아래 id와 password에 포함되지 않은 경우 에러가 발생하는 시나리오
        (id === "kevin" && password === "kevin123") ||
        (id === "admin" && password === "master")
      ) {
        onSuccess(id);
      } else {
        onError(new Error("not found"));
        // 에러 발생을 위한 에러 객체 생성
      }
    }, 2000); // 로그인 과정을 가짜로 구현하기 위한 2초의 시간 delay
  }

  getRoles(user, onSuccess, onError) {
    // 사용자 마다의 역할/권한 정보를 받아오는 함수
    setTimeout(() => {
      if (user === "kevin") {
        onSuccess({ name: "kevin", role: "developer" });
      } else {
        onError(new Error("not found"));
      }
    }, 1000);
  }
}

// 동작부분 코딩
const userStorage = new UserStorage();
const id = prompt("enter your id: ");
const password = prompt("enter your password: ");
userStorage.loginUser(
  id,
  password,
  (user) => {
    userStorage.getRoles(
      user,
      (userWithRole) => {
        alert(
          `Hello, ${userWithRole.name}. You have a ${userWithRole.role} role.`
        );
      },
      (error) => {
        console.log(error);
      }
    );
  },
  (error) => {
    console.log(error);
  }
);

 

 

- 위의 코드가 콜백지옥이 된 이유는, 코드수행결과에 따른 성공/실패여부를 처리하는 과정이 callback으로 도배되어 있기 때문이다. 그렇다면 onSuccess와 onError를 Promise로 대체할 수 있지 않을까? 왜냐면 Promise가 갖고 있는 기능이 비동기적 연산에 대한 성공/실패여부 처리기능이니까. 

 

 

 

[개선된 코드]

"use strict";

// Producer
class UserStorage {
  loginUser(id, password) {
    // 기존 코드의 성공,실패여부를 관리하던 메소드를 promise로 대체한다.
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (
          (id === "kevin" && password === "kevin123") ||
          (id === "admin" && password === "master")
        ) {
          resolve(id);
        } else {
          reject(new Error("not found"));
        }
      }, 2000);
    });
  }

  getRoles(user) {
    // 기존 코드의 성공,실패여부를 관리하던 메소드를 promise로 대체한다.
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (user === "kevin") {
          resolve({ name: "kevin", role: "developer" });
        } else {
          reject(new Error("not found"));
        }
      }, 1000);
    });
  }
}

// Customer
const userStorage = new UserStorage();
const id = prompt("enter your id: ");
const password = prompt("enter your password: ");

userStorage
  .loginUser(id, password)
  .then(userStorage.getRoles) // .then((user) => userStorage.getRoles(user));
  .then((user) => alert(`Hello, ${user.name}. You have a ${user.role} role.`))
  .catch(console.log); // .catch((error) => console.log(error));

 

 

 

[출력결과] 

성공케이스
실패케이스

 

 

[총평]

- 코드 자체의 구조가 굉장히 깔끔해졌다.

- resolve와 reject가 담당하는 역할이 명확하기 때문에 가독성이 좋아졌다. 

 

 

 

[정보출처]

 

www.youtube.com/watch?v=JB_yU6Oe2eE&t=6s

'Front-end > Vanilla JS' 카테고리의 다른 글

JS 근본 공부 - async & await  (0) 2020.11.09
JS 근본 공부 - 콜백(Callback)  (0) 2020.11.05
JS 근본 공부 - JSON  (0) 2020.11.04
JS 근본 공부 - 배열 API 연습문제  (0) 2020.11.03
JS 근본 공부 - 배열(Array)  (0) 2020.11.02

댓글