Notice
Recent Posts
Recent Comments
Link
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Archives
Today
Total
관리 메뉴

heyday2024 님의 블로그

[4주차 JS 문법(1)] 콜백함수 (제어권, this 바인딩) 본문

프론트엔드 부트캠프

[4주차 JS 문법(1)] 콜백함수 (제어권, this 바인딩)

heyday2024 2024. 10. 21. 22:29

콜백함수

// 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를 보면 둘다 function을 인자로 받는다. 이 인자로 넘겨진 함수들을 콜백함수라고 함.

 

  1. 다른 코드의 인자로 넘겨주는 함수! 인자로 넘겨준다는 얘기는 콜백함수를 넘겨받는 코드가 있다는 뜻 (like forEach, setTimeout 등).
  2. 콜백 함수를 넘겨받은 위와 같은 코드 forEach, setTimeout 등은 이 콜백 함수를 필요에 따라 적절한 시점에 실행하게됨 --> 즉, 이 기능을 실행할 제어권(action에 대한 제어권)을 forEach, setTimeout에게 부여함.
  3. callback = call(부르다) + back(되돌아오다) = 되돌아와서 호출해줘!  => 다시 말하면, 제어권을 넘겨줄테니 너가 알고 있는 그 로직으로 처리해줘!
  4. 즉, 콜백 함수는 다른 코드(함수 또는 메서드)에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수. 콜백 함수를 위임받은 코드는 자체적으로 **내부 로직**에 의해 이 콜백 함수를 적절한 시점에 실행 ← 이 적절한 시점 역시 제어권이 있는 위임받은 코드가 알아서 함.

제어권

1. 호출 시점에 대한 제어권을 가짐

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

 

//무슨 제어권?
//1. 호출시점에 대한 제어권을 갖음

let count = 0;
//setInterval: 반복해서 매개변수로 받은 콜백함수의 로직을 수행
let timer = setInterval(function () {
  console.log(count);
  if (++count > 4) {
    // clearInterval: setInterval 함수를 통해 설정한 반복적인 작업(인터벌)을 중단할 때 사용
    // 설정된 인터벌(반복 작업)을 식별하는 고유한 ID인 timer를 setInterval이 반환했고 이 ID를 clear해줌으로 동작 중단.
    clearInterval(timer);
  }
})

//0 1 2 3 4

여기서의 콜백 함수를 따로 변수로 지정해보자

그리고 같은 기능을 수행 시키기 위해 코드를 수정하면, 

 

아래와 같은 코드가 된다.

//같은 기능 수행을 위해
// 0.3초 마다 내가 직접 함수 여러번 호출해주어야함
var count = 0;
var cbFunc = function () {
	console.log(count);
	if (++count > 4) return 0;
};

cbFunc();
cbFunc();
cbFunc();
cbFunc();
cbFunc();

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

cbFunc()를 0.3마다 내가 직접 호출을 해주어야한다 ----> 즉, 호출의 주체와 제어권은 현재 모두 나에게(사용자) 있는 것!!

 

하지만, 위의 함수를 콜백함수로서 setInterval()의 인수로 넣어주면 아래의 코드처럼,

자동적으로 0.3초마다 setInterval()이 알아서 cbFunc()를 호출해준다.

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)

즉, 호출의 주체와 제어권을 setInterval()이 모두 갖게되는 것!!

 

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


2. 인자에 대한 제어권 갖음

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

만약에 currentValue, 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 ]

컴퓨터는 사람이 아니기 때문에, index와 currentVakue의 의미를 사람처럼 이해할 수 없음.

즉, 여기서 map 메서드는 첫번째 인자는 각 요소의 값, 두번째 인자는 index를 항상 뜻하는 규칙을 가지고 있음을 알수 있음. 

 

이처럼, map 메서드를 호출해서 원하는 배열을 얻고자 한다면 정의된 규칙대로 작성해야 해야함 (콜백 내부의 인자도 물론 포함!)

 

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

인자(의 순서)까지도 제어권이 그에게 있는 것이죠.

 


3. this에 대한 제어권 갖음

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

하지만, 앞서 배운 것과 같이 특정 예외 함수들은 이미 this를 별도로 지정하는 경우가 있었다.(addEventListener 등...

//이젠 이 코드를 좀 더 잘 이해할 수 있어요!!

// setTimeout은 내부에서 콜백 함수를 호출할 때, call 메서드의 첫 번째 인자에
// 전역객체를 넘겨요
// 따라서 콜백 함수 내부에서의 this가 전역객체를 가리켜요
setTimeout(function() { console.log(this); }, 300); // Window { ... }

// forEach도 마찬가지로, 콜백 뒷 부분에 this를 명시해주지 않으면 전역객체를 넘겨요!
// 만약 명시한다면 해당 객체를 넘기긴 해요!
[1, 2, 3, 4, 5].forEach(function (x) {
	console.log(this); // Window { ... }
});

//addEventListener는 내부에서 콜백 함수를 호출할 때, call 메서드의 첫 번째
//인자에 addEventListener메서드의 this를 그대로 넘겨주도록 정의돼 있어요(상속)
document.body.innerHTML += '<button id="a">클릭</button';
document.body.querySelector('#a').addEventListener('click', function(e) {
	console.log(this, e);
});

)

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

 

별도의 this를 지정하는 방식을 이해하기 위해서, 그리고 제어권에 대한 이해를 높이기 위해서

map을 직접 구현

//map 구현

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


let result = [1, 2, 3, 4].mapMade(function (element) {
  return element * 5;
});

실제로 map은 이런식으로 구현될 수 있고, call 또는 apply로 직접 this를 지정한다.

=> 제어권을 넘겨받을 코드에서 call/apply 메서드의 첫 번째 인자에서 콜백 함수 내부에서 사용될 this를 명시적으로 binding 하기 때문에 this에 다른 값이 담길 수 있는 것!!

 

//이젠 이 코드를 좀 더 잘 이해할 수 있어요!!

// setTimeout은 내부에서 콜백 함수를 호출할 때, call 메서드의 첫 번째 인자에
// 전역객체를 넘겨요
// 따라서 콜백 함수 내부에서의 this가 전역객체를 가리켜요
setTimeout(function() { console.log(this); }, 300); // Window { ... }

// forEach도 마찬가지로, 콜백 뒷 부분에 this를 명시해주지 않으면 전역객체를 넘겨요!
// 만약 명시한다면 해당 객체를 넘기긴 해요!
[1, 2, 3, 4, 5].forEach(function (x) {
	console.log(this); // Window { ... }
});

//addEventListener는 내부에서 콜백 함수를 호출할 때, call 메서드의 첫 번째
//인자에 addEventListener메서드의 this를 그대로 넘겨주도록 정의돼 있어요(상속)
document.body.innerHTML += '<button id="a">클릭</button';
document.body.querySelector('#a').addEventListener('click', function(e) {
	console.log(this, e);
});

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

 


콜백함수는 함수다!

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

var obj = {
  vals: [1, 2, 3],
  logValues: function (v, i) {
    console.log(">>> test starts");
    if (this === global) {
      console.log("this가 global 입니다. 원하지 않은 결과!!")
    }
    else {
      console.log(this, v, i);
    }

    console.log(">>>> test ends");
  },
};

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

//callback => obj를 this로 하는 메서드를 그대로 전달한게 아니에요
// 그냥 obj객체 내부의 메서드 함수의 그 내용만을 전달함. (메서드가 아니라 함수 내용 자체만!!)

//단지, obj.logValues가 가리키는 함수만 전달한거에요(obj 객체와는 연관이 없습니다)
[4, 5, 6].forEach(obj.logValues);

// [4, 5, 6].forEach(function (v, i) {
//   console.log(">>> test starts");
//   if (this === global) {
//     console.log("this가 global 입니다. 원하지 않은 결과!!");
//   } else {
//     console.log(this, v, i);
//   }

//   console.log(">>>> test ends");
// });
//이 코드와 다름 없음



// [4, 5, 6].forEach(obj.logValues(3,4));
// 이렇게 넣는거 절대 아님!! 
// 애초에 forEach의 콜백함수에서 첫번쨰 인자는 각 요소,
// 두번쨰 인자는 인덱스가 들어가는것이 규칙임
// 그것을 위처럼 이미 지정해서 넣어버리면 그건 그냥 obj.logValues(3, 4)의 결과값을
// 인자로 집어넣은 것과 다름없음(콜백함수조차 아님.)

즉, 콜백함수를 필요로 하는 함수들이 콜백함수를 특정 객체의 메서드로 받아도 그것은 method의 특성이 아닌, 일반함수로서의 역할로 기능함.

 

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

//>>> test starts
// { vals: [ 1, 2, 3 ], logValues: [Function: logValues] } 1 2
// >>>> test ends
// ---------------------------------------------------------------------------------------------------------------------

[4, 5, 6].forEach(obj.logValues);

// >>> test starts
// this가 global 입니다. 원하지 않은 결과!!
// >>>> test ends
// >>> test starts
// this가 global 입니다. 원하지 않은 결과!!
// >>>> test ends
// >>> test starts
// this가 global 입니다. 원하지 않은 결과!!
// >>>> test ends

그래서 위 코드의 결과값을 보면, method로 불렸을때 호출 주체가 obj인것인 반면, 콜백함수로 사용되었을때는 일반 함수로서의 역할을 하기 때문에 this가 전역객체를 가리킴.

 


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

Q. 콜백 함수 내부에서 this가 문맥에 맞는 객체를 바라보게 할 수는 없을까요?

A. 가능!! 이전에 객체 내부에 미리 변수를 선언해서 그 변수에 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);

closure 방식, 스코프를 나갔는데도 다른 함수로 스코프 내부의 값에 접근할 수 있음.

- return function부분에 this를 사용한 것이 아닌, 그냥 할당된 값을 사용한 것과 다름없음.

- 그리고 코드가 조금 번거로워 보임.

 

 

결국 이렇게 this를 굳이 미리 obj1으로 할당하고 쓸거면 애초에 아래 코드처럼 써도 상관없지 않나?

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

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

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

- obj1 의 func를 그 내용만 그대로 obj2의 func에 할당해서 사용하면, 손쉽게 obj2를 this가 가리키게 할 수 있음

- 마찬가지로 obj3에서는 func 속성이 없음에도 call이라는 즉시 실행 메서드를 사용해서 obj3라는 객체에 obj1.func라는 함수를 즉시 호출할 수 있음.

 

하지만 이보다도 더 좋은 방법이 있음!!!!

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를 사용해서 this를 지정해주기.

- 이러면, call을 사용하기 위해 this를 self에 할당했던 그런것 안해도 됨.

 

bind로 앞서 다양하게 제시되었던 this 우회법보더 더욱 편리하게 this를 내가 원하는 대로 바인딩할 수 있다는 것이 흥미로웠다.

thisBinding에는 bind가 사용하기 여러모로 쓸모있을 것 같다.(역시 메소드 이름값 하네~)