일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 비트코인
- Non-Blocking
- 콜백함수this
- github
- 카카오페이지클론
- ERC721
- web3.js
- 정보처리기사실기요약
- it5분잡학사전
- npm
- blockchain
- node.js
- 노마드코더
- 정보처리기사실기
- it5
- 블록체인
- ERC-721
- 2021정보처리기사실기
- git
- 노개북
- 이더리움
- 클레이튼
- solidity
- React
- Klaytn
- 솔리디티
- 정보처리기사
- ETH
- 카카오홈페이지클론
- 페이지클론
- Today
- Total
Be lazy, Be crazy
4. 콜백함수 본문
1-1 콜백함수란?
콜백함수
다른 함수나 메서드에게 인자를 넘겨줌으로써 그 제어권도 함께 위임한 함수
콜백함수를 위임받은 함수나 메서드는 자체적인 내부 로직에 의해 이 콜백 함수를 적절한 시점에 실행함
1-2 제어권
호출 시점
예시 : setInterval(func, delay[, param1, param2, ...])
let count = 0;
const cbFunc = function() {
console.log(count);
if (++count > 4) clearInterval(timer);
}
const timer = setInterval(cbFunc, 300);
- timer 변수에는 setInterval의 ID값이 담긴다.
- setInterval의 첫 번째 인자인 cbFunc 함수(콜백함수)는 두 번째 인자 300ms(0.3초)마다 자동으로 실행된다.
- 콜백함수 내부에선 count를 출력하고, count 1만큼 증가시킨 다음 4보다 크면 실행을 종료하라고 한다.
=> 결과: 콘솔창에 0.3초에 한 번씩 숫자가 0부터 1씩 증가하며 출력되다가 4가 출력된 이후 종료됨
* 코드 실행 방식과 제어권
code | 호출 주체 | 제어권 |
cbFunc(); | 사용자 | 사용자 |
setInterval(cbFunc, 300); | setInterval | setInterval |
- setInterval이라고 하는 '다른 코드'에 첫 번째 인자로서 cbFunc함수를 넘겨주자 제어권을 넘겨받은 setInterval이 스스로 판단에 따라 적절한 시점에(0.3초마다) 이 익명 함수를 실행했음.
=> 콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수 호출 시점에 대한 제어권을 가짐
인자
예시: map(callback[, thisArg]), -> callback: function(currentValue, index, array)
(* 제이쿼리라면 map의 콜백함수 인자의 순서는 function(index, currenValue) 로 바뀐다.)
=> 콜백 함수의 제어권을 넘겨받은 코드(여기서는 map)는 콜백 함수를 호출할 때 인자에 어떤 값들을 어떤 순서로 넘길 것인지에 대한 제어권을 가짐
=> 콜백 함수를 호출할 때 인자로 넘겨줄 값들 및 그 순서가 정해져 있어서 순서를 따르지 않고 코드를 작성하면 엉뚱한 결과를 얻게 됨
const 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]
- 배열 [10, 20, 30]의 각 요소를 처음부터 하나씩 꺼내어 콜백 함수를 실행한다
- 각 값을 출력한 다음 currentValue에 5를 더한다
- [15, 25, 35]라는 새로운 배열이 만들어져서 변수 newArr에 담긴다.
this
콜백 함수의 this
- 콜백 함수도 함수이기 때문에 기본적으로는 this가 전역객체를 참조하지만,
제어권을 넘겨받을 코드에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하게 됨
별도의 this를 지정하는 방식
- 동작 원리 이해를 위해 임의로 map 메서드를 작성해봄
Array.prototype.map = function (callback, thisArg) {
const mappedArr = [];
for (let i = 0; i < this.length; i++) {
const mappedValue = callback.call(thisArg || window, this[i], i, this);
mappedArr[i] = mappedValue;
}
return mappedArr;
};
- this에는 thisArg 값이 있으면 그 값을, 없으면 전역객체를 지정하고, 첫 번째 인자에는 메서드의 this가 배열을 가리킬 것이므로 배열의 i번째 요소(this[i])를, 두 번째 인자는 i 값을, 세 번째 인자에는 배열 자체를 지정해 호출한다.
- 그 결과 변수 mappedValue에 담겨 mappedArr의 i번째 인자에 할당
=> this에 다른 값이 담기는 이유: 제어권을 넘겨받을 코드에서 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.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를 받는 경우'에 해당하지만 별도 인자로 this를 넘겨주지 않았기 때문에 전역객체를 가리킴
- (3) addEventListener는 내부에서 콜백 함수 호출할 때 call 메서드의 첫 번째 인자에 addEventListener 메서드의 this를 그대로 넘기도록 정의돼 있기 때문에 콜백 함수 내부에서의 this는 addEventListener 호출 주체인 HTML 엘리먼트를 가리킴
1-3 콜백 함수는 함수다
- 콜백 함수로 어떤 객체의 메서드를 전달하더라도 그 메서드는 메서드가 아닌 함수로서 호출됨
메서드를 콜백 함수로 전달
const obj = {
vals: [1, 2, 3]
logValues: function(v, i) {
console.log(this, v, i);
}
};
obj.logValue(1, 2); // (1) { vals: [1, 2, 3], logValues: f } 1 2
[4, 5, 6].forEach(obj.logValues); // (2) Window { ... } 4 0
// Window { ... } 5 1
// Window { ... } 6 2
- (1) this는 obj를 가리키고 인자로 넘어온 1, 2가 출력
- (2) obj.logValues가 가리키는 함수만 전달.
- (2) 이 함수는 메서드로서 호출할 때가 아닌 한 obj와의 직접적인 연관이 없어짐 -> forEach에 의해 콜백이 함수로서 호출되고, 별도로 this를 지정하는 인자를 지정하지 않았으므로 함수 내부에서의 this는 전역 객체를 바라보게 됨
=> 어떤 함수의 인자에 객체의 메서드를 전달하더라도 이는 결국 메서드가 아닌 함수일 뿐
=> 객체의 메서드를 콜백 함수로 전달하면 해당 객체를 this로 바라볼 수 없다
1-4 콜백 함수 내부의 this에 다른 값 바인딩하기
- 객체의 메서드를 콜백 함수로 전달하면 해당 객체를 this로 바라볼 수 없지만,
그럼에도 콜백 함수 내부에서 this가 객체를 바라보게 하고 싶다면??
- 별도의 인자로 this를 받는 함수의 경우엔 여기에 원하는 값을 넘겨주면 되지만,
그렇지 않은 경우에는 this의 제어권도 넘겨주게 되므로 사용자가 임의로 값을 바꿀 수 없음
방법1) 전통적 방식
this를 다른 변수에 담아 콜백 함수로 활용할 함수에서는 this 대신 그 변수를 사용하게 하고, 이를 클로저로 만드는 방식
const obj1 = {
name: 'obj1',
func: function() {
const self = this;
return function() {
console.log(self.name);
};
}
};
const callback = obj1.func();
setTimeout(callback, 1000)
- 1. obj1.func 메서드 내부에서 self 변수에 this를 담고 익명 함수 선언과 동시에 반환
- 2. obj1.func 호출 -> 앞서 선언한 내부 함수가 반환되어 callback 변수에 담김
- 3. callback을 setTimeout의 인자로 전달하면 1초 뒤 callback 실행되면서 'obj1'을 출력
=> 실제로 this를 사용하지도 않을뿐더라 번거로움
방법2) 콜백 함수 내부에서 this를 사용하지 않은 경우
const obj1 = {
name: 'obj1',
func: function() {
console.log(obj1.name);
}
};
setTimeout(obj1.func, 1000);
=> 직관적이지만, 작성한 함수를 this를 이용해 다양한 상황에서 재활용할 수 없게 되어버림
=> 처음부터 바라볼 객체를 명시적으로 지정했기 때문에 어떤 방법으로도 다른 객체를 바라보게 할 수 없음
=> 다양한 객체에 재활용할 필요 없다면 이렇게 해도 아무런 문제가 없음
전통적 방식에서, func 함수 재활용
// ...
const obj2 = {
name: 'obj2',
func: obj1.func
};
const callback2 = obj2.func();
setTimeout(callback2, 1500);
const obj3 = { name: 'obj3' };
const callback3 = obj1.func.call(obj3);
setTimeout(callback3, 2000);
- callback2: obj1의 func을 복사한 obj2의 func를 실행한 결과를 담아 콜백으로 사용
- callback3: obj1의 func을 실행하면서 this를 obj3가 되도록 지정하여 이를 콜백으로 사용
-> 실행하면, 1.5초 후 'obj2', 2초 후 'obj3' 출력
=> 전통적 방식은 this를 우회적으로나마 활용함으로써 다양한 상황에서 원하는 객체를 바라보는 콜백 함수를 만들 수 있음
전통적 방식의 아쉬움을 보완한 방법 - ES5 bind 메서드 활용
const obj1 = {
name: 'obj1',
func: function() {
console.log(this.name);
}
};
setTimeout(obj1.func.bind(obj1), 1000);
const obj2 = { name: 'obj2' };
setTimeout(obj1.func.bind(obj2), 1500);
=> 사용자가 임의로 콜백 함수의 this 바꾸고 싶을 경우 bind 메서드를 활용하자
1-5 콜백 지옥과 비동기 제어
콜백 지옥
콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드 들여쓰기 수준이 깊어지는 현상
비동기 제어를 위해 콜백 함수를 사용하다 보면 콜백 지옥에 빠지기 쉬움
-> ECMAscript에는 Promise, Generator, async/await 등 콜백 지옥에서 벗어날 수 있는 훌륭한 방법 등장!
동기와 비동기
동기: 현재 실행 중 코드가 완료된 후에 다음 코드 실행 (즉시 처리 가능한 대부분의 코드, 시간 많이 걸려도 동기적 코드)
비동기: 현재 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어감 (setTimeout, addEventListener, XMKHttpRequest 등. 별도의 요청, 실행 대기, 보류 등과 관련된 코드)
콜백 지옥 예시
setTimeout(function (name) {
const coffeeList = name;
console.log(coffeeList);
setTimeout(function (name) {
coffeeList += ', ' + name;
console.log(coffeeList);
setTimeout(function (name) {
coffeeList += ', ' + name;
conosle.log(coffeeList);
setTimeout(function (name) {
coffeeList += ', ' + name;
console.log(coffeeList);
}, 500, '카페라떼');
}, 500, '카페모카');
}, 500, '아메리카노');
}, 500, '에스프레소');
콜백 지옥 해결 - 기명함수로 변환
const coffeeList = '';
const addEspresso = function (name) {
coffeeList = name;
console.log(coffeeList);
setTimeout(addAmericano, 500, '아메리카노');
};
const addAmericano = function (name) {
coffeeList += ', ' + name;
console.log(coffeeList);
setTimeout(addMocha, 500, '카페모카');
};
const addMocha = function (name) {
coffeeList += ', ' + name;
console.log(coffeeList);
setTimeout(addLatte, 500, '카페라떼');
};
const addLatte = function (name) {
coffeeList += ', ' + name;
console.log(coffeeList);
};
setTimeout(addEspress, 500, '에스프레소');
-> 일회성 함수를 전부 변수에 할당? 코드명 일일이 따라다녀야 해서 헷갈릴 수 있음
비동기 작업의 동기적 표현 - Promise
const addCoffee = function (name) {
return function (prevName) {
return new Promise(function (resolve) {
setTimeout(function () {
const newName = precName ? (prevName + ', ' + name) : name;
console.log(newName);
resolve(newName);
}, 500);
});
};
};
addCoffee('에스프레소')()
.then(addCoffee('아메리카노'));
.then(addCoffee('카페모카'));
.then(addCoffee('카페라떼'));
- ES6 Promise를 이용
- new 연산자와 함께 호출한 Promise의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행되지만 그 내부에 resolve 또는 reject 함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지는 then 또는 catch로 넘어가지 않음
=> 비동기 작업이 완료될 때 비로소 resolve/reject를 호출하는 방법으로 비동기 작업의 동기적 표현이 가능
비동기 작업의 동기적 표현 - Generator
const addCoffee = function (prevName, name) {
setTimeout(function () {
coffeeMaker.next(prevName ? prevName + ', ' + name : name);
}, 500);
};
const coffeeGenerator = function* () {
const espresso = yield addCoffee('', '에스프레소');
console.log(espresso);
const americano = yield addCoffee(espresso, '아메리카노');
console.log(americano);
const mocha = yield addCoffee(americano, '카페모카');
console.log(mocha);
const latte = yield addCoffee(mocha, '카페라떼');
console.log(latte);
};
const coffeeMaker = coffeeGenerator();
coffeeMaker.next();
- ES6 Generator를 이용 (*이 붙은 함수가 Generator함수)
- Generator 함수를 실행하면 Iterator가 반환
- Iterator로 next 메서드를 호출하면 Generator 함수 내부에서 가장 먼저 등장하는 yield에서 함수 실행을 멈춤
- 이후 다시 next 메서드 호출하면 앞서 멈췄던 부분에서 시작해서 그 다음 등장하는 yield에서 함수 실행을 멈춤 ...
=> 비동기 작업이 완료되는 시점마다 next 호출해주면 Generator 함수 내부 소스가 위에서부터 아래로 순차적으로 진행
비동기 작업의 동기적 표현 - Promise + Async/await
const addCoffee = function (name) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(name);
}, 500);
});
};
const coffeeMaker= async function() {
const coffeeList = '';
const _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();
- ES2017 async/await
- 비동기 작업을 수행하고자 하는 함수 앞에 async 표기하고 함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await를 표기하는 것만으로 뒤의 내용을 Promise로 자동 전환하고, 해당 내용이 resolve 된 이후에야 다음으로 진행(Promise의 then과 흡사한 효과)
멋쟁이사자처럼 블체 코스를 수강하느라 공부가 미뤄졌다
앞으로 다시 시간내서 꾸준히 공부해서 올려야지!!
'책 > 코어 자바스크립트' 카테고리의 다른 글
3-2. this - 명시적으로 this를 바인딩하는 방법 (1) | 2022.01.08 |
---|---|
3-1. this - 상황에 따라 달라지는 this (2) | 2022.01.03 |
2. 실행 컨텍스트 (0) | 2021.12.29 |
1. 데이터 타입 (0) | 2021.12.28 |