본문 바로가기
개발 공부 일지/코어 자바스크립트 (정재남)

[코어 자바스크립트] 비동기 제어로 콜백 지옥 탈출하기

by yelimu 2025. 2. 25.

콜백 지옥과 비동기 제어

- 콜백 지옥 : 비동기 처리를 위해 연속적으로 콜백 함수를 사용할 때, 콜백 함수를 익명 함수로 전달하는 과정이 반복되어

코드의 들여쓰기 수준이 매우 깊어지는 현상

가독성이 떨어지고 코드를 수정하기 어렵다 . (유지보수가 어렵다) -> 생산성 저하 

이런 짤 많이 보셨으리라

 

- 비동기 : 현재 실행중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어간다. 

보류, 요청, 실행 대기 등과 관련된 코드는 비동기적인 코드이다. 

  • 사용자 요청에 의해 특정 시간동안 코드 실행이 보류(setTimeout) 
  • 사용자의 직접적인 개입이 있었을때 코드를 실행(addEventListener)
  • 웹브라우저 자체가 아닌 별도의 대상에 무언가를 요청하고 응답이 왔을때 실행 (XMLHttpRequest)

 

반대로 동기적인 코드는, 현재 실행중인 코드가 완료되어야 다음 코드를 실행한다. 

CPU 계산에 의해 즉시 처리가 가능한 코드는 대부분 동기적인 코드이다. (계산 시간이 오래 걸리더라도)

 

 

웹의 복잡도가 높아지면서 비동기적인 코드 비중이 높아짐 = 콜백지옥에 빠지기 쉽다. 

setTimeout(function (name){
  var coffeeList = name; 
  console.log(coffeeList);

  setTimeout(function (name){
      coffeeList += ',' + name;
      console.log(coffeeList);

      setTimeout(function (name){
          coffeeList += ',' + name;
          console.log(coffeeList);

          setTimeout(function (name){
              coffeeList += ',' + name;
              console.log(coffeeList);
          }, 500, '카페라떼') // name 파라미터로 전달
      }, 500, "카페모카")
    }, 500, "아메리카노")
}, 500, "에스프레소")
에스프레소
에스프레소,아메리카노
에스프레소,아메리카노,카페모카
에스프레소,아메리카노,카페모카,카페라떼

 

↓ 기명 함수로 바꿔보기 

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, "에스프레소")

 


비동기적인 일련의 작업을 동기적으로 보이도록 하는 방법 (비동기를 동기처럼 보이게하는 방법)

  • Promise, Generator (ES6)
  • async/await (ES2017)

Promise + then

//new 연산자와 함께 호출한 Promise의 인자로 넘겨주는 콜백함수는 호출 시 바로 실행
new Promise(function (resolve){ 
  setTimeout(function (){
    var name = "에스프레소";
    console.log(name);
    resolve(name) // resolve함수 호출 -> 끝날때까지 then으로 넘어가지 않음
  }, 500) 
}).then(function(prevName){
  return new Promise(function (resolve){
    setTimeout(function(){
      var name = prevName + ", 아메리카노";
      console.log(name);
      resolve(name);
    }, 500);
  })
}).then(function(prevName){
  return new Promise(function (resolve){
    setTimeout(function(){
      var name = prevName + ", 까페모카";
      console.log(name);
      resolve(name);
    }, 500);
  })
}).then(function(prevName){
  return new Promise(function (resolve){
    setTimeout(function(){
      var name = prevName + ", 까페라떼";
      console.log(name);
      resolve(name);
    }, 500);
  })
})

new 연산자와 함께 호출한 Promise의 인자로 넘겨주는 콜백함수는 호출 시 바로 실행되지만, 

그 내부에 resolve 또는 reject 함수를 호출하는 구문 이 있을 경우 둘 중 하나가 실행되기 전까지는 then 또는 catch 구문으로 넘어가지 않는다. 

따라서 비동기 작업이 완료될 때 비로소 resolve / reject를 호춯하는 방법으로 비동기 작업의 동기적 표현이 가능하다 

Promise는 세 가지 상태(state)를 가질 수 있습니다. 비동기 작업이 아직 수행되지 않은 상태인 pending, 비동기 작업이 성공적으로 완료된 fulfilled 상태, 비동기 작업이 실패한 rejected 상태가 있습니다.
Promise를 생성할 때는 Promise 생성자 함수를 호출하면 됩니다. Promise 생성자 함수는 비동기 작업을 수행하고, 작업이 완료되면 Promise 객체를 반환합니다.

 

Generator

// 0.5초 후에 coffeeMaker(반환된 이터레이터).next 를 실행하는 함수
var addCoffee = function (prevName, name){
  setTimeout(function(){
    coffeeMaker.next(prevName? prevName + ',' + name : name); // next메서드 호출
  }, 500)
}
// Generator 함수
var coffeeGenerator = function*(){ 
  var espresso = yield addCoffee('', "에스프레소"); // yield 키워드-> addCoffee를 기다림
  console.log(espresso); // addCoffee의 next에 의해 코드 진행
  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(); // Iterator 객체를 반환

coffeeMaker.next() // next메서드 호출

Generator 함수 실행하면 Iterator 가 반환된다. 

Iterator의 next 메서드를 호출하면 Generator 함수 내부에서 가장 먼저 등장하는 yield에서 Generator 함수의 실행을 멈춘다

이후 다시 next 메서드를 호출하면 앞서 멈춘 부분부터 시작해서 다음에 등장하는 yield에서 함수 실행을 멈춘다

=> 비동기 작업이 완료되는 시점마다 next 메서드를 호출하면 Generator 함수 내부의 소스가 위에서 아래로 순차적으로 진행 

 

 

Promise + async / await

// 
var addCoffee = function(name){
  return new Promise(function(resolve){ // new Promise 생성
    setTimeout(function(){ 
      resolve (name) 
    }, 500) // 0.5초 후에 resolve(name) 실행 = Promise를 fulfilled로 변경
  })
};

var coffeeMaker = async function(){
  var coffeeList = "";
  var _addCoffee = async function(name){
    coffeeList += (coffeeList ? "," : "") + await addCoffee(name); //await :fulfilled까지 기다림
  };

  await _addCoffee("에스프레소");
  console.log(coffeeList)
  await _addCoffee("아메리카노");
  console.log(coffeeList)
  await _addCoffee("카페모카");
  console.log(coffeeList)
  await _addCoffee("카페라떼");
  console.log(coffeeList)
}

coffeeMaker()

 

coffeeMaker함수 내부에서 await addCoffee를 호출하면

new Promise를 생성하고, 0.5초 후에 resolve(name)으로 Promise 상태가 fulfilled가 된다.

await 키워드는 Promise 상태가 fulfilled가 되기를 기다리고, Promise가 반환한 값을 coffeeList에 추가한다. 

이 동작이 완료되면 콘솔에 출력

 

함수 앞 언더스코어( _ ) : 일반적으로 함수나 변수를 내부(private) 용도로 사용한다는 의미로 쓰이는 관례
1. 외부에서 직접 호출하지 말라는 암시
2.coffeeMaker와_addCoffee가 분리된 역할을 가지고 있음을 한눈에 알 수 있음.

 

참조 블로그


프로젝트할때는 비동기 처리 시 async, await 를 주로 사용했는데 Promise 나, 키워드의 정확한 개념을 모르고 사용했었다.

Promise와 Generator 도 잘 모르지만 일단 덮어두었던 내용이라 이번 기회에 학습할 수 있어 유익했다.

 

ES6 부터 공부를 시작한 터라 이전에 주로 사용하던 개념에 대해 이해가 부족한 경우가 많고, 새로운 문법이 왜 등장했을까? 어떤 점이 좋다는걸까? 에 대해서도 고민없이 사용했던 것 역시 반성해야 할 태도이다. 

 

이 포스팅은 <코어 자바스크립트> 를 읽고 내용을 정리한 포스팅입니다. 틀린 내용이 있다면 알려주세요 _ _