이전 포스트, 자바스크립트에서 비동기 사례와 콜백함수의 처리 방법, 단점에 대해서 설명하였다. 그렇다면 이 콜백지옥
에서 어떻게 벗어날 수 있을까? 다시 현기증나는 코드를 들여다보도록하자.
$.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
는 다음중 하나의 상태를 가진다.
그럼 실제 코드로 살펴보자.
var promise = new Promise(function(resolve, reject){
// do async action
});
Promise
는 Promise
생성자를 통해 생성한다. 이때 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
를 생성하고, then
과 catch
메서드를 체이닝 한 것을 확인할 수 있다. 이행 상태의 결과는 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
라는 것을 처음 알게 된 계기는, 실무에서 콜백 지옥을 경험하고 나서부터였다. 요구사항은 다음과 같았다.
- 지정된 디렉토리의 모든 파일을 삭제한다.
- 정적 리소스(이미지, 소스파일 등)가 압축된 zip 파일을 다운받는다.
- 다운받은 zip 파일의 압축을 해제한다.
- 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
에 대해 알아보았다. 작성한 포스팅은 단편적인 면만 설명하고 있으므로 좀더 자세한 내용은 아래 링크를 통해 참고하길 바란다.