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)을 사용해 왔습니다. 위의 코드에서 보시다시피, 단점이 명확하게 드러나고 있습니다.- 에러 처리가 어렵다.
- 중첩으로 인한 코드 가독성이 떨어지고, 복잡도가 증가
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그러나 위의 방식에서 조금 더 좋은 방법은 catch를 활용하는 것입니다. 위에서는 Promise자체의 함수에 대해서 실패하였을경우, 처리는 되지만 만약에 then 함수의 성공 함수 수행시에 실패가 되면 잡아내질 못합니다.:8:12
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!");});
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개씩 일괄 호출 후, 작업
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함께 보면 좋은 글
- [Node.JS] 프로세스 관리도구 PM2 사용법
- [Reactjs] AWS CloudFront를 통한 CDN 적용
- [DevOps] python boto3 s3 upload 작성
- [Reactjs] AWS Codepipeline을 통한 배포
새 글이 올라오면 Threads @wonizz.ai 에도 올립니다. 팔로우해 두면 피드에서 바로 볼 수 있습니다.
code 상의 =>와 같은 기호들이 HTML 특수문자(>)로 처리된 것 같아요. 수정해주세요.
“const promise = (param) => {“
좋은 피드백 감사합니다! ㅎㅎ