Ch2. 콜백
- 저번 시간에 자바스크립트 비동기 프로그래밍의 용어와 개념에 대해서 살펴봤다.
- 함수 안의 문의 예측 가능한 순서대로 실행되지만 함수 단위의 실행 순서는 이벤트에 따라 달라질 수 있다.
- 어떤 경우든 함수는 콜백 역할을 한다. 큐에서 대기 중인 코드가 처리되자마자 본 프로그램으로 되돌아올 목적지기 때문이다.
- 콜백은 사실상 자바스크립트 언어에서 가장 기본적인 비동기 패턴이다.
- 콜백의 실체를 깊이 있게 살펴보고 더 정교한 비동기 패턴이 나오게 된 계기를 설명한다.
2.1 연속성
// A
ajax("..", function () {
// C
})
// B
- A와 B는 프로그램 전반부, C는 프로그램 후반부에 각각해당한다. 전반부 코드가 곧장 실행되면 언젠가 AJAX 호출이 끝날 때 중지되기 이전 위치로 다시 돌아와서 나머지 후반부 프로그램이 이어진다. 즉 콜백 함수는 프로그램의 연속성을 감싼 캡슐화한 장치다.
2.2 두뇌는 순차적이다.
- 인간은 싱글태스커에 더 가깝다. (즉, 컨텍스트 스위칭이 일어난다는 것)
- 멀티태스킹을 하는것처럼 보여도 사실상 재빠른 컨텍스트 교환기처럼 행동하고 있을 뿐이다. 자바스크립트에서 비동기 작업이 이루어지는 모습과 비슷하다.
- 요점은, 인간의 두뇌가 이벤트 루프 큐처럼 작동한다는 사실이다.
2.2.1 실행 VS 계획
- 사실 사람이 여러 가지 작업을 계획ㄹ하는 방법과 두뇌가 이들을 처리하는 방식 사이엔 엄청난 차이가 있다.
- 예 ) 주유소 가서 기름 넣고 화장실 갔다가 집으로 돌아오는 길에 마트가서 우유사고 돌아와야지
- 위처럼 상위 수준의 사고만 보면 순차/동기적인 방향으로 계획을 한다. 실제로 보통 동기적으로 코드를 작성하는 것은 동기적으로 사고하는 두뇌와 잘 어울린다.
- 비동기 코드 작성이 어려운 이유는 인간이 비동기 흐름을 생각하고 떠올리는 일 자체가 부자연스럽기 때문이다. 인간은 단계별로 끊어 생각하는 경향이 있는데, 콜백은 단계별로 나타내기가 쉽지 않다.
2.2.2 중첩/연쇄된 콜백
listen("click", function handler(evt) {
setTImeout(function request() {
ajax("http://some.url.1", function response(text) {
if (text === "hello") {
handler()
} else if (text === "world") {
request()
}
})
}, 500)
})
위 코드는 비동기 단계를 3개의 함수가 서로 중첩된 형태로 표현한 것이다. (클릭이벤트, 타임이벤트, 네트워크 이벤트)
콜백의 첫번째 단점을 보자.
단순히 순차 실행될 경우는 많은 경우의 수 중 하나에 불과하다. 실제 비동기 자바스크립트 프로그램에는 갖가지 잡음이 섞인다. 콜백으로 가득한 코드의 흐름은 자연스럽고 쉽게 이해할만한 일은 아니다.
더 심각한 오류를 보자
doA(function () {
doB()
doC(function () {
doD()
})
doE()
})
doF()
- 위 코드의 예제 실행 순서는 A F B C E D이다. 하지만 만약 A와C가 비동기 코드가 아니라면? A C D F E B 순으로 실행된다. 중첩이 흐름을 따라가기 어렵게 만드는 원인일까? 위 예제를 순차적으로 풀어서 보면
listen("click", handler)
function handler(evt) {
setTimeout(request, 500)
}
function request() {
ajax("http:some.url.1", response)
}
function response(test) {
if (text === "hello") {
handler()
} else if (text === "world") {
request()
}
}
- 보기엔 좀 더 나아졌을 지 몰라도 불편한건 매한가지다. 실제로 코드를 보면 알겠지만 흐름을 따라가기 위해 코드 전체를 왔다갔다 해야한다. 그나마 위 예제는 아주 단순한 경우를 예로 들었는데, 실무에서 사용되는 코드들은 말도안되게 뒤죽박죽 뒤섞인 경우가 드물지 않아 추론의 어려움은 더 심해진다.
- 예를 들어, 2,3,4단계를 죽 연결하여 연속 실행하고 싶을 때 콜백만으로 할 수 있는 일은 콜백지옥을 만들어 해당 단계 안쪽에 죽 하드 코딩하여 넣는 정도다. 그러나 하드 코딩은 기본적으로 부실한 코드를 양산하기에 단계가 나아가는 도중 엉뚱한 일들이 발생하여 오류가 나는 것까지 대비할 수는 없다. 또 이것을 위해서 가능한 경우의 수를 죄다 나열하다간 코드가 너무 복잡해져 버려 관리 및 수정이 난처해진다.
2.3 믿음성 문제
- 콜백은 더 심각한 문제가 있다.
// A
ajax("..", function () {
// C
})
// B
- A와 B는 자바스크립트 메인 프로그램의 제어를 직접 받으며 지금 실행되지만, C는 다른 프로그램의 제어하에 나중에 실행된다. 제어권 교환이야말로 콜백 중심적 설계 방식의 가장 큰 문제점이다. ajax는 개발자가 작성하는 또는 개발자가 직접 제어할 수 있는 함수가 아니라 서드 파티가 제공한 유틸리티인 경우가 대부분이다. 이런 상황을 제어의 역전이라고 한다.
2.3.1 다섯 마리 콜백 이야기
- 아래는 서드 파티 함수를 호출하여 결재를 하는 가상의 코드이다. 콜백 함수가 시작되면 고객의 신용 카드를 결제하고 감사 페이지로 이동하는 코드가 잇따라 실행된다.
analytics.trackPurchase(purchaseData, function () {
chargeCreditCard()
displayThankyouPage()
})
- 문제가 생겨 해당 유틸리티가 콜백 함수를 한번만 호출해야 하는데 다섯 차례나 연달아 호출했다. 테스트 단계의 코드가 들어가서 그렇다고 한다. 결국 서드 파티에 의존하지 않고 이런 취약점으로부터 결제 시스템을 보호할 방법을 아래처럼 생각해 낸다.
var tracked = false
analytics.trackPurchase(purchaseData, function () {
if (!tracked) {
tracked = true
chargeCreditCard()
displayThankyouPage()
}
})
- 콜백 호출 시 오류가 날 만한 경우의 수를 모두 따져봤더니 아래와 같은 예외들이 있다. 그리고 모든 경우별로 보완 로직을 구현해 넣는다는 게 얼마나 끔찍한 일인지 깨닫게 된다.
- 콜백을 너무 일찍 부른다.
- 콜백을 너무 늦게 부른다.
- 콜백을 너무 적게 또는 너무 많이 부른다.
- 필요한 환경/인자를 정상적으로 콜백에 전달하지 못한다.
- 일어날지 모를 에러/예외를 무시한다.
2.3.2 남의 코드뿐만 아니라
- 매번 비동기적으로 부를 때마다 콜백 함수에 반복적인 관용 코드/오버헤드를 넣는 식으로 손수 필요한 장치를 만들어야 한다. 콜백의 가장 골치 아픈 부분이 완전히 잘못 틀어질 수 있는 제어의 역전 문제다.
- 제어의 역전으로 빚어진 믿지 못할 코드를 완화할 장치가 없는 상황에서 콜백으로 코딩하면 지금 버그를 심어놓은 것이나 별다를 바 없다. 잠재적인 버그도 버그니까.
2.4 콜백을 구하라
function success(data) {
console.log(data)
}
function failure(err) {
console.error(err)
}
ajax("http://naver.com", success, failure)
위의 설계에서 에러 처리기는 필수가 아니며, 작성하지 않으면 에러는 조용히 무시된다.
에러 우선 스타일이라는 콜백 패턴 또한 많이 쓴다. 단일 콜백 함수는 에러 객체를 첫 번째 인자로 받는다. 성공 시 이 인자는 빈/falsy 객체로 채워지지만 실패 시 truthy 또는 에러 객체로 세팅된다.
function response(err, data) {
if (err) {
console.error(err)
} else {
console.log(data)
}
}
ajax("http://naver.com", response)
- 믿음성 문제가 대체로 해결된 것처럼 보이지만 실상은 전혀 그렇지 않다. 원하지 않는 반복적인 호출을 방지하거나 걸러내는 콜백 기능이 전혀 없다. 더구나 이제는 성공/에러 신호를 동시에 받거나 아예 전혀 못받을수도 있으므로 상황별로 코딩해야 하는 부담까지 있다. 또, 재사용 불가능한, 장황한 관용 코드라서 실제로 애플리케이션을 개발할 때마다 이런식으로 타이핑 해야한다면 힘들다.
- 콜백을 한 번도 호출하지 않으면? 이런 경우는 이벤트를 취소하는 타임아웃을 걸어 놓어야 한다.
function timeoutify(fn, delay) {
var intv = setTimeout(function () {
intv = null
fn(new Error("타임아웃"))
}, delay)
return function () {
if (intv) {
clearTimeout(intv)
fn.apply(this, arguments)
}
}
}
function foo(err, data) {
if (err) {
console.error(err)
} else {
console.log(data)
}
}
ajax("http://some.url.1", timeoutify(foo, 500))
- 이벤트 루프 대기열 바로 다음 차례라고 해도 예측 가능한 비동기 콜백이 될 수 있게 항상 비동기 호출을 할 수도 있다.
function result(data) {
console.log(a)
}
var a = 0
ajax("..미리 캐시된 URL..", result)
a++
- 위 코드의 결과는 1일까 0일까? 조건에 따라 다르다. 주어진 API가 항상 비동기로 작동할지 확신이 없다면? 아래 코드같은 유틸리티를 만들어 쓰면 된다.
function asyncify(fn) {
var orig_fn = fn,
intv = setTimeout(function () {
intv = null
if (fn) fn()
}, 0)
fn = null
return function () {
if (intv) {
fn = orig_fn.bind.apply(orig_fn, [this].concat([].slice.call(arguments)))
} else {
orig_fn.apply(this, arguments)
}
}
}
// 사용법
function result(data) {
console.log(a)
}
var a = 0
ajax("..미리 캐시된 URL..", asyncify(result))
a++
- 요청을 캐싱한 상태에서 즉각 콜백을 호출하여 귀결하든지, 아니면 데이터를 다른 곳에서 가져오는 터라 나중에 비동기적으로 완료되든지 값은 항상 1이된다.
- 위 예들만 봐도 간단한 동작의 믿음성 문제를 해결하기 위해 얼마나 코드가 비대해졌는지 알 수 있다. 콜백은 늘 이런식이다. 원하는 바를 이룰 수 있게 해주지만 그걸 손에 넣으려면 고생할 각오를 해야 하는데, 코드 추론에 투자해야 할 노력이 종종 능령의 한계를 벗어나곤 한다. 결국 이런 코드를 경험한 사람들은 언어 자체의 내장 API나 다른 언어에서 지원하는 방법을 갈망해 왔는데, ES6부터 기막힌 해결책이 나오는데 그 이름이 바로 Promise다.
'JavaScript' 카테고리의 다른 글
자바스크립트 더 나은 개발자가 되기 위한 세가지 (0) | 2020.11.14 |
---|---|
[You don`t Know JS 정리] ch.3 프라미스 (0) | 2020.08.24 |
[You don`t Know JS 정리] ch.1 비동기성 (0) | 2020.08.20 |
[You don`t Know JS 정리] ch.6 작동 위임 (0) | 2020.08.18 |
[You don`t Know JS 정리] ch.4 클래스와 객체 혼합 (0) | 2020.08.17 |