Promise 패턴2 - Promise란 무엇인가

콜백지옥에서 벗어나기

Posted by Promise 패턴2 - Promise란 무엇인가 on May 03, 2018

이전 포스트, 자바스크립트에서 비동기 사례와 콜백함수의 처리 방법, 단점에 대해서 설명하였다. 그렇다면 이 콜백지옥 에서 어떻게 벗어날 수 있을까? 다시 현기증나는 코드를 들여다보도록하자.


    $.get('/task1', function (value1) {
        $.get('/task2', function (value2) {
            $.get('/task3', function (value3) {
                $.get('/task4', function (value4) {
                    $.get('/task5', function (value5) {
                        
                        // ..... 지쳐서 더 못쓰겠음.. 중간 생략
                        $.get('/task10', function (value10) {
                            
                            // do anything
                        
                        });
                        
                    });
                });
            });
        });
    });

이 코드에서 단 하나의 장점이 있다. 바로 가장 먼저 호출된 콜백 함수로부터 지역 변수를 공유할 수 있다는 것이다. 하지만 앞서 설명하였듯 가독성이 매우 떨어지며, 변수명 관리도 쉽지않다는 치명적인 단점이 있다. 여기서 Promise의 필요성이 대두된다.

Promise

말 그대로 약속이다. 비동기 작업에 대한 처리 결과 즉 약속값을 반환한다. 이 약속값을 통해 개발자는 작업에 대한 종료 시점을 알 수 있고, 그다음 작업을 결정할 수 있다. Promise는 다음중 하나의 상태를 가진다.

  • 대기중(pending): 초기 상태, 아직 작업이 완료되지 않은 상태이다.
  • 이행됨(fulfilled): 작업이 성공적으로 완료된 상태이다.
  • 거부됨(rejected): 에러등과 같은 이유로 작업이 실패한 상태이다.

그럼 실제 코드로 살펴보자.

Promise 생성하기 (대기)


   var promise = new Promise(function(resolve, reject){
        
        // do async action
        
   });
   

PromisePromise 생성자를 통해 생성한다. 이때 Promise는 비동기 작업을 처리하기 전까지 대기중 상태값을 갖는다. 비동기 작업이 완료된 이후에야 비로소 이행됨 또는 거부됨 상태로 변환하여, 개발자는 이 상태값을 가지고 이후 작업을 결정할 수 있다.

작업 이행 처리


   var promise = new Promise(function(resolve, reject){
        setTimeout(function(){
            resolve('success');
        }, 5000);
   });
   

resolve()를 눈여겨보자. 비동기 작업이 완료 된 이후, resolve()를 실행함으로써 이 작업이 성공적으로 완료되었음을 알려 줄 수 있다. 이 함수의 인자값으로 처리 걸과를 넘길 수 있다.

작업 거부 처리


   var promise = new Promise(function(resolve, reject){
        setTimeout(function(){
            reject('fail');
        }, 5000);
   });
   

reject()는 이 작업이 실패로 끝났음을 알려준다. 마찬가지로, 이 함수의 인자로써 처리 결과를 넘길 수 있다.

예제

위에서 설명한 상태들을 가지고 간단한 예제를 작성하였다.


    function httpRequest(){
       return promise = new Promise(function(resolve, reject){
          
          $.get('/test1', function(response){
                
              if (response.status !=== 200){
                    resolve('success');
              }
              else {
                    reject(new Error('http request err'));
              }
          
          })
          
       });
    }
    
    httpRequest().then(function(response){
        console.log(response);
    }).catch(function (err){
        console.log(err);
    });
  
   

http 상태 코드가 200이면, 작업을 성공 처리하여 서버에서 받아온 응답 값을 콘솔에 출력하고,200이외의 코드이면 작업을 실패 처리하여 에러를 생성하여 콘솔에 출력한다. 하단부의 코드를 보면, httpRequest()함수를 통해 promise를 생성하고, thencatch 메서드를 체이닝 한 것을 확인할 수 있다. 이행 상태의 결과는 then으로, 거부 상태의 결과는 catch에서 처리된다. 이것으로 더 많은 비동기 작업을 아름답게 처리할 수 있다.


    function httpRequest(url){
       return promise = new Promise(function(resolve, reject){
          
          $.get(url, function(response){
                
              if (response.status !=== 200){
                    resolve('success');
              }
              else {
                    reject(new Error('http request err'));
              }
          
          })
          
       });
    }
    
    
    httpRequest('/request1').then(function(response){
        return httpRequest('/request2'); 
    }).then(function(response){
        return httpRequest('/request3');  
    }).catch(function (err){
        console.log(err);
    });
  
   

위와 같이 체이닝읕 통해 여러 비동기 작업을 순차 처리하도록 하였다. 지금까지 본 promise의 흐름은 아래의 도표로 표현될 수 있다.

좋은 예와 나쁜 예

promise라는 것을 처음 알게 된 계기는, 실무에서 콜백 지옥을 경험하고 나서부터였다. 요구사항은 다음과 같았다.

  1. 지정된 디렉토리의 모든 파일을 삭제한다.
  2. 정적 리소스(이미지, 소스파일 등)가 압축된 zip 파일을 다운받는다.
  3. 다운받은 zip 파일의 압축을 해제한다.
  4. zip 파일은 삭제한다.
    (사실 이것보다 많지만 생략..)

Node.js 환경이였기 때문에 각 작업은 비동기로 동작하지만, 순차처리가 되어야 했다. 코드는 이러하였다. (최대한 간소화하여 작성한다.)


    new Promise(function(resolve, reject){
        
         // 각각의 함수는 `Promise` 객체를 리턴한다. 
         removeAllFile().then(function(){
                
            downloadZip().then(function(){
                    
                unzipFile().then(function(){
                            
                      return removeZipFile().then(function(){
                           resolve();
                      }).catch(function(err){
                           reject(err);
                      });  
                    
                }).catch(function(err){
                        reject(err);
                });     
                    
            }).catch(function(err){
                reject(err);
            });         
            
         }).catch(function(err){
            console.log(err);
         });
        
    });
  
   

(마크다운으로 작성하다보니 검사기가 없어서 괄호 열고닫는데 헷갈려 죽을뻔했다…) Promise를 사용하였지만 여전히 콜백 지옥에서 벗어날수 없었다. 만약에 이 요구사항이 변동되거나 추가된다면?? 그야말로 헬이 아닐 수 없다. (실제로 요구사항이 변동되어 수정하려고 하자 저 코드를 만났다.. 살려조 ㅠㅠ) 그러면 정말 Promise 답게 코드를 수정해보자.


    removeAllFile().then(function(){
        
        return downloadZip();
        
    }).then(function(zipPath){
        
        return unzipFile(zipPath);
        
    }).then(function(zipPath){
     
        return removeZipFile(zipPath);
    
    }).catch(function(err){
    
        throw new Error(err);
        
    });
 
   

훨씬 깔끔해졌으며 가독성도 높아졌다. 각 작업의 성공 결과 처리값은 리턴값으로 보낼 수 있으며, then에서 인자로 받아 처리할 수 있다. 거부된 상태는 catch에서 한번에 관리한다.

. . .

이렇게 2차례의 포스팅으로 Promise에 대해 알아보았다. 작성한 포스팅은 단편적인 면만 설명하고 있으므로 좀더 자세한 내용은 아래 링크를 통해 참고하길 바란다.