javascript callback mechanism

callback

비동기 프로그래밍에서 callback을 사용하여 어떻게 데이터를 처리하는지 글을 써보려한다.
고전적으로 javascript에서 callback 을 처리하는 방법과, es6 공식탑재된 promise, 그리고 es7 에 탑재될예정(?) 인 async await 을 사용하여 어떻게 callback을 처리하는지 알아보자.
다음에서 제시될 tasks(task1, task2, task3 )들은 어떠한 로직들을 처리하는 함수들이며(비동기효과를 위해 timeout으로 .. ), doProcess 함수에서 해당 tasks들을 처리하여 결과를 얻도록 구성하였다 .

classic

한번쯤은 callback hell 이라는 용어를 들어보았을것이다. 중첩된(nested) callback으로 개발 유지보수의 어려움과 코드 가독성을 현저히 떨어뜨리게 되는데 우선 고전적인 javascript callback의 사용법부터 알아보자.

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
function task1(value , callback){
if(value){
setTimeout(() => {
console.log('task1 done');
value += 1;
callback(value);
}, 1000);
}else{
callback()
}
}
function task2(value, callback){
if (value) {
setTimeout(() => {
console.log('task2 done');
value += 2;
callback(value);
}, 2000);
} else {
callback()
}
}
function task3(value, callback) {
if (value) {
setTimeout(() => {
console.log('task3 done');
value += 3;
callback(value);
}, 3000);
} else {
callback()
}
}
function doProcess(value){
task1(value, function(result1){
if(result1){
task2(result1, function(result2){
if(result2){
task3(result2, function(result3){
if(result3){
console.log('####################### process done #####################')
console.log(result3)
}else{
console.log('something wrong ! in task3');
}
})
}else{
console.log('something wrong ! in task2');
}
})
}else{
console.log('something wrong ! in task1');
}
})
}
doProcess(1);

해당프로세스가 정상적으로 수행되었다면 6초뒤 7이라는 값이 찍히도록 구성하였다 .
일단 doProcess() 를 주의깊게보면, 사전적으로 수행되어야할 task가 처리될때까지 기다린후 해당 task를 실행한다. 따라서 task들 각각의 수행중 발생할수있는 에러나, 결과 리턴에 대한 데이터 핸들링이 모두 필요할수밖에없다. 일단 가독성 측면으로 보았을때 상당히 좋지않다. 더나아가 처리될 프로세스가 더 많을 경우 더욱더 난해해질것이다. ( 전통적인 callback hell 을 보여준다 )

promise

고전적인 callback hell을 벗어나, promise가 나오게된후 개발이 무척이나 편해지고 코드가 깔끔해졌다. 다음의 코드를 보자 .

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
function task1(value){
return new Promise((resolve, reject) => {
if(value){
setTimeout(() => {
console.log('task1 done');
value += 1;
resolve(value)
}, 1000);
}else{
reject('something wrong in task1');
}
})
}
function task2(value) {
return new Promise((resolve, reject) => {
if (value) {
setTimeout(() => {
console.log('task2 done');
value += 2;
resolve(value)
}, 2000);
} else {
reject('something wrong in task2');
}
})
}
function task3(value) {
return new Promise((resolve, reject) => {
if (value) {
setTimeout(() => {
console.log('task3 done');
value += 3;
resolve(value)
}, 3000);
} else {
reject('something wrong in task3');
}
})
}
function doProcess(value) {
task1(value)
.then(result1 => task2(result1))
.then(result2 => task3(result2))
.then(result3 => {
console.log('####################### process done #####################')
console.log(result3);
})
.catch(err => console.log(err));
}
doProcess(1);

doProcess() 함수를 유심히보면, 위의 고전적인 방법보다 코드가 엄청나게 많이 줄어든것을 확인할수있다. 또한 task들의 error 핸들링은 한곳에서 처리하니, 무척이나 사용이 간편해지고 또한 중첩된 함수가 없으므로 가독성이 월등히 좋아졌다. (chain 형식으로 비동기데이터를 핸들링한다. )

async await

async await은 promise를 기반으로 동작하기에 위의 promise tasks들과 크게다르지않다. 단지 ‘이 함수는 비동기적으로 수행하는 함수야’ 라고 async키워드를 붙이고, 해당 함수를 호출할때는 ‘기다려’ await 키워드를 붙여주기만 하면된다. 다음을 보자.

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
async function task1(value) {
return new Promise((resolve, reject) => {
if (value) {
setTimeout(() => {
console.log('task1 done');
value += 1;
resolve(value)
}, 1000);
} else {
reject('something wrong in task1');
}
})
}
async function task2(value) {
return new Promise((resolve, reject) => {
if (value) {
setTimeout(() => {
console.log('task2 done');
value += 2;
resolve(value)
}, 2000);
} else {
reject('something wrong in task2');
}
})
}
async function task3(value) {
return new Promise((resolve, reject) => {
if (value) {
setTimeout(() => {
console.log('task3 done');
value += 3;
resolve(value)
}, 3000);
} else {
reject('something wrong in task3');
}
})
}
async function doProcess(value){
try{
const result1 = await task1(value);
const result2 = await task2(result1);
const result3 = await task3(result2);
console.log('####################### process done #####################')
console.log(result3);
}catch(err){
console.log(err);
}
};
(async () => {
await doProcess(1);
})()

doProcess()함수를 보면 이제는 비동기 로직을 동기코드로 동작하는 효과처럼 보인다.
개발자는 비동기함수를 호출한뒤 동기코드를 작성한것처럼 리턴받기만 하면되는데 프로세스 로직구성하기가 promise 보다 더 쉬워지고, promise와 마찬가지로 error 핸들링은 catch에서 처리하면된다 .

참고

필자는 현재시점 LTS v8.9.4 버전을 사용하였다 .
추가적으로 async await은 항상 쌍으로 사용하는것이 좋다고 생각된다. ( 시작은 당연히 async 부터 .. )
사실 위의 promise style의 tasks들은 async await tasks로 사용해도 똑같이 동작한다.
promise를 사용한다고해서 암묵적으로 async를 생략하고 사용하는것보다는 명시적으로 async를 붙여주는것이 좋다고 생각한다.