* 콜백 함수 : 다른 코드의 인자로 넘겨주는 함수 => 이것을 넘겨받는 주체(setTimeout, forEach)가 필요 함.
★이 콜백함수를 넘겨받은 주체는 자신들이 필요한 대로 적절한 시점에 함수를 실행하게 됨(action에 대한 제어권이 이 주체한테 있음) -> 제어권을 넘겨줄테니 너가 알고 있는 그 로직으로 처리해줘(ex)setTimeout : 1초뒤에 console을 출력하게 처리해줘)
☆정리☆ : 콜백함수는 다른 코드에게 인자를 넘겨줌으로써 제어권도 함께 위임한 함수 -> 위임받은 코드(콜백함수를 받은 주체)는 자체적으로 내부 로직에 의해 콜백 함수를 적절한 시점에 실행함!
//콜백 함수 ex1)
setTimeout(function() {
console.log('hello')
}, 1000) // millisecond 1000 => 1초, 1초후에 hello를 출력하는 setTimeout
=> 얘는 원래 라이브러리에 있는 함수
//콜백 함수 ex2)
const numbers = [1, 2, 3, 4, 5];
numbers.forEach(function(number) {
console.log(number)
}); //반복문
=>forEach의 파라미터에 함수를 넣는 것, 얘도 원래 라이브러리에 있음.
- 콜백함수를 넘겨받는 코드(주체)의 특징 : 1. 호출 시점에 대한 제어권을 갖는다.
* ex) setInterval(); - 반복해서 매개변수로 받은 콜백함수의 로직을 수행 해 상당히 유용(콜백함수를 인자로)
let count = 0;
let cbFunc = function() {
console.log(count);
if(++count>4) clearInterval(timer); // 멈춰주는게 필요(timer) -> 이게 claerInterval count가 5가 되어 if에 만족하면 clearInterval이 실행 되며 로직을 빠져나옴
};
let timer = setInterval(cbFunc, 300); //0.3초 간격으로 0 1 2 3 4 까지 찍어줌, 0.3초에 대한 제어권을 setInterval이 가지고 있다.
cbFunc(); // 얘는 호출 주체와 제어권은 모두 사용자(즉시 실행해줘) => 0 한 번만 출력함.
=>파라미터에 콜백함수를 넣어줬을 때(setInterval에게로 콜백함수를 넘겨 준다면)는 0.3초 간격(호출 시점)에 대한 제어권은 콜백함수를 받은 setInterval이 가지고 있다.(setInterval이 호출 주체와 제어권 모두 가짐)
- 특징 2. 인자에 대한 제어권을 갖는다.( 아래에서는 map()만이 인자에 대한 제어권을 가진다!!)
* ex) map(); : 배열에 대한 API메소드로 배열 하나하나를 순회해 기존의 배열을 변경하지 않고 새로운 배열을 생성함.
let newArr = [10, 20, 30].map(function(currentValue, index) { //첫인자는 배열의 첫값부터 하나하나 / 두번째 인자는 인덱스 0부터 1씩 증가 -> map(), forEach(), filter()전부 동일함.
console.log(currentValue, index);
return currentValue +5 ; //map은 무조건 return을 해서 크기만큼 할당 해야한다!
})
//-------------------------------------------------------------
let newArr1 = [10, 20, 30].map(function(index, currentValue) { //내가 사용하고 싶은 대로 사용x, 규칙대로 사용
return currentValue +5 ;
})
=>newArr은 [15, 25, 35]가 되지만 newArr1은 [5, 6, 7]이 됨 -> map의 첫 번째 인자는 배열의 첫값부터 하나하나씩 넣는 거고 두 번째 인자는 인덱스 0부터 1씩 증가하는 것이므로!!(원하는 것을 얻고자 하면 명세서에 작성되어 있는 대로 작성)
- 특징 3. this에 대한 부분 : 제어권을 넘겨받을 코드에서 콜백 함수에 별도의 this가 될 대상을 지정하면 그 대상을 참조 한다.(예외사항을 만든 것), this를 명시적 바인딩 시 this가 바인딩 된다.
Array.prototype.map123 = function(callback, thisArg) {
// map함수에서 return할 결과 배열
let mappedArr = [];
for(var i=0; i<this.length; i++) {
let mappedValue = callback.call(thisArg || globalThis, this[i]); //명시적 this binding (call을 이용)
mappedArr[i] = mappedValue;
}
return mappedArr;
}; //마치 맵함수처럼 호출할 수 있게끔 작성한 것!
let newArr = [1, 2, 3].map123(function(number) {
return number * 2
})
ex) 두가지 속성의 객체 일때(배열과 함수(매개변수 두 개))
var obj = {
vals: [1, 2, 3],
logValues: function(v, i) {
if(this === global) {
console.log('this가 글로벌')
} else {
console.log(this, v, i);
}
}
}; //global 출력!
obj.logValues(1, 2); //메소드로의 호출은 문제가 없음 // { vals: [ 1, 2, 3 ], logValues: [Function: logValues] } 1 2( 매개변수로 넣는 것은 함수가 아닌 함수를 실행한 결과를 넣는 것, 즉 콜백함수를 넣는 것은 그 함수 자체를 넣는것)
[4, 5, 6].forEach(obj.logValues); // 메소드형태이지만 호출부가 없음 -> 함수자체를 넣은 것이다
= [4, 5, 6].forEach(function(v, i) { //이것과 동일
if(this === global) {
console.log('this가 글로벌') //결국 this에는 global이 들어가므로 이것이 출력 (this가 글로벌 3번 반복 출력)
} else {
console.log(this, v, i);
}
})
=> 결론은 메소드로서의 호출이라는 것은 호출부로 매개변수를 넣어 호출해야 하는것, 모양은 메소드 이지만 결국 함수 자체를 전달한 것이므로 이러한 함수는 전역 객체를 참조한다.
* 콜백함수 내부의 this에 다른 값 바인딩하기 : self(전통적) / 명시적바인딩(call) 이용
var obj1 = {
name: 'obj1',
func: function() {
var self = this; //이 부분!
return function () {
console.log(self.name);
};
}
};
var callback = obj1.func(); //단순히 함수만 전달한 것이기 때문에, obj1 객체와는 상관 x
setTimeout(callback, 1000);
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); //즉시 실행하게끔 call로 this바인딩
setTimeout(callback3, 2000);
//bind사용 - 함수를 만들어놓을 때 유용 , 가장 좋은 방법임!!!!!!
var obj1 = {
name: 'obj1',
func: function () {
console.log(this.name);
}
};
let boundObj1 = obj1.func.bind(obj1); //bind이용해서 미리 this 지정
setTimeout(boundObj1, 1000); //obj1이 객체로 묶이면서 여기의 this는 obj1
let obj2 = { name : 'obj2'};
setTimeout(obj1.func.bind(obj2), 1500); //어떤 this든 원하는 대로 bind 할 수 있다
- 콜백 지옥 : 콜백함수는 익명 함수이다 보니, 매개 변수 자리에 콜백함수가 들어가는 과정이 반복되면서 들여쓰기 깊이가 Hell 수준이 되는 것, 주로 이벤트 처리 및 서버 통신과 같은 비동기적 로직 작업 수행 시, 발생함(가독성이 지옥인 문제 & 유지보수도 어렵다!)
- why 콜백함수 중첩? 비동기 작업에 동기적인 표현이 필요하므로!!(일의 순서를 보장 받기 위해서)
- 동기(synchronous) : 현재 실행중 코드가 끝나야 다음 코드를 실행하는 것, cpu 계산에 의해 즉시 처리가 가능한 대부분의 코드는 동기적 코드임& 계산이 복잡해 오래걸리는 코드 역시 동기적 코드(동기는 일의 순서가 중요!!)
- 비동기(asynchronous) : 실행중 코드와 무관하게 즉시 다음 코드로 넘어감(ex) setTimeout, addEventListener 등), 별도의 요청! 실행 대기! 보류! 등과 관련된 코드는 모두 비동기적 코드임(복잡도가 올라갈수록 비동기적 코드의 비중이 늘어남 - 콜백지옥이 늘어남)
- 비동기 특징 : 순서를 보장하지 않음! 제어권을 넘겨준 코드로부터 결과가 언제 오는지 나에게는 모름 -> setTimeout은 시간을 정해주지만 이거의 입장 말고 실제 서버/클라이언트 통신에서는 순서 예상이 불가능하다!
- 비동기 오류 예) 네이버 날씨 정보 받아 daum지도 업데이트 하고 싶다 : 네이버 날씨 정보받는게 1순위(응답 3초 소요) -> 이를 통해 daum지도 받기(응답 8초 소요) // 요렇게 진행하다가 갑자기 네이버의 서버 과부하로 작업 응답 시간이 10초가 걸려버림 -> 네이버 날씨를 가져오기도 전에 daum지도를 가져와 버려 문제 발생!(내부에서 대응 x) / 비동기코드 예)
//비동기적 코드 이해
setTimeout(function(){
console.log('여기는 과연 언제 실행될까?') //이게 뒤늦게 실행(1초 뒤에 실행)
}, 1000);
console.log('여기좀 봐주세요') //둘다 주문을 받고 위의 콜백함수는 1초뒤에 실행되므로 얘 먼저 실행함.
=> 순서는 분명 위에 부터 출력이지만 비동기적 코드(setTimeout)로 1초 뒤에 실행되어서 아래 문장 먼저 실행
* 콜백 지옥 예)
//콜백 지옥
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, '에스프레소');
=> but! 이것도 문제가 있음 - 익명함수 한 번만 사용하는 데에 이름을 붙이면 인적 리소스 낭비가 생김!!
- 콜백 지옥 해결 방법 : (ES6)Promise, Generator, (ES7) async / await
- 1. Promise : 비동기적 작업을 동기적으로 표현하는 과정으로 비동기처리가 끝나면(작업) 알려달라는 약속을 하는 것!(비동기 작업이 완료되면 resolve, reject를 호출 함) - new 연산자로 호출한 Promise의 인자로 넘어가는 콜백은 바로 실행한다! & 내부에 resolve(성공) / reject(실패) 함수 호출 구문이 있으면 둘 중 하나가 실행되기 전까지 then / catch로 넘어가지 않는다! / (위의 콜백지옥) 해결 예제 코드)
new Promise(function(resolve){ //매개변수에 resolve, reject가 들어감(실패할 수가 없어서 resolve만 넣음)
setTimeout(function() {
let name = '에스프레소';
console.log(name);
resolve(name); //resolve가 실행함으로서 다음 then으로 넘어갈 수 있게 해줌 -> resolve()해주면 됨! 여기서 인자를 넘겨줄 수 있다!(우리는 에스프레소를 넘겨줘야 하므로 name을 인자로 넘겨줌) -> then으로 받을 수 있다!!
}, 500)
})
.then(function(prevName) { //여기서의 인자는 이전에 resolve에서 넘겨준 인자임 ('에스프레소')
//여기서도 새롭게 Promise
return new Promise(function(resolve){ //매개변수에 resolve, reject가 들어감
setTimeout(function() {
let name = prevName + ', 아메리카노';
console.log(name);
resolve(name);
}, 500);
})
})
.then(function(prevName){
return new Promise(function(resolve){ //매개변수에 resolve, reject가 들어감
setTimeout(function() {
let name = prevName + ', 카페모카';
console.log(name);
resolve(name);
}, 500);
})
})
.then(function(prevName){
return new Promise(function(resolve){ //매개변수에 resolve, reject가 들어감
setTimeout(function() {
let name = prevName + ', 카페라떼';
console.log(name);
resolve(name);
}, 500);
})
})
=> 비동기 작업 실행 후 내부 resolve실행 그리고 .then실행하는 것으로 비동기 작업을 동기적으로 표현함
- refactoring : 비효율적인 코드를 효율적인 코드로 변경 하는 것!(반복되는 로직을 함수화 시키는 것 등), 바로 위의 작업을 refactoring 해보려 한다 -> 맨 처음 호출의 new Promise부분만 있으면 되고 그 다음부터 function(prevName) 부분을 넣으면 해결!(새로운 커피 이름을 넣는 작업 - 이 커피 이름을 변수로 받아 내부에 로직을 생성하는 것)
let addCoffee = (name) => { //function (name) { ~ }
return function(prevName){
return new Promise(function(resolve){ //매개변수에 resolve, reject가 들어감
setTimeout(function() {
let newName = prevName ? `${prevName}, ${name}`: name; //preName이 있다하면 앞에꺼 실행 없으면 name만 대입(간단한 로직은 3항 연산자)
console.log(newName);
resolve(newName);
}, 500);
});
};
};
addCoffee('에스프레소')() //얘는 실행하고 싶은 부분이 new Promise(~)부터 이므로 요기부분을 실행하고 싶다 => 그냥 실행하면 맨처음 괄호부분이 실행됨 하지만 우리가 원하는 부분은 return 안의 내용 부분임 => 얘를 실행하기 위해서 뒤에 괄호를 열고닫는거 일반 함수호출처럼 func2()
.then(addCoffee('아메리카노'))
.then(addCoffee('카페모카'))
.then(addCoffee('카페라떼'));
=>첫 호출 시 addCoffee의 내부 return 부분이 아닌 그안의 new 부터 실행하고 싶기에 ()를 넣어 일반 함수처럼 호출
- iterator : 반복자로 이 객체는 next메소드를 가지고 있음 - 이 메소드를 통해 자기 자신의 요소들을 순환 가능(하나하나 순환하면서 작업을 수행하는데 용이함)
- 2. Generator : 반복할 수 있는 iterator 객체를 생성함 & yield(키워드로 순서를 제어함!)가 같이 따라오게 된다(비동기작업이 완료 될 때마다 next를 호출 해 순차적으로 수행하게 함) / (콜백지옥커피) 해결 예제)
//1.제너레이터 함수 안에서 쓸 addCoffee 함수 선언
var addCoffee = function (prevName, name) {
setTimeout(function () {
coffeeMaker.next(prevName ? prevName + ', ' + name : name);
}, 500);
};
//2.제너레이터 함수 선언
//yield 키워드로 순서를 제어 함.
var coffeeGenerator = function* () { //*가 붙은 함수가 제너레이터 함수다! 이 함수를 실행하면 iterator객체가 반환이 된다. (next메소드로 계속 순환가능)
var espresso = yield addCoffee('', '에스프레소'); //yield메소드를 만나면 멈춤! - 뒤의 addCoffee가 완료 될때까지 기다린 후에 다음으로 넘어감 (addCoffee에 next가 있기 때문)
console.log(espresso);
var americano = yield addCoffee(espresso, '아메리카노');
console.log(americano); //고럼 요기 americano에 에스프레소, 아메리카노가 담김
var mocha = yield addCoffee(americano, '카페모카'); //따라서 여기 americano는 에스프레소, 아메리카노 이다.
console.log(mocha);
var latte = yield addCoffee(mocha, '카페라떼');
console.log(latte);
};
var coffeeMaker = coffeeGenerator(); //얘는 iterator객체가 된다.
coffeeMaker.next();
- 3. async / await 사용하기(ES7) / (콜백지옥커피) 해결 예제)
// Promise 반환함!
var addCoffee = function (name) {
return new Promise(function (resolve) {
setTimeout(function(){
resolve(name);
}, 500);
});
};
//★★★ 이 coffeMaker라는 함수는 function 키워드 앞에 async를 붙이면 중괄호 내의 스코프 안에 await키워드를 만난 메소드는 해당 메소드가 끝날때까지 무조건 기다려야 한다(이 메소드는 항상 프로미스를 반환해야 함)
var coffeeMaker = async function () { // async () => {
var coffeeList = '';
var _addCoffee = async function (name) {
coffeeList += (coffeeList ? ', ' : '') + await addCoffee(name);
};
//add_Coffee가 Promise를 반환하는 함수인 경우(전제하에), await를 만나면 순간 무조건 뒤의 메소드 실행이 끝날 때 까지 기다린다.
//if) _addCoffee('에스프레소')요 로직이 실행되는데 100초 걸렸다면
await _addCoffee('에스프레소');
//console.log(coffeeList);얘는 100초 뒤에 실행이 된다.
console.log(coffeeList);
await _addCoffee('아메리카노');
console.log(coffeeList);
await _addCoffee('카페모카');
console.log(coffeeList);
await _addCoffee('카페라떼');
console.log(coffeeList);
};
coffeeMaker();
=>function 앞에 async를 붙이면 중괄호 내 스코프 안에 await키워드를 만난 메소드는 무조건 기다려야 함 & await 뒤의 _addCoffee()는 항상 Promise를 반환한다는 조건으로 await 를 앞에 선언함으로 끝날 때까지 기다리라고 설정한 것
숙제) refactoring하기
// 리팩토링전
// function loadJson(url) {
// return fetch(url)
// .then(response => {
// if (response.status == 200) {
// return response.json();
// } else {
// throw new HttpError(response);
// }
// })
// }
async function loadJson(url) {
const response = await fetch(url);
if (response.status == 200) {
return response.json();
} else {
throw new HttpError(response);
}
}
=> response에 await를 붙여 fetch가 완료될 때까지 대기 하게 함 -> fetch가 완료되면 그 응답을 비교 해 성공하면 response.json()(json형태로 응답을 반환) / 실패하면 응답의 상태를 오류로 catch에 던진다.
//리팩토링 전
// function narutoIsNotOtaku() {
// let title = prompt("애니메이션 제목을 입력하세요.", "naruto");
// .then(res => {
// alert(`${res.character}: ${res.quote}.`);
// return res;
// })
// .catch(err => {
// if (err instanceof HttpError && err.response.status == 404) {
// alert("일치하는 애니메이션이 없습니다. 일반인이시면 naruto, onepiece 정도나 입력해주세요!");
// return narutoIsNotOtaku();
// } else {
// throw err;
// }
// });
// }
async function narutoIsNotOtaku() {
let title;
let res;
while(1) {
title = prompt("애니메이션 제목을 입력하세요.", "naruto"); // title을 바깥쪽에 선언한 이유는 while에 계속 선언되며 돌지 않게끔 하기 위해서 밖에 선언함(할당만 되게끔).
try {
res = await loadJson( //정상적으로 url에서의 응답을 받으면 반복문 빠져나옴, loadJson은 비동기 작업이기 때문에 await로 막음
)
break;
} catch (err) {
if (err instanceof HttpError && err.response.status == 404) {
alert("일치하는 애니메이션이 없습니다. 일반인이시면 naruto, onepiece 정도나 입력해주세요!");
} else {
throw err;
}
}
}
alert(`${res.character}: ${res.quote}.`);
return res;
}