같은 동작을 하는 함수인데 세부 내용만 조금씩 다른 경우가 있다. 예를 들어 똑같은 조건의 for문이지만, 하나는 전체 인수를 출력하고 다른 하나는 홀수인 인수만 출력할 때처럼 말이다.
// 전체 인수 출력
function repeat(n) {
for(var i=0; i<n; i++) console.log(i);
}
repeat(5);
콘솔에 출력하는 문은 완전히 정해져있기 때문에console.log(i)
다른 방식으로 재활용 할 여지가 없다. 그래서 이 방식으로는 for문이 똑같은데도 홀수 조건을 위한 if문을 새로 작성해 함수를 두 개씩 만들어야 한다.
공통 부분은 로직으로 정해두고, 상황에 따라 달라지는 로직을 추상화해서 함수 외부에서 내부로 전달한다면 어떨까. 내부 구조에 완전히 의존하지 않아서 훨씬 유연한 활용이 가능해진다.
이렇게 A 함수의 매개변수에 B 함수를 넣어 외부에서 내부로 전달할 때 B 함수를 콜백 함수(callback function)라고 부른다.
cf.
매개 변수를 통해 함수 외부에서 콜백 함수를 전달받은 함수(A 함수)는 고차 함수(Higher-Order Function, HOF)라고 한다.
위의 텍스트를 코드로 작성하면 아래와 같다.
// 공통 로직
function repeat(n, f) {
for (var i=0; i<n; i++) {
f(i);
}
}
// case 1. 전체 인수 순환 출력
var logAll = function (i) {
console.log(i);
};
repeat(5, logAll); // 0 1 2 3 4
// case 2. 홀수 인수만 순환 출력
var logOdds = function (i) {
if (i%2) console.log(i)
};
repeat(5, logOdds); // 1 3
공통 로직에서 케이스마다 달라지는 부분은 함수로 추상화f(i)
하여 전달 받은 인수, 즉 함수logAll / logOdds
에 따라 다른 동작을 진행할 수 있도록 만들었다.
콜백 함수도 함수다. 함수는 호출해야 실행된다. 그런데 콜백 함수를 사용자가 직접 호출하면 특정 시점이 아닌 호출 즉시 실행되고 만다. 고로 호출 제어권을 고차 함수에게 넘겨야 특정 시점에 호출할 수 있다. 따라서, 콜백 함수는 사용자가 호출()
하지 않고 함수 자체만 매개변수로 넘겨야 한다. 고차 함수에 인자로 넘기면서 콜백 함수에 대한 제어권 또한 넘겼다고 보면 된다.
제어권을 넘겼다는 건 어떤 의미일까?
콜백 함수의 호출에 관한 모든 것을 사용자가 아닌 고차 함수가 판단한다는 것이다. 앞서 언급한 대로 1️⃣ 호출 시점을 정하고, 2️⃣ 콜백 함수를 호출할 때 인자에 어떤 값들을 어떤 순서로 넘길지 또한 제어한다. 즉 임의로 인자의 순서를 바꿔도 자바스크립트 엔진은 사용자의 의도를 파악하지 못한 채 원래 정의된 호출 순서를 따른다.
마치 알람 시계를 맞추는 것과 같다. 아침 6시로 시간을 설정한다고 하자. 그럼 알람 시계가 내건 규칙을 따라야 한다. '알람용 침이 사용자가 원하는 시각을 가리키게 설정한 후 스위치를 ON으로 설정한다'. 이 규칙을 따라 시간을 설정하면 알람 시계는 스스로 세팅 조건에 대한 충족 여부를 확인한다. 그리고 조건이 충족되는 순간 사용자의 개입 없이 알람을 울린다.
콜백 함수는 대개 일회성이라서 익명 함수로 작성하는 게 일반적이다. 여기에 화살표 함수로 작성해 좀 더 가독성을 높일 수 있다.
repeat(5, (i) => {
if(i%2) console.log(i);
});
고차 함수 내부에서만 호출된다면 콜백 함수를 위의 모습(익명 함수 리터럴)로 정의해 곧바로 고차 함수에 전달하면 된다.
하지만 콜백 함수를 재사용할 가능성이 있다면, 함수 외부에서 콜백 함수를 정의한 후var log~ = function (i) {}
참조 전달repeat(5, log~);
하는 게 좋다.
콜백 함수는 이밖에도 이벤트 리스너, 타이머 함수(setTimeOut
, setInterval
), Ajax 통신 등 비동기 처리에 활용된다.
콜백 함수는 자신의 제어권을 가진 함수가 정의한 대로 this를 정의한다. 함수는 호출할 때 this를 특정하지 않으면 전역 객체로 선언한다.
그런데 콜백 함수는 객체의 메서드처럼 동작하는 것 같아 보여도 함수 외부의 내용을 내부로 전달하는 매개의 역할을 할 뿐이다. 객체 내에 존재하지 않기에 콜백 함수 내 this는 자신이 속한 것처럼 '보이는' 객체를 가리키지 않는다.
어떻게 하면 함수 내부에서 콜백 함수를 사용하면서도 this를 특정할 수 있을까?
바로 bind 메서드를 이용하는 거다.
Function.prototype.bind(thisArg[, arg1[, arg2]]])
함수를 즉시 호출하지 않고 넘겨 받은 this와 인수들로 새 함수를 반환하는 메서드이다. 인자로 this를 직접 설정할 수 있어서 콜백 함수 또한 특정한 this를 설정할 수 있다.
var obj = {
name: 'obj1',
func: function () {
console.log(this.name);
}
};
// obj1로 thisArg 전달
setTimeout(obj1.func.bind(obj1), 1000);
자바스크립트는 위에서부터 아래로, 코드를 한 줄씩 해석하는 인터프리터를 사용한다. 한 번에 하나의 일만 처리할 수 있는데 웹에서는 시간 소요가 많은 작업들이 선행될 가능성이 높다.
그 작업들을 기다릴 수 없다보니 자바스크립트에서는 비동기 방식을 제공한다. 순차적으로 코드를 실행하지 않고 현재 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어간다. 타이머 함수, 이벤트 처리 등 별도의 요청, 실행 대기, 보류와 관련된 코드를 비동기적 코드라고 볼 수 있다.
이처럼 비동기 코드의 비중이 높아지다 보니 콜백 지옥(callback hell)에 빠질 위험 또한 커졌다. 콜백 함수를 반복하면서 끝없는 들여쓰기가 계속되어 코드의 가독성을 해치고 유지보수를 어렵게 하는 상황을 일컫는다.
익명의 콜백 함수를 모두 기명 함수로 전환해 들여쓰기를 최소화하는 방법이 있긴 하지만, 변수를 하나하나 설정하는 게 여간 불편한 일이 아닐 수 없다.
그래서 ES6부터 Promise와 Generator를, ES2017에선 async/await를 도입해 비동기적 프로그래밍을 일련의 작업 혹은 동기적인 작업처럼 보이게 처리하는 장치를 만들었다.
📕 코어 자바스크립트
📗 자바스크립트 Deep Dive
📚 콜백 함수(Callback) 개념 & 응용 - 완벽 정리
콜백 함수 많이 들어보기만 하고 잘 몰랐는데 이해가 쏙쏙 되네요 XD!