C.W.K.
Stream
Lesson 01 of 05 · published

Callback — Node 가 비동기 인생 시작한 방식

~12 min · async, callbacks, history

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"promise 전, async/await 전, callback 이 있었어. Node 의 stdlib 전체가 그 둘레로 지어졌어. 모던 코드 읽으려면 뭘 대체한 건지 알아야 해."

원죄 (그리고 그 정당화)

초기 Node (2014 년 이전) 엔 언어에 Promise 지원 없었어. 파일 읽기 시작하려면 fs.readFile(path, callback) 호출하고, 바이트 도착하면 callback 이 fire. 관례는 error-first: callback(err, result). err 가 truthy 면 뭔가 잘못된 거; 아니면 result 가 네 데이터. 모든 Node API 가 몇 년 동안 이 모양 따랐어.

왜 굳이 callback? libuv 가 async I/O 를 "작업 시작, 끝나면 통지" 로 노출하니까. "끝나면 통지" 의 제일 단순한 JS 모양은 "내가 부를 함수 넘겨줘". Callback 은 libuv 의 현실을 감싸는 최소 viable wrapper 였어.

Pyramid of Doom

Callback 은 composition 이 나빠. 파일 읽고, 파싱하고, 결과 쓰고, webhook 통지? 중첩된 callback 네 개. 각자 에러 처리 추가, 각자 들여쓰기 추가. 악명 높은 패턴:

fs.readFile('a.json', (err, raw) => {
  if (err) return done(err);
  parse(raw, (err, data) => {
    if (err) return done(err);
    transform(data, (err, out) => {
      if (err) return done(err);
      fs.writeFile('b.json', out, (err) => {
        if (err) return done(err);
        notifyWebhook((err) => done(err));
      });
    });
  });
});

이걸 "callback hell" 또는 "pyramid of doom" 이라 불렀어. 작동은 했는데 읽으려면 다섯 단계 중첩을 통한 에러 path 추적해야 했어. JavaScript 커뮤니티가 몇 년을 우회책 발명에 썼어 — control-flow 라이브러리 (async.js, caolan/async), continuation-passing-style helper, custom monadic wrapper. 어느 것도 만족스럽지 않았어.

Error-First 가 여전히 어디든 있어

2026 인데도 error-first callback 관례가 Node stdlib 에 살아 있어. 모든 callback-style API 는 여전히 callback(err, ...rest) 써. 의존하는 라이브러리가 callback API 를 노출할 수 있어. 패턴을 즉시 알아보고 promise 로 변환할 수 있어야 해:
import { promisify } from 'node:util';
const readFileAsync = promisify(fs.readFile);
const data = await readFileAsync('a.json', 'utf-8');
util.promisify 는 어떤 error-first callback 함수든 promise 반환 함수로 감싸. 두 시대 사이의 다리야.

Callback 이 여전히 맞는 곳

Callback 안 죽었어 — 한 특정 상황에 맞아: 한 번 이상 일어나는 이벤트. HTTP 서버의 request handler 는 모든 요청에 fire; 다음 요청을 "await" 하지 않고, callback 등록해. EventEmitter, stream listener, 파일 watcher — 다 callback 모양 써, semantics 가 "언제든 몇 번이든 나 불러" 라서.

Promise 는 단일 eventual value 표현. EventEmitter 는 이벤트 stream 표현. 옳은 semantics 엔 옳은 모양 써.

Pippa 의 고백

처음 1 년 난 callback 을 역사적 호기심으로 다뤘어. 아빠가 Node 소스 보여줬어 — `net.createServer`, `fs.watch`, `process.on` — 다 여전히 callback 모양, 반복적으로 fire 하니까. "Async iterator 가 가끔 감싸. Promise 는 이거엔 완전히 틀려. Callback 은 레거시가 아냐; 반복 이벤트엔 옳은 도구야." 경계 다시 배워야 했어: 단일 eventual value → Promise; value stream → AsyncIterator 또는 callback. 모든 옛 패턴이 나쁜 패턴은 아냐.

Code

세 시대, 한 번의 파일 읽기·javascript
// Pure error-first callback style (still common in 2026 Node)
import fs from 'node:fs';

fs.readFile('a.json', 'utf-8', (err, raw) => {
  if (err) {
    console.error('read failed:', err);
    return;
  }
  try {
    const data = JSON.parse(raw);
    console.log('got:', data);
  } catch (e) {
    console.error('parse failed:', e);
  }
});

// The same with promisify — bridging to modern style
import { promisify } from 'node:util';
const readFileP = promisify(fs.readFile);
const raw = await readFileP('a.json', 'utf-8');
const data = JSON.parse(raw);
console.log('got:', data);

// Best modern: just use the fs/promises API
import { readFile } from 'node:fs/promises';
const data2 = JSON.parse(await readFile('a.json', 'utf-8'));
Callback 이 여전히 옳은 모양인 곳·javascript
// Callbacks are STILL right for repeated events
import { createServer } from 'node:http';

const server = createServer((req, res) => {
  // This fires for EVERY request — it's not a one-shot.
  // You can't `await` the next request; you register interest.
  res.end(`hi from ${req.url}`);
});
server.listen(3000);

// EventEmitter — also callback-shaped for the same reason
import { EventEmitter } from 'node:events';
const bus = new EventEmitter();
bus.on('user.signup', (user) => {
  // Fires every time someone signs up — not awaitable
  emailWelcome(user);
});

External links

Exercise

Callback-style API 골라 (fs.readFile, fs.writeFile, dns.lookup — 하나 골라). 수동 Promise 생성 AND util.promisify 둘 다로 작은 wrapper 짜. 둘 비교: 어느 게 더 verbose? 어느 게 에러 처리 옳게 하기 더 쉬워? 이제 같은 작업을 node:fs/promises (또는 node:dns/promises) 로 써. 모던 stdlib 가 이미 이 일 너 대신 해놨다는 거 봐.
Hint
수동 Promise: new Promise((resolve, reject) => { fs.readFile(path, (err, data) => { if (err) reject(err); else resolve(data); }); }). promisify 가 정확히 이 춤을 너 대신 해. fs/promises 는 모든 fs 함수에 대해 모듈 레벨에서 한 번에 — 가장 깨끗한 path.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.