Back Ground

Callback Hell 이란 [ 해결방법 ] 본문

Javascript/Node.js

Callback Hell 이란 [ 해결방법 ]

Back 2019. 1. 23. 22:50

콜백 지옥(Callback hell)

콜백 지옥은 비동기 처리 로직을 위해 콜백 함수를 연속해서 사용할 때 발생하는 문제이다.

1
2
3
4
5
6
7
8
9
$.get('url'function (response) {
    parseValue(response, function (id) {
        auth(id, function (result) {
            display(result, function (text) {
                console.log(text);
            });
        });
    });
});
cs



웹 서비스를 개발하다 보면 서버에서 데이터를 받아와 화면에 표시하기까지 인코딩, 

사용자 인증 등을 처리해야 하는 경우가 있다.

만약 이 모든 과정을 비동기로 처리해야 한다고 하면 

위와 같이 콜백 안에 콜백을 계속 무는 형식으로 코딩을 하게 된다.


이러한 코드 구조는 구조는 가독성도 떨어지고 로직을 변경하기도 어렵다.

이와 같은 코드 구조를 콜백 지옥이라고 한다.





콜백 지옥을 해결하는 방법 


일반적으로 콜백 지옥을 해결하는 방법에는 Promise 나 Async를 사용하는 방법이 있다.



- 코딩 패턴으로만 해결 

만약 코딩 패턴으로만 콜백 지옥을 해결하려면 아래와 같이 각 콜백 함수를 분리해주면 된다.


1
2
3
4
5
6
7
8
9
10
11
12
function parseValueDone(id) {
    auth(id, authDone);
}
function authDone(result) {
    display(result, displayDone);
}
function displayDone(text) {
    console.log(text);
}
$.get('url'function (response) {
    parseValue(response, parseValueDone);
});
cs


위 코드는 앞의 콜백 지옥 예시를 개선한 코드이다.

중첩해서 선언했던 콜백 익명 함수를 각각의 함수로 구분하였다.

정리된 코드를 간단하게 살펴보겠다.

먼저 Ajax통신으로 받은 데이터를 parseValue()메서드로 파싱한다.

parseValueDone()에 파싱한 결과값인 id가 전달되고 auth() 메서드가 실행된다.

auth()메서드로 인증을 거치고 나면 콜백 함수 authDone()이 실행 된다. 

인증 결과 값인 result로 display()를 호출하면 마지막으로 displayDone()메서드가 수행되면서 text가 콘솔에 출력된다.


출처 : https://joshua1988.github.io/web-development/javascript/javascript-asynchronous-operation/#%EC%BD%9C%EB%B0%B1-%EC%A7%80%EC%98%A5-callback-hell






- Promise


자바스크립트와 노드에서는 주로 비동기 프로그래밍을 한다.
특히 이벤트 주도 방식 때문에 콜백 함수를 자주 사용한다.
ES2015부터는 자바스크립트와 노드의 API들이 콜백 대신 프로미스 기반으로 재구성된다.
그래서 악명 높은 콜백 헬(callback hell)을 극복했다는 평가를 받고 있다.

프로미스는 다음과 같은 규칙이 있다.
먼저 프로미스 객체를 생성해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const condition = true// true면 resolve, false면 reject
const promise = new Promise( (resolve, reject) => {
    if(condition){
        resolve('성공');
    }else{
        reject('실패');
    }
});
 
promise
    .then( (message) => {
        console.log(message); // 성공(resolve)한 경우 실행 
    })
    .catch( (error) => {
        console.error(error); // 실패(reject)한 경우 실행 
});
cs

new Promise로 프로미스를 생성할 수 있으며, 
안에 resolve와 reject를 매개변수로 갖는 콜백 함수를 넣어준다.
이렇게 만든 promise변수에 then과 catch메서드를 붙일 수 있다.
프로미스 내부에서 resolve가 호출되면 then이 실행되고, 
reject가 호출되면 catch가 실행된다.



resolve와 reject에 넣어준 인자는 각각 then과 catch의 매개변수에서 받을 수 있다.

즉, resolve('성공')가 호출되면 then의 message가 '성공'이 된다.

만약 reject('실패')가 호출되면 catch의 error가 '실패'가 되는 것이다.

condition 변수를 false로 바꿔보면 catch에서 에러가 로깅된다.


then이나 catch에서 다시 다른 then이나 catch를 붙일 수 있다.

이전 then의 return값을 다음 then의 매개변수로 넘긴다.

프로미스를 return한 경우 프로미스가 수행된 후 다음 then이나 catch가 호출된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
promise
    .then( (message) => {
        return new Promise( (resolve, reject) => {
            resolve(message2);
        });
    })
    .then( (message2) => {
        console.log(message2);
        return new Promise( (resolve, reject) => {
            resolve(message2);
        });
    })
    .then( (message3) ) =>{
        console.log(message3);
    })
    .catch( (error) => {
        console.error(error);
    })
});
 
cs


처음 then에서 message를 resolve하면 다음 then에서 받을 수 있다.

여기서 다시 message2를 resolve했으므로 다음 then에서 message3를 받는다.


이것을 활용해서 콜백을 프로미스로 바꿀 수 있다. 다음은 콜백을 쓰는 콜백을 쓰는 패턴 중 하나이다.

이를 프로미스로 바꿔보겠다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function findAndSaveUser(Users){
    Users.findOne( {}, ( err, user ) => { //첫 번째 콜백
        if(err){
            return console.error(err);
        }
        user.name = 'zero';
        user.save( (err) => { // 두 번째 콜백
            if(err){
                return console.error(err);
            }
        }
        Users.findOne( {gender : 'm'}, (err, user) => { // 세 번째 콜백
            //생략
        });
    });
  });    
}
cs


콜백 함수가 세 번 중첩되어 있다. 

콜백 함수가 나올 때마다 코드의 깊이가 깊어진다.

각 콜백 함수마다 에러도 따로 처리해줘야 한다. 

이 코드를 다음과 같이 바꿀 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function findAndSaveUser(Users){
    Users.findOne( {} ) 
        .then( (user) => {
            user.name = 'zero';
            return user.save();
        }) 
        .then( (user) => {
            return Users.findOne( {gender : 'm' });
        })
        .then( (user) => {
            // 생략
        })
        .catch( err => {
            console.error(err);
        });
}
cs


코드의 깊이가 더 이상 깊어지지 않다.

then메소드들은 순차적으로 실행 된다. 

콜백에서 매번 따로 처리해야 했던 에러도 마지막 catch에서 한번에 처리할 수 있다.

하지만 모든 콜백함수를 위와 같이 바꿀 수 있는 것은 아닙니다. 

메서드가 프로미스 방식을 지원해야 한다. 

예제의 코드는 findOne과 save메서드가 내부적으로 프로미스 객체를 가지고 있어서 가능한 것이다.



1
2
3
4
5
6
7
8
9
const promise1 = Promise.resolve('성공1');
const promise2 = Promise.resolve('성공2');
Promise.all([promise1, promise2])
    .then( ( result) => {
        console.log(result); // ['성공1', '성공2']
    })
    .catch( (error) => {
        console.error(error);
    });
cs


Promise.resolve는 즉시 resolve하는 프로미스를 만드는 방법이다.

비슷한 것으로 즉시 reject하는 Promise.reject도 있다.


프로미스가 여러 개 있을 때 Promise.all에 넣으면 모두 resolve될 때까지 기다렸다가 then으로 넘어간다.

result 매개변수에 각각의 프로미스 결괏값이 배열로 들어 있다.

Promise 중 하나라도 reject가 되면 catch로 넘어간다.



'Javascript > Node.js' 카테고리의 다른 글

Node - 요청과 응답 [http모듈 웹 서버]  (0) 2019.02.23
Node - Node 기능  (0) 2019.02.15
NodeJS - ES2015+  (0) 2019.01.18
NodeJS - RestFul [ES5]  (0) 2018.11.11
NodeJS - Router 사용해 요청 라우팅하기 [ES5]  (0) 2018.10.07
Comments