콜백은 자바스크립트 (이하 JS)에서 비동기성을 표현하는 기본 단위입니다.
// A
ajax('..', function() {
// C
})
// B
A와 B는 지금 당장, C는 나중에 실행됩니다.
콜백 함수는 이 나중에 실행되는 함수를 의미하며 프로그램의 연속성을 감싼/캡슐화한 장치입니다.
두뇌는 병렬 멀티스레딩이 불가능합니다.
우리의 신경은 병렬적으로 동작하지 않고 이벤트 루프 큐처럼 작동합니다.
우리가 동시에 많은 일을 하고 있다고 생각할 수 있지만, 이것은 사실 멀티태스킹이 아니라 그냥 매우 재빠른 컨텍스트 교환입니다.
사람의 두뇌는 순차적, 중단적, 단일 스레드 방식으로 계획하는 데 익숙하지만 콜백은 비동기 흐름을 비선형적, 비순차적인 방향으로 나타내므로 구현된 코드를 제대로 이해가기가 매우 어렵습니다.
A(function() {
B();
C(function() {
D();
})
E();
})
F();
위의 코드의 실행 순서는 어떻게 될까요?
이런 순서로 동작합니다.
만약, A와 D가 비동기 코드로 아니라면 순서는 또 달라집니다.
순서가 이렇게 뒤바뀌게 됩니다.
위의 예는 정말 간단한 코드 실행의 예이므로 실제 비즈니스 로직에서 이러한 실행 순서를 일일이 따라간다는 것은 굉장히 비효율적입니다.
또한, 일일이 모든 내용을 단계별로 하드 코딩하는 방법도 가능하긴 하지만 높은 확률로 다른 단계나 비동기 흐름에서는 재사용할 수 없는, 매우 중복되는 코드 낭비가 초래될 것입니다.
이것이 콜백 지옥입니다.
콜백 중심적 설계 방식의 가장 큰 문제점은 제어의 역전이 발생한다는 점입니다.
// A
ajax("..", function() {
// C
})
// B
A와 B는 JS 메인 프로그램의 제어를 직접 받으며 지금 실행되지만, C는 ajax 함수의 제어 하에 나중에 실행됩니다. 이렇게 제어권이 넘어가면서 개발자가 직접 제어할 수 없게 됩니다.
내가 작성하는 프로그램인데도 실행 흐름은 비동기 함수 (주로, 서드 파티 유틸리티)에 의존해야 하는 이런 상황을 제어의 역전이라고 합니다.
콜백 호출시 오류가 날 만한 경우의 수를 모두 따져본다고 해봅시다.
모든 경우별로 보완 로직을 구현해 넣는다는 것이 얼마나 끔찍한 일인지 상상이 가지 않을 정도입니다.
분할 콜백은 한쪽은 성공 알림, 다른 쪽은 에러를 알려주는 기능입니다.
이러한 기능을 제공하는 API가 존재합니다.
function success(data) { console.log(data); }
function failure(err) { console.error(err); }
ajax('url', success, failure);
이러한 API 설계에서 failure는 필수가 아니며, 작성하지 않으면 에러는 조용히 무시됩니다.
에러 우선 스타일이라는 콜백 패턴 또한 최근 많이 사용됩니다.
function response(err, data) {
if (err) {
console.log(err);
} else {
console.log(data);
}
}
ajax("url", response);
에러를 첫 번째 인자로, 성공시의 데이터를 두 번째 인자로 받게 됩니다.
위의 두 경우 모두 개선된 콜백의 구조라고 생각할 수 있지만 몇 가지 문제가 있습니다.
콜백은 잠재적인 오류를 내장하고 있으며 동기냐 비동기냐에 관한 비결정성은 버그를 추적하기가 거의 항상 곤욕스럽습니다.