자바스크립트를 공부하면서 중요한 개념을 뽑으라고 하면 저는 당연하게 콜백 함수의 개념에 대해 뽑을것 같습니다. 콜백 함수는 나중에 Promise와 async/await로 비동기 프로그래밍를 자유롭게 다루기 위해서는 꼭 숙지하고 넘어가야하는 개념이라고 생각합니다. 그러면 자바스크립트에서 콜백함수는 무엇이고, 이것의 역할과 문제점은 어떤것이 있는지 살펴보겠습니다.
우선 그러려면 동기/비동기의 차이점을 알아야하는데, 아주 간단하게 정의를 하면 동기란 특정 코드가 끝날때까지 모든 작업을 기다려 작업이 순차적으로 실행되도록 하며 위에 언급했듯이 하나의 작업이 완료되기 전까지는 다음 작업이 실행되지 않습니다. 그리고 비동기란 특정 코드가 끝날때까지 코드의 실행을 멈추지 않고 다음 코드를 먼저 실행하는 것을 의미합니다. 기본적으로 자바스크립트라는 언어는 한번에 하나의 작업을 수행하는 동기식 언어이지만 비동기 방식을 사용해야 하는때도 분명 존재하기 때문에 비동기적으로 동작할수도 있어야합니다. 동기 방식을 간단한 예시로 살펴보면 사용자가 "다운로드" 버튼을 눌렀을 때, 서버에서 파일을 받아오는 동안 브라우저는 다른 작업을 하지않고 해당 다운로드 작업만 수행합니다.
동일한 예시를 비동기 방식으로 바꿔보면 사용자가 "다운로드" 버튼을 눌렀을 때, 서버에서 파일을 다운로드 하는 동안 브라우저는 다른 작업을 계속 처리하고, 해당 다운로드가 완료되면 알림이 나타납니다. 이때 동기 방식과는 다르게 사용자는 다운로드를 기다리는 동안 웹사이트의 다른 부분 (ex: 메뉴 검색, 텍스트 입력)을 자유롭게 사용할 수 있습니다.
이렇듯 웹사이트에서는 비동기 방식을 활용하여 데이터를 처리하고, 동시에 동기 작업으로 기본 UI를 안정적으로 제공하며 두 방식을 적절하게 모두 사용하는것이 중요합니다.
동기 방식과 비동기 방식의 차이점
구분 동기 비동기 작업 실행 방식 순차적으로 실행 병렬적으로 실행 장점 단순하고 직관적 효율적이고 빠른 실행 가능 단점 시간이 오래 걸리는 작업에서 멈춤 코드의 복잡성 증가 가능
콜백함수는 다른 함수에게 매개변수로 넘겨주고, 그 함수 내부에서 사용해야할때 호출되어 실행되는 함수를 말합니다. 저는 처음에 콜백함수라는 개념을 이해할때 생각보다 이 개념에 대해 이해가 되지않았던 기억이 있는데, 간단하게 예시를 보면서 살펴보는게 더 이해하기 편할것이라고 생각합니다.
콜백함수의 이름처럼 이 함수는 필요한 시점에 불려서 실행되는 함수이기 때문에, 한 함수가 작업을 마친 뒤 그 결과를 가지고 어떤 동작을 해야 할 때 다른 함수를 호출하는 방식입니다.
function sayHello(name, callback) {
console.log(`Hello, ${name}!`);
callback(); // 인수로 받은 콜백함수 실행시키는 코드
}
function sayGoodbye() {
console.log("Goodbye!");
}
sayHello("SeungIn", sayGoodbye); // sayHello 함수의 인수로 sayGoodbye 함수를 받아 sayGoodbye 함수 호출
/*
실행결과
Hello, SeungIn!
Goodbye!
*/
위 코드에서 sayHello 함수는 name과 함께 callback이라는 함수를 인수로 받습니다. sayHello함수를 실행하면 실행시킨 인수로 name과 sayGoodbye를 받았기때문에 사용자가 지정한 이름과 sayGoodbye에서 만들어준 콜백 함수를 호출시킵니다. 콜백 함수는 다른 함수의 인수로 전달되어, 해당 함수 내부에서 실행될 수 있는 사용자 정의 함수입니다 라는 개념이 콜백함수의 개념을 가장 쉽게 설명하는것 같습니다.
콜백함수는 동기와 비동기 두 방식 모두 사용하지만 비동기 방식에서 사용할때 가장 강력한 기능을 나타냅니다. forEach와 같은 자바스크립트의 내장함수를 사용해 동기 방식에서 콜백함수를 사용할수 있지만 직관적으로 동기 방식을 알아볼수 있는 코드로 살펴보면
function ForEach(array, callback) {
for (let i = 0; i < array.length; i++) {
callback(array[i]); // forEach를 직관적으로 살펴보기 위해 함수로 표현
}
}
const numbers = [1, 2, 3, 4, 5];
ForEach(numbers, (num) => { // 인수로 numbers와 화살표 함수로 콜백함수 호출
console.log(num);
});
/*
실행결과
1
2
3
4
5
*/
ForEach 함수에서 인수로 array와 callback을 받고, 사용할때 numbers 배열과 화살표 함수로 표현한 함수를 통하여 콜백함수를 구현하였습니다!
그리고 이번엔 비동기 방식에서의 콜백함수를 예시로 살펴보면
function createCallBack(callback) {
setTimeout(() => { // setTimeout 함수를 사용하여 일정시간 뒤에 데이터를 불러옴
const data = "임의 데이터";
callback(data); // 콜백 호출
}, 2000); // 현재는 2초 뒤 데이터를 가져오도록 설정
}
function bringCallBack() { // 콜백 함수를 불러오는 함수
createCallBack((data) => {
console.log(data); // 콜백 함수
});
}
bringCallBack();
/*
실행결과
(2초 뒤 실행)
임의 데이터
*/
현재 이 코드에서는 createCallBack 함수가 콜백 함수로 (data) => { console.log(data); } 를 받아 2초 대기 후에 callback(data)를 호출하여 콜백 함수를 실행시킵니다.
비동기 방식에서 콜백 함수의 역할은 데이터를 가져오는 작업이 비동기적으로 이루어지기 때문에 데이터가 언제 준비될지 모르는데, 이때 준비된 데이터를 적절히 처리하기 위해 콜백 함수를 전달합니다. 그리고 데이터가 준비되면 콜백 함수가 호출되어, 데이터를 출력하는 작업이 수행하게 됩니다.
콜백함수는 자바스크립트에서 비동기 방식을 처리할수 있게 해주고, 다양한 상황에서 달라질 수 있는 사용자 정의 동작을 정의하는 유연한 방법을 제공하지만 이런 콜백 함수에도 문제점은 존재합니다. 프로그래밍을 할때 코드를 가독성있게 작성하는것은 필수적이면서 중요한데, 이러한 콜백 함수를 남발하게 된다면 흔히 말하는 콜백지옥에 빠질수 있습니다. "콜백 지옥"은 콜백 함수가 중첩된 구조로 작성되어 가독성이 떨어지고 유지보수가 어려운 코드를 말합니다.
위에서 비동기 방식으로 사용한 코드에서 함수를 실행시키기 위해 콜백 함수를 연속적으로 중첩하여 작성하게 된다면
function createCallBack(callback) {
setTimeout(() => {
const data = "임의 데이터";
callback(data);
}, 2000);
}
function bringCallBack() {
createCallBack((data1) => {
console.log("첫 번째 데이터:", data1);
// 두 번째 데이터 가져오기
createCallBack((data2) => {
console.log("두 번째 데이터:", data2);
// 세 번째 데이터 가져오기
createCallBack((data3) => {
console.log("세 번째 데이터:", data3);
// 네 번째 데이터 가져오기
createCallBack((data4) => {
console.log("네 번째 데이터:", data4);
// 이런식으로 끝없이 작성하게 된다면 매우 복잡하고 가독성이 떨어지는 코드가 됨
});
});
});
});
}
bringCallBack();
이런식으로 끝없이 콜백 함수를 써서 코드를 작성하면 가독성이 저하, 유지보수의 어려움, 어디에서 에러가 발생한지 찾기 힘듬 등의 여러 문제가 발생할수 있습니다.
이런 문제에 대한 해결방법으로 Promise, async/await 등이 등장하였는데 두 방식 역시 비동기 프로그래밍에서 현대에 매우 빈번하고, 중요하게 쓰이고 있기때문에 다음 포스팅에서 다뤄보려고 합니다.
사실 콜백 함수는 초기의 간단한 비동기 처리 방식에서 사용되었고, 현대에서 콜백 함수를 비동기 프로그래밍에서의 직접적인 주 방식으로 사용하진 않습니다. 현대에서는 주로 async/await 방식을 사용하여 비동기 방식을 다루기 때문에 저 역시도 프로젝트에서 비동기 방식을 다룰때 콜백함수 하나만을 사용하여 코드를 구성해보진 않아서 콜백 함수의 용어만 알고있을뿐 깊은 개념에 대해서는 잘 알고있지 못했던것 같습니다. 하지만 비동기 방식은 현대에서 매우 빈번하게 사용하는 중요한 방식이고, 이러한 비동기 방식을 구성하는데 가장 기본이 되는 개념이 콜백 함수입니다.
이런 콜백 함수 개념을 모르고 Promise, async/await 등을 사용하는것은 의미가 없다고 생각이 듭니다. 콜백 함수는 특정 동작을 함수의 외부에서 정의할 수 있게 하여, 함수의 재사용성과 유연성을 극대화하기때문에 개념과 사용법을 잘 알고 사용한다면 좀 더 좋은 코드를 작성할수 있고, 데이터를 처리할 시점을 명확히 제어할 수 있게 됩니다. 콜백 지옥과 에러 처리의 어려움이라는 단점이 존재하는 콜백 함수이지만, 정확한 개념을 이해하여 사용한 간단한 비동기 방식에서의 콜백 함수와, 이 개념을 기반으로 async/await를 사용한다면 비동기 방식 처리에 대해서 두려움을 느끼지않고 좋은 처리를 할수 있을것이라고 생각합니다!
PS. 요즘들어 배워야하는게 많다고 생각이 들어, 기초를 다지지않고 어렵지만 실전에서 많이 사용하는 개념들에 대해서만 급하게 머릿속으로 집어넣어서 당장 빠르게 사용하려고만 하는것 같다는 생각이 들었습니다. 하지만 이런 행동은 좋지않은 행동이고 실전에서 많이 사용하는 개념들을 잘 사용하려면 기본이 되는 개념들도 확실히 잡고가야한다는 생각이 많이들었습니다.
콜백 함수의 개념도 이러한 생각이 들게 한 중요 개념중에 하나라고 생각합니다. 프로젝트에서 급하게 배운 async/await 개념을 사용할땐 비동기 처리 방식에 대해 확실하게 이해를 못했다고 생각하는데, 비동기 프로그래밍에서 기본적이지만 핵심적인 역할을 하는 콜백 함수의 개념을 다지고 다른 추가 기능들을 살펴보니 어느정도 비동기 프로그래밍의 동작 방식을 이해하는데 더 수월했던것 같습니다.
앞으로도 이런 중요하고 기본적인 개념들에 대해 확실하게 이해하고, 이를 기반으로 한 응용개념과 실전에서 사용하는 방식들도 탄탄하게 배워나가려고 합니다!
다음은 이러한 콜백 함수를 기반으로 한 Promise, async/await 방식을 알아보겠습니다
참고:
https://developer.mozilla.org/ko/docs/Glossary/Callback_function
https://inpa.tistory.com/entry/JS-%F0%9F%93%9A-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%BD%9C%EB%B0%B1-%ED%95%A8%EC%88%98
이미지 출처:
https://dev-ini.tistory.com/49