노트

콜백

Callback Function

CS#js#python · 연결된 개념 9개

쉽게 말하면

콜백은 세탁소에 옷을 맡기며 '다 되면 이 번호로 전화 주세요' 하고 남기는 연락처예요. 내가 계속 확인하는 대신, 알맞은 때에 상대가 불러 주게 맡기는 거죠.

비유가 깨지는 곳 전화처럼 꼭 나중에 오는 건 아니에요. map의 콜백은 그 자리에서 바로 불리고, 언제·몇 번 부를지는 받는 쪽이 정해서 두 번 부르거나 안 부를 수도 있어요.

콜백(callback)은 다른 함수에 인자로 넘겨, 그 함수가 알맞은 때에 대신 불러 주게 하는 함수다. 함수를 인자로 받는 함수는 고차 함수(higher-order function)라고 한다. 핵심은 "직접 부르지 않고, 불러 달라고 맡긴다"는 점이다.

콜백이 곧 비동기는 아니다

콜백을 언제 부를지는 받는 쪽이 정한다. 그 자리에서 바로 부르면 동기 콜백, 나중에 부르면 비동기 콜백이다(동기·비동기와 블로킹·논블로킹).

const doubled = [3, 1, 2].map((n) => n * 2)    // 동기: map이 끝나기 전에 전부 불림
const sorted = [3, 1, 2].sort((a, b) => a - b) // 동기: 비교 함수
 
setTimeout(() => console.log('later'), 0)   // 비동기: 지금 코드가 끝난 뒤
button.addEventListener('click', onClick)   // 비동기: 클릭할 때마다

setTimeout이나 이벤트 핸들러 같은 비동기 콜백은 이벤트 루프의 태스크 큐에서 기다리다가 콜 스택이 비면 실행된다. Promise의 then 콜백은 마이크로태스크 큐로 간다.

제어를 넘긴다는 것

콜백을 넘기면 "언제, 몇 번, 어떤 인자로 부를지"를 받는 쪽이 정한다(제어의 역전, inversion of control). 템플릿 메서드·전략 패턴과 같은 아이디어를 함수 하나로 표현한 셈이다. 편리한 만큼 대가도 따른다.

  • 믿을 수 없는 호출: 남의 코드가 콜백을 두 번 부르거나, 아예 부르지 않거나, 예외를 삼킬 수 있다
  • 콜백 지옥(callback hell): 비동기 작업을 차례로 이으면 콜백 안에 콜백이 들어가 들여쓰기가 깊어지고, 에러 처리가 단계마다 흩어진다
  • 에러 우선 콜백(error-first callback): Node.js의 콜백 API는 (err, result) => {}처럼 첫 인자로 에러를 넘기는 관례를 따른다(에러가 없으면 null). 단계마다 if (err)를 직접 확인해야 한다
readFile('a.txt', (err, a) => {
  if (err) return done(err)
  readFile(a, (err, b) => {
    if (err) return done(err)
    save(b, (err) => done(err))
  })
})

Promise는 이 문제를 풀려고 나왔다. 결과를 콜백으로 받는 대신 앞으로 올 결과를 나타내는 객체를 돌려준다. Promise는 한 번만 이행되거나 거부되고, 순서는 then 체인으로, 에러는 끝에 단 catch 하나로 모아 다룬다. React의 Render Props 패턴가 중첩되며 깊어지던 것도 같은 꼴의 문제다.

때로는 동기, 때로는 비동기로 부르지 않는다

캐시가 있으면 콜백을 바로 부르고, 없으면 네트워크를 다녀와서 부르는 API는 호출하는 쪽에서 실행 순서를 예측할 수 없다. 그래서 콜백을 받는 API는 항상 동기로 부르거나 항상 비동기로 불러야 한다. Node.js 문서는 "API는 굳이 그럴 필요가 없을 때도 항상 비동기여야 한다"는 설계 철학을 소개하며, 바로 줄 수 있는 결과(예: 인자 검사 에러)도 process.nextTick으로 미뤄 두었다가 콜백을 부르는 예를 든다.

다른 언어

  • 파이썬: sorted(items, key=len)의 key, asyncio의 add_done_callback(asyncio 태스크 예외가 조용히 사라지는 문제)
  • 자바: Runnable·Comparator 같은 함수형 인터페이스에 람다를 넘긴다. 비동기 결과는 CompletableFuture.thenApply로 잇는다
  • React: 자식에게 넘기는 이벤트 핸들러도 콜백이다. 참조를 유지해야 할 때는 useCallback을 쓴다(memo·useMemo·useCallback)

출처: MDN — Callback function · MDN — Using promises: Error handling · Node.js — JavaScript Asynchronous Programming and Callbacks · Node.js — The Node.js Event Loop: Why would that be allowed?

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • 스택

    나중에 넣은 것을 먼저 꺼내는(Last In, First Out) 자료구조. 접시를 쌓듯 맨 위에서만 넣고(push) 꺼낸다(pop). 넣기·꺼내기 모두 O(1)이다.

  • requestAnimationFrame

    "다음 화면을 그리기 직전에 이 함수를 실행해 달라"고 브라우저에 예약하는 API. 콜백은 태스크 큐도 마이크로태스크 큐도 아닌 별도 목록에 들어가고, 이벤트 루프의 렌더링 단계에서 페인트 직전에 한꺼번에 실행된다.

  • 재귀

    함수가 자기 자신을 다시 호출해 문제를 푸는 방식. 같은 함수를 점점 작은 입력으로 부르다가 더 나눌 필요가 없는 지점(기저 조건, Base Case)에서 멈춘다.

  • 예외 삼키기

    예외를 잡아 놓고 아무 처리 없이 흘려보내는 안티패턴(Anti-pattern). 빈 catch뿐 아니라 debug 레벨로만 찍고 정상인 척 진행하는 것도 마찬가지다.

  • Kafka

    Kafka는 이벤트(메시지)를 디스크의 추가 전용 로그(append-only log)에 쌓아 두고, 여러 소비자가 각자 읽은 위치를 기억하며 꺼내 가게 하는 분산 이벤트 스트리밍 플랫폼(distributed event streaming platform)이다. 흔히 메시지 큐(message queue)라고 부르지만 동작은 큐보다 로그에 가깝다.

보기 옵션