ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Promise를 이용한 비동기 자바스크립트
    Web/자바스크립트 2021. 2. 21. 04:22
    반응형

    Promise는 Callbak Hell에서 벗어나, 비동기 자바스크립트를

    좀 더 효율적으로 다루기 위해 ES6에서 새로 생겨난 개념이다.

    정확히는, 좀 더 깔끔한 비동기 코드를 만들기 위해서 생겨난 개념이다.

     

    Promise는 특정 이벤트가 발생했는지 / 아닌지를 추적하는 객체이다.

    만약 그 이벤트가 발생하면, Promise는 다음에 무엇(future value, 미래 값)이 발생할지를 결정해준다.

    여기서 이벤트는 비동기 이벤트, 즉 타이머 종료나 AJAX 호출로부터 데이터가 오는 등의 이벤트를 의미한다.

     

    만약 우리가 '야, background 에서 서버로부터 데이터 좀 가져와줘' 라고 요청하면

    Promise는 우리에게 그 데이터를 가져다 주겠다고 약속(promise)한다.

    그러면 우리는 미래에 그 데이터를 다룰 수 있게 되는 것이다.

     

    Promise가 미래의 값을 다루지만, 우리가 다루는 코드들은 현재 시간에 민감한 코드들이다.

    그래서, 자바스크립트는 promise로 하여금 몇 가지의 다른 상태들을 가질 수 있게 해주었다.

    우선, 이벤트가 발생하기 전에는 promise는 'pending' 상태에 있다.

    그러다가 이벤트가 발생하고나면 promise는 'resolved(=settled)' 상태가 된다.

    Promise가 결과를 가져오는 데에 성공해서, 우리가 결과를 이용할 수 있게 되면 이 때는 'fulfilled' 상태라고 부른다.

    반대로, 이 때 성공하지 못하고 에러가 발생하면 'rejected' 상태가 된다.

    이게 promise가 가질 수 있는 상태의 전부다. 상당히 간단하다.

     

    간단하지만, 우리가 코드로 'resolved'와 'rejected'에 관해 다뤄야 하기 때문에

    이 둘을 아는 것은 꽤나 중요하다.

     

    보통 promise는 'produce'와 'consume'을 할 수 있다.

    우리가 promise를 produce한다고 하면, 새로운 promise를 생성하고 해당 promise를 이용하여 결과를 전송한다.

    반대로, promise를 consume한다고 하면,

    우리는 콜백 함수를 이용하여 promise의 fulfillment와 rejection을 다루게 된다.

     

    사실 대부분의 라이브러리에서 Promise를 produce한 채로 제공해주기 때문에

    우리는 주로 consume을 하는 경우가 많겠지만,

    아직은 배우는 단계이므로 produce하는 과정까지 예제로 살펴보자.


    new 연산자를 통해 Promise 객체를 생성할 때,

    우리는 executor라고 불리는 콜백 함수를 인자로 넘겨준다.

    이 executor 함수는 Promise가 생성되자마자 바로 호출된다.

    const getIDs = new Promise((resolve, reject) => { 
      // executor function
    });

    executor 함수는 resolvereject, 두 개의 함수를 인자로 넘겨받는다.

    executor 함수가 promise에게, 다루고 있는 이벤트가 성공했는지 / 실패했는지를 알려주는 역할을 하기 때문.

    (성공 시 resolve 함수를 호출, 실패 시 reject 함수를 호출)

     

    우리는 Promise 내부에서 setTimeout을 이용해서 AJAX 호출을 흉내낼 것이다.

    AJAX 호출을 통해 recipeID를 가져오고, recipeID를 이용해 특정 요리의 🍔레시피🍕를 가져오는

    일련의 과정을 시뮬레이션할 것이다.

     

    setTimeout에서 설정한 1.5초가 지나면 recipeID들을 받아오고 싶은데,

    이를 위해 resolve 함수를 호출해줄 것이다.

    resolve 함수는 이벤트가 성공했을 때 호출된다고 했으므로,

    resolve 함수를 호출한다는 건 promise를 fulfilled(= successful)로 만든다는 뜻이다.

     

    resolve 함수는 promise의 결과를 인자로 받는다.

    즉, 성공했을 경우에 우리가 promise로부터 어떤 결과를 돌려받을 것인가를 다루는 것이다.

    우리는 여기서 recipeID들로 이루어진 배열을 받기를 원하므로, resolve 함수의 인자로 해당 배열을 넣어준다.

     

    참고로 예제에서 사용해준 setTimeout의 경우는 실패하는 경우가 없으므로

    이 경우에는 우리가 reject 함수를 호출할 필요가 전혀 없다.

    다만, 나중에 서버로부터 날아오는 데이터를 다루거나 할 때는 

    매우 다양한 종류의 에러들이 발생할 수 있기 때문에, 그 때는 reject 함수가 필요해질 것이다.

    const getIDs = new Promise((resolve, reject) => { // executor function
      setTimeout(() => {
        resolve([523, 863, 432, 974]);
      }, 1500);
    });

    ■ 이렇게, recipeID의 배열을 만들어내는 Promise를 간단히 produce 해봤다.

    이제는 이 promise를 consume할 차례인데, consume을 위해서는 우리가

    Promise 객체의 두 가지 메소드를 활용할 수 있다.

    그것은 바로 then 메소드🔥catch 메소드🔥.


    Then 메소드는 promise가 fulfilled된 경우(결과가 발생하는 경우)를 대비한 이벤트 핸들러를 추가시켜준다.

    그러면 이 then 메소드에서 우리가 해줄 것은, 'promise가 successful인 경우를 대비하는' 콜백 함수를 넘겨주는 것.

    getIDs.then(IDs => {
      console.log(IDs); // [523, 883, 432, 974]
    });

    이 콜백 함수는 successful promise의 결과인자로 받을 수 있다.

    이 예제의 경우에는, 우리가 받고자 했던 [523, 883, 432, 974]라는 배열 전체이다.

    이 결과를 'IDs'라는 이름의 인자로 넘겨받기로 한다.

     

     Catch 메소드는 promise가 rejected된 경우(에러가 발생하는 경우)를 대비한 이벤트 핸들러를 추가시켜준다.

    Catch 메소드는 발생하는 에러를 인자로 받아올 수 있다.

    getIDs
    .then(IDs => {
      console.log(IDs); 
    })
    .catch(error => {
      console.log(error);
    });
    

    하지만 우리의 코드는 setTimeout을 통해 resolve되는 경우만 존재하므로

    억지로 실패 케이스를 발생시키기 위해 아래와 같이,

    setTimeout 함수 안의 resolve 함수를 reject 함수로 바꿔보자.

    const getIDs = new Promise((resolve, reject) => {
      setTimeout(() => {
        reject([523, 883, 432, 974]); // resolve를 reject로 바꿔주었다.
      }, 1500);
    });
    

    결과는 이번에도 마찬가지로 [523, 883, 432, 974] 이지만, 

    reject 함수를 사용했기 때문에 여기서는 해당 배열을 '에러'로써 넘겨받은 상황이다.

    만약 reject로 이렇게 에러를 return 받은 상태에서 catch로 처리를 해주지 않으면

    'Uncaught (in promise)' 에러가 발생하게 된다.

     

    우리가 reject 함수를 사용해서 promise가 rejected 되었다고 표시했기 때문에

    자바스크립트는 여기서 에러가 발생했다고 인식하고 catch문을 호출하게 되는 것.


    이제 다시 reject를 resolve로 되돌려놓고, ID에 기반하여 recipe를 받은 후

    해당 recipe의 publisher 정보를 이용해 또 다른 recipe를 받아오는 일련의 과정을 promise로 구현해보자.

    const getRecipe = recipeID => {
      // recipeID를 넘겨받아서, getIDs에서처럼 new Promise()를 만들어서 return해준다.
    
      // Promise 안에는 executor function이 필요하다.
      // executor function part의 인자부분은 항상 동일하다. resolve와 reject.
      return new Promise((resolve, reject) => {
        setTimeout(ID => {
          const recipe = {title : 'Fresh tomato pasta',
                          publisher : 'Jonas'};
          resolve('${ID}: ${recipe.title}');
        }, 1500, recipeID); // setTimeout의 인자로 recipeID를 넘겨준다.
      });
    };
    
    getRecipe(IDs[2]);

    참고로 getRecipe를 getID와 달리 (promise를 포함한) 함수로 구현해 준 이유는

    promise에다가 recipeID를 넘겨줘야 하기 때문(인자가 필요하기 때문에 함수로 감싸줬다).

     

    getRecipe 함수는 promise를 return하기 때문에 거기에다가 then 함수를 사용해주면 되는데

    우리가 이전에 일반적으로 썼던 방식을 사용하면 다음과 같은 코드를 작성할 수 있다.

    getIDs
    .then(IDs => {
      console.log(IDs);
      getRecipe(IDs[2]).then( ... )
    })
    .catch(error => ...
    });
    

    근데, 이렇게 되면 then 안에 또 then, 그 안에 또 다른 then.. 이 계속 연결되면서

    Callback Hell을 전혀 해결하지 못하게 된다. then으로 이루어진 Callback Hell이 또 다시 생겨난 것.

    이를 해결하기 위해선 promise의 엄청난 장점인 'Chaining'을 활용하면 된다.

     

    첫 번째 then은 getRecipe 함수에 의해 생성된 promise를 return하게 된다.

    그 promise를 인자로 받는 또 다른 then을 그냥 뒤에 이어붙여주면 된다.

    getIDs
    .then(IDs => {
      console.log(IDs);
      return getRecipe(IDs[2]); // [523, 883, 432, 974]
    })
    .then(recipe => {
      console.log(recipe); // 432 : Fresh tomato pasta
    });
    

    이렇게 꼬리에 꼬리를 물며 then이 이어지는 방식을 Chaining이라고 한다.

     

    첫 번째 then의 getRecipe가 실행된 결과로 promise가 생성되고, 그것을 return문으로 반환해주었다.

    그러면 그 resolved 된 promise의 결과를 다루기 위해 다음 then이 추가되는 것이다.

    이런 식으로 promise의 chaining을 이용하면, 이전에 나타났던 callback hell 없이도 동일한 동작을 구현할 수 있다.

    또, promise를 produce하는 부분과 거기서 생성된 promise를 consume하는 부분이 명확하게 나뉘어져 

    훨씬 더 보기 깔끔한 코드가 완성된다.


    이제 처음에 받아온 recipe의 publisher 정보를 바탕으로 또 다른 recipe를 받아오는 함수를 구현해보자.

    const getRelated = publisher => {
      return new Promise((resolve, reject) => {
        setTimeout(pub => {
          const recipe = {title: 'Italian Pizza',
                          publisher: 'Jonas'};
          resolve(`${pub}: ${recipe.title});
        }, 1500, publisher);
      });
    };

    그리고, 이를 consume 하는 함수를 작성해보자.

    이전과 동일하게 then 메소드를 활용하면 보기 좋아진다.

    getIDs
    .then(IDs => {
      console.log(IDs);
      return getRecipe(IDs[2]); // [523, 883, 432, 974]
    })
    .then(recipe => { // 여기서 recipe를 받아서
      console.log(recipe); // 432 : Fresh tomato pasta
      return getRelated(recipe.puslisher); 
      // getRelated를 호출한 결과를 return한다.
      // 그러면 여기서 new Promise가 return될 것이고, 동일한 방식으로 then을 사용하여 받아준다.
    }) 
    .then(recipe => {
      // 다른 recipe이긴 하지만 역시나 recipe이기 때문에, 
      // 그리고 다른 method라서 이름이 겹쳐도 상관없기 때문에 그냥 recipe라는 이름을 다시 사용해줌.
      console.log(recipe); // undefined: [object Object]
    });

    그런데 버그가 발생했다. undefined: [object Object] 라는 전혀 예상치 못한 결과가 발생한 것.

    그 이유는, 다시 올라가서 promise를 produce해주는 부분의 코드를 보면 알겠지만, 

    getRecipe 함수에서 resolve해준 것이 recipe에 관한 Object가 아니라 그냥 문자열이기 때문이다.

    따라서 이후의 then 메소드에서 'recipe.publisher'를 가져오려고 했을 때

    recipe.publisher를 읽어올 수가 없었기 때문이다.

     

    그냥, 예제니까 간단하게 해결책으로 'recipe.publisher' 대신에 'Jonas'를 넣어주면 문제는 해결된다.

    getIDs
    .then(IDs => {
      console.log(IDs);
      return getRecipe(IDs[2]); // [523, 883, 432, 974]
    })
    .then(recipe => { 
      console.log(recipe); // 432 : Fresh tomato pasta
      return getRelated('Jonas'); 
    }) 
    .then(recipe => {
      console.log(recipe); // Jonas: Italian Pizza
    });

    이전에 '콜백을 이용한 비동기 자바스크립트'에서 예시로 들었던 callback hell 코드들을

    promise를 이용해서 동일하게 구현해보았다.

     

    코드 줄 수가 조금 늘어나긴 했지만, produce part와 consume part가 깔끔하게 나뉘어져있어 

    보기에도 훨씬 편하고, 나중에 관리하기에도 훨씬 편할 것이다.

     

    한 가지 더, 얼마전에 새로 알게 된 Promise의 특징이 있는데, 바로 우선순위가 높다는 것.

    기존에 존재하던 타이머나 DOM 함수 등은 'Task Queue'라는 곳에 들어가서 실행 대기를 하게 되는데,

    Promise는 'Microtask Queue'라는 곳에 따로 들어가서 실행 대기를 하게 된다고 한다.

    이 microtask queue는 task queue보다 우선 순위가 더 높기 때문에,

    우선순위에 따라 promise를 적절하게 사용해주는 것이 좋다고 한다.

    아직 나도 대충 개념만 알고 있는 정도기 때문에, 나중에 공부를 더 해서 포스팅을 따로 하던가 해야겠당 😋

     

     

     

    전체 코드

    const getIDs = new Promise((resolve, reject) => { // executor function
      setTimeout(() => {
        resolve([523, 863, 432, 974]);
      }, 1500);
    });
    
    const getRecipe = recID => {
      return new Promise((resolve, reject) => {
        setTimeout(ID => {
          const recipe = {title : 'Fresh tomato pasta',
                          publisher : 'Jonas'};
          resolve('${ID}: ${recipe.title}');
        }, 1500, recID);
      });
    };
    
    const getRelated = publisher => {
      return new Promise((resolve, reject) => {
        setTimeout(pub => {
          const recipe = {title: 'Italian Pizza',
                          publisher: 'Jonas'};
          resolve(`${pub}: ${recipe.title});
        }, 1500, publisher);
      });
    };
    
    getIDs
    .then(IDs => {
      console.log(IDs);
      return getRecipe(IDs[2]); 
    })
    .then(recipe => { 
      console.log(recipe); 
      return getRelated('Jonas'); 
    }) 
    .then(recipe => {
      console.log(recipe); 
    })
    .catch(error => {
      console.log(error);
    });
    
    반응형

    댓글

Designed by Tistory.