콜백 함수 (2): 콜백 지옥과 비동기 제어

summereuna🐥·2024년 6월 3일

JS 문법 정리

목록 보기
15/20

콜백 지옥과 비동기 제어



1. 콜백지옥이란



(이미지 출처 : https://preiner.medium.com/callback지옥에-promise-적용하기-d02272ecbabe)

  1. 콜백 함수를 익명 함수(매개변수로 전달..)로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 헬 수준인 경우를 말한다. ^~^

  2. 주로 이벤트 처리서버 통신과 같은 비동기적 작업을 수행할 때 발생

  3. 문제점: 가독성 지옥(hell), 오랜 상태로 이렇게 짜여왔기 유지보수가 어렵다.


2. 동기 vs 비동기


2-1. 동기와 비동기의 개념


예시

  • 동기: 주문 후, 커피가 나올 때 까지 기다렸다가, 커피 가져가면 => 그 다음 손님 주문 반복
  • 비동기: 주문 후, 다음 손님 즉시 주문 => 먼저 준비된 음료 진동벨 울린 손님들은 먼저 가져감

1. 동기 : synchronous ⇒ (sync)

  1. 현재 실행중인 코드가 끝나야 다음 코드를 실행하는 방식
  2. CPU의 계산에 의해 즉시 처리가 가능한 대부분의 코드
  3. 계산이 복잡해서 CPU계산하는 데에 오래 걸리는 코드

2. 비동기 : a + synchronous ⇒ (async)

  1. 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어가는 방식
  2. setTimeout, addEventListner
  3. 별도의 요청, 실행 대기, 보류 등과 관련된 코드는 모두 비동기적 코드
    => 대부분의 서버,클라이언트 웹 통신

복잡도가 올라갈 수록 비동기적 코드의 비중이 늘어난다!

(예시) 비동기 코드인 setTimeout 함수의 동작원리

// setTimeout 함수의 동작원리

setTimeout(function(){
	// 기본적으로 1000ms이 지나야 여기 로직이 실행 된다
	console.log('여기가 먼저 실행될까?');
}, 1000);


console.log('여기 봐주세요~');
  • 동기적 코드라면, setTimeout() 내부의 코드가 끝나야 밑에 콘솔이 찍힌다.
  • 하지만 setTimeout은 비동기적 코드이기 때문에 setTimeout() 내부 코드 끝나는 것 기다리지 않고 바로 아래 콘솔 찍힌다.

3. 콜백지옥의 예시와 해결방안


3-1. 익숙한 setTimeout을 통해 콜백 지옥의 간단한 예시


  • 들여쓰기 수준 📉
  • 값 전달 순서 : 아래 → 위
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,
              "카페라떼"
            );
          },
          500,
          "카페모카"
        );
      },
      500,
      "아메리카노"
    );
  },
  500,
  "에스프레소"
);

//"에스프레소"
//"에스프레소", "아메리카노"
//"에스프레소", "아메리카노", "카페모카"
//"에스프레소", "아메리카노", "카페모카", "카페라떼"

3-2. 콜백지옥 코드 해결방안


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. 두 번째 해결방법: 비동기적인 작업을 동기적인 것 처럼 보이도록 처리해주는 JS의 장치


이런 경우 때문에 자바스크립트에서는 비동기적인 작업을 동기적으로(동기적인 것 처럼 보이도록) 처리해주는 장치를 계속해서 마련해주고 있다.

  • Promise
  • Generator(ES6)
  • async/await(ES7)
  • 등..

4. 비동기 작업의 동기적 표현


4-1. 비동기 작업의 동기적 표현이 🌟필요한 이유


먼저, 왜 중첩을 해야만 하는가를 생각해보자.

비동기 작업의 특징: 순서를 보장하지 않는다.
그렇기 때문에 언제 제어권이 다시 나에게 올지 모른다는 것이 비동기 작업의 특징이다.

setTimeout()은 몇 초 뒤인지 알잖아요?
그건 setTimeout입장이고, 제어권 넘겨준 코드 입장에선 모른다.

비동기 처리 서버 통신을 예시로 들어보자.

naver 날씨 api 정보 얻고, 그 날씨 정보를 바탕으로 kakao 지도 api 정보를 얻어 오고자 한다면?
일의 순서가 naver 먼저 선행되야 하고 다음에 kakao에 보내야 한다.

  • 원래는 네이버 3초 걸리고 kakao는 8초 걸린다고 해보자.

  • 그런데 네이버에 과부하 걸려서 시간 10초 이상으로 더 많이 걸려 버린다면 kakao한테 정보를 전달할 수가 없으므로 내부 서버 입장에서 대응 할 수 없게 되어 버린다.

  • 이 처럼 서버 통신 처럼 비동기 함수 로직으로 가게 되면 이런 상황 발생할 수 있기 때문에,
    비동기 작업을 동기적으로 표현하는 시도가 필요하다.

4-2. 비동기 작업의 동기적 표현 - Promise, Generator, async/await


4-1. <비동기 작업의 동기적 표현(1) - Promise(1)>


Promise는 비동기 처리에 대해, 처리가 끝나면 알려달라는 ‘약속이다.

  • new 연산자로 호출한 Promise의 인자로 넘어가는 콜백은 바로 실행된다.
  • 그 내부의 resolve(성공) (또는 reject(실패)) 함수를 호출하는 구문이 있을 경우,
    resolve(또는 reject) 둘 중 하나가 실행되기 전까지는 다음(then), 오류(catch)로 넘어가지 않는다.
  • 따라서, 비동기작업이 완료될 때 비로소 resolve, reject를 호출한다.

짧은 예시

  • 네이버 날씨 정보 잘 얻어온 경우에는 resolve > Promise.then 으로 넘어감
  • 네이버 서버 다운으로 못 얻어온 경우 응답 못 주니까 rejcect > Promise.catch 로 넘어감

이런 방법으로 비동기 -> 동기적 표현을 구현할 수 있다.

커피 예제: Promise로 비동기 작업의 동기적 표현하기

// Promise 만들기
// 즉시 실행되는 콜백 함수 넣기 <- 인자로 resolve로 넣자
new Promise(function (resolve) {
  //0.5초 후에 커피 이름 찍기
	setTimeout(function () {
		var name = '에스프레소';
		console.log(name);
		resolve(name); // resolve() 실행되면
	}, 500);
  // then(다음 약속)으로 넘어간다
  // 콜백함수 인자엔 resolve(name) name이 들어가는데
  // 안 헷갈리게 prevName 이름으로 넘겨 주자
}).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);
	});
});

4-2. <비동기 작업의 동기적 표현(2) - Promise(2) 리팩토링하기>


리팩토링 = 다시 구조화 한다.
비효율적인 코드를 효율적인 코드로 변경하는 작업을 말한다.

커피 예제: Promise로 비동기 작업의 동기적 표현하기

  • 직전 예제의 반복 부분을 함수화 한 코드
  • trigger를 걸어주기 위해 클로저 개념이 나왔지만, 여기서는 skip 다음 chapter에서 다루자 ^~^
// 반복되는 코드 함수화 하기

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

//addCoffee('에스프레소') 는 
//function ('에스프레소') {
//  return new Promise(function (resolve) {
//			setTimeout(function () {
//				var newName = '에스프레소' ? `${'에스프레소'}, ${name}` : name;
//				console.log('에스프레소');
//				resolve('에스프레소');
//			}, 500);
//		});
//	};

//니까 
//addCoffee('에스프레소')() 라고 해줘야 리턴 문이 실행된다.
addCoffee('에스프레소')() 
	.then(addCoffee('아메리카노'))
	.then(addCoffee('카페모카'))
	.then(addCoffee('카페라떼'));

4-3. <비동기 작업의 동기적 표현(3) - Generator>


이터러블 객체(Iterable)
iterable: 반복될 수 있는, 반복할 수 있는 제너레이터 문법

  1. *가 붙은 함수가 제너레이터 함수이다.
    제너레이터 함수는 실행하면, Iterator 객체가 반환(next()를 가지고 있음)된다.
    따라서 제너레이터는 반복할 수 있는 이터레이터 객체를 생성한다.

  2. iterator 객체next 메서드로 순환 할 수 있는 객체이다.
    next 메서드 호출 시, Generator 함수 내부에서 가장 먼저 등장하는 yield에서 stop 이후 다시 next 메서드를 호출하면 멈췄던 부분
    -> 그 다음의 yield까지 실행 후 stop

    • yield: 양보하다, 미루다
      순서를 기다리지 않는 비동기 작업을 양보하고 미루게 하면서 순서를 기다리게 만드는 역할

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

비동기적 작업은 순서를 보장 받을 수 없기 때문에,
순서를 보장 받기 위해서 동기적으로 표현하도록 계속 노력하는 것!
잊지 말자!

커피 예제: Generator로 비동기 작업의 동기적 표현하기

// *가 붙은 함수가 제너레이터 함수
// 이 함수를 실행하면 => iterator 객체가 반환된다.


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

// 1. 제너레이터 함수인 coffeeGenerator 선언
var coffeeGenerator = function* () {
   // Generator 함수 내부에서 yield 키워드로 순서 제어함
   // 5-1. Generator 함수 내부에서 가장 먼저 등장하는 yield 키워드를 만나면 stop!
   // 그 로직이 끝날 때 까지 기다린 후,
   // 5-2. 이후 다시 next 메서드를 호출하면 멈췄던 부분 그 다음의 yield까지 실행 후 stop!
   // 끝날때 까지 반복 ㅇㅇ! <- 스탑을 걸어준다! 는 느낌
	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);
};

// 3. coffeeGenerator()  -> 제너레이터 함수인 coffeeGenerator를 실행하면
// iterator 객체를 반환하여 갖게 된다.
var coffeeMaker = coffeeGenerator();

// 4. 따라서 coffeeMaker는 iterator 객체이고,
// iterator 객체는 next() 메서드로 순환할 수 있다.

// 5. coffeeMaker.next(); 로 next 메서드 호출 시
coffeeMaker.next();

4-4. <비동기 작업의 동기적 표현(4) - Promise + Async/await>


ES2017에서 새롭게 추가된 async(비동기)/await(기다리다) 문을 이용하여 비동기 작업을 동기적으로 표현할 수 있다.

  • 비동기 작업을 수행 하고자 하는 함수 앞에 async를 붙이고,
  • 함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await를 붙여주면 된다.
  • Promise ~ then(~그러면)과 동일한 효과를 얻을 수 있다.

커피 예제: Promise + Async/await 으로 비동기 작업의 동기적 표현하기


// 1. coffeeMaker 함수에서 호출할
// Promise 리턴하는 addCoffee 함수를 선언
var addCoffee = function (name) {
	return new Promise(function (resolve) {
		setTimeout(function(){
			resolve(name);
		}, 500);
	});
};


// 2. coffeeMaker 함수는 비동기 함수로 async 를 붙여주면,
var coffeeMaker = async function () {
	var coffeeList = '';
	var _addCoffee = async function (name) {
		coffeeList += (coffeeList ? ', ' : '') + await addCoffee(name);
	};
  
  // 3. Promise를 반환하는 함수인 경우에는 (addCoffee),
  // await 키워드를 만나면, 무조건 그 메서드 끝날 때 까지 기다린다.
  // 기다리게 해줘라
	await _addCoffee('에스프레소');  // 이 로직이 모두 실행될 때까지 기다림, 100초 걸리면
	console.log(coffeeList);      // 이 콘솔은 100초 뒤에 찍힘
	await _addCoffee('아메리카노');
	console.log(coffeeList);
	await _addCoffee('카페모카');
	console.log(coffeeList);
	await _addCoffee('카페라떼');
	console.log(coffeeList);
};


coffeeMaker();
profile
Always have hope🍀 & constant passion🔥

0개의 댓글