처음부터 차근차근

[코어 자바스크립트] Callback 함수 & 동기/비동기 처리 본문

Language/JavaScript

[코어 자바스크립트] Callback 함수 & 동기/비동기 처리

HangJu_95 2023. 6. 11. 17:28
728x90

Callback(콜백함수)이란??

출처 https://ko.wikipedia.org/wiki/%EC%BD%9C%EB%B0%B1

프로그래밍에서 Callback 또는 Callback function이란 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 말한다.

콜백을 넘겨받은 코드는 이 콜백을 필요에 따라 즉시 실행할 수도 있고, 나중에 실행할 수도 있다.

 

간단한 예시로, setTimeout, 혹은 forEach 메소드를 통해 알아보자.

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

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

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

여기서 봤을 때 Callback은 Call(부르다) + back(되돌아오다) = 되돌아와서 호출해줘!

→ 다시 말하면, 제어권을 넘겨줄테니 너가 알고 있는 그 로직으로 처리해줘!

이 의미가 된다.

 

즉, 콜백 함수는 다른 코드(함수 또는 메서드)에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수. 콜백 함수를 위임받은 코드는 자체적으로 내부 로직에 의해 이 콜백 함수를 적절한 시점에 실행

제어권

1. 호출 시점

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

Ex) 콜백 함수의 제어권을 넘겨받은 코드(=setInterval)가 언제 콜백함수를 호출할지에 대한 제어권을 가지게 된다.

var count = 0;

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

2번째 예시를 표로 정리해보자.

code 호출 주체 제어권
cbFunc(); 사용자 사용자
setInterval(cbFunc,300); setInterval setInterval

 

2. 인자

콜백 함수를 넘겨받는 코드에게 인자(의 순서)까지도 제어권이 있다.

 

예시로 Map 함수를 확인하자. 

Map 함수는 기존 배열을 변경하지 않고, 새로운 배열을 생성한다.

// 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 ]

그러나 여기 담긴 Cur과 index 변수 순서를 바꾼다면??

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

// -- 실행 결과 --
// 10 0
// 20 1
// 30 2
// [ 5, 6, 7 ]

변수의 순서를 바꾼다고, 자동으로 인식하지 않는다.(컴퓨터는 사람이 아니기 때문)

이처럼 map 메서드를 호출해서 원하는 배열을 얻고자 한다면 정의된 규칙대로 작성해야 한다.

모든 것은 전적으로 map 메서드, 즉 콜백 함수를 넘겨받은 코드에게 그 제어권이 있다.

 

3. this

콜백 함수도 함수이기 때문에 기본적으로는 this가 전역객체를 참조한다.

But, 제어권을 넘겨받을 코드에서 콜백 함수를 별도로 this가 될 대상을 지정할 경우에는 그 대상을 참조한다.

Ex)  map 함수 만들어보기

// 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가 될 대상을 지정할 수 있다.

Callback 함수는 함수다

콜백 함수로 어떤 객체의 메서드를 전달하더라도, 그 메서드는 함수로 호출된다.

어떤 함수의 인자에 객체의 메서드를 전달하더라도, 이는 결국 메서드가 아닌 함수일 뿐이다.

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

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

 - 전통적 방식(self를 사용한 방법)

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

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

→ 실제로는 this를 사용하는게 아니고, 코드가 번거로운 단점

 

- this를 아예 사용하지 않는 방법

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

→ 결과만을 위한 코딩이 되어버림

 

첫번째 예시를 재활용하는 방법

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

- 가장 좋은 방법 : 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);

Callback 지옥과 비동기 제어

콜백지옥이란

 - 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 헬 수준인 경우

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

 - 가독성이 hell

https://preiner.medium.com/callback지옥에-promise-적용하기-d02272ecbabe

 

동기 vs 비동기

 - 동기 : synchronous

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

 - 비동기 : asynchronous → async

  1. 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어가는 방식
  2. setTimeout, addEventListner 등
  3. 별도의 요청, 실행 대기, 보류 등과 관련된 코드는 모두 비동기적 코드

https://velog.io/@mrbartrns/til-16-asynchronous-of-js

 

※ 웹의 복잡도가 올라갈 수록 비동기적 코드의 비중 증가

 

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

아래 예시는 콜백 지옥의 예시를 보여준다.

값 전달 순서 : 아래 → 위

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

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

가독성이 좋지만, 이름을 다 붙여서 쓰는 단점 존재

→ 자바스크립트에서 비동기적인 작업을 동기적으로 처리해주는 장치가 많다.

※ Promise, Generator, async/await

 

비동기 작업의 동기적 표현이 필요합니다.

 

1) Promise

Promise는 비동기 처리에 대해, 처리가 끝나면 알려달라는 일종의 '약속'

  • new 연산자로 호출한 Promise의 인자로 넘어가는 콜백은 바로 실행
  • 그 내부의 resolve(또는 reject) 함수를 호출하는 구문이 있을 경우 resolve(또는 reject) 둘 중 하나가 실행되기 전까지는 다음(then), 오류(catch)로 넘어가지 않는다.
  • 따라서, 비동기작업이 완료될 때 비로소 resolve, 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 안에 return 대신 resolve(실패했을 경우 reject)로 넣고, 다음(then), 오류(catch)를 작성하며, 내부 resolve가 실행되기 전까지 다음으로 넘어가지 않는다.

 

trigger를 걸어주기 위해 클로저 개념이 나왔다.

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('카페라떼'));

2) Generator

※ 이터러블 객체(Iterable)

*가 붙은 함수가 제너레이터 함수. 제너레이터 함수는 실행되면, Iterator 객체가 반환(next()를 가지고 있음)

 

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

 

즉, 비동기 작업이 완료되는 시점마다 next 메서드를 호출해주면

generator 함수 내부소스가 위 → 아래로 순차적으로 진행

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

3) Promise + Async/await

ES2017에 새롭게 추가

비동기 작업을 수행코자 하는 함수 앞에 async 함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await를 붙여주면 된다.

Promise ~ then과 동일한 효과

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

 

출처 : 코어자바스크립트