오늘은 4단원인 "콜백 함수" 를 포스팅 할 것이다.
중간에 "콜백 함수 내부에서의 this" 에 대해서 깊게 다루니
이전 포스팅인 "this" 에 대해서 필수적으로 알고 있어야 한다.
https://ilikezzi.tistory.com/58
04. 콜백 함수
콜백 함수란?
콜백 함수는 쉽게 말해 함수 안에 함수다.
즉, 다른 함수의 인자로 전달되는 함수이다.
비동기 작업이 끝나면 어떤 작업을 해야 할지를 콜백 함수로 지정할 수 있어서
주로 비동기 작업에서 자주 사용된다.
콜백 함수는 제어권과 관련이 깊다.
예를 들어, 내가 친구에게 "나 이따가 연락할 테니까, 그때 전화받아줘!"라고 부탁했다고 해보자.
여기서 친구는 "전화받는 일"이라는 특정 작업을 담당하게 되고, 그 '제어권'을 가지게 된다.
내가 전화를 걸면 친구는 그 전화를 받아 '약속한 대로' 행동을 하는게 이게 바로 제어권을 넘겨주는 상황이다!
콜백 함수도 비슷하다.
특정 함수나 이벤트가 일어났을 때, "이제 너 차례야, 이 일을 해줘!"라고 다른 함수에게 제어권을 넘기는 거다.
인자
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]
이런식으로 할당 되는 콜백함수 예제가 있다.
map 메서드의 구조를 확인해보면 다음과 같다.
Array.prototype.map(callback[, thisArg])
callback: function(currentValue, index, array)
map 메서드는 첫번째 인자로 콜백함수를 받고, 생략가능한 두번째 인자로 콜백함수 내부에서의 this를 지정할 수 있다.
thisArg를 생략할 경우엔 일반 함수와 마찬가지로 전역객체가 바인딩 된다.
첫 코드에서 콘솔값이 할당되는건 쉽게 이해를 했을것이다.
그럼 한번 콜백함수 내부의 첫번째 인자를 index, 두번째 인자를 currentValue가 오게 변경해보자.
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]
새로만든 newArr2를 보면
사람은 'index' , 'currentValue' 단어로 접근하기 때문에 순서를 바꿔도 의미가 바뀌지 않으니깐
문제 없을 것이라 생각하기 쉽지만, 컴퓨터는 그저 첫번째 두번째 순서에 의해서만 구분한다.
따라서 마지막에 콘솔값을 보면 각각 [15, 25, 35] // [5, 6, 7] 로 서로 다르게 나온다.
newArr2에 currentValue라고 명명한 인자의 위치가 두번째라서 컴퓨터가 여기에 인덱스 값을 부여하게 된 것이다.
콜백함수를 호출하는 주제가 사용자가 아닌 map 메서드이므로
원하는 배열을 얻으려면 map 메서드에 정의된 규칙에 따라 함수를 작성해야 한다.
map메서드가 콜백함수를 호출할 때 인자에 어떤값들을 어떤순서로 넘길 것인지가
전적으로 map메서드에게 달린것이다.
이처럼 콜백함수의 제어권을 넘겨받은 코드는 콜백함수를 호출할 때
인자에 어떤값들을 어떤순서로 넘길 것인지에 대한 제어권을 갖는다.
this
콜백함수도 함수이기 때문에 기본적으로는 this가 전역객체를 참조하지만,
제어권을 넘겨받을 코드에서 콜백함수에 별도로 this가 될 대상을 지정한 경우엔 그 대상을 참조한다.
setTimeout(function () { console.log(this); }, 300);
[1, 2, 3, 4, 5].forEach(function(x) {
console.log(this);
});
// (1) Window {...}
// (2) Window {...}
위 두 콜백함수 내부에서의 this는 콘솔을 찍어보면 둘 다 전역객체를 가리킨다.
둘다 따로 명시적으로 this를 바인딩하지 않았기 때문에
기본적으로 전역 컨텍스트에서 실행되기 때문에 Window {...} 가 출력된다.
const obj = {
vals: [1, 2, 3],
logValues: function (v, i) {
console.log(this, v, i);
},
};
obj.logValues(1, 2); // { vals: [1, 2, 3], logValues: f} 1 2
[4, 5, 6].forEach(obj.logValues); // Window {...} 4 0
// Window {...} 5 1
// Window {...} 6 2
해당 코드에서 첫번째의 출력을 보면
이름앞에 점이 있으니 메서드로서 호출 한 것이다.
따라서 this는 obj를 가리키고 인자로 넘어온 1, 2 가 출력된다.
두번째의 출력은 다른 방식으로 작동한다.
forEach에서 obj.logValues가 콜백으로 전달되면, 이 함수는 일반 함수로 호출된다.
그래서 this가 전역 객체를 가리키는 거다. (obj X)
원래 obj.logValues는 obj라는 객체 안에 있었지만, forEach 안에서는 그 연결이 끊어지게 된다.
만약 this를 obj로 지정하고 싶다면 별도로 this인자를 지정하거나
call, apply, bind 같은 메서드를 사용해 this를 지정해야 한다.
이전 "this" 포스팅에서 다뤘지만 한번 더 call, apply, bind에 대해서 짚고 넘어가자.
call
Func.call(obj, arg1, arg2, ...);
1. obj는 명시적으로 this를 어떤 객체로 바인딩할지 정해준다.
2. 그 다음으로 함수에 필요한 인자값들을 차례대로 넘겨준다.
apply
Func.apply(obj, [arg1, arg2, ...]);
1. obj는 명시적으로 this를 어떤 객체로 바인딩할지 정해준다.
2. 그 다음으로 함수에 필요한 인자값들을 차례대로 넘겨준다.
(call 과 차이점은 인자를 배열 형태로 넘긴다)
bind
const boundFunc = Func.bind(obj);
1. 새로운 함수를 만들어서 반환한다.
2. 그 함수의 this는 bind로 지정한 객체로 고정되어 있다.
콜백 지옥
콜백 지옥이라는 건, 콜백 함수가 다른 콜백 함수 내부에서 또 호출되고,
그 안에서 또 다른 콜백 함수가 호출되는 등 이런식으로 깊어지는 상황을 말한다.
이렇게 되면 코드가 정말 복잡해져서 읽기도 어렵고, 유지보수도 힘들어진다.
// 콜백 지옥
setTimeout(() => {
console.log('첫 번째 작업');
setTimeout(() => {
console.log('두 번째 작업');
setTimeout(() => {
console.log('세 번째 작업');
}, 1000);
}, 1000);
}, 1000);
콜백 지옥을 보면 들여쓰기가 깊어지는 것처럼 코드가 >)>)>)>) 이런 모양이 돼서, "피라미드 코드"라고도 부르기도 한다.
이게 코드의 가독성을 낮추고, 오류를 찾기 어렵게 만든다.
이 가독성 문제와 어색함을 동시에 해결하는 가장 간단한 방법은
익명의 콜백 함수를 모두 기명함수로 전환하는 것이다.
// 기명 함수
function thirdJob() {
console.log('세 번째 작업');
}
function secondJob() {
console.log('두 번째 작업');
setTimeout(thirdJob, 1000);
}
function firstJob() {
console.log('첫 번째 작업');
setTimeout(secondJob, 1000);
}
setTimeout(firstJob, 1000);
기명 함수는 재사용하기 좋고, 디버깅할 때도 편하다.
이름이 있으니까 함수의 목적이 뭔지 더 쉽게 알 수 있다.
하지만 이렇게 일회성 함수를 전부 변수에 할당하는 것이 불만일수도 있다.
그래서 마침 ES6에서 Promise 가 도입되었다.
// Promise
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
wait(1000).then(() => {
console.log('첫 번째 작업');
return wait(1000);
}).then(() => {
console.log('두 번째 작업');
return wait(1000);
}).then(() => {
console.log('세 번째 작업');
});
Promise에 대해서 간략히 설명하자면,
Promise는 크게 resolve와 reject 두 가지 상태를 가지고 있다.
resolve는 작업이 성공적으로 끝났을 때 호출되고,
reject는 에러가 발생했을 때 호출된다.
이 코드에서는 wait라는 함수를 선언했는데, 이 함수는 일정 시간(ms)이 지난 후에 resolve를 호출한다.
그래서 .then()을 통해 이후에 실행할 작업을 쭉 이어나갈 수 있다.
첫 번째 wait(1000)은 1초 후에 resolve되고, .then() 안의 첫 번째 작업이 실행된다.
그 다음에 또 wait(1000)을 호출하고, 두 번째 작업을 실행한다.
마지막으로 세 번째 작업까지 이어나간다.
이렇게 Promise를 사용하면, 각 단계별로 어떤 작업을 해야 할지 명확하게 표현할 수 있어서 코드가 훨씬 깔끔하고 이해하기 쉬워진다.
그 후 ES2017에서는 가독성이 뛰어나면서 작성법도 간단한 새로운 기능이 추가되었다.
바로 async / await 이다.
// async / await
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
async function run() {
await wait(1000);
console.log('첫 번째 작업');
await wait(1000);
console.log('두 번째 작업');
await wait(1000);
console.log('세 번째 작업');
}
run();
비동기 작업을 수행하고자 하는 함수앞에 async를 표기하고,
함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await를 표기하는 것만으로
뒤의 내용을 Promise로 자동 전환하고, 해당 내용이 resolve된 이후에야 다음으로 진행한다.
즉 Promise의 then과 흡사한 기능을 하게된다.
오늘 이렇게 콜백 함수에 대해서 자세히 알아보았다.
다음 챕터는 "클로저" 이다.
'Programming Language > Javascript' 카테고리의 다른 글
[Javascript] 비동기의 핵심, 이벤트 루프(Event loop) 파헤치기 (0) | 2023.12.25 |
---|---|
[Javascript] 클로저_코어 자바스크립트 (0) | 2023.09.29 |
[Javascript] this_코어 자바스크립트 (0) | 2023.08.22 |
[Javascript] console.dir()로 객체의 모든 depth 구조 출력 (0) | 2023.08.15 |
[Javascript] new Set() 배열에서 쉽게 중복값 제거하기 (0) | 2023.08.14 |