Javascript - Promise

자바스크립트에서 Promise는 비동기 함수의 실행 후에 해야 할 일을 관리하는 오브젝트입니다. 

사실 Promise가 없이도 비동기 함수 실행 후에 할 일을 코드로 작성할 수 있습니다. Promise에 대해 더 자세히 알아보기 전에, 우선 setTimeout 함수를 사용하여 Promise없이 비동기 함수들이 연속해서 실행되는 코드의 예를 살펴봅시다. 

가상의 작업 task 1, task 2, task 3를 실행하는 코드들과 이 작업을 실행하는 runTasks 함수가 있습니다. task 1 코드는 runTasks함수 실행 후 1초 뒤에 실행되어야 하고, task 2 코드는 task 1코드가 모두 실행된 후 2초 뒤에 실행되어야 하고, task 3 코드는 task 2 코드가 모두 실행된 후 3초 뒤에 실행되어야 합니다.

function runTasks() {
  setTimeout(function task1() {
    // task 1 코드 - 1초후 실행되어야 함
    // ...
    console.log("task 1 completed");
    setTimeout(function task2() {
      // task 2 코드 - task 1 코드 실행 후 2초 후에 실행되어야 함
      // ...
      console.log("task 2 completed");
      setTimeout(function task3() {
        // task 3 코드 - task 2 코드 실행 후 3초 후에 실행되어야 함
        // ...
        console.log("task 3 completed");
      }, 3000);
    }, 2000);
  }, 1000);
}

이런식으로 코드를 작성할 수 있겠죠. 위 예제에서는 task가 3개 밖에 안되서 위 코드에 크게 문제가 있는 것처럼 보이지 않지만, 갯수가 많아지면 1. 여러개의 비동기 함수들과 callback함수들이 겹치게 되면서 가독성이 떨어지는 문제가 있고, 2. task2함수가 task3함수를 품고 있고, task1함수가 task2함수를 품고 있으므로 task1, task2, task3은 한무더기의 함수가 되어 개별 함수의 재활용이 불가능한 문제가 있습니다. 즉 task 1만 따로 때서 실행할 수가 없게 되는 것이죠. 특히 1번의 문제는 콜백지옥(callback hell)이라는 용어가 생길 정도로 피해야 할 코딩입니다.

이를 해결하기 위해 나온 것이 Promise입니다. Promise 객체의 형태와 사용법을 알아봅시다.

function 함수명(파라메터){
  return new Promise(function(resolve, reject){ 
    //할일
  });
}

위 형태로 새로운 Promise 를 생성할 수 있으며, 할일 부분에는 비동기 함수 또는 코드가 사용됩니다. resolve와 reject는 Promise에 의해 주어지는 callback 함수로 각각 함수는 하나의 parameter를 받아 전달 할 수 있습니다. 할일의 내용이 성공적으로 끝난 경우 resolve를, 할일의 내용이 성공적이지 못한 경우에는 reject를 호출하면 됩니다.

예제를 살펴봅시다.

function myPromise(value){
  return new Promise(function(resolve, reject){
    console.log("3초 후 인사메세지를 출력합니다..");
    setTimeout(function(){    
      if(value){
        console.log("안녕하세요!");
        resolve("성공");
      }
      else {
        reject(Error("실패"));
      }
    }, 3000);
  });
}

function myResolve(value){
  console.log("resolved: ", value);
} 

function myReject(value){
  console.log("rejected: ", value);
}

myPromise(true).then(myResolve,myReject); // myResolve 실행
myPromise(false).then(myResolve,myReject); // myReject 실행

myPromise는 3초를 기다린 후 인사메세지를 출력하는 함수입니다. 실행의 성공 여부를 조절하기 위해 value를 받아서 그 값이 참에 가까우면(truthy) 인사를 하고 함수를 성공(resolve)시키고, value가 거짓에 가까우면(falsy) 인사를 하지 않고 실패(reject)시킵니다. Promise의 함수의 결과를 성공으로 하여 resolve함수를 실행시킬지 아니면 promise를 실패로 하여 reject를 실행시킬지는 전적으로 작성자의 코딩에 달려 있습니다.

Promise.then은 Promise의 실행후 이어서 할 일을 지정하는 함수입니다.  Promise함수.then(resolve처리함수,reject처리함수)의 형태로 Promise에서 resolve를 호출하면 resolve처리함수가, reject를 호출하면 reject처리함수가 호출됩니다.

then을 이어서 쓰게 되면 다음과 같은 형태로 만들 수도 있습니다.

myPromise(true)
.then(myPromise)
.then(myPromise)
.then(myResolve)
.catch(myReject);

myPromise를 3번 반복하는 코드인데, then에서 reject부분이 사라지고 대신 마지막에 .catch가 들어갔습니다. 이렇게 reject를 빼고 catch를 넣으면 3번 반복 중 어디에서 reject가 나더라도 무조건 catch로 가게 됩니다. 여기서는 같은 promise를 3번 반복했지만 실제로는 필요한 과정이 각각 입력되겠지요.

제일 처음 봤던 안좋은 예를 Promise로 바꿔 봅시다.

function task1() {
  return new Promise(function(resolve, reject){
    setTimeout(function(){
     // task 1 코드
     console.log("task 1 completed");
     resolve();
    }, 1000);
  });
}

function task2() {
  return new Promise(function(resolve, reject){
    setTimeout(function(){
     // task 2 코드
     console.log("task 2 completed");
     resolve();
    }, 2000);
  });
}

function task3() {
  return new Promise(function(resolve, reject){
    setTimeout(function(){
     // task 3 코드
     console.log("task 3 completed");
     resolve();
    }, 3000);
  });
}


task1()
.then(task2)
.then(task3)

task1, task2, task3함수가 완전히 분리되어 각각의 함수를 이해하기도 쉬워졌고 함수의 순서도 원하는대로 바꿀 수 있게 되었습니다.

위 예제에는 예제의 단순화를 위해 Promise의 실패(reject)가 없으므로 마지막으로 좀 더 실전에 가까운 예제를 살펴봅시다. 아래 예제는 실제로 실행되지는 않습니다. reject함수와 에러 처리를 어떻게 하는지에 집중하여 살펴봅시다.

Promise를 사용하지 않은 코드:

$.ajax({
  url : '/posts',
  type: 'GET',
  success : function(result1){
    var posts = result1.data;
    $.ajax({
      url : '/posts/'+posts[0]._id,
      type: 'GET',
      success : function(result2){
        var post = result2.data;
        $.ajax({
          url : '/users/'+post.author._id,
          type: 'GET',
          success : function(result3){
           var user = result3.data;
           // user로 할일 - 3
          }
        });
        // 그외 post로 할일 - 2
      }
    });
    // 그외 posts로 할일 - 1
  }
});

전체 게시물 목록을 호출한 후 게시물의 첫번째 게시물을 호출하고 해당 게시물의 작성자를 호출하는 ajax 코드입니다.

Promise를 사용하여 수정된 코드: 

function getPosts(){
  return new Promise(function(resolve, reject){
    $.ajax({
      url : '/posts',
      type: 'GET',
      success : function(result){
        var posts = result.data
        resolve(posts);
        // 그외 posts로 할일
      }
    });
  });
}

function getFirstPost(posts){
  return new Promise(function(resolve, reject){
    $.ajax({
      url : '/post/'+posts[0]._id,
      type: 'GET',
      success : function(result){
        var post = result.data
        resolve(post);
        // 그외 post로 할일
      },
    });
  });
}

function getPostAuthor(posts){
  return new Promise(function(resolve, reject){
    $.ajax({
      url : '/user/'+post.author._id,
      type: 'GET',
      success : function(result){
        var user = result.data
        resolve(user);
        // user로 할일
      }
    });
  });
}

function errorHandler(value){
  console.log("error: ", value);
}

getPosts()
.then(getFirstPost)
.then(getPostAuthor)
.catch(errorHandler);

 코드는 길어졌지만 가독성은 좋아진 것을 알 수 있습니다.

댓글

이수혁 2018.07.19
굿굿굿
I
Ian H 2018.07.23
@이수혁,
감사합니다^^
댓글쓰기

이 글에 댓글을 다시려면 SNS 계정으로 로그인하세요. 자세히 알아보기

UP