본문 바로가기

Prog. Langs & Tools/JavaScript

JS #9. 자바스크립트 콜백(Callback)

지난 포스팅 : JS #7. 비동기성: 지금과 나중

지난 포스팅에서 비동기가 무엇인지, 단일 스레드 방식의 이벤트 큐가 무엇인지, 동시성 패턴 등등에 대해서 알아 보았다. 함수 안의 문은 예측 가능한 순서대로 (컴파일러 상위 수준에서) 실행되지만 함수 단위의 실행순서는 이벤트(비동기 함수 호출)에 따라 달라질 수 있다. 어떤 함수든 콜백(Callback) 역할을 한다. 왜냐하면 큐에서 대기 중인 코드가 처리되자마자 본 프로그램으로 '되돌아올' 목적지이기 때문이다.

콜백은 자바스크립트에서 비동기성을 관리하는 가장 일반적인 기법이자, 자바스크립트의 가장 기본적인 비동기 패턴이다. 물론 콜백에도 단점이 있고 이 단점을 보완하기 위해 새로운 개념들이 나오게 되었으나, 그 이전에 콜백의 실체를 정확하게 아는 것은 필요해 보인다.

// A
ajax("..", function(){
  // C
});
// B  

A와 B는 프로그램 전반부('지금'), C는 프로그램 후반부('나중')에 해당된다. 전반부 코드가 곧장 실행되면 비결정적(indeterminate) 시간 동안 중지되고 언젠가 ajax 호출이 끝날 때 중지되기 이전 위치로 다시 돌아와서 나머지 후반부 프로그램이 이어진다. 다시 말해, 콜백 함수는 프로그램의 연속성(continuation)을 감싼(wrapping)/캡슐화(encapsulation)한 장치이다.

인간의 두뇌는 한 번에 한 가지의 일만 생각할 수 있는 싱글태스커이다. 많은 사람들이 본인이 멀티태스커라고 생각을 하지만(예를 들면 키보드를 입력하면서 전화 통화를 한다든지) 이러한 상황들은 우리가 아주 재빠르게 문맥 전환(context switching)을 하고 있을 뿐인 것이다. 바꿔 말하면 여러 작업 사이를 재빨리 연속적으로 왔다갔다 하면서 각 작업을 아주 작고 짧은 덩이로 쪼개어 동시에 처리하는 것이다. 이 작업을 아주 빠르게 하면 외부에서는 마치 여러 작업이 병렬적으로 실행되고 있는 것 처럼 보인다. 결론은 인간의 두뇌는 이벤트 루프 큐처럼 작동한다는 것이다.

동기적인 코드는 인간의 두뇌가 생각하는 방법과 비슷해서 작성하는 것이 상대적으로 수월하다. 하지만 비동기 코드의 경우 인간이 비동기 흐름을 생각하고 떠올리는 일 자체가 부자연스럽기 때문에 작성하는 것이 어렵다. 인간은 단계별로 끊어서 생각하는 경향이 있는데 콜백은 동기->비동기로 전환된 이후 단계별로 나타내기가 쉽지 않다.

비동기 코드가 항상 작성한 대로 순차적으로 실행이 되면 참 좋겠지만, 그렇게 되는 경우는 정말 많은 경우의 수 중 한 가지일 뿐이다. 실제 비동기 자바스크립트 프로그램에는 갖가지 잡음이 섞이게 되고 실제 프로덕션에서 이와 같이 복잡한 코드를 이해하는 일은 상당히 어렵다. 하드 코딩을 통해 어느정도 보완을 할 수는 있겠으나, 하드 코딩은 기본적으로 부실한 코드가 될 가능성이 높으며 단계가 진행되면서 엉뚱한 일들이 발생하여 오류가 나는 것까지 대비할 수는 없다.

또한 콜백 중심으로 설계를 하게 되면 제어권 교환(control switch)이 일어나게 되고 이는 큰 문제점이다. 콜백을 넘겨주는 함수의 경우 개발자가 직접 작성하거나 제어할 수 있는 함수가 아니라 서드 파티 유틸리티인 경우가 많기 때문이다. 몇 번의 에러를 겪고 나면 방어 코드를 작성하게 되면서 코드는 필요 이상으로 복잡해지고 지저분해지는 경우가 많다. 한 마디로 콜백은 믿지 못하는 코드를 작성하게 되는 것이다.

콜백을 해결하기 위해 여러가지 노력을 해볼 수 있다. 예를 들어, 에러 우선 스타일(Error-First Style)이라는 콜백 패턴이 있다. 단일 콜백 함수가 첫번째 인자를 에러 객체로 받는 것이다. 그리고 콜백이 한 번도 호출되지 않은 경우 이벤트를 취소하는 타임아웃을 설정한다.

정리하면, 콜백은 자바스크립트에서 비동기성을 표현하는 기본 단위이다. 하지만 비동기 프로그래밍 환경에서 콜백만 가지고는 충분하지 않다. 그 이유는 첫 번째로, 사람의 두뇌는 순차적, 중단적, 단일-스레드 방식으로 계획하는데 익숙하지만 콜백은 비동기 흐름을 비선형적, 비순차적인 방향으로 나타내므로 구현된 코드를 제대로 이해하기가 매우 어렵다. 두 번째로, 콜백은 프로그램을 진행하기 위해 제어를 역전, 즉 제어권을 다른 파트(ex. 서드 파티 유틸리티)에 암시적으로 넘겨줘야 하고 예상보다 더 자주 콜백을 호출하는 등의 믿을 수 없는 코드가 작성되게 된다. 임시 로직을 통해 당장은 난관을 해결할 수도 있지만, 그 과정을 계속하면 거칠고 유지보수가 어려운 코드가 되게 된다.

그래서 등장한 기술이 바로... 다음 포스팅에서 소개할 프로미스(Promise)이다.