본문으로 건너뛰기
김신건의 로그

[Javascript] 콜백

· 수정 · 📖 약 2분 · 828자/단어 #javascript #async #function #fundamentals
콜백, callback, 콜백 함수, callback function, JavaScript callback, JS callback

정의

콜백 (callback): 다른 함수의 인자로 넘겨져, 받은 쪽(caller)이 적절한 시점에 호출하는 함수.

일급 함수 특성이 있어야 가능한 패턴이다. JavaScript 에서는 함수를 값으로 넘길 수 있으므로 콜백이 자연스럽다.

function greet(name, callback) {
  const message = `안녕, ${name}!`;
  callback(message);  // 받은 함수를 적절한 시점에 호출
}

greet('철수', (msg) => console.log(msg));
// 안녕, 철수!

사용 상황

  • 이벤트 처리: addEventListener('click', fn)
  • 타이머: setTimeout(fn, 1000), setInterval(fn, 500)
  • 비동기 I/O: fs.readFile(path, fn) (Node.js)
  • 배열 변환: map(fn), filter(fn), sort(fn) (동기 콜백)
  • 제어 흐름 역전 (Inversion of Control): 라이브러리가 호출 시점 결정

시각화

동기 콜백과 비동기 콜백의 실행 시점 차이:

sequenceDiagram
    participant 호출자
    participant 고차함수
    participant 콜백

    호출자->>고차함수: "fn(callback)"
    rect rgb(200, 230, 255)
        Note over 고차함수,콜백: 동기 콜백 - 즉시 호출
        고차함수->>콜백: "callback() 호출"
        콜백-->>고차함수: 반환
    end
    고차함수-->>호출자: 반환

    Note over 호출자,콜백: 비동기 콜백 - 나중에 호출
    호출자->>고차함수: "setTimeout(callback, 1000)"
    고차함수-->>호출자: "즉시 반환 (등록만)"
    Note over 호출자: 호출자 계속 실행
    고차함수->>콜백: "1000ms 후 callback() 호출"

기본 사용법

동기 콜백

배열 메서드의 콜백은 동기적으로 즉시 실행된다.

const numbers = [1, 2, 3];

// map 의 콜백: 배열 순회 중 즉시 호출
const doubled = numbers.map(x => {
  console.log(`처리 중: ${x}`);
  return x * 2;
});
// 처리 중: 1
// 처리 중: 2
// 처리 중: 3
console.log('map 완료');
// map 완료
// doubled = [2, 4, 6]

비동기 콜백

타이머나 I/O 콜백은 나중에 호출된다.

console.log('시작');

setTimeout(() => {
  console.log('타이머 콜백');  // 나중에 실행
}, 1000);

console.log('끝');
// 시작
// 끝
// (1초 후) 타이머 콜백

setTimeout 은 콜백을 등록만 하고 즉시 반환한다. 콜백은 이벤트 루프가 처리한다.

에러 우선 콜백 (Error-First Callback)

Node.js 가 정착시킨 관행: 첫 인자는 에러 (없으면 null), 두 번째 인자는 결과.

const fs = require('fs');

fs.readFile('config.json', 'utf8', (err, data) => {
  if (err) {
    console.error('읽기 실패:', err.message);
    return;  // 반드시 return 으로 이후 코드 실행 방지
  }
  const config = JSON.parse(data);
  console.log(config);
});

에러 체크를 빠뜨리면 dataundefined 인 채로 진행되어 조용히 실패한다.

this 바인딩 문제

콜백에서 this 는 호출하는 쪽의 컨텍스트가 되어 예상과 다르게 동작할 수 있다.

문제

class Timer {
  constructor() {
    this.count = 0;
  }

  start() {
    // ❌ function 콜백: this 가 Timer 인스턴스가 아님
    setInterval(function() {
      this.count++;        // this 는 globalThis 또는 undefined (strict)
      console.log(this.count);  // NaN 또는 TypeError
    }, 1000);
  }
}

해결 1: 화살표 함수

class Timer {
  constructor() {
    this.count = 0;
  }

  start() {
    // ✅ 화살표 함수: this 를 외부(Timer 인스턴스)에서 lexical 로 캡처
    setInterval(() => {
      this.count++;
      console.log(this.count);  // 1, 2, 3, ...
    }, 1000);
  }
}

const timer = new Timer();
timer.start();

해결 2: bind

class Timer {
  constructor() {
    this.count = 0;
    this.tick = this.tick.bind(this);  // 생성자에서 미리 바인딩
  }

  tick() {
    this.count++;
    console.log(this.count);
  }

  start() {
    setInterval(this.tick, 1000);
  }
}

해결 3: 클로저로 this 저장 (구식)

class Timer {
  constructor() {
    this.count = 0;
  }

  start() {
    const self = this;  // this 를 변수에 저장
    setInterval(function() {
      self.count++;
      console.log(self.count);
    }, 1000);
  }
}

화살표 함수가 도입된 이후로는 self = this 패턴은 거의 쓰지 않는다.

실전 예시

커스텀 이벤트 시스템

class EventEmitter {
  constructor() {
    this.listeners = {};
  }

  on(event, callback) {
    (this.listeners[event] ??= []).push(callback);
    return this;  // 메서드 체이닝 지원
  }

  emit(event, ...args) {
    (this.listeners[event] ?? []).forEach(cb => cb(...args));
  }

  off(event, callback) {
    this.listeners[event] = (this.listeners[event] ?? [])
      .filter(cb => cb !== callback);
  }
}

const emitter = new EventEmitter();

const onData = (data) => console.log('받음:', data);
emitter.on('data', onData);
emitter.on('data', (data) => console.log('로깅:', data));

emitter.emit('data', { id: 1 });
// 받음: { id: 1 }
// 로깅: { id: 1 }

emitter.off('data', onData);
emitter.emit('data', { id: 2 });
// 로깅: { id: 2 }

병렬 콜백 수집 (Promise 이전 패턴)

여러 비동기 콜백 결과를 모을 때의 고전 패턴:

function parallel(tasks, done) {
  const results = [];
  let remaining = tasks.length;

  if (remaining === 0) return done(null, []);

  tasks.forEach((task, i) => {
    task((err, result) => {
      if (err) return done(err);
      results[i] = result;
      if (--remaining === 0) done(null, results);
    });
  });
}

// 사용
parallel(
  [
    cb => setTimeout(() => cb(null, 'A'), 100),
    cb => setTimeout(() => cb(null, 'B'), 200),
    cb => setTimeout(() => cb(null, 'C'), 50),
  ],
  (err, results) => {
    if (err) return console.error(err);
    console.log(results);  // ['A', 'B', 'C'] (순서 보장)
  }
);

오늘날에는 Promise.all 이 이 역할을 대신한다.

콜백을 Promise 로 변환 (promisify)

function promisify(fn) {
  return (...args) => new Promise((resolve, reject) => {
    fn(...args, (err, result) => {
      if (err) reject(err);
      else resolve(result);
    });
  });
}

// Node.js 의 콜백 스타일 API 를 Promise 로
const readFile = promisify(require('fs').readFile);

const data = await readFile('config.json', 'utf8');
console.log(data);

Node.js 는 util.promisifyfs/promises 모듈로 표준 제공한다.

동기 vs 비동기 콜백 비교

구분동기 콜백비동기 콜백
호출 시점즉시 (함수 내부에서)나중에 (이벤트 루프)
예시map, filter, sortsetTimeout, fetch, fs.readFile
try/catch가능불가 (콜 스택 사라짐)
반환값 활용가능불가 (콜백으로만 전달)

함정

WARNING

Inversion of Control: 콜백을 외부 라이브러리에 넘기면 호출 시점, 횟수, 에러 처리 방식이 모두 상대방 책임이 된다. 콜백이 여러 번 호출될 수도, 아예 호출 안 될 수도 있다.

WARNING

에러 전파 불가: 비동기 콜백이 호출되는 시점에는 원래 콜 스택이 이미 사라졌으므로 try/catch 로 잡을 수 없다.

// ❌ 작동 안 함
try {
  setTimeout(() => {
    throw new Error('콜백 내부 에러');
  }, 100);
} catch (err) {
  console.error(err);  // 잡히지 않음
}

// ✅ 콜백 내부에서 직접 처리
setTimeout(() => {
  try {
    riskyOperation();
  } catch (err) {
    handleError(err);
  }
}, 100);

WARNING

콜백 지옥: 순차 의존 비동기 작업을 콜백으로 연결하면 들여쓰기가 오른쪽 끝까지 깊어진다. (콜백 지옥 참고)

// 콜백 지옥의 시작
loadUser(42, (err, user) => {
  loadPosts(user.id, (err, posts) => {
    loadComments(posts[0].id, (err, comments) => {
      // 점점 깊어짐...
    });
  });
});

이 문제들을 풀기 위해 Promise 가 도입되었다.

관련 위키

이 글의 용어 (8개)
[Javascript] 이벤트 루프javascript
정의 이벤트 루프 (Event Loop) 는 싱글 스레드 JavaScript 엔진이 비동기 작업을 처리하기 위해 돌리는 무한 루프다. 콜 스택이 비어있을 때마다 마이크로태스크 큐…
[Javascript] 콜백 지옥javascript
정의 콜백 지옥 (callback hell) 또는 둠 피라미드 (pyramid of doom): 순차 의존이 있는 비동기 작업을 으로 연결할 때 들여쓰기가 오른쪽으로 점점 깊어지…
[Javascript] Arrow Functionjavascript
정의 화살표 함수 (arrow function) 는 ES2015 (ES6) 에서 도입된 함수 표현식. 모양. 더 짧은 문법뿐 아니라 가 lexical 로 바인딩 되는 본질적 차이…
[Javascript] async/awaitjavascript
정의 / 는 기반 비동기 코드를 마치 동기 코드처럼 쓸 수 있게 해주는 ES2017 의 문법 설탕. 본질은 Promise 그 자체, 문법만 다르다. 전체 동작 메커니즘은 글 참조…
[Javascript] Promisejavascript
정의 Promise 는 미래의 어떤 시점에 결정될 값을 나타내는 객체다. ECMAScript 2015 (ES6) 에 도입되어 기반 비동기의 가독성과 에러 처리 문제를 해결했다. …
[Javascript] this 바인딩javascript
정의 JavaScript 의 는 호출 방식에 따라 결정 되는 동적 바인딩. 4 가지 호출 방식 + arrow function (lexical) 규칙. 5 가지 규칙 | # | 규…
고차 함수javascript
정의 고차 함수 (Higher-Order Function, HOF): 다음 조건 중 하나 이상을 만족하는 함수. 1. 함수를 인자로 받는다. 2. 함수를 반환한다. 특성이 있어야…
일급 함수javascript
정의 일급 함수 (First-Class Function): 함수를 숫자, 문자열, 객체처럼 값으로 다룰 수 있는 언어의 특성. 다음 세 가지가 모두 가능하면 그 언어의 함수는 일…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기