본문 바로가기
JavaScript

JavaScript - Promise 에 대해서 (수정 중..)

#Promise

Promise 는 자바스크립트에 내장된 오브젝트이며 ES6에서 도입되었습니다.

비동기 처리를 위한 callback 함수 대신에 유용하게 사용할 수 있으며, 모던 브라우져 (크롬, 파이어폭스 등)에서는

완벽하게 지원된다고 합니다.

(IE 에 대응하려면 ES5 로 트랜스파일.. 해야 한다고 하지만, 일단 이 부분은 패스)

 

#프로듀서와 컨슈머

Promise 에는 프로듀서와 컨슈머라는 개념이 있습니다.

프로듀서는 데이터를 제공한다는 의미이고, 컨슈머는 제공된 데이터를 사용한다는 의미입니다.

let userInfo = { name: 'test', age: '12' };

// 프로듀서
const test1 = function () {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(userInfo);
    }, 1000);
  });
};

// 컨슈머
test1().then((result) => {
  ...
});

 

#상태 값

Promise 는 다음과 같은 상태 값을 갖습니다. 

<pending> : 대기 >> 처리가 이행되지 않거나, 거부되지 않은 초기 상태.

<fulfilled>  : 이행 >> 처리가 성공적으로 완료된 상태

<rejected> : 거부 >> 처리가 실패한 상태

 

 

아래의 코드를 실행하면 대기 상태가 나옵니다.

Promise 는 객체를 리턴하는 함수 test1 은 setTimeout() 메서드로 인해서 딜레이가 걸려 비동기 처리가 되었기 때문에,

동기적으로 console.log 로 출력하면 아직 실행이 되지 않은 상태가 됩니다.

let userInfo = { name: 'test', age: '12' };

const test1 = function () {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(userInfo);
    }, 1000);
  });
};

test1().then((result) => {
  ...
});

console.log(test1()); // pending

 

딜레이 처리를 걷어내고 실행을 해보면 이행 상태인 fulfilled 가 출력됩니다.

let userInfo = { name: 'test', age: '12' };

const test1 = function () {
  return new Promise((resolve, reject) => {
    resolve(userInfo);
  });
};

test1().then((result) => {
  ...;
});

console.log(test1()); // fulfilled

 

그리고 resolve 대신 reject 콜백함수를 호출하게 되면 거부 상태인 rejected 가 출력됩니다.

let userInfo = { name: 'test', age: '12' };

const test1 = function () {
  return new Promise((resolve, reject) => {
    reject(userInfo);
  });
};

test1().then((result) => {
  ...;
});

console.log(test1()); // rejected

 

 

 

#then, catch, finally

프로듀서 쪽의 처리 결과를 받아서 컨슈머 쪽에서 프로미스를 실행시키면 다음과 같은 구조를 갖습니다.

promise
  .then((value) => {
    console.log(value);
  })
  .catch((error) => {
    console.log(error);
  })
  .finally(() => {
    // 처리 성공, 실패에 상관없이 항상 해야 하는 것을 구현
    console.log('finally');
  });

 

promise.then(function(){...} ) :

프로듀서 쪽에서 안전하게 처리가 끝나서 resolve 콜백함수를 호출하게 되면 then 이 받아서,

그 다음에 하고 싶은 처리를 할 수 있습니다. then() 안에 있는 함수는 resolve 콜백함수와 같습니다.

물론 arrow function 사용도 가능합니다.

 

promise.catch(function(){...}) :

catch 는 프로듀서 쪽의 reject 와 매치되어 에러를 핸들링하는데 사용됩니다.

 

promise.finally() :

finally 는 처리의 성공, 실패 유무와 상관없이 반드시 실행되기 되며, 인자 값을 갖지 않습니다.

따라서 로직에서 필요할 때 적절하게 사용하면 됩니다.

 

 

아래는 지난 포스트에 있던 callback hell 구조를 promise 를 사용하여 바꾼 코드입니다.

연습용으로 만들었기 때문에 적절한 예시는 아니지만 여러가지 배울 수 있었습니다. 

로직 순서는 아래와 같습니다.

 

1. 로그인 버튼을 누릅니다.

2. 상품 정보를 가져오는 Promise 비동기 처리가 실행됩니다.

3. 아이디와 패스워드를 가져와서, 유효성 검사를 한다. (그냥 비어있지 않으면 OK)

4. 입력 값인 아이디와 패스워드를, 디비에서 가져온 아이디와 패스워드와 비교합니다. (임의로 가정한 값)

5. 4번에서 OK 가 되면, 화면에 아이디와 나이를 뿌려줍니다.

6. 상품 정보가 화면에 표시됩니다.

 

=======로직에 헛점이 굉장히 많습니다.

=======HTML 요소를 가져와서 사용하고 있기 때문에 HTML 파일은 따로 만들어야 합니다.

 

const INPUT_CHECK_ERR_MSG = '입력 값이 잘못되었습니다.';
const USERINFO_VALIDATION = 'ID 혹은 PW 가 잘못되었습니다.';
const GETPRODUCT_ERR_MSG = '상품 정보를 가져오지 못했습니다.';

const input_name = document.querySelector('.input_name');
const input_pw = document.querySelector('.input_pw');
const login_bt = document.querySelector('.login_bt');
const input_err = document.querySelector('.input_err');
const name = document.querySelector('.name');
const age = document.querySelector('.age');
const show_product = document.querySelector('.product');
const getInfo_err = document.querySelector('.getInfo_err');
const userinput = document.querySelector('.userinput');
const show_userinfo = document.querySelector('.show_userinfo');

// 유저 정보
let userInfo = {
  name: '',
  pw: '',
  age: '',
  toString: function () {
    return this.name + ' ' + this.age;
  }
};

const getProduct_promise = function () {
  return new Promise((resolve, reject) => {
    setTimeout(function () {
      let products = new Array();
      products.push({ product_name: '에어조던1', price: '10,000' });
      products.push({ product_name: '에어조던2', price: '50,000' });
      if (!products.length) {
        reject();
      } else {
        for (let pro of products) {
          show_product.textContent = show_product.textContent + pro.product_name;
        }
        resolve(products);
      }
    }, 3000);
  });
};

const getUserInfo_promise = function () {
  return new Promise((resolve, reject) => {
    setTimeout(function () {
      let inputName = input_name.value;
      let inputPw = input_pw.value;
      if (inputName === '' || inputPw === '') {
        input_err.textContent = INPUT_CHECK_ERR_MSG;
        reject(new Error(INPUT_CHECK_ERR_MSG));
      } else {
        userInfo.name = inputName;
        userInfo.pw = inputPw;
        resolve(userInfo);
        // resolve(name, pw);
      }
    }, 1000);
  });
};

const compareUserData_promise = function (result) {
  return new Promise((resolve, reject) => {
    setTimeout(function () {
      let server_name = 'test';
      let server_pw = '123';
      let server_age = '14';
      if (result.name !== server_name || result.pw !== server_pw) {
        input_err.textContent = USERINFO_VALIDATION;
        reject(new Error(USERINFO_VALIDATION));
      } else {
        result.age = server_age;
        resolve(result);
      }
    }, 1000);
  });
};

const showUserData_promise = function (result) {
  return new Promise((resolve, reject) => {
    if (result) {
      input_err.textContent = '';
      name.textContent = name.textContent + result.name;
      age.textContent = age.textContent + result.age;
      resolve(result);
    } else {
      reject(new Error('유저 정보 표시 실패'));
    }
  });
};


//===========================HTML 버튼에 onClick="login()" 을 달아주어야 합니다.
function login() {
  // 상품 정보 가져오기
  getProduct_promise()
    .then((products) => {
      console.log('1');
    })
    .catch(function () {
      show_product.textContent = GETPRODUCT_ERR_MSG;
    });

  // 유저 정보 검증, 출력
  getUserInfo_promise()
    .then((result) => {
      console.log('2');
      return compareUserData_promise(result);
    })
    .then((result) => {
      console.log('3');
      return showUserData_promise(result);
    });
}

 

이렇게 해서 콜백 함수로만 이루어졌었던 복잡한 코드가 조금이나마 간소해졌습니다.

하지만 catch() 를 전부 구현한다면 역시 코드가 복잡해집니다.

다수의 연관된 비동기 처리를 구현할 때 promise 를 체이닝 하는 방식의 로직도 역시 그리 좋지 못한 것 같습니다.

 

#이 연습을 하면서 몇 가지 알게된 사실

resolve()

 이 콜백함수는 파라미터를 하나만 받을 수 있음. 따라서 복수의 파라미터를 넘기고 싶을 때는 오브젝트화 시켜야 함.

 reject() 역시 인자를 하나만 받는다.

 

arrow function

 arrow function 에서 생략되어있는 return 은 {} 이 블럭 안에서는 직접 명시해줘야 한다는 것.

 

느낀점

비지니스 로직은 consumer 쪽에서 하는 것이 맞는 것 같다.

arrow function 문법과 callback 함수 작동 원리에 대해서 익숙하지 않다보니, 시행착오가 많았다.