본문 바로가기
항해99

JavaScript 문법 종합반 4주차

by Hyeon_E 2023. 6. 20.

[ 콜백함수 ]

▶ 콜백함수

예시

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

 

  • 다른 코드의 인자로 넘겨주는 함수. 인자로 넘겨준다는 얘기는 콜백함수를 넘겨받는 코드가 있다는 것
    • 여기서 넘겨받는 코드는 setTimeout이 됨
  • 콜백함수를 넘겨받는 코드는 콜백함수를 필요에 따라 적절한 시점에 실행하게 됨
  • action에 대한 제어권은 함수에 있음
  • callbkack의 의미는 call(부르다) + back(되돌아오다)로 되돌아와서 호출해줘라는 뜻

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

 

▶ 제어권

1. 호출시점

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

 

아래 예시에서는 콜백 함수의 제어권을 넘겨받은 코드(setInterval)가 언제 콜백함수를 호출할지에 대한 제어권을 가지게 됨. 0.3초라는 적절한 시점을 본인의 함수에 적어놓은 대로 실행하는 것

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)

 

  • 원래 cbFunc()를 수행한다면 그 호출주체와 제어권은 모두 사용자가 됨
  • setInterval로 넘겨주게 되면 그 호출주체와 제어권은 모두 setInterval이 됨

2. 인자

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 ]

 여기서 currentValue, index 이 변수의 순서를 바꾸면 컴퓨터는 사람처럼 이해할 수 없어 의도하지 않는 값이 나옴

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

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

인자(의 순서)까지도 제어권이 그에게 있는것 그래서 제어권이 넘어갈 map 함수의 규칙에 맞게 호출해야 함

 

3.this

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

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

 

예시

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를 명시적으로 binding 하기 때문에 this에 다른 값이 담길 수 있음

제어권을 넘겨받을 코드에서 콜백 함수에 별도로 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);
});

 

▶ 콜백 함수는 함수다

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

 

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에 다른 값 바인딩하기

콜백 함수 내부의 this에 다른 값을 바인딩하는 방법

 

<전통적 방식>

 

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

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

 

실제로 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를 우회적으로나마 활용하여 원하는 객체를 바라보게 할수 있음

하지만 이러한 부분을 쉽게 사용할 수 있는 방법이 있는데 이것이 바로 bind 메서도 이용

 

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

 

▶ 콜백 지옥과 비동기 제어

1. 콜백지옥이란

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

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

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

가독성이 매우 지옥(나빠짐)이고 오랜상태로 이렇게 짜여왔기 때문에 수정도 어려움

 

2. 동기 vs 비동기

  • 동기(synchronous)
    • 현재 실행중인 코드가 끝나야 다음 코드를 실행하는 방식
    • CPU의 계산에 의해 즉시 처리가 가능한 대부분의 코드는 동기적 코드
    • 계산이 복잡해서 CPU가 계산하는 데에 오래 걸리는 코드 역시도 동기적 코드
  • 비동기(a + synchronous ⇒ async라고 흔히 부름)
    • 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어가는 방식
    • setTimeout, addEventListner 등
    • 별도의 요청, 실행 대기, 보류 등과 관련된 코드는 모두 비동기적 코드
    • 웹의 복잡도가 올라갈 수록 비동기적 코드의 비중이 늘어남

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

 

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

 

위에서 아래로 코드흐름이 이어지기 때문에 가독성이 좋지만 한번만 쓰는데도 이렇게 이름을 다 붙여야 하는것은 매우 불편함. 근본적인 해결책은 될 수 없음. 이런경우 때문에 JS에서는 비동기적인 작업을 동기적으로(동기적으로 보이도록) 처리해주는 장치를 계속해서 마련해주고 있음 

  • Promise, Generator - ES6
  • async/await - ES7

비동기 작업의 동기적 표현이 필요함(순서를 보장하지 않지만 보장하는것처럼 써야함)

 

<비동기적 작업의 동기적 표현 - 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);
	});
});

 

반복부분을 함수화 한 코드. 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('카페라떼'));

 

<비동기 작업의 동기적 표현 - Generator>

- 이터러블 객체(Iterable)

generator라는 것은 반복할 수 있는 Iterable 객체를 생성함

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

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

 

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

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

 

 

'항해99' 카테고리의 다른 글

리액트 입문주차 2  (0) 2023.06.23
리액트 입문주차 1  (0) 2023.06.23
띵동코딩 CSS기초_Level1_2  (0) 2023.06.01
띵동코딩 CSS기초_Level1_1  (0) 2023.06.01
JavaScript 문법 종합반 3주차  (0) 2023.05.25

댓글