본문 바로가기
코딩일기/TIL

TIL-8 콜백 함수와 동기/비동기 처리

by 2pro.e_pro 2024. 6. 22.
728x90
반응형

TIL-8 콜백 함수와 동기/비동기 처리

prologue. 콜백 함수 하나 추가요~

그거 콜백지옥 아니야?

 

 

 

 

1. 콜백함수

이번 시간에는 TIL - 7 까지 자주 언급되었던 콜백함수에 대하여 조금 더 자세하게 알아보려고 한다.

 

// setTimeout
setTimeout(function() {
  console.log("Hello, world!");
}, 1000);

// forEach
const numbers = [1, 2, 3, 4, 5];

numbers.forEach(function(number) {
  console.log(number);
});

 

콜백함수는 위 예제와 같이 setTimeout나 배열에 대한 forEach와 같은  구문에서 사용되었었다.

 

1- 1 콜백함수란

이쯤에서 다시한번 콜백함수란 무엇인지 생각해 보자.

 

콜백함수는 기존에 설정한 함수를 다른 코드의 인자로 넘겨주는함수이고

다른 코드의 인자로 넘겨준다는 것은 해당 콜백 함수를 넘겨받는 코드가 있다는 이야기다.

 

앞서 설명한 setTimeout이나 forEach와 같은 코드 등이

이런 콜백함수를 필요에 따라 적절한 시점에 실행하게 되는 것이다.

즉, 제어권이 위와 같은 코드에게 있는것.

 

callback = call(부르다) + back(되돌아오다) = 되돌아와서 호출해줘

 

다시말하면 "제어권을 넘겨줄테니 너가 구성된 그 로직으로 처리해줘" 라는 뜻이 된다.

 

정리하자면 콜백함수는 다른코드(함수 또는 메서드)에게 인자로 넘겨줌으로써

그 제어권도 함께 위임한 함수이며, 콜백함수를 위임받은 코드는 자체적으로 내부 로직에 의해

이 콜백함수를 적절한 시점에 실행하게 되는것이다.

 

1- 2 제어권

콜백함수를 넘겨받은 코드들은 어떤 제어권을 갖게 될까?

다양한 예제를 통해 제어권에 대해 조금 더 알아보자.

 

콜백함수의 제어권을 넘겨받은 코드는 콜백함수 호출 시점에 대한 제어권을 가진다.

 

var count = 0;

// timer : 콜백 내부에서 사용할 수 있는 '어떤 게 돌고있는지'
// 알려주는 id값
var timer = setInterval(function() {
	console.log(count);
	if(++count > 4) clearInterval(timer);
}, 300);

 

위 예제의 경우 count는 0으로 초기화 되며,

setInterval 이라는 함수가 첫번째 인수로 주어진 콜백함수를 지정된 시간 간격마다 반복실행한다.

 

결국 콜백함수 실행은

setInterval 함수에 의해 300밀리초 마다 콜백함수가 실행되게 되는것이다.

 

정리 하자면 콜백함수는 다른 함수(예제상 setInterval)에 의해 호출되는 함수이고,

여기서의 콜백함수는 setInterval의 첫번째 인수로 전달된 익명함수가 된다.

 

이제 아래 예제를 이해하는데에도 도움이 될것이다.

var count = 0;
var cbFunc = function () {
	console.log(count);
	if (++count > 4) clearInterval(timer);
};
var timer = setInterval(cbFunc, 300);

// 실행 결과
// 0 (0.3sec)
// 1 (0.6sec)
// 2 (0.9sec)
// 3 (1.2sec)
// 4 (1.5sec)

 

count는 0으로 초기화 되고, cbFunc는 콜백함수를 참조하게 되는데,

이 함수는 count 변수를 출력하고 조건이 만족 되면 타이머를 중지한다.

timer 변수는 setInterval 함수의 반환값(타이머 ID)를 저장한다.

 

그래서 cbFunc 함수는 현재 count 값을 console.log로 출력하며

count 값을 1증가시키고  count 가 4보다 큰 경우 clearInterval(timer)를 호출하여 타이머를 중지하게 된다.

 

결국 setInterval 함수는 cbFunc 함수를 300밀리초(0.3초) 마다 반복실행하며,

반환된 타이버 ID는 timer 변수에 저장된다.

 

또한 인자와 this에 대해서도 다뤄볼 수 있는데,

 

// map 함수에 의해 새로운 배열을 생성해서 newArr에 담고 있네요!
var newArr = [10, 20, 30].map(function (currentValue, index) {
	console.log(currentValue, index);
	return currentValue + 5;
});
console.log(newArr);

// -- 실행 결과 --
// 10 0
// 20 1
// 30 2
// [ 15, 25, 35 ]

 

위 예제처럼 map 함수는 각배열요소를 변환하여 새로운 배열을 반환한다.

쉽게 말하자면 기존배열을 변경하지 않고, 새로운 배열을 생성한다는 것이다.

 

그래서 위 예제에서는 원본배열 [10, 20, 30]에 대하여  map 함수는 새로운 배열을 생성해서 newArr에 담고 있으며

map 함수에 의해 배열의 각 요소를 순회하며, 주어진 콜백함수를 호출하게 된다.

 

여기서 콜백함수는 currentValue(현재 요소의 값)와 index(현재 요소의 인덱스) 두개의 인수를 받게 된다.

 

결과 적으로 각 요소(원본배열의 [0, 1, 2] 인덱스)에 대해 콜백함수가 실행되면, 

콜백 함수 내부에서 currentValue와 index가 console.log로 출력되며,

currentValue + 5의 결과가 반환되어 새로운 배열의 해당 위치에 저장되기 때문에,

 

원본배열에 +5가 추가된 15, 25, 35가 출력되게 된다.

 

this 의 경우에 대해서도 다뤄볼 수 있는데, 앞서 데이터와 실행컨텍스트를 다루며,

콜백함수도 함수기 때문에 기본적으로는 this가 전역객체(global or window)를 참조한다고 했다.

 

그렇지만 제어권을 넘겨받을 코드에서 콜백함수에 별도로 this가 될 대상을 지정한 경우에는,

그 대상을 참조한다는 예외사항도 언급했었다.

 

이전 TIL의 말미에 소개된 call과 apply에 힌트가 있는데,

 

// Array.prototype.map을 직접 구현해봤어요!
Array.prototype.mapaaa = function (callback, thisArg) {
  var mappedArr = [];

  for (var i = 0; i < this.length; i++) {
    // call의 첫 번째 인자는 thisArg가 존재하는 경우는 그 객체, 없으면 전역객체
    // call의 두 번째 인자는 this가 배열일 것(호출의 주체가 배열)이므로,
		// i번째 요소를 넣어서 인자로 전달
    var mappedValue = callback.call(thisArg || global, this[i]);
    mappedArr[i] = mappedValue;
  }
  return mappedArr;
};

const a = [1, 2, 3].mapaaa((item) => {
  return item * 2;
});

console.log(a);

 

위 예제와 같이 바로 제어권을 넘겨 받을 코드에서 call / apply 메서드의 첫번째 인자에서,

콜백함수 내부에서 사용될 this를 명시적으로 바인딩 하기 때문에 this에 다른값이 담길 수 있는것이다.

 

그래서 위 예제처럼 Array.prototype.map 메서드로 직접 구현하여

배열의 각 요소를 순회하며 콜백함수를 적용하여 새로운 배열을 생성할 수 있게 되며

call 메서드에 this arg를 사용하여 콜백함수 내부의 this값을 지정하도록 할 수 있다.

 

1- 3 콜백함수는 함수다

이렇듯 다양한 상황 별 콜백함수의 쓰임새와 활용법을 알아보고 있는데,

콜백함수로 어떤 객체의 메서드를 전달 하더라도,

그 메서드는 메서드가 아닌 함수로 호출한다는것도 우리가 알아야 한다.

 

var obj = {
    vals: [1, 2, 3],
    logValues: function (v, i) {
        console.log(this, v, i);
    },
};

//method로써 호출
obj.logValues(1, 2);

//callback => obj를 this로 하는 메서드를 그대로 전달한게 아니에요
//단지, obj.logValues가 가리키는 함수만 전달한거에요(obj 객체와는 연관이 없습니다)
[4, 5, 6].forEach(obj.logValues);

 

위 예제를 통해 조금 더 설명하자면,

obj.logValues(1, 2);  여기서 <=== logValues는 obj 객체의 메서드로 호출되며,

메서드로 호출 될때 this는 해당 메서드를 호출한 객체를 참조한다.

 

따라서 This는 obj를 참조하게 되며, 출력값은 obj 객체의 1, 2 이며

 

[4, 5, 6].forEach(obj.logValues);    의 경우 forEach를 통해 obj.logValues를 순회 하고

forEach의 경우 value와 다음 인덱스를 포함하여 출력하기 때문에,

 

출력값은 

 

  • 첫 번째 호출: this는 obj, v는 4, i는 0
  • 두 번째 호출: this는 obj, v는 5, i는 1
  • 세 번째 호출: this는 obj, v는 6, i는 2

 

위와같이 나오게 된다.

 

1- 4 콜백함수 내부의 this에 다른 값 바인딩 하기

콜백함수 내부에서의 this가 문맥에 맞는 객체를 바라보게 하려면 어떻게 해야 할까?

그리고 콜백함수 내부의 this에 다른값을 바인딩 하는 방법은 어떤것이 있을까?

 

var obj1 = {
	name: 'obj1',
	func: function() {
		var self = this; //이 부분!
		return function () {
			console.log(self.name);
		};
	}
};

// 단순히 함수만 전달한 것이기 때문에, obj1 객체와는 상관이 없어요.
// 메서드가 아닌 함수로서 호출한 것과 동일하죠.
var callback = obj1.func();
setTimeout(callback, 1000);

 

전통적인 방식으로는 위 예제와 같이. self this와 같이 

강제로 this를 제어하는 방법이 있지만

 

해당 방법은 단순히 함수만 전달한 것이기 때문에, obj1 객체와는 관계가 없다.

정리하자면 메서드가 아닌 함수로써 호출한 것과 동일한 형태가 되기 때문이다.

 

그렇다면 콜백함수 내부에서 아예 this를 사용하지 않는다면 어떻게 될까?

 

var obj1 = {
	name: 'obj1',
	func: function () {
		console.log(obj1.name);
	}
};
setTimeout(obj1.func, 1000);

 

위 예제와 같이 콜백함수 내부에서 아예 this를 사용하지 않으면서 코드구문을 작성하면,

코드 구문 자체가 간결해진만큼 가독성이 높아지지만, 결과만을 위한 코딩이 되어 버리기 때문에,

this를 이용해서 다양한 것을 할 수 있는 장점을 놓치게 된다.

 

오히려 첫번째 예제 코드를 재활용하여 발전 시킨다면 아래와 같은 예제로 만들 수도 있다.

 

var obj1 = {
	name: 'obj1',
	func: function() {
		var self = this; //이 부분!
		return function () {
			console.log(self.name);
		};
	}
};

// ---------------------------------

// obj1의 func를 직접 아래에 대입해보면 조금 더 보기 쉽습니다!
var obj2 = {
	name: 'obj2',
	func: obj1.func
};
var callback2 = obj2.func();
setTimeout(callback2, 1500);

// 역시, obj1의 func를 직접 아래에 대입해보면 조금 더 보기 쉽습니다!
var obj3 = { name: 'obj3' };
var callback3 = obj1.func.call(obj3);
setTimeout(callback3, 2000);

 

위 방법은 조금 번거 롭긴 해도 this를 우회적으로 나마 활용하여 원하는 객체를 바라보게 할 수 있다.

 

설명을 조금 더 해보자면,

 

var obj1 = {
    name: 'obj1',
    func: function () {
        var self = this; // 이 부분!
        return function () {
            console.log(self.name);
        };
    },
};

 

해당 코드 구문은 self 변수를 사용하여, this 값을 저장하는 부분이며

그렇게 셀프 this 바인딩된 func 메서드는 익명한수를 반환한다.

 

이 익명함 수 내부에서 self.name을 참조하여 obj1의 name 프로퍼티를 출력한다.

 

 

// obj2 정의 및 콜백 함수 설정
var obj2 = {
    name: 'obj2',
    func: obj1.func,
};
var callback2 = obj2.func(); // self는 obj2를 참조
setTimeout(callback2, 1500); // 1.5초 후에 'obj2' 출력

 

두번째 obj2의 경우 name 프로퍼티와 obj1.func 메서드를 참조하는 func 프로퍼티를 가지고 있으며,

obj2.func()를 호출하면, func 메서드 내부의 this는 obj2를 참조한다.

 

그러나 self는 this값을 저장하여 반환된 익명함수에서 사용되기 때문에,

callback2는 self가 obj2를 참조하도록 바인딩 된 상태에서 반환된 익명함수가 되고.

 

따라서,  setTimeout(callback2, 1500);   은 1.5초 후에 callback2를 호출하며 self.name을 출력한다.

 

 

var obj3 = { name: 'obj3' };
var callback3 = obj1.func.call(obj3); // self는 obj3를 참조
setTimeout(callback3, 2000); // 2초 후에 'obj3' 출력

 

세번째  코드 구문의 경우 obj3 객체는 name 프로퍼티를 가지고 있으며,

var callback3 = obj1.func.call(obj3);   는 func 메서드를 호출하면서 this를 obj3로 설정한다.

 

따라서 self는 obj3를 참조하게 되며,

var callback3   는 self가 obj3를 참조하도록 바인딩된 상태에서 반환된 익명함수기 때문에,

setTimeout(callback3, 2000);   은 2초 후에 callback3를 호출하며, self.name을 출력하게 된다.

 

 

최종 실행 결과는

 

  • 1.5초 후: 'obj2'
  • 2초 후: 'obj3'

위 방법은 조금 번거롭긴 해도 this를 우회적으로 활용하여 원하는 객체를 바라보게 할 수 있었다.

이렇듯 전통적인 해결방법도 있고 조금더 쉽게 사용하는 방법도 있는데

 

바로 bind 메서드를 활용하는 방법이다.

var obj1 = {
    name: 'obj1',
    func: function () {
        console.log(this.name);
    },
};
//함수 자체를 obj1에 바인딩
//obj1.func를 실행할 때 무조건 this는 obj1로 고정해줘!
setTimeout(obj1.func.bind(obj1), 1000);

var obj2 = { name: 'obj2' };
//함수 자체를 obj2에 바인딩
//obj1.func를 실행할 때 무조건 this는 obj2로 고정해줘!
setTimeout(obj1.func.bind(obj2), 1500);

 

위 예제와 같이 bind 메서드를 활용할수 있는데, 

 

주어진 예제코드는 setTimeout 함수와 bind 메서드를 사용하여,

함수 내부의 this 바인딩을 명시적으로 설정하는 코드 구문이다.

이를 통해 this가 항상 특정 객체를 참조하도록 강제 할 수 있다.

 

var obj1 = {
    name: 'obj1',
    func: function () {
        console.log(this.name);
    },
};

 

obj 객체는 name 프로퍼티와 func 메서드를 가지고 있고

func: function () {}   func 메서드는 this.name을 출력한다.

 

setTimeout(obj1.func.bind(obj1), 1000);   해당 함수는 지정된 시간 후에 주어진 콜백 함수를 실행하며,

 

이중 obj1.func.bind(obj1)   은 func 메서드를 호출할때 this를 obj1로 고정한 새로운 함수를 반환한다.

반환된 함수는 1초 후에 실행되며, 이때 this는 항상 obj1을 참조하기때문에,

 

따라서, 1초 후에 obj1.func가 실행될 때 this는 obj1을 참조하므로 obj1.name인 obj1이 출력된다.

 

var obj2 = { name: 'obj2' };
//함수 자체를 obj2에 바인딩
//obj1.func를 실행할 때 무조건 this는 obj2로 고정해줘!
setTimeout(obj1.func.bind(obj2), 1500);

 

obj2 객체는 name 프로퍼티만 가지고 있으며,

 

setTimeout(obj1.func.bind(obj2), 1500);   해당 함수는 지정된 시간 후에 주어진 콜백 함수를 실행하며,

 

이중 obj1.func.bind(obj2)   은 func 메서드를 호출할때 this를 obj2로 고정한 새로운 함수를 반환한다.

또한 반환된 함수는 1.5초 후에 실행되며, 이때 this는 항상 obj2를 참조하기 때문에,

 

따라서, 1.5초 후에 obj2.func 가 실행될 대 this는 obj2를 참조하므로 obj2.name인 obj2가 출력된다.

 

최종 실행결과는 

 

  • 첫 번째 setTimeout:
    • 1초 후에 실행됩니다.
    • this는 obj1로 고정되어 있으므로 obj1.name인 'obj1'을 출력합니다.
    • 출력: 'obj1'
  • 두 번째 setTimeout:
    • 1.5초 후에 실행됩니다.
    • this는 obj2로 고정되어 있으므로 obj2.name인 'obj2'을 출력합니다.
    • 출력: 'obj2'

 

 

1- 5 콜백 지옥과 비동기 제어

콜백 지옥은 자바스크립트와 같은 비동기 프로그래밍 언어에서 흔히 발생하는 문제로,

여러 비동기 작업을 중첩된 콜백함수로 처리할 때 코드가 복잡해지고,

가독성이 떨어지는 상황을 말한다.

 

출처 : https://dmchoi77.github.io/JavaScript/Promise/

 

콜백 지옥의 문제점은 아래와 같다.

 

 

  • 가독성 저하: 콜백 함수가 중첩될수록 코드의 들여쓰기가 깊어지며 가독성이 떨어진다.
  • 유지보수 어려움: 코드를 수정하거나 디버깅하기 어렵다.
  • 에러 처리 복잡성: 각 콜백에서 에러를 처리해야 하므로 에러 처리가 복잡해진다.

 

 

 

 

콜백지옥은 코드의 가독성을 크게 저하시켜 유지 보수를 어렵게 만들고,

이를 해결하기 위해 여러가지 패턴과 도구가 개발되었다.

 

설명에 앞서 두가지 개념을 알고 가면 좋은데,

 

동기와 비동기 이다.

 

동기는 현재 실행중인 코드가 끝나야 다음코드를 실행하는 방식을 말하며,

cpu의 계산에 의해 즉시 처리가 가능한 대부분의 코드는 동기적 코드이며,

계산이 복잡해서 cpu가 계산하는데 오래 걸리는 코드 역시 동기적 코드이다.

 

비동기는 현재 실행중인 코드의 완료여부와 무관하게 즉시 다음 코드로 넘어가는 방식이며,

setTimeout, addEventListner등 별도의 요청, 실행대기, 보류 등과 관련된 코드는

모두 비동기적 코드로 분류하고, 웹의 복잡도가 올라갈 수록 비동기적 코드의 비중이 늘어난다.

 

1) 콜백지옥의 예시와 해결방안

// setTimeout 함수의 동작원리
setTimeout(function(){
	// 기본적으로 1000ms이 지나야 여기 로직이 실행이 된답니다 :)
	console.log('hi');
}, 1000);

최근 언급을 자주 했던 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,
    '에스프레소'
);

 

위 예제와 같은 코드구문이 있다고 가정할 때

 

첫번째 해결방법은 기명함수로 변환하는 방법이 있다.

 

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

 

예를들어 위 코드처럼 전부 기명함수화 하여 가독성을 높이는 방법인데,

여기서의 문제는 쓰임의 횟수가 적은 코드들인데 기명함수 전환해야 하는 코드의 양은 많다면,

비효율 적이기 때문에, 근본적인 해결책은 아니다.

 

이런경우 때문에 javaScript에서는 비동기적인 작업을 동기적인 것 처럼 보이도록 처리해주는 방법을

계속해서 마련해 주고 있으며, promise, Generator, async/await 같은 것들이다.

2) 비동기 작업의 동기적 표현(1) - Promise(1)

promise는 비동기 처리에 대해, 처리가 끝나면 알려달라는 약속같은 것인데,

new 연산자로 호출한 promise의 인자로 넘어가는 콜백은 바로 실행되며,

그 내부의 resolve(또는 rejenct)함수를 호출하는 구문이 있을경우,

resolve(또는 reject) 둘중 하나가 실행되기 전까지는 다음(then), 오류(catch)로 넘어가지 않는다.

 

따라서, 비동기 작업이 완료될때 비로소 resoleve, reject를 호출한다.

 

new Promise(function (resolve) {
	setTimeout(function () {
		var name = '에스프레소';
		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);
	});
}).then(function (prevName) {
	return new Promise(function (resolve) {
		setTimeout(function () {
			var name = prevName + ', 카페라떼';
			console.log(name);
			resolve(name);
		}, 500);
	});
});

 

위 예제를 참고하여 더 알아보면,

 

new Promise(function (resolve) {
    setTimeout(function () {
        var name = '에스프레소';
        console.log(name);
        resolve(name);
    }, 500);
})

 

새로운 promise 객체를 생성하여 생성자 함수의 인수로 전달된 콜백함수는 resolve 함수를 받는다.

 

setTimeout을 사용하여 500 밀리초 이후에 에스프레소를 출력하고,

이후 resolve(name)을 호출하여 promise를 이행한다.

 

그런다음 첫번째 에서 세번째 then 까지를 실행하는데

    .then(function (prevName) {
        return new Promise(function (resolve) {
            setTimeout(function () {
                var name = prevName + ', 아메리카노';
                console.log(name);
                resolve(name);
            }, 500);
        });
    })

 

아메리카노 then을 예를 들어 설명하자면 

첫번째 promise가 이행된 후 then 메서드가 호출되며,

then 메서드는 prevname을 인수받아 새로운 promise를 반환한다.

이때 setTimeout은 500 밀리초 이후에 이전 이름에 아메리카노를 출력하고,

새로운 이름을 resolve 하는데 두번째 세번째 then도 동일한 원리로 실행되며, 

 

이것은 promise와 then 체이닝을 사용한 여러 비동기 작업을 순차적으로 처리하는 방법이라고 한다.

 

3) 비동기 작업의 동기적 표현(2) - Promise(2)

promise의 직전 예제의 반복부분을 함수화 하는 코드를 만들수도 있는데,

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('에스프레소')()
	.then(addCoffee('아메리카노'))
	.then(addCoffee('카페모카'))
	.then(addCoffee('카페라떼'));

 

위 예제의 중점은 비동기 작업을 처리하는 promise 체인을 단순화 하기위해,

반복되는 부분을 함수화 하여 가독성을 높인 예제라고 볼 수 있다.

 

addCofee 함수는 새로운 커피 이름을 추가하는 작업을 비동기로 처리하는 promise를 반환하는데,

이를 통해 각 커피 이름을 순차적으로 추가하면서, 콜백지옥을 피하고 간결하여 가독성이 높은 코드를 작성 할 수 있다.

 

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);
        });
    };
};

 

그래서 위 와 같이 예제의 첫번째 코드 구문을 보면,

addCofee 함수는 name 인수를 받아 내부함수(클로저)를 반환하는데,

내부함수는 prevName 인수를 받아서 새로운 promise를 반환한다.

 

promise는 setTimeout을 사용하여 500밀리초 이후에 새로운 커피 이름을 생성하고 출력하며,

생성된 새로운 커피 이름을 resolve 하여 다음 then 체인으로 전달한다.

 

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

 

생성된 새로운 커피 이름을 resolve 하여 다음 then 체인으로 전달할때

addCoffee('에스프레소')   는 name이 '에스프레소'인 내부함수를 반환하며

내부함수를 즉시 호출하여, prevName을 전달하지 않기 때문에,

초기 커피 이름을 '에스프레소'로 설정하고, 반환된 promise가 첫번째 then 체인을 시작한다.

 

위 이론을 기반으로 나머지 then이 모두 실행된다.

 

최종 실행결과는 아래와 같다.

 

 

  • 첫 번째 Promise 이행 (0.5초 후):
    • '에스프레소' 출력
  • 두 번째 Promise 이행 (1초 후):
    • '에스프레소, 아메리카노' 출력
  • 세 번째 Promise 이행 (1.5초 후):
    • '에스프레소, 아메리카노, 카페모카' 출력
  • 네 번째 Promise 이행 (2초 후):
    • '에스프레소, 아메리카노, 카페모카, 카페라떼' 출력

 

 

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

Generator 함수는 function* 키워드로 정의되며, 

yield 키워드를 사용하여 실행을 주이하고, 값을 반환한다.

 

yield 키워드는 제너레이터 함수의 실행을 일시 중지하고, 호출자에게 값을 반환하는데,

이때 Generator 함수는 이터레이터 객체를 반환한다.

 

이터레이터 (Iterator)

  • 이터레이터 객체는 next 메서드를 가지고 있다.
  • next 메서드는 제너레이터 함수의 다음 yield 표현식까지 실행을 재개한다.
  • next 메서드는 { value: any, done: boolean } 형태의 객체를 반환한다.
    • value는 yield 표현식의 반환 값이다.
    • done은 제너레이터 함수가 끝났는지를 나타내는 불리언 값이다.

 

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

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 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 coffeeGenerator = function* ()  라는 이름으로 Generator 함수가 설정되고,

각 yield 표현식은 addCofee 함수를 호출하여 비동기 작업을 수행하며,

작업이 완료 되면, 다음 단계로 진행한다.

 

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

 

cofeeGenerator 함수를 호출하여 이터레이터 객체인 coffeemaker를 생성하게 되는데,

이때 coffeeMaker.next();   를 호출하여 제너레이터 함수의 실행을 시작한다.

 

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

 

끝으로 addCofee 함수는 setTimeout을 사용하여 500 밀리초 이후에 비동기 작업을 수행하는데,

비동기 작업이 완료되면 coffeMaker.next()를 호출하여 제너레이터 함수의 실행을 재개하며,

 

prevName과 name을 결합하여 새로운 커피 이름을 생성하고 이를 next 메서드에 전달한다.

 

 

전체 실행 과정을 정리해보면

 

  • 첫 번째 호출: coffeeMaker.next()를 호출하여 제너레이터 함수의 첫 번째 yield까지 실행
    • addCoffee('', '에스프레소')가 호출되고, 500밀리초 후에 coffeeMaker.next('에스프레소')가 호출
    • espresso 변수에 '에스프레소'가 할당되고, console.log(espresso)가 실행
  • 두 번째 호출: coffeeMaker.next()를 호출하여 두 번째 yield까지 실행
    • addCoffee('에스프레소', '아메리카노')가 호출되고,
      500밀리초 후에 coffeeMaker.next('에스프레소, 아메리카노')가 호출
    • americano 변수에 '에스프레소, 아메리카노'가 할당되고, console.log(americano)가 실행
  • 세 번째 호출: coffeeMaker.next()를 호출하여 세 번째 yield까지 실행
    • addCoffee('에스프레소, 아메리카노', '카페모카')가 호출되고,
      500밀리초 후에 coffeeMaker.next('에스프레소, 아메리카노, 카페모카')가 호출
    • mocha 변수에 '에스프레소, 아메리카노, 카페모카'가 할당되고, console.log(mocha)가 실행
  • 네 번째 호출: coffeeMaker.next()를 호출하여 네 번째 yield까지 실행
    • addCoffee('에스프레소, 아메리카노, 카페모카', '카페라떼')가 호출되고,
      500밀리초 후에 coffeeMaker.next('에스프레소, 아메리카노, 카페모카, 카페라떼')가 호출
    • latte 변수에 '에스프레소, 아메리카노, 카페모카, 카페라떼'가 할당되고, console.log(latte)가 실행

 

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

마지막으로 promise + async / await를 사용하여 비동기 작업을 순차적으로 처리 하는 예제를 알아보자.

해당 원리를 예제와 함께 알아보기에 앞서 이번에 다룰 개념에 대한 간략한 설명은 아래와 같다.

 

주요 개념

Promise

  • Promise는 비동기 작업의 성공 또는 실패를 나타내는 객체다.
  • Promise 객체는 then과 catch 메서드를 사용하여 비동기 작업의 결과를 처리한다.

async/await

  • async 키워드는 함수가 Promise를 반환하도록 만든다.
  • await 키워드는 Promise가 이행될 때까지 함수의 실행을 일시 중지하고,
    await 키워드는 async 함수 내부에서만 사용할 수 있다.

 

 

var addCoffee = function (name) {
	return new Promise(function (resolve) {
		setTimeout(function(){
			resolve(name);
		}, 500);
	});
};
var coffeeMaker = async function () {
	var coffeeList = '';
	var _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();

 

그러면 위 예제를 통해 개념과 원리를 알아보자.

 

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

 

addCoffee 함수는 promise를 반환하며, 

setTimeout을 사용하여 500밀리초 이후에 resolve 함수를 호출하여 name을 반환하는데,

 

var coffeeMaker = async function () {
    var coffeeList = '  ';
    var _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();

 

coffeMaker 함수는 async 함수로 정의되며, 이는 coffeeMaker 함수가 promise를 반환하고,

내부에서 await 키워드를 사용할 수 있음을 의미한다.

 

 var coffeeList = '  ';    coffeList 변수는 추가된 커피 이름을 저장하는 문자열이며,

 var _addCoffee = async function (name) { }     _addCoffee 함수는 async 함수로 정의되며,

addCoffee 함수를 호출하고, 그 결과를 coffeeList에 추가한다.

 

이 때 

await _addCoffee('name');   을 사용하여,

addCoffee 함수가 반환하는 promise가 이행될때까지 기다리며, coffeList에 커피이이 추가되며,

await _addCoffee('에스프레소'); 를 사용하여 addCoffee('에스프레소')의 결과를 기다린다.

결과가 반환되면, coffeeList를 출력하게 된다.

 

위와 동일한 방식으로 다른 커피 이름을 추가하고 결과를 출력하게 된다.

 

최종결과는 아래와 같은 결과값이 시간차를 두고 순차적으로 출력되게 된다.

 

 

 

 

💡 총 정리

 

스파르타 코딩클럽의 JavaScript과정을 배우며 느끼는 점은

처음부터 최신 문법을 알려주려 하지않고

레거시한 방법을 통해 기초 부터 점진적으로 발전된 방법을 알려주심에

체계적이라는 생각이 들면서도 제로베이스인 나로선 이해할것들이

급물살 처럼 몰려오는것처럼 느껴져서 버겁기도 하다.

 

이번에도 몇가지 내용들이 반복적으로 사용되어 TIL로 정리할때는

약간의 감이 오긴 했지만 생각보다 다양한 개념을 소화하려고 하니

 

방금 배운 메소드나 원리가 복잡하게 느껴지고 이해가 더뎌지는 부분도 있긴하다.

 

그러나 배울 수 있는 부분들이 제대로 체화 될 수 있도록

노력해야하는 것도 배움의 연속이기에 꾸준한 TIL을 통해

지식을 곱씹어 보겠다.

728x90
반응형

댓글