javascript - Promise(콜백의 진화형)

Promise란

Promise는 javascript의 callback의 단점을 해결 하기 위한 1단계 진화형 형태이다. 프라미스가 콜백을 대체하는 것은 아니다. 사실 프라미스에서도 콜백을 사용한다. 프라미스는 콜백을 예측 가능한 패턴으로 사용할수 있게 하며, 프라미스 없이 콜백만 사용했을 때 나타날 수 있는 엉뚱한 현상이나 찾기 힘든 버그를 해결할수 있다. 특히 보기싫은 중첩콜백과 콜백헬을 해결해준다.


Promise의 기본개념

프라미스 기반 비동기적 함수를 호출하면 그 함수는 Promise 인스턴스를 반환한다. 프라미스는 성공(fulfilled)하거나, 실패(rejected)하거나 단 두 가지뿐이다. 프라미스는 성공 혹은 실패 둘 중 하나만 일어난다고 확신할 수 있다. 성공한 프라미스가 나중에 실패하는 일 같은 건 절대 없다. 또한, 성공이든 실패든 단 한 번만 일어난다. 프라미스가 성공하거나 실패하면 그 프라미스를 결정됬다(settled)고 한다.

프라미스는 객체이므로 어디든 전달할 수 있다는 점도 콜백에 비해 간편한 장점이다. 비동기적 처리를 여기서 하지 않고 다른 함수에게(또는 다른 동료가) 처리하게 하고 싶다면 프라미스를 넘기기만 하면 된다.

사실 이렇게 말로만 들으면 Promise에 대한 감이 전혀 오질 않을수도 있다.

일단 만들어 보자.


Promise 만들고 실행하기

setTimeout을 이용하여 10초 이하 카운팅하는 Promise를 만들어보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function countdown(seconds){
return new Promise(function(resolve, reject){
const timeoutIds = [];
for(let i = seconds; i>=0; i--){
timeoutIds.push(setTimeout(function(){
if(i > 10) {
timeoutIds.forEach(clearTimeout); // 등록된 setTimeout 스케쥴을 다 지워버린다.
return reject(new Error("10초를 초과하는 수는 셀수 없다. 입력받은 초:" + i));
}
if(i>0) console.log(i + '...');
else resolve(console.log("GO!"));
}, (seconds-i)*1000));
} //-for
});
}

비동기적 동작을 원하는 내용을 콜백으로 new Promise의 인자로 넘긴다.

중요한 문법적 요소는 Promise의 인자로 넘겨지는 콜백함수의 매개변수 resolve(성공), reject(실패)이다.

resolve(성공)는 Promise에 인자인 콜백이 정상동작을 했을 경우 동작할 로직이고, reject(실패)는 그 반대로 콜백이 실패한경우 동작할 로직이다.

즉 resolve, reject도 콜백이다.

그리고 resolve, reject의 중요한 특징은 여러번 호출하든, 섞어서 호출하든 첫번째로 호출한 것만 의미가 있다는 것이다.

자 그러면 이제 반환된 Promise를 호출하는 부분이다.

1
2
3
4
5
6
7
8
countdown(13).then(
function(){
console.log("countdown 정상 종료"); // resolve 호출시 동작
},
function(err){
console.log("countdown 동작 에러:"+ err.message); // reject 호출시 동작
}
);

then의 첫번째 인자가 resolve, 두번째 인자가 reject를 대체하는 것을 알수 있다.

Promise의 인자콜백에서 로직상으로 reject는 없을수 있지만, 반드시 성공했을 경우 resolve를 명시적으로 호출해 줘야 한다.

그래야만 Promise에서 호출하는 부분에서 정상적으로 동작했는지 여부를 확인 할 수 있다.

아래처럼 resolve, reject를 둘로 나눠서 사용할 수도 있다.

1
2
3
4
5
6
7
const p = countdown(13);
p.then(function(){
console.log("countdown 정상 종료"); // resolve 호출시 동작
});
p.catch(function(err){
console.log("countdown 동작 에러:"+ err.message); // reject 호출시 동작
});

또는 아래처럼 더 간단하게도 사용이 가능하다.

1
2
3
4
5
6
const p = countdown(13);
p.then(function(){
console.log("countdown 정상 종료"); // resolve 호출시 동작
}).catch(function(err){
console.log("countdown 동작 에러:"+ err.message); // reject 호출시 동작
});

Promise를 실행하는 방법중 위 방법을 가장 추천한다.

그 이유는 then의 콜백에서 발생하는 error도 맨 마지막 catch에서 처리할 수 있기 때문이다.

그리고 콜백헬을 대체 할 Promise체인을 사용하기 편하다.


Promise Chain(프라미스 체인)

프라미스가 완료되면 그 다음단계 진행해야할 로직들을 묶어서 순차적으로 실행할수 있다.

이 체인기능은 callback hell 이라는 문제점을 해결해준다.

예를 들어 a.txt, b.txt, c.txt 파일을 순서대로 읽어야 한다고 가정하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//파일을 읽는 Promise
function readFile(fileName){
return new Promise(function(resolve, reject){
fs.readFile(fileName, "utf-8" ,function(err, data){
if(err) return reject(err);
resolve(console.log(data));
});
});
}

//파일을 읽는 Promise
function readFileBBBBB(fileName){
return new Promise(function(resolve, reject){
fs.readFile(fileName, "utf-8" ,function(err, data){
if(err) return reject(err);
resolve(console.log(data));
});
});
}

const p = readFile("a.txt");

p.then(readFileBBBBB("b.txt"))
.then(function(){
//Promise 채인에 일반 콜백 연결
fs.readFile("c.txt", "utf-8", function(err, data){
if(err) return console.error("c.txt 파일 읽기 실패");
console.log(data);
});
}).catch(function(err){
console.error("에렁:" + err.message);
});

위 코드 결과

위 코드는 많은 것을 보여주고 있다.

  • 최초 호출된 Promise를 통해서 비동기적으로 동작하는 로직을 순서대로 묶을수 있다.

  • 최초 호출을 제외하고 그 다음 단계부터 Promise와 콜벡을 혼용해서 순서를 만들수 있다.

  • Promise체인 어디에서든 에러가 생기면 체인 전체가 멈추고 그 에러 처리를 마지막의 catch 핸들러 한 곳에서 처리 할 수 있다.

  • callback hell의 중첩으로 쌓인 코드보다 훨씬 보기가 쉽다.

프로미스 체인기능은 아주 직관적이고 강력한 기능이라 생각된다.

위 예제에는 없지만 resolve의 매개인자를 통해서 앞선 단계의 데이터를 다음단계로 계속 넘길수도 있다.

마무리

일단 여기까지만... 이게 공부해보니 상당히 어려운 javascript 문법기술이다. 결정되지 않는 프라미스 방지나, 프라미스 체인에서 재귀적으로 promise를 호출했을 경우등.... 아직 공부해야 할 부분이 많아 보인다. 이 글에서는 promise의 개념과 간단한 사용법 정도만 보고 넘어가야 겠다.