[기술면접/JS] Promise

강민혁·2023년 2월 16일
0

기술면접 | JS

목록 보기
4/17

Promise에 대해 설명하세요

Keyword

ES6, 비동기 처리, 콜백 지옥, 가독성, 에러처리, 비동기 처리 시점 명시, resolve, reject, pending, fulfilled, rejected, Promise 객체


Script

ES6 이전에는 콜백 패턴으로 비동기 처리를 수행했습니다. 하지만 콜백 지옥이라는 문제 때문에, ES6부터는 비동기 처리를 위한 패턴으로 Promise를 도입했습니다. Promise를 사용하면 콜백 패턴보다 가독성도 좋고 비동기 처리 시점을 명확하게 표현하고 에러 처리에 있어서도 기존 단점을 보완할 수 있습니다.

Promise 생성자 함수는 인자로 전달 받은 콜백 함수에서 비동기 처리를 수행합니다. 그리고 콜백 함수resolvereject를 인자로 전달받습니다. 이후 비동기 처리가 성공하면 resolve 함수를 호출하고, 실패할 경우 reject 함수를 호출합니다. 그래서 Promise는 pending, fulfilled, rejected라는 총 3개의 상태를 가집니다. 비동기 처리 결과에 따라 이 상태들이 변경되어 반환되는 Promise 객체에 담깁니다. 결국 이 반환되는 Promise 객체를 통해서 수행된 작업의 결과를 받아올 수 있습니다.


Additional

비동기 처리에서 Callback과 Promise의 차이점

callback 함수로 비동기 로직을 처리할 때는 callback 내부에서만 처리가 가능하다.

기본적으로 비동기 함수는 비동기 함수 내부에서 비동기로 동작하는 코드들의 처리 결과를 외부로 반환하거나 상위 스코프의 변수에 할당할 수 없기 때문이다.

그 이유는 비동기 함수를 호출하면, 함수 내부의 비동기로 동작하는 코드가 완료되지 않았다 하더라도 이를 기다리지 않고 즉시 종료하기 때문이다. 즉, 비동기 함수가 종료된 이후에, 내부 비동기 코드가 완료된다.

아래 예제를 보자.

let g = 0;
setTimeout(() => {g = 100;}, 0 );
console.log(g);
// 0

위 예제를 보면, setTimeout에 전달된 콜백 함수는 g 라는 변수에 100을 할당한다. 하지만 이후 console.log로 g를 출력하면 0이 출력된다.

그 이유는 setTimeout은 비동기 함수이고, 비동기 함수는 task queue에 저장되기 때문이다. 즉, call stack에 저장되는 let g = 0;console.log(g)가 먼저 실행된 이후에 setTimeout의 콜백함수가 call stack으로 이동한다.

결국 비동기 로직은 실행 순서가 불명확해지게 된다. 하지만 만약 비동기 처리 되는 로직 간에 순서가 중요해진다면 어떻게 될까?

그렇다면, 비동기 함수의 콜백 함수에서 다음 순서로 작동할 콜백 함수를 호출하면 된다.

아래 코드를 보자.

// ctrl + option + N
let g = 0;

const add = (a, b, callbackFunction) => {
  g = a + b;
  console.log(`${a} add ${b}: `, g);
  callbackFunction(g);
};

add(1, 2, (prev1) => {
  add(prev1, 3, (prev2) => {
    add(prev2, 4, (prev3) => {
      add(prev3, 5, (prev4) => {
        console.log("final: ", prev4);
      });
    });
  });
});

/*
1 add 2:  3
3 add 3:  6
6 add 4:  10
10 add 5:  15
final:  15
*/

실제로 add 함수는 비동기함수는 아니지만, 설명을 간단하게 하기 위해 비동기 함수로 가정해보자. 그리고 1부터 5까지 순서대로 더해나가야 하는 로직을 수행할 때, 위와 같은 코드가 필요하다. 즉, 비동기 함수 내부에 콜백 함수를 주입하고 순서에 맞게 작동하도록 프로그래밍할 수 있다. 이것이 바로 callback 패턴을 사용한 비동기 처리 과정이다.

그렇다면, Promise를 사용한 비동기 처리는 어떻게 될까? 아래 코드를 먼저 보자.

const add = (a, b) => {
  return new Promise((resolve, reject) => {
    resolve(a + b);
  });
};

const c = add(1, 5).then((result) => console.log("here 1: ", result));
console.log("here 2: ", c);

/*
here 2:  Promise { <pending> }
here 1:  6
*/

위 코드는 Promise를 통해 add 함수를 구현했다. 재밌는 것은 이 함수는 실제로 비동기로 처리될만한 작업이 아닌데도, 명확하게 비동기로 작동한다. 보면, here 2 부분이 먼저 출력되고, here 1 부분이 나중에 출력된다. 그 이유는 add가 Promise에 의해 비동기 로직을 포함한 함수가 되었고, 그래서 이 작업은 call stack이 아니라 task queue에 들어간다. 그래서 call stack에 들어간 here 2 부분이 먼저 출력되고, 나중에 here 1이 출력되는 것이다.

그리고 c는 Promise { <pending> } 이라는 객체를 담고 있는데, 이게 바로 Promise가 반환하는 Promise 객체이다. 여기에는 위에서 언급했듯 작업 처리 상태를 담고 있다.

그리고 Promise의 후속 처리 메서드인 then 메서드를 통해 작업이 완료된 결과인 a+b를 전달 받게 된다.

then method
then 메서드는 두 개의 콜백 함수를 인자로 전달받는데, 아래 예제를 한번 보자.

new Promise((resolve, reject) => {
  resolve("Fulfilled!!");
}).then(
  (result) => {
    console.log(result);
  },
  (error) => {
    console.error(error);
  }
);

new Promise((resolve, reject) => {
  reject(new Error("Error!!"));
}).then(
  (result) => {
    console.log(result);
  },
  (error) => {
    console.error(error);
  }
);

두개의 Promise를 작동시켰는데, 결과는 다음과 같다.

Fulfilled!!
Error: Error!!
    at /Users/minhyeok/Desktop/local-development/javascript-env/test.js:15:10
    at new Promise (<anonymous>)
    at Object.<anonymous> (/Users/minhyeok/Desktop/local-development/javascript-env/test.js:14:1)
    at Module._compile (node:internal/modules/cjs/loader:1155:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1209:10)
    at Module.load (node:internal/modules/cjs/loader:1033:32)
    at Function.Module._load (node:internal/modules/cjs/loader:868:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    at node:internal/main/run_main_module:22:47

즉, then은 첫번째 인자로 들어간 콜백 함수는 resolve 되었을때, resolve 된 값을 넘겨주면서 호출되고, 만약 resolve되지 않고 reject 되었다면, reject에 넣어진 값을 콜백 함수에 넘겨주며 두번째 인자로 들어간 콜백 함수가 호출된다.

그럼 아까와 같은 순차적인 비동기 처리가 필요할 때는 다음과 같은 코드를 볼 수 있다.

let g = 0;

const add = (a, b) => {
  return new Promise((resolve, reject) => {
    g = a + b;
    console.log(`${a} add ${b}: `, g);
    resolve(a + b);
  });
};

add(1, 2)
  .then((result1) => add(result1, 3))
  .then((result2) => add(result2, 4))
  .then((result3) => add(result3, 5));

/*
1 add 2:  3
3 add 3:  6
6 add 4:  10
10 add 5:  15
*/

훨씬 가독성도 좋아지고, 쓸데없이 반복적인 들여쓰기도 존재하지 않으면서, 처리 시점도 명확하게 파악할 수 있는 코드로 변모했다.

길게 설명했지만, 지금까지의 내용이 기본적으로 callback 패턴과 Promise의 비동기 처리 방식의 차이라고 할 수 있다.


Reference

BOOK - modern javascript deep dive

profile
with programming

0개의 댓글