JavaScript

(CallBack)콜백 함수의 콜백 지옥과 비동기 제어

추운날_너를_기다리며 2024. 8. 14. 17:47

콜백 지옥이란 간단하게 말해서 남을 고려하지 않은 코드를 작성한 겁니다.

즉, 그저 내가 표현하고 싶은 것을 위해 콜백을 이어붙여 가독성, 유지보수를 최악으로 만드는 것을 뜻합니다.

 

그렇다면, 콜백 지옥을 해결하기 위한 비동기 제어에 대해서 알아보겠습니다.

비동기(asynchronous)란, 실행 중인 코드의 여부와 무관하게 즉시 다음 코드로 넘어가는 방식으로 setTimeout, addEventListener 등이 있습니다.

또한, 별도의 요청, 실행 대기, 보류 등과 관련된 코드는 모두 비동기적 코드입니다.

 

동기(synchronous)란, 현재 실행중인 코드가 끝나야 다음 코드를 실행하는 방식을 말합니다.

 

비동기의 간단한 이해를 돕기 위해 예시를 들겠습니다.

// 비동기적 코드의 이해!
setTimeout(function(){
    console.log('여기가 먼저 실행될까?!?!?');
}, 1000);

console.log('여기좀 봐주세요!!!!');

위의 코드가 실행 되면 어떻게 될까요?

여기좀 봐주세요!!!!
여기가 먼저 실행될까?!?!?

로 실행이 됩니다.

이게 바로 비동기 입니다. (직렬, 병렬중에서 병렬에 가까운 것이 비동기 입니다.)

 

콜백 지옥의 해결방법을 설명하겠습니다.

[1] 기명함수로 변환해주는 겁니다.

var coffeeList = '';

var addEspresso = function (name) {
    coffeeList = name;
    console.log(coffeeList);
    setTimeout(addAmericano, 500, '아메리카노');
};

var addAmericano = function (name) {
    coffeeList += ', ' + name;
    console.log(coffeeList);
    setTimeout(addMocha, 500, '카페모카');
};

var addMocha = function (name) {
    coffeeList += ', ' + name;
    console.log(coffeeList);
    setTimeout(addLatte, 500, '카페라떼');
};

var addLatte = function (name) {
    coffeeList += ', ' + name;
    console.log(coffeeList);
};

setTimeout(addEspresso, 500, '에스프레소');

이런식으로 나타내 줍니다.

하지만 어차피 익명 함수로 쓰고 한번만 쓸건데 이름을 다 붙였습니다.

따라서 인적 리소스의 엄청난 낭비가 있기 때문에 비동기 작업의 동기적 표현이 필요합니다.

 

[2] 비동기 작업의 동기적 표현 - Promise

Promise는 비동기 처리에 대해, 처리가 끝나면 알려달라는 '약속' 으로 밑의 3가지를 알아야합니다.

1) new 연산자로 호출한 Promise의 인자로 넘어가는 콜백은 바로 실행됩니다.

2) 그 내부의 resolve(또는 reject) 함수를 호출하는 구문이 있을 경우 resolve(또는 reject) 둘 중 하나가 실행되기 전까지는 다음(then), 오류(catch)로 넘어가지 않아요.

3) 따라서, 비동기작업이 완료될 때 비로소 resolve, reject를 호출합니다.

 

그렇다면 바로 예시로 넘어가겠습니다.

new Promise(function(resolve) {
    setTimeout(function(){
        var name = "에스프레소";
        console.log(name);
        resolve(name); // resolve로 인자를 넘겨줌
    }, 500);
}).then(function(prevName){
    // 이 안에서도 새롭게 Promise를 만들어요!
    return new Promise(function(resolve) {
        setTimeout(function(){
            var name = prevName + ", 아메리카노";
            console.log(name);
            resolve(name); // resolve로 인자를 넘겨줌
        }, 500);
    });
}).then(function(prevName){
    // 이 안에서도 새롭게 Promise를 만들어요!
    return new Promise(function(resolve) {
        setTimeout(function(){
            var name = prevName + ", 카페모카";
            console.log(name);
            resolve(name); // resolve로 인자를 넘겨줌
        }, 500);
    });
}).then(function(prevName){
    // 이 안에서도 새롭게 Promise를 만들어요!
    return new Promise(function(resolve) {
        setTimeout(function(){
            var name = prevName + ", 카페모카";
            console.log(name);
            resolve(name); // resolve로 인자를 넘겨줌
        }, 500);
    });
})

위에 예시처럼 각 함수가 끝날 때마다 then을 붙여주고 Promise 함수가 끝날 때마다 resolve로 인자를 넘겨줍니다.

이렇게 하면 될것 같지만 너무 코드가 길기 때문에 반복되는 함수 부분을 합쳐주겠습니다.

 

[3] 비동기 작업의 동기적 표현 - Promise 완벽한 해결책

var addCoffee = (name) => {
    return function (prevName) {
        return new Promise(function (resolve) {
            setTimeout(function () {
                // 백틱
                var newName = prevName ? `${prevName}, ${name}` : name;
                console.log(newName);
                resolve(newName);
            }, 500);
        });
    };
};

addCoffee('에스프레소')()
    .then(addCoffee('아메리카노'))
    .then(addCoffee('카페모카'))
    .then(addCoffee('카페라떼'));

 

위의 코드를 설명해 드리겠습니다.

일단 addCoffee('에스프레소')가 실행이 되면 밑에 부분이 return이 되는 것이기 때문에 우리가 첫번째로 실행하려고 하는 부분인 return new Promise(function(resolve) { }가 실행되지 않습니다.

var addCoffee = (name) => {
    return function (prevName) {
        ....
    };
};

 

따라서, 우리가 원한 부분을 실행해주기 위해서

addCoffee('에스프레소')()

를 먼저 적어주고 그 뒤에 then을 붙여서 그다음 부분들을 넣어주면

addCoffee('에스프레소')()
    .then(addCoffee('아메리카노'))
    .then(addCoffee('카페모카'))
    .then(addCoffee('카페라떼'));

이렇게 실행을 해주면 

addCoffee('에스프레소')()

밑에 코드가 실행이 되고

return new Promise(function (resolve) {
            setTimeout(function () {
                // 백틱
                var newName = prevName ? `${prevName}, ${name}` : name;
                console.log(newName);
                resolve(newName);
            }, 500);
        });
addCoffee('에스프레소')()
    .then(addCoffee('아메리카노'))

위의 코드가 실행이 되면 new Promise가 두번 실행이 되는 겁니다.

그럼 resolve(newName);을 통해서 그다음 코드가 실행될 때 그전의 newName이 prevName으로 넘어가는 형식이 됩니다.

그렇게 우리가 원하는 깔끔한 코드가 완성되었습니다.

 

[4] 비동기 작업의 동기적 표현 - Generator

1) 제너레티어 함수는 기본적으로 실행하면, iterator 객체를 반환(next() 를 가지고 있습니다.)

2) 즉, 제너레이터는 반복할 수 있는 iterator 객체를 생성해줍니다.

3) 따라서 비동기 작업이 완료되는 시점마다 next 메서드를 호출해주면 Generator 함수 내부소스가 위 -> 아래 순차적으로 진행이 됩니다.

4) *가 붙은 함수가 제너레이터 함수입니다.

5) yield와 stop 그리고 next함수가 있는데 yield 키워드를 통해서 순서를 제어해 주고 다 끝나면 그다음 것이 실행되고 stop을 하면 next함수가 호출되기 전까지 다시 실행되지 않습니다.

(아직 stop을 배우진 않았습니다.)

 

이제 예시로 배우겠습니다.

// (1) 제너레이터 함수 안에서 쓸 addCoffee 함수 선언
var addCoffee = function (prevName, name) {
    setTimeout(function () {
        coffeeMaker.next(prevName ? prevName + ', ' + name : name);
    }, 500);
};

// (2) 제너레이터 함수 선언!!!
// yield 키워드로 순서 제어
var coffeeGenerator = function* () {
    var espresso = yield addCoffee('', '에스프레소');
    console.log(espresso);
    var americano = yield addCoffee(espresso, '아메리카노');
    console.log(americano);
    var mocha = yield addCoffee(americano, '카페모카');
    console.log(mocha);
    var latte = yield addCoffee(mocha, '카페라떼');
    console.log(latte);
};

var coffeeMaker = coffeeGenerator();
coffeeMaker.next();

위의 코드를 해석해보면

var coffeeMaker = coffeeGenerator();
coffeeMaker.next();

위의 코드를 통해서 제너레이터 함수를 통해서 coffeeMaker가 iterator 객체가 되고 그 iterator 객체를 next함수로 첫번째 실행을 해줍니다.

coffeeGenerator 함수 안이 실행이 되고

var espresso = yield addCoffee('', '에스프레소');

여기서 첫번째로 멈춥니다.

addCoffee('','에스프레소');가 실행 되면서 addCoffee  의 setTimeout() 메서드가 실행되고 그다음 next가 실행되면서 순차적 실행이 일어납니다.

 

[5] 비동기 작업의 동기적 표현 - Promise + Async/await

이전의 방법 : then

이번에 쓸 방법 : async(비동기) / await(기다리다)

바로 예시로 들어가겠습니다.

// coffeeMaker 함수에서 호출할 함수, 'addCoffee'를 선언
// Promise를 반환
var addCoffee = function (name) {
    return new Promise(function (resolve) {
        setTimeout(function(){
            resolve(name);
        }, 500);
    });
};

//
var coffeeMaker = async function () {
// var coffeeMaker = async () => {
    var coffeeList = '';
    var _addCoffee = async function (name) {
        coffeeList += (coffeeList ? ', ' : '') + await addCoffee(name);
    };

    // Promise를 반환하는 함수인 경우, await를 만나면 무조건 끝날 때 까지 기다린다.
    // _addCoffee('에스프레소') 이 로직이 실행되는데 100초가 걸렸다.
    await _addCoffee('에스프레소');
    // console.log(coffeeList)는 100초 뒤 실행...
    console.log(coffeeList);

   
    await _addCoffee('아메리카노');
    console.log(coffeeList);
    await _addCoffee('카페모카');
    console.log(coffeeList);
    await _addCoffee('카페라떼');
    console.log(coffeeList);
};

coffeeMaker();

1) new Promise() 로 Promise함수를 바로 실행하는 함수 addCoffee를 만들어 줍니다.

2) coffeeMaker라는 비동기 함수를 선언해 줍니다.

3) 안에서 기다려주는 await 메서드를 사용해서 구분지어 줍니다.

4) coffeeMaker안에 addCoffee가 실행을 자동으로 해줄 새로운 함수 _addCoffee를 비동기 함수로 만들어 줍니다.

5) 모든 비동기 함수안에는 각 함수가 순차적으로 실행이 되게 await 메서드를 메서드가 실행되기 전에 적어 줍니다.

'JavaScript' 카테고리의 다른 글

가장 많이 받은 선물 -JS  (0) 2024.08.20
Math.min(Array)를 넣었더니 NaN이 나왔다  (0) 2024.08.19
클로저 (Closure)  (0) 2024.08.16
(CallBack)콜백 함수  (0) 2024.08.14
왜 이렇게 호출 되는가?  (0) 2024.08.13