01 콜백 함수 callback function
요약 : 다른 코드의 인자로 넘겨주는 함수. 필요에 따라 넘겨받은 후 적절한 시점에 시행되며 제어권과 연관이 있다
어떤 이벤트가 발생한 후, 수행될 함수를 의미
다른 코드 (함수 또는 메서드)에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수
콜백 함수를 위임받은 코드는 자체적인 내부 로직에 의해 콜백 함수를 적절한 시점에 실행
JS에서 함수는 1급객체이므로, 인자 전달 시 함수를 전달할 수 있기 때문에 콜백함수가 가능
일급 객체는 ...
- 변수나 데이터 구조 안에 담을 수 있다.
- 파라미터로 전달 할 수 있다.
- 반환값으로 사용 할 수 있다.
- 런타임에 생성될 수 있다.
CallBack 뜻
call(부르다, 호출하다, 실행하다) + back (뒤돌아오다, 뒤돌다)
// 구글링에서는 called at the back ..이게 더 이해가 잘 간다.
= 함수 X를 호출하면서 특정 조건일 때 함수 Y를 실행해서 나에게 알려달라고 명령하는 것
-> 함수X는 해당 조건이 갖춰졌는지 여부를 스스로 판단하고 Y를 직접 호출한다
예 : 자동적으로 정해진 시간에 울리는 기상알람
-> 요청할 때마다 수동적으로 시간 정보를 제공할 필요 없이
요청을 받은 뒤 자체적으로 수행하다가 적절한 시점에 수행
즉 함수에게 요청을 하면서 일을 하는 명령에 대한 '제어권'을 스마트폰에게 넘긴 것
예 2 : 콜백 함수의 동작 방식은 일종의 식당 자리 예약과 같습니다. 일반적으로 맛집을 가면 사람이 많아 자리가 없습니다. 그래서 대기자 명단에 이름을 쓴 다음에 자리가 날 때까지 주변 식당을 돌아다니죠. 만약 식당에서 자리가 생기면 전화로 자리가 났다고 연락이 옵니다. 그 전화를 받는 시점이 여기서의 콜백 함수가 호출되는 시점과 같습니다. 손님 입장에서는 자리가 날 때까지 식당에서 기다리지 않고 근처 가게에서 잠깐 쇼핑을 할 수도 있고 아니면 다른 식당 자리를 알아볼 수도 있습니다.
자리가 났을 때만 연락이 오기 때문에 미리 가서 기다릴 필요도 없고, 직접 식당 안에 들어가서 자리가 비어 있는지 확인할 필요도 없습니다. 자리가 준비된 시점, 즉 데이터가 준비된 시점에서만 저희가 원하는 동작(자리에 앉는다, 특정 값을 출력한다 등)을 수행할 수 있습니다.
//https://joshua1988.github.io/web-development/javascript/javascript-asynchronous-operation/
즉시 실행 되는 동기식 콜백 예시 :
실행하면 이름을 입력하라는 알람창이 뜨고 입력했을 시에만 'Hello ' + name 이 뜨게 된다
function greeting(name) {
alert('Hello ' + name);
}
function processUserInput(callback) {
var name = prompt('Please enter your name.');
callback(name);
}
processUserInput(greeting);
//https://developer.mozilla.org/en-US/docs/Glossary/Callback_function
비동기식 콜백 예시 :
아래 예시는 submitBtn 클래스를 가진 요소를 클릭했을 때, 콜백 함수가 실행되는 jQuery 코드이다.
(jQuery : HTML의 클라이언트 사이드 조작을 단순화 하도록 설계된 크로스 플랫폼의 자바스크립트 라이브러리)
비동기 이벤트인 click에 대한 이벤트 리스너로 콜백 함수가 작성되었고 알림창을 띄우는 alert()를 호출중
click의 콜백 함수는 naming을 할 필요가 없는 익명함수로 대부분의 콜백 함수는 이렇게 익명함수로 작성됨
$(".submitBtn").click(function(){
alert("제출버튼을 클릭했습니다!");
})
//https://victorydntmd.tistory.com/48
비동기 처리 : 특정 코드의 연산이 끝날때까지 기다려주지 않고 다음 코드가 먼저 실행된다.
비동기 관련 메서드 : setTimeout(), .then(), fetch() 등등 아래에서 다룰 것이다. setTimeOut()은 대충 얼마간 기다려주는 함수 느낌인데 비동기 처리 방식의 간단한 문제점으로는 setTimeout()메서드가 실행되고 n초를 기다릴동안, 그 다음 코드는 기다려 주지않고 실행되기 때문에-즉, 비동기로 처리되기 때문에 문제가 있을 수 있다.
콜백함수를 통해 특정함수가 처리가 된 이후에 처리시킬 함수를 콜백함수화 시키면 비동기처리의 문제점을 해결 할 수 있다. 비동기방식을 사용하는 ajax 통신 코드도 콜백 함수로 개선할 수 있다.
결론 : 콜백함수는 특정 로직이 끝났을때 원하는 동작을 실행시키는 함수라고 이해하면 된다.
콜백함수를 통해 특정함수가 처리가 된 이후에 처리시킬 함수를 콜백함수화 시키면 비동기처리의 문제점을 해결 할 수 있다.
02 제어권
4-2-1 호출 시점
var count = 0;
var timer = setInterval(function(){
console.log(count);
if(++count > 4) clearInterval(timer);
}, 300);
setInterval을 호출할 때 두 개의 매개변수를 전달 (익명 함수, 300) 했습니다.
setInterval의 구조는 다음과 같습니다.
var intervalID = scope.setInterval(func, delay [, param1, param2, ...]);
scope : Window 객체나 Worker의 인스턴스가 들어옵니다. (두 객체 모두 setInterval 메서드 제공)
func, delay : 값 전달 필수. 세 번째 매개변수([ ] 안의 것들) 부터는 선택
delay : 밀리초(ms) 단위의 숫자. 매 ms마다 func에 넘겨준 함수가 실행
param1, param2, .. : func 함수 실행 시 매개변수로 전달할 인자
setInterval을 실행하면 반복적으로 실행되는 내용 자체를 특정할 수 있는 고유한 ID 값이 반환됩니다.
setInterval 함수는 리턴 값 없이 매개변수안에 있는 실행구문을 실행시켜주고 끝이 아니라, 코드 내 유일한 timer id를 int 형태로 반환합니다. (timer id값은 clearInterval 함수로 타이머 함수를 종료시킬 때도 사용 가능한 유니크 키 값)
같은 코드를 좀 수정했습니다. timer 변수에는 setInterval의 ID 값이 담깁니다. setInterval에 전달한 첫 번째 인자 cbFunc 콜백 함수는 0.3초마다 자동 실행됩니다. 콜맥 함수 내부에서 count 값을 출력하고 , 1 증가시킨 뒤 그 값이 4보다 크면 반복 실행을 종료합니다.
var count = 0;
var cbFunc = function(){
console.log(count);
if(++count > 4) clearInterval(timer);
};
var timer = setInterval(cbFunc, 300);
cbFunc()의 호출 주체와 제어권은 모두 사용자이며, setInterval의 호출주체와 제어권은 setInterval입니다. setInterval이라는 다른 코드에 첫 번째 인자로 cbFunc 함수를 넘겨주자 제어권을 넘겨받은 setInterval이 스스로 판단, 적절한 시점에 익명 함수를 실행합니다 => 콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수 호출 시점에 대한 제어권을 가짐
4-2-2 인자
var newArr = [10, 20, 30].map(function(currentValue, index) {
console.log(currentValue, index);
return currentValue + 5;
});
console.log(newArr);
var newArr = [10, 20, 30].map(function(currentValue, index) {
console.log("1", currentValue, index);
return currentValue + 5;
});
console.log("2",newArr);
map 메서드의 동작 방식을 알기 위해 Array의 prototype에 담긴 map 메서드의 구조를 알아보겠습니다.
Array.prototype.map(callback[, thisArg]);
callback: function(currentValue, index, array)
map 메서드는 첫 번째 인자로 callback 함수를 받고, 생략 가능한 두 번째 인자로 콜백 함수 내부에서 this로 인식할 대상을 특정할 수 있습니다. 생략할 경우 일반 함수처럼 전역객체가 바인딩됩니다.
map 메서드는 메서드의 대상이 되는 배열의 모든 요소들을 처음부터 끝까지 하나씩 꺼내 콜백 함수를 반복 호출하고,
콜백 함수의 실행 결과들을 모아 새 배열을 만듭니다.
콜백 함수의 인자에는 순서대로 배열의 요소 중 현재값, 현재값의 인덱스, map 메서드의 대상이 되는 배열 자체가 담깁니다. 콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수를 호출할 때 인자에 어떤 값들을 순서로 넘길 것인지에 대한 제어권을 가집니다.
4-2-3 this
별도의 this를 지정하는 방식과 제어권에 대한 이해를 높이기 위해 임의로 map 메서드를 직접 구현합니다.
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값을, 세 번째 인자에는 배열 자체를 지정해 호출합니다. 그 결과가 변수 mappedArr 에 담겨 mappedArr의 i번째 인자에 할당됩니다.
제어권을 넘겨받을 코드에서 call/apply 메서드의 첫 번째 인자에 콜백 함수 내부에서의 this가 될 대상을 명시적으로 바인딩하기 때문에 this에 다른 값이 담기는 것입니다.
setTimeout(function() {console.log(this); }, 300); // (1) Window { ... }
[1,2,3,4,5].forEach(function (x) {
console.log(this); // (2) Window { ... }
});
document.body.innetHTML += '<button id="a">클릭</button>';
document.body.querySelector('#a').addEventListener('click', function(e) {
console.log(this, e); // (3) <button id="a">클릭</button>
} // MouseEvent { isTrusted: true, ... }
);
각각 콜백 함수 내 this를 살펴보겠습니다.
(1)의 setTimeout은 내부에서 콜백 함수를 호출할 때 call 메서드의 첫 번째 인자에 전역객체를 넘기기 때문에 콜백함수 내부에서의 this는 전역객체입니다.
(2)의 forEach는 '별도의 인자로 this를 받는 경우'지만 별도의 인자로 this를 넘기지 않았기 때문에 전역객체를 가리킵니다.
(3)의 addEventListener는 내부에서 콜백 함수를 호출할 때 call 메서드의 첫 번째 인자에 addEventListener 메서드의 this를 그대로 넘기도록 정의되어 있어서 콜백 함수 내부에서의 this가 addEventListener를 호출한 주체인 HTML Element를 가리키게 됩니다.
03 콜백 함수는 함수다 ★
콜백 함수로 어떤 객체의 메서드를 전달하든 관계없이 그 메서드는 함수로서 호출됩니다.
// 4-7 메서드를 콜백 함수로 전달한 경우
var obj = {
vals : [1, 2, 3],
logValues : function(v, i) {
console.log(this, v, i);
}
};
obj.logValues(1, 2);
// 메서드로 정의 및 호출
// this는 obj, 인자는 1과 2
// { vals: [1, 2, 3], logValues: f } 1 2
[4, 5, 6].forEach(obj.logValues);
// forEach 함수의 콜백함수로 전달
// obj.logValues가 가리키는 함수만 전달했는데 이 함수는 메서드로서 호출할 때가 아니면
// obj와의 연관이 없다
// this는 window (전역객체)
// 메서드로 정의되어 있지만 콜백으로 넘어가면 함수가 된다
// Window { ... } 4 0
// Window { ... } 5 1
// Window { ... } 6 2
● 메서드와 함수의 차이 (클래스 함수 vs 함수)
함수가 더 포괄적인 의미입니다. 메서드는 클래스/구조체/열거형 내부에 작성된 것
04 콜백 함수 내부의 this에 다른 값 바인딩하기
객체의 메서드를 콜백 함수로 전달하면 해당 객체를 this로 바라볼 수 없지만, 콜백 함수 내부에서 this가 객체를 바라보게 하려면 별도의 인자로 this를 받는 함수는 원하는 값을 넘기면 되고 그렇지 않은 경우 this의 제어권도 넘겨주게 되므로 사용자가 임의로 원하는 값을 바꿀 수 없습니다.
그래서 this를 다른 변수에 담아 콜백 함수로 활용할 함수에서 그 변수를 사용하고, 이를 클로저로 만드는 방식을 많이 씁니다.
// 4-8 콜백 함수 내부의 this에 다른 값을 바인딩하는 방법(1)
var obj1 = {
name : 'obj1',
func : function() {
var self = this;
return function() {
console.log(self.name);
};
}
};
var callback = obj1.func();
setTimeout(callback, 1000);
obj1.func 메서드 내부에서 self라는 변수에 this를 담고 익명 함수를 선언과 동시에 반환합니다.
obj1.func를 호출하면 내부 함수가 반환되어 callback 변수에 담깁니다.
callback을 setTimeout 함수에 인자로 전달하면 1초 뒤 callback이 실행되면서 'obj1'을 출력합니다.
// 4-9 콜백 함수 내부에서 this를 사용하지 않은 경우
var obj1 = {
name : 'obj1',
func : function() {
console.log(obj1.name);
}
};
setTimeout(obj1.func, 1000);
this를 사용하지 않고 같은 결과를 도출하는 예입니다. 간결하나 this의 다양한 활용은 물 건너 갔습니다.
obj1를 다른 객체에 재활용하는 상황의 코드입니다.
// 4-10 예제 4-8의 func 함수 재활용
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);
callback2에는 obj1.func를 복사한 obj2.func를 실행한 결과를 담아 콜백으로 사용했습니다.
callback3은 obj1.func를 실행하면서 this를 obj3가 되도록 지정해 콜백으로 사용했습니다.
실행 시점으로부터 1.5초 후 'obj2', 2초 후에는 'obj3'이 출력됩니다.
// 4-11 콜백 함수 내부의 this에 다른 값을 바인딩하는 방법 (2) - bind 메서드 활용
var obj1 = {
name : 'obj1',
func : function() {
console.log(self.name);
}
};
setTimeout(obj1.func.bind(obj1), 1000);
var obj2 = { name: 'obj2' };
setTimeout(obj1.func.bind(obj2), 1500);
위와 같이 ES5의 bind 메서드를 활용하는 방법도 있습니다.
05 콜백 지옥과 비동기 제어
콜백 지옥이란 콜백 함수를 익명 함수로 전달하는 과정이 반복되는 것으로 JS에서 흔히 발생하는 문제입니다. 이벤트 처리나 서버 통신 등 비동기 작업을 수행하기 위해 이 형태가 등장하는데 가독성이 떨어지며 코드 수정이 어려운 문제점이 있습니다.
동기적인 코드는 현재 실행중인 코드가 완료되고 나서 다음 코드를 실행하는 방식이며 비동기적인 코드는 현재 실행중인 코드의 완료 여부와 무관하게 다음 코드로 넘어가는 것입니다.
CPU의 계산에 의해 즉시 처리 가능한 대부분의 코드는 동기적 코드입니다.
반면 사용자의 요청에 의해 특정 시간이 경과되기 전까지 어떤 함수의 실행을 보류하거나 사용자의 직접적 개입이 있을 때 어떤 함수를 실행하도록 대기하거나, 웹브라우저 외 별도의 대상에 무언가를 요청하고 응답을 받았을 때 어떤 함수를 실행하도록 대기하는 등 별도의 요청, 실행 대기, 보류 등과 관련된 코드는 비동기적 코드입니다.
// 4-12 콜백 지옥 예시
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, '에스프레소');
0.5초마다 커피 목록을 수집해 출력하는 결과를 보입니다. 각 콜백은 커피 이름을 전달하고 목록에 이름을 추가합니다.
값이 전달되는 순서가 아래에서 위로 향하고 있어 어색하며 가독성도 좋지 않습니다.
이를 해결하는 방법은 익명의 콜백 함수를 모두 기명함수로 전환하는 것입니다.
// 4-13 콜백 지옥 해결 - 기명 함수로 변환
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, '에스프레소');
코드의 가독성이 좋아지고 순서대로 함수를 읽는 데에도 어려움이 없습니다.
변수의 외부 노출 문제도 즉시 실행 함수 등으로 전체를 감싸는 것으로 해결 가능합니다.
일회성 함수를 변수에 할당하는 것이 단점인데,
ES6의 Promise, Generator 등과 ES2017의 async/await 등으로 수정해 볼 수 있습니다.
아래 코드는 이해가 어렵다면 패스해도 됩니다.
// 4-14 비동기 작업의 동기적 표현(1) - Promise(1)
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 + ', 아메리카노';
consoel.log(name);
resolve(name);
}, 500);
});
}).then(function (prevName) {
return new Promise(function (resolve){
setTimeout(function () {
var name = prevName + ', 카페모카';
consoel.log(name);
resolve(name);
}, 500);
});
}).then(function (prevName) {
return new Promise(function (resolve){
setTimeout(function () {
var name = prevName + ', 카페라떼';
consoel.log(name);
resolve(name);
}, 500);
});
});
new 연산자와 함께 호출한 Promise의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행되지만 그 내부에 resolve 또는 reject 함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지 다음(then) 또는 오류 구문(catch)로 넘어가지 않습니다.
따라서 비동기작업이 완료될 때, resolve/reject 를 호출하는 방법으로 비동기 작업의 동기적 표현이 가능합니다.
// 4-15 비동기 작업의 동기적 표현(2) - Promise(2)
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('카페라떼'));
4-15는 4-14를 더욱 짧게 표현했는데, 2~3번째 줄은 클로저가 사용되었습니다. 클로저는 차후 다룹니다.
// 4-16 비동기 작업의 동기적 표현(3) - 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();
제너레이터(generator)를 사용하면 여러 개의 값을 필요에 따라 하나씩 반환(yield)할 수 있습니다.
제너레이터 함수를 실행하면 Iterator가 반환되는데, Iterator는 next라는 메서드를 가지고 있습니다.
iternator : 배열이나 유사한 자료 구조 내부의 요소를 순회(traversing)하는 객체입니다.
build-in object 중 iterable을 갖고 있는 객체는 아래 5가지입니다.
Array.prototype[@@iterator]()
TypedArray.prototype[@@iterator]()
String.prototype[@@iterator]()
Map.prototype[@@iterator]()
Set.prototype[@@iterator]()
이 next 메서드를 호출하면 Generator 함수 내부에서 가장 먼저 등장하는 yield에서 함수의 실행을 멈춥니다.
이후 다시 next 메서드를 호출하면 멈췄던 부분부터 시작해서 그 다음에 등장하는 yield에서 함수의 실행을 멈춥니다.
즉, 비동기 작업이 완료되는 시점마다 next 메서드를 호출해준다면 Generator 함수 내부의 소스가 위에서부터 아래로 순차 진행됩니다.
yield : 제너레이터 함수(function* 또는 레거시 generator 함수)를 중지하거나 재개하는데 사용
(재개 시 expression 의 값은 제너레이터의 caller로 반환)|
[rv] = yield [expression];
ES2017에서는 가독성이 뛰어나면서 작성법도 간단한 기능의 async/await가 추가되었습니다.
비동기 작업을 수행하고자 하는 함수 앞에 async를 표기하고, 함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await를 표기하는 것만으로 뒤의 내용을 Promise로 자동 전환하고, 해당 내용이 resolve 된 이후에 다음으로 진행합니다.
Promise의 then과 흡사한 효과를 얻을 수 있습니다.
// 4-17 비동기 작업의 동기적 표현(4) - Promise + Async/await
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();
'Javascript > 코어자바스크립트' 카테고리의 다른 글
코어자바스크립트 요약 - 06. 클로저 (0) | 2020.03.07 |
---|---|
코어자바스크립트 요약 - 05. 클로저 (0) | 2020.02.17 |
코어자바스크립트 요약 - 03. this (0) | 2020.02.09 |
코어자바스크립트 요약 - 02.실행 컨텍스트 (0) | 2020.02.08 |
코어자바스크립트 요약 - 01.데이터타입 (0) | 2020.02.02 |