[코어 자바스크립트] 콜백 함수

김진서·2025년 5월 21일

우아한테크코스 7기

목록 보기
45/56
post-thumbnail

👉 ‘코어 자바스크립트’의 ‘04 콜백 함수’를 읽으며 기억할 내용들과 새로 알게 된 내용을 정리하였습니다.

🔽 4-1 콜백 함수란?


  • 콜백 함수: 다른 코드의 인자로 넘겨주는 함수. 필요에 따라 적절한 시점에 실행. → 제어권과 관련이 깊다.
    • A와 B는 08시에 약속. 적어도 6시에 기상해야 함.

    • A는 불안한 마음에 수시로 깨어 시계 확인. 잠을 설치다가 5시에 기상.

      • 시계: 수시로 시간을 구하는 함수를 직접 호출.
    • B는 6시에 알람을 세팅하고, 꿀잠을 잠.

      • 시계: 알람을 설정하는 함수 호출. → 호출 당시에는 아무것도 하지 않다가 B가 정재훈 시각에 알람을 울리는 결과를 반환.

      ⇒ 시계는 A에게 요청마다 수동적으로 시간을 제공. B에게 요청을 받은 후 자체적으로 수행하다가 적절한 시점에 적극적으로 통보.

      ⇒ A의 경우 시계 함수 제어권이 A에게, B의 경우 알람을 울리는 명령에 대한 제어권을 시계에게.

🔽 4-2 제어권


  • 4-2-1 호출 시점
    var count = 0;
    var timer = setInterval(function () {
    	console.log(count);
    	if (++count > 4) clearInterval(timer);
    }, 300);
    var intervalID = scope.setInterval(func, delay[, param1, param2, ...]);
    • setInterval()
      - scope에는 Window 객체 or Worker의 인스턴스. 두 객체 모두 setInterval 메서드를 제공.
      - 일반적인 브라우저 환경에서는 window를 생략해서 함수처럼 사용 가능.
      - 매개변수로 func, delay 값은 required이고, 3번째부터 optional.
      - func에 넘겨준 함수는 매 delay(ms)마다 실행. return은 없음.
      - setInterval()을 실행하면 반복적으로 실행되는 내용 자체를 특정할 수 있는 고유한 ID 값 반환.
      - 이를 변수에 담는 이유는 반복 실행되는 중간에 종료(clearInterval())하기 위함.

      var count = 0;
      var cbFunc = function () {
      	console.log(count);
      	if (++count > 4) clearInterval(timer);
      };
      var timer = setInterval(cbFunc, 300);
      
      // -- 실행 결과 --
      // 0  (0.3초)
      // 1  (0.6초)
      // 2  (0.9초)
      // 3  (1.2초)
      // 0  (1.5초)
    • timer 변수: setInveral의 ID 값.

      code호출 주체제어권
      cbFunc();사용자사용자
      setInterval();setIntervalsetInterval
    • 콜백함수의 제어권을 넘겨받은 코드는 콜백함수 호출 시점에 대한 제어권을 가진다.

  • 4-2-2 인자
    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]
    Array.prototype.map(callback[, thisArg])
    callback: function(currentValue, index, array)
    • map()

      • 배열의 요소를 하나씩 꺼내어 콜백 함수를 반복 호출하고, 콜백의 return 값들을 모아 새로운 배열로 반환.
    • jQuery의 메서드들은 기본적으로 첫 번째 인자에 index, 두 번째 인자에 currentValue.

      var newArr = [10, 20, 30].map(function (index, currentValue) {
      	console.log(index, currentValue);
      	return currentValue + 5;
      });
      console.log(newArr);
      
      // -- 실행 결과 --
      // 10 0
      // 20 1
      // 30 2
      // [5, 6, 7]
    • 콜백 함수를 호출하는 주체가 map 메서드이므로 map 메서드가 콜백 함수를 호출할 때 인자에 어떤 값들을 어떤 순서로 넘길지는 전적으로 map 메서드에 달려 있다.

    • 콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수를 호출할 때 인자에 어떤 값들을 어떤 순서로 넘길 것인지에 대한 제어권을 가진다.

  • 4-2-3 this
    • 콜백함수도 함수이므로 기본적으로 this가 전역객체를 참조하지만, 제어권을 넘겨받을 코드에서 콜백함수에 별도로 this를 지정하면, 그 대상을 참조하게 된다.

      Array.prototype.map = function (callback, thisArg) {
      	var mappedArr = [];
      	for (var i = 0; i < this.length; i++) {
      		var mappedValue = callback.call(thisArg || window, this[i], i, this);
      		mappedArr[i] = mappedValue;
      	}
      	return mappedArr;
      };
    • 구현의 핵심은 call / apply 메서드.

    • this에는 thisArg 값이 있을 경우 thisArg, 없을 경우 전역객체 지정.

    • 첫 번째 인자에 this 배열의 i 번째 요소 값, 두 번째 인자에 i 값, 세 번째 인자에 배열 자체를 지정 호출.

    • 그 결과를 mappedValue에 담아 mappedArr의 i 번째 요소에 할당.

    • this에 다른 값이 담기는 이유: 제어권을 넘겨받을 코드에서 call / apply 메서드의 첫 번째 인자에 콜백 함수 내부에서의 this가 될 대상을 명시적으로 바인딩.

      setTimeout(function () { console.log(this); }, 300);  // (1) Window { ... }
      
      [1, 2, 3, 4, 5].forEach(function (x) {
      	console.log(this);  // (2) Window { ... }
      };
      
      document.body.innerHTML += '<button id="a">클릭</button>';
      document.body.querySelector('#a').addEventListener('click', function (e) {
      	console.log(this, e);  // (3) <button id="a">클릭</button>
      });                      // MouseEvent { isTrusted: true, ... }
    • (1): setTimeout은 내부에서 콜백 함수를 호출할 때, call 메서드의 첫 번째 인자에 전역객체를 넘기므로 콜백 함수 내부에서의 this가 전역객체를 참조.

    • (2): forEach는 별로의 인자로 this를 받지만, 별도의 인자를 지정하지 않아 전역객체를 참조.

    • (3): addEventListener는 내부에서 콜백 함수를 호출할 때, call 메서드의 첫 번째 인자에 addEventListener 메서드의 this를 그대로 넘기므로 콜백 함수 내부에서의 this가 addEventListener를 호출한 주체인 HTML element를 참조.

🔽 4-3 콜백 함수는 함수다


  • 콜백 함수는 함수다: 어떤 객체의 메서드를 전달하더라도 메서드가 아닌 함수로서 호출.
var obj = {
	vals: [1, 2, 3],
	logValues: function(v, i) {
		console.log(this, v, i);
	}
};
obj.logValues(1, 2);  // { vals: [1, 2, 3], logValues: f } 1 2
[4, 5, 6].forEach(obj.logValues);  // Window { ... } 4 0
                                   // Window { ... } 5 1
                                   // Window { ... } 6 2
  • obj 객체의 logValues는 메서드로 정의. 메서드로서 호출 시에는 this가 객체 obj를 참조.
  • forEach의 콜백 함수로 호출 시에는 별도로 this를 지정하는 인자를 지정하지 않았으므로 this가 전역 객체를 참조.

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


  • 별도의 인자로 this를 받는 경우는 넘겨주면 되지만, 그렇지 않은 경우는 this의 제어권도 넘어가므로 사용자가 임의로 변경 불가.
  • 전통적으로 this를 다른 변수에 담아 콜백 함수로 활용할 함수 내에서 this 대신 변수를 사용하고, 이를 클로저로 구현.
var obj1 = {
	name: 'obj1',
	func: function () {
		var self = this;
		return function () {
			console.log(self.name);
		};
	}
};
var callback = obj1.func();
setTimeout(callback, 1000);
  • 실제 this를 사용하지도 않고, 딱 봐도 번거롭고, 차라리 this를 안 쓰는 게 나을지도..
var obj1 = {
	name: 'obj1',
	func: function () {
		console.log(obj1.name);
	}
};
settimeout(obj1.func, 1000);
  • 훨씬 간결하고, 직관적이지만, 아쉽다. this로 다양한 상황에 재활용이 불가.
var obj1 = {
	name: 'obj1',
	func: function () {
		var self = this;
		return function () {
			console.log(self.name);
		};
	}
};
var callback = obj1.func();
setTimeout(callback, 1000);

var obj2 = {
	name: 'obj2',
	func: obj1.func
};
var callback2 = obj2.func();
setTimeout(callback2, 1500);

var obj3 = { name: 'obj3' };
var callback3 = obj1.func.call(obj3);
setTimeout(callback3, 2000);
  • 번거롭지만, this를 우회적으로나마 재활용 가능.
  • bind 메서드를 이용하여, 아쉬움을 보완.
var obj1 = {
	name: 'obj1',
	func: function () {
		console.log(this.name);
	}
};
setTimeout(obj1.func.bind(obj1), 1000);

var obj2 = { name: 'obj2' };
setTimeout(obj1.func.bind(obj2), 1500);

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


  • 콜백 지옥: 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상.
    • 이벤트 처리, 서버 통신 등 비동기 작업에 자주 등장.
    • 가독성 저하, 유지보수 난이도 상승.
  • 비동기(Asynchronous): 현재 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드 실행.
    • 사용자 요청에 의해 특정 시간이 경과되기 전까지 실행을 보류(setTimeout)하거나 직접적인 개입이 있을 때 실행하도록 대기(addEventLinstener)하거나 요청 후 응답을 받아야 실행하도록 대기(XMLHttpRequest)하는 등의 별도의 요청, 실행 대기, 보류 등.
  • 동기(Synchronous): 현재 실행 중인 코드가 완료된 후 다음 코드 실행.
    • 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, '카페라떼');
		}, 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, '에스프레소');
  • 가독성 + 순서를 모두 챙김.
  • 변수를 최상단으로 끌어올림으로서 외부에 노출되지만, 전체를 즉시 실행 함수로 감싸면 됨.
  • 그러나 일회성 함수를 전부 변수에 할당해야 함.
  • 비동기 작업을 동기적으로 or 동기적인 것처럼 보이게 처리하는 방법.
    • ES6: Promise, Generator
    • ES2017: async / await
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의 인자로 넘긴 함수는 바로 실행.
  • 그 내부에 resolve or reject 함수를 호출하는 구문이 있을 경우, 둘 중 하나가 실행되기 전까지 다음(then) or 오류(catch)로 넘어가지 않음.
  • 비동기가 완료될 때, resolve or reject를 호출하는 방법으로 비동기의 동기적 표현 가능.
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('카페라떼'));
  • 반복 내용 함수화.
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();
  • function* 함수가 Generator 함수.
    • Iterator 반환. next 메서드 호출 시 Generator 함수 내 yield까지 실행 후 대기. next 호출 시 다음 yield까지 실행 후 대기.
    • 비동기 완료 시점마다 next 호출 시 순차적 실행.
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();
  • 비동기 작업 함수 앞에 async 표기.
  • 함수 내부에서 실질적 비동기 작업 위치마다 await을 표기.
    • 자동으로 Promise 전환.
    • 해당 내용이 resolve 되어야 다음으로 진행.

🔽 4-6 정리


  • ✅ 콜백 함수는 다른 코드에 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수이다.
  • ✅ 제어권을 넘겨받은 코드는 다음과 같은 제어권을 가진다.
    • 1️⃣ 콜백 함수를 호출하는 시점을 스스로 판단해서 실행한다.
    • 2️⃣ 콜백 함수를 호출할 때 인자로 넘겨줄 값들 및 그 순서가 정해져 있다. 이 순서를 따르지 않고 코드를 작성하면 엉뚱한 결과를 얻게 된다.
    • 3️⃣ 콜백 함수의 this가 무엇을 바라보도록 할지가 정해져 있는 경우도 있다. 정하지 않은 경우에는 전역객체를 바라본다. 사용자 임의로 this를 바꾸고 싶을 경우 bind 메서드를 활용하면 된다.
  • ✅ 어떤 함수에 인자로 메서드를 전달하더라도 이는 결국 함수로서 실행된다.
  • ✅ 비동기 제어를 위해 콜백 함수를 사용하다 보면 콜백 지옥에 빠지기 쉽다. 최근의 ECMAScript에는 Promise, Generator, async / await 등 콜백 지옥에서 벗어날 수 있는 훌륭한 방법들이 속속 등장하고 있다.
profile
PAy IT forwaRD를 실천하는 프론트엔드 개발자.

0개의 댓글