[Javascript] Promise 사용법

[Javascript] Promise 사용법

안녕하세요? 정리하는 개발자 워니즈입니다. 이번시간에는 javascript의 promise사용법에 대해서 정리릏 해보려고 합니다. 필자는 front-end에 굉장히 흥미를 많이 느끼고 있습니다. 화면에 바로바로 적용한 내용들이 노출되는데다가, 스크립트를 통해서 액션에 대한 동작을 하다보니까 확실히 재밌는 것 같습니다.

웹트렌드에서 굉장히 많이 활용했던 것이 jQuery였는데요. jQuery에서 제공해주는 여러가지 function을 통해서 dom을 제어하고 callback을 통해서 액션 이후에 여러가지 작업을 수행 할 수 있었습니다.

promise_1

1. Callback 지옥에서 빠져나오기.

서두에 내용을 살짝 젂어뒀지만, jQuery를 쓰다보면, callback함수를 많이 쓰게 됩니다. 예를들어 button을 click하고 다음의 액션을 수행하는 정도의 액션은 손쉽게 javascript를 통해서 구현을 할 수 있습니다.

$('.content').click(function(){
  $('.content').hide();
});

하지만, A부터 Z까지의 액션이 순차적으로 이루어지는 어플리케이션은 어떻게 구현일 될까요?

a(() => {
  b(() => {
    c(() => {
      d(() => {
        // and so on ...
      });
    });
  });
});

위와 같이 중첩되는 경우가 바로 콜백 지옥입니다. a라는 작업을 수행하고, b,c,d까지 이어지는 흐름에서 코드 블록을 확인하기가 굉장히 어렵습니다. 위에서는 수도 코드로 나타내서 눈에 어느정도 보이지만, 코드량이 많아지고 복잡해지면 말그대로 콜백지옥에서 헤어나오기 쉽지가 않습니다.

이러한 상황을 해결해주는 것이 오늘 포스팅할 Promise라는 패턴입니다.

2. Promise란 무엇인가요?

싱글쓰레드인 자바스크립트에서 비동기 처리를 위해 콜백(callback)을 사용해 왔습니다. 위의 코드에서 보시다시피, 단점이 명확하게 드러나고 있습니다.

  • 에러 처리가 어렵다.
  • 중첩으로 인한 코드 가독성이 떨어지고, 복잡도가 증가

이러한 단점을 해결하기 위해서 프로미스의 개념이 등장했고, 이것을 ES6에서는 언어적 차원에서 지원하게 되었습니다.

Promise.resolve()
  .then(a)
  .then(b)
  .then(c)
  .then(d)
  .catch(console.error);

위의 코드만 보더라도 훨씬 간결하고 명확해졌습니다. a,b,c,d를 순차적으로 수행한다. 에러가 발생하면 catch로 이동한다.

Promise는 말그대로 “약속” 입니다. 어떤 데이터를 요청하게 되면, 약속을 한 상황이기 때문에 지금 당장은 못주더라도 나중에 준다는 약속입니다.

약속에는 상태가 4가지 형태로 있습니다.

  • pending : 아직 약속을 수행 중인 상태입니다.
  • fulfilled : 약속이 지켜진 상태 입니다.
  • rejected : 약속이 어떤 이유에 의해서 거절 된 상태입니다.
  • settled : 최종적으로 약속 이후에 상태를 말합니다.

3. Promise 사용 패턴

Promise를 사용하는 방식은 다양하지만, 필자가 주로 사용하는 방식에 대해서 정리하도록 하겠습니다.

  • promise 함수 구현
const promise = (param) => {
    return new Promise((resolve, reject) => {
        window.setTimeout(function () {
            if (param) {
                resolve("약속 완료");
            }
            else {
                reject(Error("약속 실패"));
            }
        }, 3000);
    });
};

위에서 보는 것과 같이 promise에 대한 함수를 생성해 줍니다. 그리고 return으로 새로운 promise를 반환 하는 구조입니다. promise를 반환할 때, resolve,reject라는 파라미터를 받게 되고 실제로 약속 완료, 실패 여부에 대해서 함수를 호출하게 되면 최종적으로 완료되는 구조입니다.

  • promise 함수 호출
promise(true)
.then(function (text) {
    // 성공시
    console.log(text);
}, function (error) {
    // 실패시 
    console.error(error);
});

호출부에서는 promise를 호출하는 순간 Promise객체가 생성이 됩니다. Promise는 비동기 작업이 완료되면 실행되는 then이라는 API가 존재합니다. 위의 예제는 Promise가 정상적으로 fulfilled된 이후에 then에 의해서 해당 파라미터(text)를 받아서 콘솔로 출력해주는 내용입니다.

4. Promise에서 에러 처리

then 함수를 통해서 chaining으로 연결을 계속해서 수행하다가 에러가 발생하게 되면 어떻게 처리를 하는지 정리하도록 하겠습니다. 위의 코드에서 에러 같은 경우 reject함수에 의해서 전달 되는 인자가 then함수의 2번재 function에서 실행이 되도록 합니다.

promise(false)
.then(function (text) {
    // 성공시
    console.log(text);
}, function (error) {
    // 실패시 
    console.error(error);
});

즉, 위의 호출부에서 param인자를 false로 줘서 강제로 reject를 호출 하도록 했습니다. 약속이행에 대해서 실패 함수를 호출했고, then 함수의 2번째 인자 함수가 실행이 됩니다.

VM50:7 Error: 약속 실패
at :8:12

그러나 위의 방식에서 조금 더 좋은 방법은 catch를 활용하는 것입니다. 위에서는 Promise자체의 함수에 대해서 실패하였을경우, 처리는 되지만 만약에 then 함수의 성공 함수 수행시에 실패가 되면 잡아내질 못합니다.

promise(false)
.then(function (text) {
    // 성공시
    console.log(text);
}).catch(function(error){
    // 실패시
    console.log(error)
});

위와 같이 then, catch를 같이 사용하게 되면 훨씬 코드 가독성이 높아집니다.

아래는 가장 대표적으로 샘플 케이스로 나와있는 promise catch chaining 입니다.

asyncThing1()
    .then(function() { return asyncThing2();})
    .then(function() { return asyncThing3();})
    .catch(function(err) { return asyncRecovery1();})

    .then(function() { return asyncThing4();}, function(err) { return asyncRecovery2(); })
    .catch(function(err) { console.log("Don't worry about it");})

    .then(function() { console.log("All done!");});

promise_2

5. 샘플 코드

필자가 Promise를 통해서 개발했던 샘플 코드를 보여드리겠습니다. 내용은 외부 API를 호출하고 결과물에 대해서 json객체로 다시 정리를 하는 내용인데요. API를 호출하고 이후의 작업이 되어야 하기 때문에 Promise를 적극 활용했습니다.

  • 호출부
(async() => {
    try{
        const siteCodeArr = [process.argv[2]];
            const siteCode = siteCodeArr[0];
            //0. 초기 호출 : maxIdx를 알아 낸 이후, 작업 시작.
            const max = await FirstCallBulkApis(siteCode);
            await callBulkApis(siteCode, 0, max);

    }catch(e){
        console.log(e);
    }
})();
  • 함수 구현부
const FirstCallBulkApis = (siteCode) => {
        return new Promise(async (resolve, reject) => {
                try{
                        const _resultStr = await requestCall(siteCode, curIndex);
                        const _resultJson = JSON.parse(_resultStr);
                        maxIdx = Math.floor(_resultJson.total_number_of_products / 50);
                        resolve(maxIdx);
                }catch(e){
                    reject(e);
                }
        });
};
const requestCall = (siteCode, i) => {
    return new Promise((resolve, reject) => {
        const uri = siteCode === 'in' ? `https://ecom.${siteCode}.samsung.com/v4/configurator/syndicated-product` : `https://${siteCode}.ecom.samsung.com/v4/configurator/syndicated-product`;
        const options = {
            uri: uri,
            method: 'POST',
            body: JSON.stringify({
                "skus": ["ALL"],
                "offset": i,
                "count": 50,
            }),
            encoding:null
        };
        request(options, (error, response, body) => {
            try{
                if (error) reject(error);
                if (response.statusCode !== 200) {
                    console.log(response.statusCode);
                    let bodyStr = body.toString('utf-8');
                    console.log(bodyStr);
                    reject(`Failed with ${response.statusCode}`);
                } else {
                    let bodyStr = body.toString('utf-8');
                    resolve(bodyStr);
                }
            }catch(e){

            }

        });
    });
};
//0. 초기 호출 : maxIdx를 알아 낸 이후, 작업 시작.
const max = await FirstCallBulkApis(siteCode);
await callBulkApis(siteCode, 0, max);

외부 API를 호출하고, offset으로 총 몇페이지가 되는지를 계산을 합니다. 그리고 나서, 10개씩 호출을 수행하고, promise를 통해서 이후에 작업을 진행하는 내용입니다.

  • 10개씩 일괄 호출 후, 작업

while문에서 curIndex = 0으로 시작하고, maxIdx는 페이지의 총 갯수입니다.
curIndex하위로 다시 whilte문이 돌면서 셋팅한 iteration(10개) 만큼 돌면서 array를 생성합니다.

이후에 request를 10개를 호출하게 되고, promise all을 통해서 모두 기다린다음에, 돌아온 json에 대해서 작업을 진행합니다.

const callBulkApis = (siteCode, thread, maxIdx) => {
    return new Promise(async (resolve, reject) => {
        try{
            while(curIndex <= maxIdx){
                let iterators = [];
                let iteratorIndex = 0;
                while(iteratorIndex < iteration){
                        if(curIndex + iteratorIndex >= maxIdx) break;
                        iterators.push(curIndex + iteratorIndex);
                        iteratorIndex++;
                }
                let requests = iterators.map(iterator => (requestCall(siteCode, iterator)));
                await Promise.all(requests).then(function (values){
                        console.log(`${siteCode} 처리 진행중 : ${curIndex + iterators.length}/${maxIdx} 까지 처리 완료`);
                        //병렬 처리에 따른 pase부분 처리 변경
                        values.map((resultStr) => {
                                const resultJson = JSON.parse(resultStr);
                                //일괄 처리 작업 
                        });
                });
                curIndex += iteration;
            }
            resolve();
        }catch(e){
            reject(e);
        }
    });
};

6. 마치며..

이전의 jQuery를 통해서 callback 지옥을 만드는것에서 벗어나서 이제는 코드가독성도 높이면서 ES6문법을 활용해서 좀더 효율적으로 코드를 작성했습니다. Promise는 잘만 활용하면 코드를 가독성있게 유지하고 이후에 유지보수하기도 훨신 편리한 것 같습니다.

다음에는 async와 await에 대해서 정리를 해보도록 하겠습니다.

7. 참조

자바스크립트 Promise 쉽게 이해하기
[JavaScript] 바보들을 위한 Promise 강의 – 도대체 Promise는 어떻게 쓰는거야?
The JavaScript Promise Tutorial

2 Replies to “[Javascript] Promise 사용법”

  1. code 상의 =>와 같은 기호들이 HTML 특수문자(>)로 처리된 것 같아요. 수정해주세요.
    “const promise = (param) => {“

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다