Javascript의 주요 개념 중 콜백 함수의 개념을 학습하면서 정리한 글이다.
들어가며
자바스크립트를 통해 프로그래밍 하다보면 map이나 forEach와 같은 배열 메서드들을 사용하는 경우가 많다.
이때 첫 번째 인자로 콜백함수를 넣게 되는데, 대게 이 파라미터들은 순서와 의미가 정해져 있다.
그러다 문득, 왜 배열 메서드들은 모두 콜백 함수를 인자로 넘기도록 구현이 되어 있는지, 콜백 함수를 넘겨주면 어떤 방식으로 동작하는 것인지에 대한 의문이 들어 학습한 내용을 작성해보았다.
콜백 함수란?
함수의 매개변수를 통해 다른 함수의 내부로 전달되는 함수
즉, 함수 A의 매개변수로 함수 B를 전달할 때 B가 콜백 함수가 되는 것이다.
예시를 통해 알아보자.
function sayHi() {
console.log('Hi');
}
function greet(greetFunc) {
setTimeout(() => {
greetFunc();
}, 2000);
}
greet(sayHi);
greet이라는 함수에 greetFunc라는 함수인 매개변수를 받도록 하고,
greet이라는 함수를 호출할 때 sayHi라는 함수를 인자로 넘겨줬다.
그러면 greet이라는 함수는 2초뒤에 sayHi 함수를 실행시키게 된다.
이때 greet이라는 함수에 인자로 넘어간 sayHi가 콜백 함수가 되는 것이다.
콜백 함수는 언제 사용될까?
콜백 함수를 사용하는 경우는 크게 3가지가 있다.
1. 코드의 중복을 줄이고 재사용성을 높이고자 할 때
2. 제어권을 위임할 때
3. 비동기적인 작업을 처리할 때
1. 코드의 중복을 줄이고 재사용성을 높이고자 할 때
위 예시를 보면 궁금증이 생길 수 있다.
굳이 복잡하게 함수에 함수를 넘겨줘야 하는 이유가 있을까?
아래처럼 sayHi 함수 내부에 있는 코드를 setTimeout 내부에 넣어도 똑같이 동작하는 것을 알 수 있다.
function greetEng() {
setTimeout(() => {
console.log('Hi');
}, 2000);
}
greetEng();
하지만, 다음과 같은 경우를 생각해보자.
만약 2초 뒤에 안녕하세요! 를 출력하는 함수를 만들고 싶다면 어떨까?
function greetEng() {
setTimeout(() => {
console.log('Hi');
}, 2000);
}
function greetKor() {
setTimeout(() => {
console.log('안녕하세요!');
}, 2000);
}
greetEng();
greetKor();
위와 같은 경우에는 greetKor이라는 함수를 새로 만드는 수 밖에 없다.
여기서 우리는 한 가지 개선점을 발견할 수 있다.
바로 greetKor 함수와 greetEng 함수 내부에 있는 setTimeout의 로직이 동일하게 작성되어, 중복된 setTimeout 함수의 호출을 공통으로 로직화 할 수 없을지 고민하게 된다.
이러한 문제를 해결하기 위해 사용될 수 있는 것이 바로 콜백 함수이다.
함수 내부에 있는 변하지 않는 공통적인 로직은 따로 분리하고, 케이스에 따라 변경되는 로직은 함수 외부에서 주입해주는 형태로 분리하는 것이다.
아래와 같이 말이다.
function greetEng() {
console.log('Hi');
}
function greetKor() {
console.log('안녕하세요');
}
function greet(greetFunc) {
setTimeout(() => {
greetFunc();
}, 2000);
}
greet(greetEng);
greet(greetKor);
콜백 함수로 분리하기 전과 후를 나타내면 다음과 같다.
공통되는 로직은 greet이라는 한 차원 높은 함수에 정의하고, 가변적인 로직은 매개변수로 처리하도록 한다.
자바스크립트의 함수는 일급 객체이므로 함수의 매개변수를 통해 함수를 전달할 수 있다.
콜백 함수로 분리하지 않았을 때와 했을 때를 비교해보면, greet이라는 함수에 setTimeout을 한 번만 정의하여 중복을 줄인 것을 볼 수 있다.
2. 제어권을 위임할 때
넘겨주게 될 제어권에는 3가지가 있다.
1. 실행 시점
2. 매개변수
3. this
1. 실행 시점
콜백 함수를 사용하면 실행 시점을 제어할 수 있다.
아래 코드를 보자.
setInterval(function () {
console.log('Hi');
}, 1000);
setInterval은 일정 간격으로 함수를 실행하는 함수다.
내가 직접 코드의 실행을 제어하지 않고, JavaScript 엔진이 알아서 정해진 시간마다 함수를 실행한다.
즉, 코드를 실행시키는 주체가 내가 되는 것이 아니라 setInterval이 주체가 되어 알아서 실행을 해주는 것이다.
만약 setInterval 함수를 쓰지 않고 1초에 한 번씩 함수를 호출해야한다면 어떻게 해야할까?
이런 제어 위임이 없다면, 매 초마다 실행을 체크하고 관리하는 복잡한 코드를 내가 직접 작성해야 할 것이다.
아래와 같이 말이다.
async function timer() {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Hi');
timer();
}
timer();
이처럼 콜백 함수는 실행 시점의 제어를 다른 주체에게 위임할 때 사용된다.
2. 매개변수
실행 시점 뿐만이 아니라 매개변수도 제어할 수 있다.
아래는 배열 메서드인 forEach를 사용한 코드의 예시이다.
const arr = [1, 2, 3];
arr.forEach(function (ele, idx) => {
console.log(ele, idx, this[idx]);
}, ['a', 'b', 'c']);
forEach 함수는 배열에 있는 값을 하나씩 꺼내서 첫 번째 매개변수에는 배열의 각 요소에 해당하는 값을, 두 번째 매개변수에는 인덱스 번호를 부여하면서 이 함수를 실행한다.
mdn을 살펴보면 아래와 같이 forEach 메서드를 쓰기 위해서 필요한 매개변수가 이미 정해져 있다. 즉, forEach에게 결정권이 있다는 것이다. 따라서 forEach에 넘기는 콜백 함수의 파라미터는 전적으로 forEach에 정의된 방식에 따를 수 밖에 없다.
forEach는 내부적으로 아래와 같은 형태로 구현이 되어 있을 것이다.
Array.prototype.forEach = function(callback) {
for (let i = 0; i < this.length; i++) {
callback(this[i], i, this);
}
};
forEach 함수 내부에서 어떠한 값을 파라미터로 호출할 지 결정하기 때문에 제어권을 넘겨준다고 할 수 있다.
3. this
콜백 함수로 this도 제어할 수 있다.
아래 예제를 보자.
document.body.innerHTML = '<div id="apple">apple</div>';
document.getElementById('apple').addEventListener('click', function(param) {
console.log(this, param);
});
위 코드를 javascript 파일에서 실행하고 apple이라고 적힌 텍스트를 클릭하면 아래의 값이 출력된다.
this는 클릭한 대상인 id는 apple인 dom 자체가 출력되었고, param에는 PointerEvent 객체가 출력되었다.
이는 addEventListener 함수 내부에서 this는 eventTarget으로 하고, 콜백함수의 첫 번째 인자는 event 객체를 넘겨주도록 정의했기 때문이다.
이렇게 3가지 경우에 대해서 제어권을 넘겨주는 콜백 함수의 예시 상황들을 살펴보았다.
3. 비동기적인 작업을 처리할 때
자바스크립트는 setTimeout 코드를 통해 비동기적으로 작업을 처리할 수 있다. 다음 예제를 보자.
function loadFruit() {
let fruit = null;
setTimeout(() => {
fruit = ['사과', '바나나', '딸기'];
}, 1000);
return fruit;
}
const fruits = loadFruit();
console.log(fruits); // null
fruit에 있는 변수는 1초 뒤에 값이 할당되기 때문에 loadFruit 함수로부터 받은 fruit에는 fruit의 초기값인 null이 반환되는 것을 볼 수 있다.
하지만 여기서 콜백 함수를 사용한다면, 비동기 작업이 완료된 후에 데이터를 받아올 수 있다.
아래와 같이 말이다.
function loadFruits(callback) {
setTimeout(() => {
const fruits = ['사과', '바나나', '딸기'];
callback(fruits);
}, 1000);
}
loadFruits((fruit) => {
console.log(fruit);
});
마무리하며
지금까지 콜백 함수란 무엇인지와 어떤 방식으로 쓰이는지를 알아보았다.
정리하자면 콜백 함수는 코드의 제어권을 다른 주체에게 위임하고, 공통된 로직을 분리할 때 사용된다고 생각하면 될 것 같다.
평소에 배열 함수를 쓰면서, 이렇게만 파라미터에 넣으면 알아서 내가 원하는 값을 도출해준다고? 라는 궁금증과 의심이 있었다.
이번 기회에 배열 내부 함수의 내부 구현을 생각해보고 직접 구현해보면서, 내부적으로 잘 동작할 것이기 때문에 배열 함수를 필요에 맞게 잘 사용하기만 하면 될 것이라는 생각이 들었다.
참고 자료
잘못된 내용이 있다면 이메일(tnghk9611@naver.com)또는 댓글 남겨주시면 감사하겠습니다.
'☘️ Front end > 🌱 Javascript' 카테고리의 다른 글
[Javascript] 클로저에 대해서 알아보자 (closure) (0) | 2025.01.28 |
---|---|
[JavaScript] Javascript의 this에 대해 알아보자 (1) | 2025.01.22 |
[JavaScript] 실행 컨텍스트 이해하기 (0) | 2025.01.20 |