일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 탐욕법
- Deep Dive
- winston
- Interceptor
- html
- css
- 알고리즘
- Kubernetes
- node.js
- 자료구조
- LifeCycle
- 인접리스트
- MySQL
- javascript
- nestjs
- dfs
- TIL
- 인접행렬
- puppeteer
- JWT
- 코딩테스트
- OOP
- 프로그래머스
- typescript
- Linux
- bean
- Spring
- java
- REST API
- GraphQL
- Today
- Total
처음부터 차근차근
[코어 자바스크립트] Callback 함수 & 동기/비동기 처리 본문
Callback(콜백함수)이란??
프로그래밍에서 Callback 또는 Callback function이란 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 말한다.
콜백을 넘겨받은 코드는 이 콜백을 필요에 따라 즉시 실행할 수도 있고, 나중에 실행할 수도 있다.
간단한 예시로, setTimeout, 혹은 forEach 메소드를 통해 알아보자.
// setTimeout
setTimeout(function() {
console.log("Hello, world!");
}, 1000);
// forEach
const numbers = [1, 2, 3, 4, 5];
numbers.forEach(function(number) {
console.log(number);
});
여기서 봤을 때 Callback은 Call(부르다) + back(되돌아오다) = 되돌아와서 호출해줘!
→ 다시 말하면, 제어권을 넘겨줄테니 너가 알고 있는 그 로직으로 처리해줘!
이 의미가 된다.
즉, 콜백 함수는 다른 코드(함수 또는 메서드)에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수. 콜백 함수를 위임받은 코드는 자체적으로 내부 로직에 의해 이 콜백 함수를 적절한 시점에 실행
제어권
1. 호출 시점
콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수 호출 시점에 대한 제어권을 가진다.
Ex) 콜백 함수의 제어권을 넘겨받은 코드(=setInterval)가 언제 콜백함수를 호출할지에 대한 제어권을 가지게 된다.
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)
2번째 예시를 표로 정리해보자.
code | 호출 주체 | 제어권 |
cbFunc(); | 사용자 | 사용자 |
setInterval(cbFunc,300); | setInterval | setInterval |
2. 인자
콜백 함수를 넘겨받는 코드에게 인자(의 순서)까지도 제어권이 있다.
예시로 Map 함수를 확인하자.
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 ]
그러나 여기 담긴 Cur과 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 ]
변수의 순서를 바꾼다고, 자동으로 인식하지 않는다.(컴퓨터는 사람이 아니기 때문)
이처럼 map 메서드를 호출해서 원하는 배열을 얻고자 한다면 정의된 규칙대로 작성해야 한다.
모든 것은 전적으로 map 메서드, 즉 콜백 함수를 넘겨받은 코드에게 그 제어권이 있다.
3. this
콜백 함수도 함수이기 때문에 기본적으로는 this가 전역객체를 참조한다.
But, 제어권을 넘겨받을 코드에서 콜백 함수를 별도로 this가 될 대상을 지정할 경우에는 그 대상을 참조한다.
Ex) map 함수 만들어보기
// Array.prototype.map을 직접 구현해봤어요!
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가 될 대상을 지정할 수 있다.
Callback 함수는 함수다
콜백 함수로 어떤 객체의 메서드를 전달하더라도, 그 메서드는 함수로 호출된다.
어떤 함수의 인자에 객체의 메서드를 전달하더라도, 이는 결국 메서드가 아닌 함수일 뿐이다.
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에 다른 값 바인딩하기
- 전통적 방식(self를 사용한 방법)
var obj1 = {
name: 'obj1',
func: function() {
var self = this; //이 부분!
return function () {
console.log(self.name);
};
}
};
// 단순히 함수만 전달한 것이기 때문에, obj1 객체와는 상관이 없어요.
// 메서드가 아닌 함수로서 호출한 것과 동일하죠.
var callback = obj1.func();
setTimeout(callback, 1000);
→ 실제로는 this를 사용하는게 아니고, 코드가 번거로운 단점
- this를 아예 사용하지 않는 방법
var obj1 = {
name: 'obj1',
func: function () {
console.log(obj1.name);
}
};
setTimeout(obj1.func, 1000);
→ 결과만을 위한 코딩이 되어버림
첫번째 예시를 재활용하는 방법
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);
- 가장 좋은 방법 : 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);
Callback 지옥과 비동기 제어
콜백지옥이란
- 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 헬 수준인 경우
- 주로 이벤트 처리 및 서버 통신과 같은 비동기적 작업을 수행할 때 발생
- 가독성이 hell
동기 vs 비동기
- 동기 : synchronous
- 현재 실행중인 코드가 끝나야 다음 코드를 실행하는 방식
- CPU의 계산에 의해 즉시 처리가 가능한 대부분의 코드는 동기적 코드
- 계산이 복잡해서 CPU가 계산하는 데에 오래 걸리는 코드 역시도 동기적 코드
- 비동기 : asynchronous → async
- 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어가는 방식
- setTimeout, addEventListner 등
- 별도의 요청, 실행 대기, 보류 등과 관련된 코드는 모두 비동기적 코드
※ 웹의 복잡도가 올라갈 수록 비동기적 코드의 비중 증가
콜백지옥의 예시와 해결방안
아래 예시는 콜백 지옥의 예시를 보여준다.
값 전달 순서 : 아래 → 위
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,
"에스프레소"
);
1) 기명 함수로 변환
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, '에스프레소');
가독성이 좋지만, 이름을 다 붙여서 쓰는 단점 존재
→ 자바스크립트에서 비동기적인 작업을 동기적으로 처리해주는 장치가 많다.
※ Promise, Generator, async/await
비동기 작업의 동기적 표현이 필요합니다.
1) 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);
});
});
new Promise 안에 return 대신 resolve(실패했을 경우 reject)로 넣고, 다음(then), 오류(catch)를 작성하며, 내부 resolve가 실행되기 전까지 다음으로 넘어가지 않는다.
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('카페라떼'));
2) 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* () {
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();
3) Promise + 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();
출처 : 코어자바스크립트
'Language > JavaScript' 카테고리의 다른 글
[코어 자바스크립트] Class (0) | 2023.06.12 |
---|---|
Javascript DOM (0) | 2023.06.12 |
[코어 자바스크립트] This (0) | 2023.06.10 |
[코어 자바스크립트] 실행컨텍스트(Scope, Var, Object, Hoisting) (0) | 2023.06.05 |
[코어 자바스크립트] JavaScript 데이터 타입(심화) (0) | 2023.05.28 |