콜백 함수란 무엇인가? MDN 웹문서에는 콜백 함수를 아래와 같이 정의하고 있다.
A callback function is a function passed into another function as an argument
이를 직역하면 콜백 함수는 다른 함수의 인자로 넘어가는 함수를 말한다. 콜백에 대해서는 이게 전부다. 그런데 콜백 함수는 대부분 비동기 함수에서 쓰이기 때문에 '콜백 함수는 비동기 처리된다'라는 오개념이 생기기 쉽다. 하지만 이는 틀렸다. 콜백 함수라고 전부 비동기 처리되지 않는다. 아래 예시를 보자.
function increase_Sync(n, callback) {
console.log(n);
callback(n + 1);
}
console.log('**** Start Script ****');
increase_Sync(n = 1, n => {
increase_Sync(n, n => {
increase_Sync(n, () => {
console.log('End Sync')
})
})
})
console.log('**** End Script ****');
/** 결과 **
**** Start Script ****
1
2
3
End Sync
**** End Script ****
/*
위 코드에서 콜백이 비동기 처리되었으면 'Start Script'와 'End Script'가 먼저 출력돼야 한다. 하지만 출력 결과를 보면 'End Script'가 가장 마지막에 출려되는 것을 확인할 수 있다. 이는 코드가 동기적으로, 즉 순서대로 실행됐다는 것을 의미한다.
그러면 콜백 함수가 비동기 처리될 때는 언제일까? 그야 당연히 콜백 함수가 비동기 함수의 인자로 들어갈때다. 당연한 얘기지만 나는 이 당연한걸 오랜 시간동안 잘못 알고 있었다. 그도 그럴게 콜백 함수에 대한 글을 찾아보면 십중팔구 setTimeout으로 설명하고 있다. 그런데 이는 단순히 setTimeout이 가장 대표적인 비동기 함수중 하나이기 때문이다. 그래서 나 역시 비동기 처리는 setTimeout으로 설명하겠다.
비동기 함수의 콜백 예제를 보자. 동기 함수의 예제와 거의 비슷하다.
function increase_Async(m, callback) {
setTimeout(() => {
console.log(m);
callback(m + 1);
}, 1000);
}
console.log('**** Start Script ****');
increase_Async(m = 1, m => {
increase_Async(m, m => {
increase_Async(m, () => {
console.log('End Async');
})
})
});
console.log('**** End Script ****');
/** 결과 **
**** Start Script ****
**** End Script ****
1
2
3
End Async
*/
동기 함수와 비교해봤을때 increase 함수 내부가 setTimeout으로 감싸진 것만 다르다. setTimeout은 비동기 함수이기 때문에 스크립트의 끝까지 먼저 실행되고, 1초 뒤에 콜백 함수가 차례대로 실행된 것을 확인할 수 있다. 동작 원리에 대한 상세한 내용은 곧 다루니 우선 넘어가자.
그런데 위 코드에서 한가지 짚고 넘어가야할 부분이 있다. setTimeout 내부에서 callback을 호출한게 어떤 의미일까? setTimeout은 일정 시간 이후에 실행시키는 함수다. 따라서 callback 또한 일정시간 이후에 실행된다.
코드 흐름은 위 그림과 같다. 왼쪽부터 callback 부분은 setTimeout에 의해 1초 간격으로 실행된다. 결론적으로 위 코드는 매 초마다 시간(초)를 출력해주는 코드다. 약간 바꿔 말하면 setTimeout을 동기적으로 실행시켜주는 코드다. setTimeout 내부 함수가 호출될때마다 다시 callback을 호출함으로써 비동기 함수를 동기적으로 실행시킬 수 있는 것이다!
이처럼 콜백을 잘 활용하면 비동기 함수를 동기적으로 실행시킬 수 있다. 그리고 이게 바로 콜백이 비동기 함수에서 많이 사용되는 이유다. 비동기 함수의 인자로 콜백 함수를 넘겨주고, 비동기 처리가 끝난 뒤에 콜백 함수를 호출하면, 비동기 함수를 동기화시킬 수 있다.
그런데 콜백은 가독성이 떨어진다는 치명적인 단점이 있다. 아까 다뤘던 비동기 코드를 약간만 수정해 7까지 출력해보자. 코드는 아래와 같다.
increase_Async(m = 1, m => {
increase_Async(m, m => {
increase_Async(m, m => {
increase_Async(m, m => {
increase_Async(m, m => {
increase_Async(m, m => {
increase_Async(m, () => {
console.log('End Async');
})
})
})
})
})
})
});
이러면 7초가 되면 7을 출력하면서 함수가 종료될 것이다. 지금은 함수가 increase_Async밖에 없어서 그렇지.. 조금만 복잡해져도 대환장 파티가 된다. 이렇게 콜백에 콜백을 계속 넣는 형식을 '콜백 지옥(Callback Hell)'이라고 한다. 콜백 지옥의 해결책으로 나온게 promise이고, promise조차 'then의 무한 체이닝으로 가독성이 좋지 않다!!'고 해서 나온게 async/await 다. promise와 async/await는 이전 글에서 자세하게 설명해놨으니 자세한 설명은 생략하고, 대신 위 코드를 promise 버전과 async/await 버전으로 바꾸는 것에서 끝내겠다.
function increase_promise(m) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(m);
resolve(m + 1);
}, 1000)
})
}
// 1. then 체이닝
increase_promise(1)
.then(m => increase_promise(m))
.then(m => increase_promise(m))
.then(m => increase_promise(m))
.then(m => increase_promise(m))
.then(m => increase_promise(m))
.then(m => increase_promise(m))
// 2. async/await
async function increase_asyncAwait(m) {
let n = await increase_promise(m);
n = await increase_promise(n);
n = await increase_promise(n);
n = await increase_promise(n);
n = await increase_promise(n);
n = await increase_promise(n);
n = await increase_promise(n);
}
increase_asyncAwait(1);
자바스크립트 런타임 작동 방식도 정리하려고 자료를 찾아보다가 너무 깔끔하게 잘 정리된 글을 봐서 링크를 달아놓는다. 아래 글은 꼭 읽어보도록 하자!! 자바스크립트를 배운다면 무조건 알고 있어야 하는 내용이다.
<참조>
https://developer.mozilla.org/en-US/docs/Glossary/Callback_function
'JavaScript' 카테고리의 다른 글
[ Javascript ] 조합, 중복조합, 순열, 중복순열 (feat. 재귀함수) (1) | 2022.06.25 |
---|---|
[ JavaScript ] 프로토타입(Prototype) (0) | 2022.05.15 |
[ JavaScript ] Rest parameter, Spread Syntax, 구조 분해 할당 (0) | 2022.04.30 |
[ JavaScript ] async/await (0) | 2022.04.29 |
[ JavaScript ] Promise (0) | 2022.04.27 |