[내배캠] 4/25 9일차

초이·2024년 4월 25일
0

콜백 함수?

: 다른 코드의 인자로 넘겨주는 함수.

콜백함수 예시

  • setTimeout
    	setTimeout(function(){
    	console.log('time');
    }, 1000);
  • forEach
    arr.forEach(function (num) {
    	console.log(num);
    }

콜백함수의 특징

action에 대한 제어권을 함수에게 위임하는 개념이다. 내가 직접 함수를 호출하는게 아닌, 콜백 함수 자체적으로 내부 로직에 맞춰 제어권을 가지고 기능을 실행하게 하는 것.

콜백함수의 제어권

  1. 호출 시점에 대한 제어권을 갖는다.

    var func = function () {
    	console.log(count);
    	if(++count > 4) clearInterval(timer);
    }
    
    //우리가 직접 호출
    func();
    
    //setInterval로 넘겨줌
    var timer = setInterval(func, 3000); 
    //3초라는 실행 타이밍을 주체적으로 가짐
    • func() : 호출 주체, 제어권 ⇒ 사용자
    • setInterval : 호출주체, 제어권 ⇒ setInterval
  2. 인자에 대한 제어권

    map은 기존 배열을 변경하지 않고 새로운 배열을 생성한다.

    var newArr = [10, 11, 12]
    
    newArr.map(function(item, index){ //콜백함수
    	console.log(item, index); //10 0 11 1 12 2
    });
    
    console.log(newArr); //undefined

    현재 코드는 return이 없으면 반환값이 없어서 새로운 배열이 없기때문에 newArr를 정의하지 못한다.

    여기서, item과 index는 매개변수로써 배열의 값을 나타내준다.

    **❓ 그렇다면 이 콜백 함수의 변수의 이름을 바꾼다면? 값도 바뀔까?

    결론은 바뀌지 않는다**. 이게 콜백 함수가 인자에 대한 규칙에 맞게 제어권을 가진다는 것을 의미한다. 사람이 알아듣는 언어를 사용하여도 첫 번째 자리는 배열의 값, 두 번째 값은 인덱스 값을 가지는 거다.

  3. this

    앞에서 this를 공부했을 때, 함수는 전역 객체를 메서드는 앞선 객체를 호출하는 것을 배웠고 호출 함수 또한 함수이기 때문에 전역 객체를 참조한다고 공부했다.

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

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

    // Array.prototype.map을 직접 구현해봤어요!
    Array.prototype.mapaaa = function (callback, thisArg) { //callback이라는 함수, this
      var mappedArr = [];
    
      for (var i = 0; i < this.length; i++) { //this => 호출 주체가 배열임. 배열을 가리
        // 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);

    이것이 직접 수제작한 map 함수이다.

    배열.map((함수));

    의 형태로 사용이 되지만 위에서 callback.call(thisArg || global, this[i]);를 볼 때, call이 전역 변수만 받게 하는게 아니라 this값이 있다면 thisArg에 this값도 받게 하기 때문에

    >>콜백함수의 예외상황<<
    배열.map((함수), this);

    이런식으로 사용할 경우 this값에 전역변수가 아닌 다른 값이 담길 수 있게 된다.

콜백 함수는 함수다

다음의 코드를 보자

var obj = {
	logValues : function(v,i){
		console.log('hi');
	},
	val : 1
}

[1, 2, 3].forEach(obj.logValues);

마지막 줄만 보면, forEach에는 콜백 함수가 들어가야 하는데 obj라는 오브젝트의 객체가 들어간 것처럼 보인다. 하지만 obj.logValues는 함수의 형태를 가지기 때문에 콜백 함수로써의 기능을 수행한다.

따라서 들어간 값이 obj라는 호출객체가 있는 것 처럼 보이지만 실제로는 함수의 형태만 들어간 것이기 때문에 forEach에서 obj를 this 범위로 사용할 수 없다. logValues 안에 함수는 전역객체를 this값으로 사용하게 된다.

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

현재 전통적인 방식으로는 함수의 상위 객체 값을 저장하는 방식으로 다른 값을 바인딩할 수 있다.

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

이런 함수를 실행할 때 함수가 끝나게 되어도 함수에 저장한 값이 살아있는 것을 클로저라고 할 수 있다.

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

setTimeout(obj1.func, 1000);

이렇게 할 수 있기도 하지만, this를 사용하지 않으면서 this에 대한 장점을 놓치게 된다.

var obj2 = {
	name: 'obj2',
	func: obj1.func //this를 바인딩한 obj1의 func를 불러옴
};
var callback2 = obj2.func();
setTimeout(callback2, 1500); //obj2가 뜨게

이렇게하면, 번거롭지만 this를 우회적으로나마 원하는 객체를 바라보게 할 수 있다.

func()의 상위객체가 obj2이기 때문에..

var obj3 = { name: 'obj3'};
var callback3 = obj1.func.call(obj3);
setTimeout(callback3, 2000);

이렇게도 사용이 가능하다. 이 방식은 객체에 값이 담겨져있고

call을 사용해서 즉시 실행으로 obj3의 name값을 불러오게 함으로 obj1의 func을 사용한다.

여기서 bind 메소드를 이용해서 this값을 불러오는 방식도 있다.

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

var obj2 = { name: 'obj1' };

setTimeout(obj1.func.bind(obj1), 1000);
setTimeout(obj1.func.bund(obj2), 1000); //

이런식으로 활용하면.. obj1에 있는 func에는 obj1.name이 바인딩 되어있으니까 bind로 obj1의 name을 받아오게 된다.

마찬가지로 obj2라는 값에서 name을 받아오는 형식을 이용해 obj2의 영역을 bind해서 name을 받아오는 식으로 bind를 이용할 수 있다.

콜백 지옥

콜백 지옥이란?

매개변수 자리에 익명 함수(콜백 함수)가 계속 들어가면 엄청나게 들여쓰기가 많이 들어가면서 콜백 지옥이 된다.

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

이런 콜백 지옥은 유지보수가독성이 지옥과도 같다.

**동기 vs 비동기

  • 동기 : synchronous**
    현재 실행중인 코드가 끝나야 다음 코드를 실행하는 방식.
    CPU 계산에 의해 즉시 처리가 가능한 대부분의 코드는 동기적 코드

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

  • 비동기 코드의 이해!
setTimeout(function () {
	console.log("주문1");
}, 1000);

console.log("주문2");

코드 순서 상이라면 “주문1”이 먼저 실행 되어야 한다. 하지만 setTimeout은 비 동기적 작업이기 때문에 1초 나중에 실행되도록 되어있다. 때문에 주문 1이 들어왔지만 끝나고 난 다음 다음 코드를 읽는 게 아니라 1초란 시간이 걸리기 때문에 주문 2가 읽히게 되고 주문2 다음 주문1이 실행되게 된다.

통신이 들어간(서버와의 작업) 코드는 대부분 비동기적이다.

비동기 작업

: 순서를 보장하지 않는다. 그래서 순서를 보장하는 것처럼 보이게 하는 작업을 비동기 작업이라고 한다.

  1. promise (ES6)

    비동기 처리에 대해 처리가 끝나면 알려달라는 약속(promise)이다.

    비동기 작업이 완료될 때 resolve(성공), reject(실패)를 호출한다.

    new Promise(function(resolve){
    	setTimeour(function(){
    		var name = 'coffee';
    		console.log(name);
    		resolve(name);
    	}, 500);
    }).then(function(prev){ //promise를 작성(chaining)
    	return new Promise(function (resolve){
    		setTimeout(function () {
    			var name = prev + "latte";
    			console.log(name);
    		}
    	}, 500);
    }).then ... //promise를 작성(chaining)

    이렇게 반복되는 로직을 가독성과 유지보수 좋게 바꾸는 작업이 필요하다. (refactoring)

    똑같은 함수가 반복되니까 변수로 넣어서 내부의 로직을 생성하게 하는 방법.

    • 위의 코드를 refactoring한 것
    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('어쩌구')
    	.
    	.
    	.;

    이렇게 addCoffee에 함수를 넣어서 then다음에 계속 넣게 된다면 전보다 가독성이 있어진다.

  1. 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* () { //<<generator함
      	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();
  1. async / await (ES2017)

    비동기 작업이 필요한 함수 앞에 async를 붙이고 실질적인 비동기 작업이 필요한 위치마다 await를 붙여주면 된다.

    Promise ~ then과 동일한 효과

    var addCoffee = function (name) {
    	return new Promise(function (resolve) {
    		setTimeout(function(){
    			resolve(name);
    		}, 500);
    	});
    };
    //var coffeeMaker = async () => { : 화살표 함수 시 괄호 앞에 
    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();

    소감

    오늘은 콜백함수에 대해서 공부를 했다. 콜백에 대해서 진하게 공부하니까 코드 가독성과 유지보수 측면, 그리고 비동기와 동기에 대해서 이해하면서 서버 통신때 효율적이고 좋은 코드를 짜는 법에 대해 공부하게 된 것같아서 뿌듯하고 조금 힘들었다 ㅎㅎ..


    그리고 예상치 못하게 알고리즘 푸는 것에 은근히 시간이 많이 사용되어서 계획했던 강의를 좀 더 늦게 수강하였다. 하지만 그럼에도 오늘안에 하고자 한 것을 다 해냈다는게 중요한 것같다!! 내일은 개인 프로젝트에 많은 시간을 소요할 예정이다. 아마 좀 더 욕심내서 주말을 사용해서 만들 예정이다. 주말에는 TIL 내용을 정리해서 따로 포스팅하고 개인 프로젝트 기능 구현도 정리해서 올려야겠다.

profile
개발 일기장

0개의 댓글