본문 바로가기
Language/JavaScript

[JS] Promise란?

by 오우영 2021. 7. 2.
Promise
프로미스는 자바스크립트에서 제공하는 비동기를 간편하게 처리할 수 있도록 도와주는 객체

정해진 시간의 기능을 수행하고 정상적으로 기능이 수행이 됐다면 성공의 메세지와 함께 처리된 결과값을 전달
기능을 수행하다가 예상치 못한 문제가 발생했다면 Error를 전달

State : pending(보류) -> fulfilled(이행) or rejected(거부)

 

Promise는 클래스이기 때문에 new라는 키워드를 이용해서 Object를 생성할 수 있다

 

const promise = new Promise()

 

 

Promise의 Constructor안을 보면 executor라는 콜백 함수를 전달해 줘야하고 이 콜백 함수에는 또 다른 두 가지의 콜백  함수를 받는다
-> 새 Promise가 생성되면 executor 콜백 함수는 자동으로 실행된다 !
기능을 정상적으로 수행해서 마지막 최종 데이터를 전달하는 resolve 콜백 함수
기능을 수행하다가 문제가 생기면 호출하게 될 reject 콜백 함수

 

Callback으로 비동기 요청을 처리했을 때 발생하는 문제

 

let a = null;
let b = null;

A(function(res){
  a = res;
  if(a !== null && b !== null){
    console.log(a + b);
  }
});
B(function(res){
  b = res;
  if(a !== null && b !== null){
    console.log(a + b);
  }
});

 

여러 개의 비동기를 호출한 경우 A가 먼저 도착할지, B가 먼저 도착할지 아무도 알 수 없습니다
이를 방지하기 위해 a, b의 변수 값이 세팅되었는지 확인하는 작업을 각 콜백에 삽입해야 하는데, 관련 비동기 호출이 증가할수록 건드려야 하는 코드의 양이 기하급수적으로 늘어나 좋은 방법이 아닙니다
-> Callback 대신 Promise로 비동기를 처리하면 좋은 이유 !

 

Promise Callback
이전 함수의 결과에 따라 그 다음(.then)에 무엇을 할지에 대한 코드를 작성하면 된다 이전 함수에 호출 결과로 무엇을 할지 미리 알고 있어야 한다
-> 순서를 임의로 지정해주어야한다
프로미스에 원하는 만큼 .then을 호출할 수 있다 콜백은 하나만 가능하다
Promise를 사용하면 흐름이 자연스럽고 유연한 코드를 작성할 수 있다

 

Promise 동작 원리

 

fs.readFile를 이용하여 데이터를 받아오고 비동기 처리를 위해 프로미스 대신에 콜백 함수 사용 예시

 

fs.readFile(path[, options], callback)
fs.readFile 메소드 비동기적으로 파일 내용 전체를 읽습니다
- path <string> | <Buffer> | <URL> | <integer>
  - path에는 파일 이름을 인자로 넘길 수 있습니다. 네 가지 종류의 타입을 넘길 수 있지만 보통은 문자열(<string>)로 넘깁니다
- options <Object> | <string>
  - 대괄호로 감싼 두번째 인자 options는 넣을 수도 있고, 넣지 않을수도 있습니다
- callback <Function>
  - err <Error>
  - data <string> | <Buffer>
  - 콜백 함수에는 두가지 파라미터가 존재합니다.
    - 에러가 발생하지 않으면 err는 null 이 되며, data에 문자열이나 Buffer라는 객체가 전달될 것입니다

 

const getDataFromFile = function (filePath, callback) {
  fs.readFile(filePath, "utf8", function (err, file) {
    if (err) {
      // 에러가 발생했을 때, 첫번째 파라미터 값을 전달
      callback(err, null);
    } else {
      // 데이터를 성공적으로 받아오면, 두번째 파라미터에 처리된 결과값을 전달
      callback(null, file.toString());
    }
  });
};

// 기능이 정상적으로 수행되면 data에 file.toString() 값이 전달
// err라면 data에 null이 전달
getDataFromFile('README.md', (err, data) => console.log(data));

 

위 코드에 프로미스를 적용한 예시

 

const getDataFromFilePromise = filePath => {
  return new Promise((resolve, reject) => {
    fs.readFile(filePath, "utf8", function (err, file) {
      if (err) {
        // 예상치 못한 문제가 발생하면 error를 전달
        reject(err);
      } else {
        // 정상적으로 동작하면, 성공의 메세지와 함께 처리된 결과값 전달
        resolve(file.toString());
      }
    });
  });
};

// 데이터를 성공적으로 받아오면 then을 통해 처리된 결과값을 전달 할 수 있다
// 그렇지 않다면 catch
getDataFromFilePromise('README.md').then(data => console.log(data));

 

위 예시는 프로미스의 동작과정을 좀 더 쉽게 이해하기 위한 코드로 실제 프로미스를 이용할 때엔 위와 같이 if(err), else와 같이 조건문을 달지 않아도 자동으로 성공했을 땐 resolve로 전달되고 실패했을 땐 reject로 에러를 전달해준다

 

const promise = new Promise((resolve, reject) => {
  // 시간이 오래 걸리는 무거운 처리 (network, read files ..)
  console.log(`doing something...`);
  setTimeout(() => {
    resolve(`wooyoung`); // 성공 하면
    // reject(new Error(`no network`)); // 실패 하면
  }, 2000);
});

 

then, catch, finally

then과 catch로 결과를 받습니다. resolve(결과)의 결과가 then의 result로 가고, reject(err)의 err이 catch의 err로 갑니다

 

체인 마지막의 .catch는 try..catch와 유사한 역할을 합니다. .then 핸들러를 원하는 만큼 사용하다 마지막에 .catch 하나를 붙이면 .then 핸들러에서 발생한 모든 에러를 처리할 수 있습니다
.then첫 번째 인수는 프로미스가 이행되었을 때 실행되는 함수이고, 여기서 실행 결과를 받습니다
.then두 번째 인수는 프로미스가 거부되었을 때 실행되는 함수이고, 여기서 에러를 받습니다
.catch로 에러가 발생한 경우만 다루고 싶다면 .then(null, errorHandlingFunction) 같이 null을 첫 번째 인수로 전달하면 됩니다.
.catch(errorHandlingFunction)를 써도 되는데, 이는 .then에 null을 전달하는 것과 동일하게 작동합니다

 

promise
  .then((value) => {
    console.log(value);
  })
  .catch(error => {
    console.log(error);
  })
  .finally(() => { // 성공하든 실패하든 마지막에 실행됨
    console.log(`finally`);
  })

 

promise chaining(연결하기)

https://ko.javascript.info/promise-chaining

new Promise(function(resolve, reject) {

  setTimeout(() => resolve(1), 1000); // (*)

}).then(function(result) { // (**)

  console.log(result); // 1
  return result * 2;

}).then(function(result) { // (***)

  console.log(result); // 2
  return result * 2;

}).then(function(result) { // (****)

  console.log(result); // 4
  return result * 2;

});
위 예시는 아래와 같은 순서로 실행됩니다
1. 1초 후 최초 프라미스가 실행됩니다. – (*)
2. 이후 첫번째 .then 핸들러가 호출됩니다. –(**)
3. 2에서 반환한 값은 다음 .then 핸들러에 전달됩니다. – (***)
4. 3에서 반환한 값은 다음 .then 핸들러에 전달됩니다. - (****)

 

.then은 값을 바로 전달 할 수도 있고, 리턴으로 Promise를 전달해도 됩니다

 

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

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

 

Error handling

- catch로 error를 핸들링 할 수 있다
- error가 발생해도 catch를 이용하면 전체적인 Promise 체인에 문제가 발생하지 않도록 빵꾸처리가 가능하다

 

const getHen = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(`🐓`), 1000);
  });
};
const getEgg = hen =>
  new Promise((resolve, reject) => {
  	// 정상적으로 데이터를 받아올 때 (1)
    // setTimeout(() => resolve(`${hen} => 🥚`), 1000);
    
  	// 에러발생
    setTimeout(() => reject(new Error(`error! ${hen} => 🥚`)), 1000);
  });
const cook = (egg) => 
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(`${egg} => 🍗`), 1000);
  });

getHen()
  .then(hen => getEgg(hen))
  .catch(error => { // catch가 없다면 (2) 있으면 (3)
    console.log(error);
    return `🥖`; // 빵꾸처리
  })
  .then(egg => cook(egg))
  .then(result => console.log(result))
  .catch(error => {console.log(error)})

 

1. 정상적으로 기능이 수행될 때
🐓 => 🥚 => 🍗
2. 중간에 에러가 발생했을 때
error! 🐓 => 🥚
3. catch를 이용해 Promise 체인에 문제가 발생하지 않게 처리했을 때
🥖 => 🍗
마지막에 .catch를 붙이면 .then 핸들러에서 발생한 모든 에러를 처리
중간에 붙이면 해당하는 .then 핸들러에서만 에러 처리

 

throw로 에러를 발생시키면 Promise 체이닝을 끊을 수도 있다
이 경우 뒤의 then들이 호출되지 않고 바로 catch로 넘어간다

 

new Promise((resolve, reject) => {
  resolve(true);
}).then((result) => {
  if (result) {
    throw new Error('도중에 중단!');
  }
}).then(() => {
  console.log('실행되지 않아요');
}).catch((err) => {
  console.error(err);
});

 

promise.all

promise.all 메소드로 여러 프로미스 객체들을 한번에 모아서 처리할 수도 있습니다
이 메소드는 모든 프로미스가 성공하면 then, 하나라도 실패하면 catch로 연결됩니다

 

let p1 = Promise.resolve('wooyoung'); // new Promise 없이 성공한 Promise 객체를 만드는 방법
let p2 = Promise.resolve('Oh!');
let p3 = Promise.reject('error'); // new Promise 없이 실패한 Promise 객체를 만드는 방법

Promise.all([p1, p2, p3]).then((result) => {
  console.log(result); // 만약 p3가 resolve였다면 ['wooyoung', 'Oh!', 'error']
}).catch((err) => {
  console.error(err); // error (p3가 reject이기 때문)
});

 

promise.race

promise.race는 여러 프로미스 중 가장 빨리 성공하거나 실패한 결과를 보여줍니다

 

let p4 = new Promise((resolve, reject) => {
  setTimeout(resolve, 400, "1");
});
let p5 = new Promise((resolve, reject) => {
  setTimeout(resolve, 300, "2");
});
let p6 = new Promise((resolve, reject) => {
  setTimeout(reject, 500, "error");
});
Promise.race([p4, p5, p6]).then(
  (result) => {
    console.log(result); // 2
  },
  (err) => {
    console.error(err); // p6가 p4나 p5보다 더 빠르면 실패한 걸로
  }
);

 

프로미스의 3가지 상태

여기서 말하는 상태란 프로미스의 처리 과정을 의미
1. Pending(대기) : 비동기 처리 로직이 아직 완료되지 않은 상태
2. Fulfilles(이행) : 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태
3. Rejected(실패) : 비동기 처리가 실패하거나 오류가 발생한 상태

 

MDN

Pending(대기)

executor는 아래 그림과 같이 프로미스의 상태를 둘 중 하나로 변화시킵니다

https://ko.javascript.info/promise-basics#ref-260

 

아래와 같이 new Promise() 메서드를 호출하면 대기(Pending) 상태가 됩니다

 

new Promise();

 

new Promise() 메서드를 호출할 때 콜백 함수를 선언할 수 있고, 콜백 함수의 인자는 resolve, reject입니다

 

Fulfilled(이행)

https://ko.javascript.info/promise-basics#ref-260

 

콜백 함수의 인자 resolve를 아래와 같이 실행하면 이행(Fulfilled) 상태가 됩니다

 

new Promise(function(resolve, reject) {
  resolve();
});

 

그리고 이행 상태가 되면 아래와 같이 then()을 이용하여 처리 결과 값을 받을 수 있습니다

 

function getData() {
  return new Promise(function(resolve, reject) {
    var data = 'done';
    resolve(data);
  });
}

// resolve()의 결과 값 data를 resolvedData로 받음
getData().then(function(resolvedData) {
  console.log(resolvedData); // done
});

 

Rejected(실패)

 

https://ko.javascript.info/promise-basics#ref-260

 

new Promise()로 프로미스 객체를 생성하면 콜백 함수 인자로 resolve와 reject를 사용할 수 있다고 했습니다. 여기서 아래와 같이 호출하면 실패(rejected) 상태가 됩니다

 

new Promise(function(resolve, reject) {
  reject();
});

 

그리고, 실패 상태가 되면 실패한 이유(실패 처리의 결과 값)를 catch()로 받을 수 있습니다

 

function getData() {
  return new Promise(function(resolve, reject) {
    reject(new Error("Request is failed"));
  });
}

// reject()의 결과 값 Error를 err에 받음
getData().then().catch(function(err) {
  console.log(err); // Error: Request is failed
});

 

'Language > JavaScript' 카테고리의 다른 글

[JS] Window, DOM, BOM  (0) 2021.07.08
[JS] 호이스팅이란?  (0) 2021.07.07
[JS] undefined, null  (0) 2021.06.24
[JS] Iterable object, Array-like object  (0) 2021.06.23
[JS] ES6 특징  (0) 2021.06.22

댓글