[JS] 콜백지옥과 비동기 제어

고정원·2021년 6월 22일
0

1Q/1Day

목록 보기
8/13
post-thumbnail

정재남,『코어자바스크립트』를 읽고 정리한 내용입니다. 이해가 부족한 부분은 책과 동일하게 작성하였습니다.

이전글 콜백함수란?에 이어서 콜백함수를 익명 함수로 전달하는 과정의 반복이 감당하기 힘들 정도로 깊어지는 콜백지옥에 대해 이야기 해보자.

5. 콜백지옥과 비동기 제어

  • 동기적
    : 요청(request)과 응답(response)을 즉시 처리하는 것
  • 비동기적
    : 요청(request)과 응답(response)이 동시에 이루어지지 않는 것
    비동기적인 코드는 사용자의 요청에 의해 특정 시간이 경과되기 전까지 어떤 함수의 실행을 보류한다거나(setTimeout) , 사용자의 직접적인 개입이 있을때 비로소 어떤함수를 실행하도록 대기한다거나(addEventListener) ,웹브라우저자체가 아닌 별도의 대상에 무언가를 요청하고 그에 대한 응답이 왔을 때 비로소 어떤 함수를 실행하도록 대기하는(XMLHttpRequest) 등.. 별도의 요청,실행 대기, 보류 등과 관련된 작업들

🥴웹의 복잡도가 높아지면서 자바스크립트의 비동기적 코드의 비중도 훨씬 높아졌다. 콜백지옥에도 빠질일이 많아졌겠지~

1) 콜백지옥

setTimeout(function (name) {
  let 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, '카페라떼');
    }, 500, '카페모카');
  }, 500, '아메리카노');
}, 500, '에스프레소');
  • 목적 달성에는 지장이 없지만 들여쓰기 수준이 과도하게 깊어짐
  • 값이 전달되는 순서가 '아래에서 위로' 향하고 있어 어색하게 느껴짐

2) 콜백지옥 해결 → 기명함수로 변환

let coffeeList = '';

const addEspresso = function (name) {
  coffeeList = name;
  console.log(coffeeList); // "에스프레소"
  setTimeout(addAmericano, 500, '아메리카노');
};

const addAmericano = function (name) {
  coffeeList += ', ' + name;
  console.log(coffeeList); // "에스프레소, 아메리카노"
  setTimeout(addMocha, 500, '카페모카');
};

const addMocha = function (name) {
  coffeeList += ', ' + name;
  console.log(coffeeList); // "에스프레소, 아메리카노, 카페모카"
  setTimeout(addLatte, 500, '카페라떼');
};

const addLatte = function (name) {
  coffeeList += ', ' + name;
  console.log(coffeeList); // "에스프레소, 아메리카노, 카페모카, 카페라떼"
};

setTimeout(addEspresso, 500, '에스프레소');
  • 코드의 가독성을 높임
  • 함수 선언과 함수 호출만 구분할 수 있다면 위에서부터 아래로 순서대로 읽어 내려가는데 어려움이 없음

🤔잠깐! 비동기 처리를 왜 반드시 해야할까?

  • JS엔진은 코드 블록안에 코드를 동기적으로 실행함
  • 시간이 오래걸리는(응답) 코드를 비동기 처리를 전혀 안하면, 다음 코드에 문제 생길 수 있음
    예를 들어 서버에서 data를 받아와서 웹페이지에 출력하는 시나리오가 있다.
    → 데이터를 받아오는데 10초가 걸림, 근데 비동기 처리를 안하면 텅 빈 data를 출력 해버린다🥶

3) ✨비동기 작업의 동기적표현

Promise(1)

내가 언제 유저의 data를 받아 올진모르겠지만 내가 약속할게, Promise라는 object를 가지고 있으면 여기에 니가 then이라는 콜백함수만 등록해 놓으면 유저의 data가 준비 되는 대로 니가 등록한 콜백함수 불러줄께! 굿!?? 오키!!

프로미스는 어떤 기능을 실행 후
- 정상적으로 동작하면 → 성공의 메시지와 함께 처리된 결과값을 전달
- 예상치 못한 에러 발생 → Error를 전달
State : pending(보류) → fulfilled(이행) or rejected(거부)

new Promise(function (resolve) {
  setTimeout(function () {
    const name = '에스프레소';
    console.log(name);
    resolve(name);
  }, 500);
}).then(function (prevName) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      const name = prevName + ', 아메리카노';
      console.log(name);
      resolve(name);
    }, 500);
  });
}).then(function (prevName) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      const name = prevName + ', 카페모카';
      console.log(name);
      resolve(name);
    }, 500);
  });
}).then(function (prevName) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      const name = prevName + ', 카페라떼';
      console.log(name);
      resolve(name);
    }, 500);
  });
})
  • new 연산자와 함께 호출한 Promise의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행
  • 그 내부에 resolve 또는 reject 함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지는 then 또는 catch 로 넘어가지 않음

Promise(2)

const addCoffee = function (name) {
  return function (prevName) {
    return new Promise(function (resolve) {
      setTimeout(function () {
        const newName = prevName ? (prevName + ', ' + name) : name;
        console.log(newName);
        resolve(newName);
      }, 500);
    });
  }
};

addCoffee('에스프레소')()
  .then(addCoffee('아메리카노'))
  .then(addCoffee('카페모카'))
  .then(addCoffee('카페라떼'))
  • 반복적인 내용을 함수화해서 더욱 짧게 표현

Generator

ES6에 추가된 '*'이 붙은 함수

const addCoffee = function (prevName, name) {
  setTimeout(function () {
    coffeeMaker.next(prevName ? prevName + ', ' + name : name);
  }, 500);
};

const coffeeGenerator = function* () {
  const espresso = yield addCoffee('', '에스프레소');
  console.log(espresso);
  
  const americano = yield addCoffee(espresso, '아메리카노');
  console.log(americano);
  
  const mocha = yield addCoffee(americano, '카페모카');
  console.log(mocha);
  
  const latte = yield addCoffee(mocha, '카페라떼');
  console.log(latte);
};

const coffeeMaker = coffeeGenerator();
coffeeMaker.next();
  • Generator 함수를 실행하면 Iterator가 반환되고, Iterator는 next라는 메서드를 가지고 있음
  • next 메서드를 호출하면 Generator 함수 내부에서 가장 먼저 등장하는 yield에서 함수의 실행을 멈춤
  • 이후 다시 next 메서드를 호출하면 앞서 멈췄던 부분부터 시작해서 그 다음에 등장하는 yield에서 함수의 실행을 멈춤
  • 비동기 작업이 완료되는 시점마다 next 메서드를 호출하면 Generator 함수 내부의 소스가 위에서부터 아래로 순차적으로 진행 됨

💡프로미스 연결도 계속하면 코드 가독성 떨어져~~

✨Promise + Async/Await

const addCoffee = function (name) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(name);
    }, 500);
  });
};

const coffeeMaker = async function () {
  let coffeeList = '';
  const _addCoffee = async function (name) {
    coffeeList += (coffeeList ? ',' : '') + await addCoffee(name);
  };
  
  await _addCoffee('에스프레소');
  console.log(coffeeList);
  await _addCoffee('아메리카노');
  console.log(coffeeList);
  await _addCoffee('카페모카');
  console.log(coffeeList);
  await _addCoffee('카페라떼');
  console.log(coffeeList);

};

coffeeMaker();
  • ES8(ES2017)에서 가독성이 뛰어나면서 작성법도 간단한 새로운 기능이 추가
  • async를 사용하면 함수의 코드 블록이 자동으로 Promise로 변환이 되어짐
  • 비동기 작업을 수행하고자 하는 함수 앞에 async 표기하고, 함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await 표기
  • await는 async내부에서만 사용 가능!
  • await 뒤의 내용이 Promise로 자동 전환, 해당 내용이 resolve 된 이후에야 다음으로 진행

정리

  • 비동기제어를 위해 콜백 함수를 사용하다 보면 콜백지옥에 빠지기 쉽다. 최근의 ECMAScript에는 Promise, Generator, async/await 등 콜백 지옥에서 벗어날 수 있는 훌륭한 방법들이 있다.

[참고한자료]
정재남, 『코어자바스크립트』, 위키북스(2019)
https://hsolemio-lee.github.io/core-javascript1/
https://youtu.be/aoQSOZfz3vQ

profile
해결문제에 대해 즐겁게 대화 할 수 있는 프론트엔드 개발자

0개의 댓글