[개발일지] 콜백을 프로미스로 바꿔보자

김선종·2022년 3월 27일
0

개요

자바스크립트로 개발 해본 사람이라면 절대 간과하고 넘어갈 수 없는 개념인 비동기. 비동기 처리를 위해 자바스크립트에선 Promise 객체를 통해 비동기 로직을 구현하곤 한다. 기존의 콜백을 사용하던 오래된 패턴에서 프로미스를 사용한 패턴으로 어떻게 리팩토링 했는지 알아보자.

리팩토링 시작!

피키팜에서는 배송지 정보를 입력할 때, 다음 우편번호 API를 통해 주소를 입력받는다. API 자체가 오래된 감이 있고, 크로스 브라우징을 고려(라고 쓰고 IE때문이라 읽지만...)해야 하기 때문에라도 콜백 패턴을 사용하여 액션 처리가 되어있다. 근데 이거를 프로미스로 바꿔보자고....!

기존의 코드는 이랬다

<script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
<script>
    new daum.Postcode({
        oncomplete: function(data) {
            // 팝업에서 검색결과 항목을 클릭했을때 실행할 코드를 작성하는 부분입니다.
            // 예제를 참고하여 다양한 활용법을 확인해 보세요.
        }
    }).open();
</script>

보면 알겠지만, 우편번호 검색 창에서 원하는 주소를 얻으면, 팝업이 닫히면서 콜백으로 데이터를 넘겨준다. 그래서 기존에는 이를 어떤식으로 이용을 했었냐면...

new daum.Postcode({
        oncomplete: function (data) {
            let addr = '';
            let extraAddr = '';

            document.getElementById('id_full_address').value = addr;
            if (document.getElementById('delivery-address')) {
                document.getElementById('delivery-address').value = addr;
            }
            document.getElementById('id_sido').value = data.sido;
            document.getElementById('id_sigungu').value = data.sigungu;
            document.getElementById('id_zipcode').value = data.zonecode;

            document.getElementById('id_detail_address').focus();
        },

        shorthand: false,
        animation: true,
    }).open({
        q: document.getElementById('id_full_address').value,
    });

주소 parsing하는 부분은 중요하지 않아 지웠다. 대충 보면, 콜백으로 받아오는 정보를 가지고 완료되는 시점에 그냥 HTML 엘리먼트에 집어넣는 방식이다. 극혐

어떻게 이걸 바꿀까

프로미스로 변환하는 법

이 문서를 참고하면 좋다. 요약하자면 다음의 단계를 거치면 된다.

  1. 콜백을 받는 함수가 있다.
  2. Promise를 리턴하면서, 1번의 함수의 callback 내부에서, 콜백 로직을 수행하지 않고, resolve를 실행하도록 바꾼다
  3. 이후 프로미스화 된 함수에서 promise.then() 내부에서 원래 로직을 이어나간다.

그래서 프로미스로 이렇게 바꾸어 보았다.

우선 기존의 다음 API를 바로 프로미스화 하기에는 아래의 이유가 발목을 잡았다.

  • 기존의 API를 수정하면 업데이트 대응이 불가능하다.
  • 객체를 선언하면서, 콜백 함수를 argument로 받는게 아닌, 옵션 객체 내부에 선언하도록 되어있다.

따라서 기존의 다음 API 함수를 명시적으로 콜백을 하도록 한번 더 wrapping 해주었다.

function executeDaumPostcodeAPI(cb) {
    const addressInfo = {
        address: '',
        zipCode: '',
    };

    new daum.Postcode({
        oncomplete: function (data) {
			// some codes that bring zipcode info
          
            cb(addressInfo);
        },
    }).open();
}

그런 다음, 이 콜백 형태의 함수를 프로미스로 한번 더 감싸주었다.

const executeDaumPostcodeAPIPromise = function () {
    return new Promise((resolve, reject) => {
        executeDaumPostcodeAPI((data) => {
            resolve(data);
        });
    });
};

그래서 왜좋은데

프로미스로 바꿔서 단순히 최신 문법을 써봤다 정도면 굳이 기존의 코드를 바꾸려 하지 않았겠지만,,, 프로미스를 쓰는 이유는 결과적으로 chaining이 가능해지기 떄문이었다. 피키팜의 배송비 결정 로직은 아래의 단계를 거친다

  1. 다음 우편번호 API를 통해 주소와 우편번호를 검색하고
  2. 이 우편번호가 제주 및 도서산간지역인지 판단하여 추가 배송비를 더할지 판단해 최종 배송비를 계산한다음
  3. 배송비를 업데이트한다.

여기서 2번의 부분은 비동기 로직이긴 하지만, 예전 jQuery AJAX를 사용한 방법이라서, 이 또한 프로미스 체이닝이 가능하도록 리팩토링 해 보았다.

function getDeliveryFeeByZipCode({
    farmerZipcode,
    friendZipcode,
    productPK,
    quantity,
}) {
    /* 서버에 AJAX 보내서 계산된 배송비 업데이트 할 것 */
    return new Promise((resolve, reject) => {
        $.ajax({
            type: 'POST',
            url: CALCULATE_DELIVERY_FEE_URL,
            data: {
                farmerZipcode,
                friendZipcode,
                productPK,
                quantity,
            },
            dataType: 'json',
            success: (res) => {
                resolve(res['delivery_fee']);
            },
            error: (err) => {
                reject(err);
            },
        });
    });
}

$.ajax.done()을 사용해 체이닝 하는 방법이 더 좋은데, 기존의 옵션 객체를 사용하는 AJAX 방식에서 코드 수정을 최소화 하려다 보니, 굉장히 괴상한 형태의 함수가 만들어지긴 했다. 추후 이 부분 또한 리팩토링을 해봐야겠다.

그래서 결과물은

handleAddressFindButtonClick: function (idx) {
    const clickedFriend = this.friends[idx];
  
    executeDaumPostcodeAPIPromise()
        .then((data) => {
            clickedFriend.address.sigungu = data.address;
            clickedFriend.address.zipCode = data.zipCode;
      
            return {
                farmerZipcode: FARMER_ZIPCODE,
                friendZipcode: clickedFriend.address.zipCode,
                productPK: PRODUCT_PK,
                quantity: clickedFriend.quantity,
            };
        })
        .then((data) => {
            return getDeliveryFeeByZipCode(data);
        })
        .then((deliveryFee) => {
            clickedFriend.deliveryFee = parseInt(deliveryFee);
        });
},

이런식으로 프로미스가 체이닝이 가능해져 메서드들이 조금 더 확장성을 가질 수 있게 되었다.

결론

프로미스 짱짱. 이번 기회에 겉할기만 했던 프로미스가 어떤 방식으로 되어있는지에 대해 생각해 볼 수 있던 계기인것 같다.
결국 더 좋은 가독성을 위해서 async/await를 사용하겠지만, 결국 비동기 함수는 프로미스를 리턴해야 하는거니까..

profile
개발자가 되고싶다 열심히하자

0개의 댓글