JavaScript로 코딩을 하다보면 I/O 연산은 논블로킹 방식으로 처리되기 때문에 비동기 처리를 반드시 적절하게 해야 한다. I/O함수에 콜백함수를 등록해 콜백함수 내에서 I/O 이후 로직을 처리하면 되지만 콜백이 중첩되다보면 콜백 지옥(callback hell)에 빠지게 된다. 뿐만 아니라 콜백방식을 사용하게 되면 중첩된 콜백함수 중 하나에서 에러가 발생했을 때 예외 처리가 꽤 까다롭다.
아래 순서도와 같이 데이터를 처리 한다고 가정해보자.
콜백 방식으로 처리
위 순서도를 처리하기 위한 콜백 방식의 코드를 보자. 아래 코드는 남자의 키 데이터를 요청하고, 그 후 여자의 키 데이터를 요청한다. 이 때 여자의 키 데이터를 요청 후 응답은 50% 확률로 실패하고 실패하면 null을 반환한다고 가정하자.
function requestBoysHeights (callback) {
const heights = [175, 181, 165, 190, 166];
setTimeout(()=> {
callback(heights);
}, 3*1000);
};
function requestGirlsHeights (callback) {
const heights = 0.5 < Math.random()? [156, 164, 171, 160, 178]: null;
setTimeout(()=> callback(heights), 1*1000);
};
function run (callback) {
console.log('start');
callback();
};
run( ()=> {
requestBoysHeights((heights1) => {
requestGirlsHeights((heights2) => {
console.log('boys heights:', heights1);
console.log('girls heights:', heights2);
console.log('end');
});
});
});
실행결과이다. 50% 확률로 여자의 키 값을 null로 받게된다.
start
boys heights: [ 175, 181, 165, 190, 166 ]
girls heights: null
end
이러한 콜백방식에서 여자의 키 데이터 값을 반드시 받기위해 실패시 재요청하고 싶은데 어떻게 처리하면 될까? 아래와 같이 retry 함수를 등록해 처리하면 된다.
function requestBoysHeights (callback) {
const heights = [175, 181, 165, 190, 166];
setTimeout(()=> callback(heights), 3*1000);
};
function requestGirlsHeights (callback) {
const heights = 0.5 < Math.random()? [156, 164, 171, 160, 178]: null;
setTimeout(()=> callback(heights), 1*1000);
};
function retry(n, requestFunction, callback) {
requestFunction( heights => {
if ( !heights ) { retry(n+1, requestFunction, callback); }
else { callback(n, heights); }
});
}
function run (callback) {
console.log('start');
callback();
};
run( ()=> {
requestBoysHeights( (heights1) => {
retry(0, requestGirlsHeights, (n, heights2)=> {
console.log('retry cnt:', n);
console.log('boys heights:', heights1);
console.log('girls heights:', heights2);
console.log('end');
});
});
});
결과,
start
retry cnt: 1
boys heights: [ 175, 181, 165, 190, 166 ]
girls heights: [ 156, 164, 171, 160, 178 ]
end
Promise ?
Promise object는 비동기적 연산에 대한 성공, 실패 그리고 결과값을 나타낸다.
Promise는 ES6 부터 비동기처리를 위해 나오게 됐다. 내가 JavaScript를 좋아하는 이유는 ES6부터 Promise가 나온 것 처럼, 계속해서 JavaScript라는 언어는 코딩하기 좋은 방향으로 발전하고 있다고 생각하기 때문이다.
Promise는 다음과 같이 세 가지의 상태가 있다.
- pending: 처음 상태로, fulfilled도 rejected도 되지 않은 상태
- fulfilled: 연산이 성공적으로 수행된 상태
- rejected: 연산이 실패한 상태
그리고 Promise.prototype.then( ), Promise.prototype.catch( ) 로 다음과 같이 체이닝 할 수 있다.
Promise 방식으로 처리
그렇다면 위 순서도의 내용을 Promise로 코딩하면 어떻게 될까?
function requestBoysHeights (callback) {
const heights = [175, 181, 165, 190, 166];
setTimeout(()=> callback(heights), 3*1000);
}
function requestGirlsHeights () {
return new Promise ((resolve, reject) => {
const heights = 0.5 < Math.random()? [156, 164, 171, 160, 178]: null;
setTimeout(()=> {
if (heights) {
resolve(heights);
}
reject(heights);
}, 1*1000);
});
}
function retry(n, promise) {
return new Promise ((resolve, reject) => {
promise()
.then( heights => resolve([n, heights]))
.catch( error => retry(n+1, promise).then(resolve).catch(reject));
});
}
function run () {
console.log('start');
requestBoysHeights( height1 => {
retry(0, requestGirlsHeights)
.then((result) => {
console.log('retry cnt:',result[0]);
console.log('boys heights:', height1);
console.log('girls heights:', result[1]);
})
.finally(()=> {
console.log('end');
});
});
}
run();
위와 같이 pomise( ) 함수인 retry( ) 함수를 재귀적으로 처리하면 된다. 이 때, 반복요청 할 promise( ) 함수를 파라미터로 넘긴다면 다른 함수에서도 이 retry ( ) 함수를 사용할 수 있을 것이다.
결과는 다음과 같다. 위 코드와 달리 retry 횟수를 추가했다.
start
retry cnt: 4
boys heights: [ 175, 181, 165, 190, 166 ]
girls heights: [ 156, 164, 171, 160, 178 ]
end
run( )을 .then( ) 방식에서 async-await 방식으로 코딩하면 아래와 같을 것이다. 사실 내게는 async-await 방식을 사용해 try-catch 하는 것이 더 쉬운 것 같다. 비동기적 처리지만 동기적 처리 결과를 변수에 입력하는 것 처럼 보여서 말이다.
function runAsync () {
console.log('start');
requestBoysHeights( async height1 => {
try {
const results = await retry(0, requestGirlsHeights);
console.log('retry cnt:',results[0]);
console.log('boys heights:', height1);
console.log('girls heights:', results[1]);
} finally {
console.log('end');
}
});
}
runAsync();
이렇게 Promise로 요청실패시 retry하는 법을 정리해봤다. callback 방식이 Promise보다 예외처리가 어렵기 때문에 callback 방식으로 retry 하는 것이 어려울 줄 알았으나 생각보다 callback 방식으로 retry도 하는 것이 쉽다. callback 방식과 Promise의 예외처리 방식에 대해서는 다음에 따로 포스팅해봐야 겠다. 다음 번에는 Promise에서 유용한 Promise.all( ) 과 Promise.race( ) 메쏘드에 대해 위의 순서도 상황을 응용해 정리해야 겠다.