Skip to content

Latest commit

 

History

History
158 lines (136 loc) · 9.44 KB

Chapter2.md

File metadata and controls

158 lines (136 loc) · 9.44 KB

Chapter2. 콜백

  • 콜백은 자바스크립트에서 비동기성을 표현하고 관리하는 가장 일반적인 기법이자, 사실상 자바스크립트 언어에서 가장 기본적인 비동기 패턴이다. 이번 장에서는 콜백의 실체를 깊이 있게 살펴보고 이보다 더 정교한 비동기 패턴이 나오게 된 계기를 살펴본다.

2.1 연속성

// A
ajax("..", function(..) {
  // C
});
// B
  • A와 B는 지금, C는 나중에 해당한다.
  • 콜백 함수는 프로그램의 연속성을 감싼/캡슐화한 장치다.

2.2 두뇌는 순차적이다

  • 인간이 정말 동시에 두 개의 의식을 갖고 의도적인 행동을 하면서 정확히 같은 순간에 두 가지 일을 한꺼번에 생각/추론할 수 있을까? 두뇌 기능이 최고 수준에 이른다면 과연 이러한 병렬 멀티스레딩이 가능할까?
    • 아니오
    • 인간의 두뇌는 그렇게 만들어지지 않았다. 인간은 싱글태스커에 더 가깝다.
    • 멀티태스킹을 하는 것처럼 보이는 상황도 실은 우리가 아주 재빠른 콘텍스트 교환기 Context Switcher처럼 행동하고 있을 뿐이다. 다시 말해 여러 작업 사이를 재빨리 연속적으로 왔다갔다 하면서 각 작업을 아주 작고 짧은 덩이로 쪼개어 동시에 처리하는 것이다. 인간의 두뇌는 이벤트 루프 큐처럼 작동한다.

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)
})
  • 위 예제는 비동기 단계(작업, 프로세스)를 세 개의 함수가 서로 중첩된 형태로 표현했다. 이른바 콜백 지옥이다. 또는 운명의 피라미드라고도 불린다.
  • 중첩없이 다시 써보면 아래와 같다.
listen("click", handler)

function handler() {
  setTimeout(request, 500)
}

function request() {
  ajax("http://some.url.1", response)
}

function response(text) {
  if (text === "hello") {
    handler()
  } else if (text === "world") {
    request()
  }
}
  • 위의 예제는 중첩/들여쓰기로 도배했던 이전 버전보다는 알아보기 훨씬 편하다. 하지만 어찌 왰든 콜백 지옥에 취약한 건 매한가지다. 이유는?
    • 선형적으로 이 코드를 추론하자면 한 함수에서 다음 함수로, 또 그 다음 함수로, 시퀀스 흐름을 따라가기 위해 코드 베이스 전체를 널뛰기해야 한다.

2.3 믿음성 문제

  • 제어권 교환은 콜백 중심적 설계 방식의 가장 큰 (그리고 발견하기 어려운) 문제점이다. 내가 작성한 프로그램이라도 실행 흐름을 서드 파티에 의존해야 하는 상황을 제어의 역전 Inversion of Control이라고 한다.

2.3.1 제어권 교환이 뭐 그리 대수.. 대수인 이유 알아보기

  • 고가의 TV를 판매하는 쇼핑몰의 전자 상거래 결제 시스템은, (1) 사용자가 마지막 페이지에서 확인 버튼을 클릭하면, (2) 분석 추적 솔루션을 만들어 납품하는 모 회사에서 제공한 서드 파티 함수를 호출하여 구매 정보를 추적할 수 있도록 되어 있다.
    • 이 추적 유틸리티는 성능 문제로 비동기 방식으로 코딩되었으며, 그래서 호출 시 콜백 함수를 넘겨야 한다. 콜백 함수가 시작되면 고객의 신용 카드를 결제하고 감사 페이지로 이동하는 코드가 잇따라 실행된다.
analytics.trackPurchase(purchaseData, function() {
  chargeCreditCard()
  displayThankyouPage()
})
  • 어느 날 문제가 발생했다. 고객 한명이 TV를 구매했는데, 다섯 번이나 연속 결제됐다며 열받아서 이야기한다.
  • 로그 파일을 캐보니 원인은 하나, 위 추적 유틸리티가 콜백 함수를 한 번만 호출하는 대신 다섯 차례나 연달아 불렀다.
  • 알고 보니, 솔루션 업체가 납품한 코드 중 초당 한 번씩 주어진 콜백 함수를 호출하는데 최대 5초 동안 재시도하다가 타임아웃 에러를 내도록 만들어진 테스트 단계의 코드가 포함된 것이 문제였다. 이것이 서드 파티에 제어권이 있는 코드의 취약점이다.
  • 개미 굴을 파는 심정으로 콜백 호출 시 오류가 날만한 경우의 수를 모두 따져보기로 했다:
    • 콜백을 너무 일찍 부르는 경우
    • 콜백을 너무 늦게 부르는 경우
    • 콜백을 너무 적게 또는 너무 많이 부르는 경우
    • 필요한 환경/인자를 정상적으로 콜백에 전달하지 못하는 경우
    • 일어날지 모르는 에러나 예외는 무시...
  • 그런데 이렇게 모든 경우별로 보완 로직을 구현해 넣는다는 게 얼마나 끔찍한 일인지, 그리고 추적 유틸리티에 넘겨주는 콜백이 전적으로 믿을만한지도 이제 의문이다. 이것이 콜백 지옥이다.

2.3.2 남의 코드뿐만 아니라

  • 서드 파티를 안쓰고, 자체 코드 베이스에서 이론적으로 제어하는 유틸리티라면 신뢰할 수 있을까? ... 결국에는 매번 비동기적으로 부를 때마다 콜백 함수에 반복적인 관용 코드/오버헤드를 넣는 식으로 손수 필요한 장치를 넣어주어야 한다. 이렇게 삽입을 해도 완전히 잘못 될 수 있는 게 제어의 역전 문제다.
  • 제어의 역전으로 빚어진 믿지 못할 코드를 완화할 장치가 없는 상황에서 콜백으로 코딩하면 지금 버그를 심어놓은 것이나 다름없다. 잠재적인 버그도 버그는 버그니까.

2.4 콜백을 구하라

  • 에러 우선 스타일 Error-First Style이라는 콜백 패턴(노드 스타일이라고도 불림)을 살펴보자. 이의 콜백 함수는 에러 객체를 첫번째 인자로 받는다. 성공 시 이 인자는 빈/falsy 객체로 채워지지만, 실패 시 truthy 또는 에러 객체로 세팅된다.
function response(err.data) {
  // 에러인가?
  if (err) {
    console.error(err)
  }
  // 아니면 성공한 것으로 본다
  else {
    console.log(data)
  }
}

ajax("http://some.url.1", response)
  • 실패한 경우든 성공한 경우든, 다음 몇 가지 사실을 알 수 있다.
    1. 원하지 않는 반복적인 호출을 방지하거나 걸러내는 콜백 기능이 전혀 없다.
    2. 표준적인 패턴의 모습을 띠고 있음에도 불구하고 재사용 불가능한, 장황한 관용 코드라서 실제로 앱을 개발할 때 매 콜백마다 타이핑해야 한다.
    3. 콜백을 한번도 호출하지 않는다면? 이런 경우가 중요하게 처리되어야 한다면, 이벤트를 취소하는 타임아웃을 걸어놓아야 한다.
  • 동기냐 비동기냐에 관한 비결정성은 버그를 추적하기 아주 곤욕스럽게 만든다. 주어진 API가 항상 비동기로 작동할지 확신이 없다면? 다음과 같은 유틸리티를 만들어 쓸 수도 있다.
function asyncify(fn) {
  var orig_fn = fn,
      intv = setTimeout(function() {
        intv = null
        if (fn) fn()
      }, 0)
      
      fn = null

      return function() {
        // 비동기 차례를 지나갔다는 사실을 나타내기 위해
        // intv 타이머가 기동하기도 전에 너무 빨리 발사
        if (intv) {
          fn = orig_fn.bind.apply(
            orig_fn,
            // 인자로 전달된 값들을 커링하면서 this에 bind() 호출 인자를 추가한다
            [this].concat([].slice.call(arguments))
          )
        }
        // 이미 비동기다
        else {
          orig_fn.apply(this, arguments)
        }
      }
}


// 아래와 같이 asyncify 함수를 사용할 수 있다
function result(data) {
  console.log(a)
}

var a = 0

ajax("..미리 캐시된 URL..", asyncify(result))
a++

2.5 정리하기

  • 콜백은 자바스크립트에서 비동기성을 표현하는 기본 단위다.
    • 그러나 자바스크립트와 더불어 점점 진화하는 비동기 프로그래밍 환경에서 콜백만으로는 충분치 않다.
  1. 사람의 두뇌는 순차적, 중단적, 단일-스레드 방식으로 계획하는 데 익숙한데, 콜백은 비동기 흐름은 비선형적, 비순차적인 방향으로 나타내므로 구현된 코드를 제대로 이해하기가 매우 어렵다. 추론하기 곤란한 코드는 곧 악성 버그를 품은 나쁜 코드로 이어진다. 그래서 비동기성을 좀더 동기적, 순차적, 중단적인 모습으로, 즉 우리 두뇌가 사고하는 방식과 유사하게 표현할 방법이 필요하다.
  2. 콜백은 프로그램을 진행하기 위해 제어를 역전, 즉 제어권을 다른 파트에 암시적으로 넘겨줘야 하므로 골치가 아프다. 이런 문제를 해결하기 위해 임시 로직을 짜넣으면 당장은 난관을 모면할 수는 있지만, 생각만큼 구현이 쉽지 않고 계속 이런 식으로 하다 보면 거칠고 유지 보수가 어려운 코드가 된다.
  • 콜백을 능가하는 뭔가, 더욱 정교하고 역량있는 비동기 패턴이 절실하게 필요하다. 바로 프라미스 Promise와 그 이후에 나타난 최신 기술들이다.