Callback

자바스크립트에서 콜백(Callback)은 다른 함수에 인자로 전달되는 함수를 의미한다.
이 함수는 특정 작업이 완료된 후에 실행되도록 설계되어 있다.
콜백은 자바스크립트의 비동기 프로그래밍의 기초가 되는 개념이다.

자바스크립트에서 콜백은 여전히 중요한 개념이며, 현대적인 비동기 패턴(Promise, Async/Await)의 기초가 되었다.

콜백은 다음과 같은 핵심 영역에서 여전히 중요한 역할을 한다:

  1. 이벤트 처리: 사용자 상호작용, 타이머, 네트워크 이벤트 등
  2. 함수형 프로그래밍: 고차 함수와 데이터 변환 파이프라인
  3. API 설계: 유연한 확장성과 컴포지션을 가능하게 함
  4. 비동기 흐름 제어: 기본적인 비동기 작업 조정

비록 콜백 지옥과 같은 문제로 인해 Promise와 Async/Await가 더 선호되는 경우가 많지만, 적절한 패턴과 함께 사용되면 콜백은 여전히 강력하고 유용한 도구이다. 현대적인 자바스크립트 개발자는 콜백을 효과적으로 사용하는 방법과 더 고급 비동기 패턴으로 전환하는 방법을 모두 이해하는 것이 중요하다.

콜백의 정의

콜백 함수는 다음과 같은 특징을 가진다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 기본적인 콜백 구조
function doSomething(callback) {
  // 어떤 작업 수행
  // ...
  
  // 작업 완료 후 콜백 실행
  callback();
}

// 콜백 함수 전달
doSomething(function() {
  console.log("콜백 함수가 실행되었습니다!");
});

콜백의 작동 원리

자바스크립트는 단일 스레드 언어이지만, 브라우저 환경에서는 Web API를 통해 비동기 작업을 수행할 수 있다.
콜백은 이러한 비동기 작업의 완료 시점을 처리하기 위한 방법이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
console.log("시작");

setTimeout(function() {
  console.log("2초 후 실행되는 콜백");
}, 2000);

console.log("끝");

// 출력:
// 시작
// 끝
// 2초 후 실행되는 콜백

위 예제에서 볼 수 있듯이, setTimeout의 콜백은 동기 코드가 모두 실행된 후에 실행된다.

콜백의 종류

동기 콜백(Synchronous Callbacks)

동기 콜백은 함수가 즉시 실행되고 결과를 바로 반환한다.
주로 배열 메서드와 같은 데이터 처리에 사용된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 동기 콜백 예시
const numbers = [1, 2, 3, 4, 5];

// map 메서드는 각 요소에 콜백 함수를 적용하고 결과 배열을 반환합니다
const doubled = numbers.map(function(number) {
  return number * 2;
});

console.log(doubled); // [2, 4, 6, 8, 10]

// 다른 동기 콜백 예시들:
numbers.forEach(number => console.log(number));
const evenNumbers = numbers.filter(number => number % 2 === 0);
const sum = numbers.reduce((acc, number) => acc + number, 0);

비동기 콜백(Asynchronous Callbacks)

비동기 콜백은 즉시 실행되지 않고, 특정 이벤트가 발생하거나 작업이 완료된 후에 실행된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 비동기 콜백 예시 1: 타이머
setTimeout(() => {
  console.log("타이머 완료 후 실행");
}, 1000);

// 비동기 콜백 예시 2: 이벤트 리스너
document.querySelector("#myButton").addEventListener("click", function(event) {
  console.log("버튼이 클릭되었습니다");
});

// 비동기 콜백 예시 3: AJAX 요청
function fetchData(url, callback) {
  const xhr = new XMLHttpRequest();
  xhr.open("GET", url);
  xhr.onload = function() {
    if (xhr.status === 200) {
      callback(null, JSON.parse(xhr.responseText));
    } else {
      callback(new Error(`요청 실패: ${xhr.status}`));
    }
  };
  xhr.onerror = function() {
    callback(new Error("네트워크 오류"));
  };
  xhr.send();
}

fetchData("https://api.example.com/data", function(error, data) {
  if (error) {
    console.error("에러:", error);
    return;
  }
  console.log("데이터:", data);
});

콜백 패턴

에러 처리 패턴 (Error-First Callbacks)

Node.js에서 널리 사용되는 콜백 패턴으로, 첫 번째 매개변수는 항상 에러 객체이다.
에러가 없으면 이 값은 null이 된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function readFile(path, callback) {
  fs.readFile(path, 'utf8', function(err, data) {
    if (err) {
      callback(err);
      return;
    }
    
    // 에러가 없으면 데이터 전달
    callback(null, data);
  });
}

readFile('/path/to/file.txt', function(err, data) {
  if (err) {
    console.error('파일 읽기 실패:', err);
    return;
  }
  
  console.log('파일 내용:', data);
});

옵션 객체 패턴 (Options Object Pattern)

콜백을 포함한 여러 옵션을 객체로 전달하는 패턴.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function loadImage(options) {
  const img = new Image();
  
  img.onload = function() {
    options.success(img);
  };
  
  img.onerror = function() {
    options.error('이미지 로드 실패');
  };
  
  img.src = options.url;
}

loadImage({
  url: 'image.jpg',
  success: function(img) {
    document.body.appendChild(img);
  },
  error: function(message) {
    console.error(message);
  }
});

메서드 체이닝 (Method Chaining)

콜백을 메서드 체인의 일부로 사용하는 패턴.
jQuery나 Promise에서 자주 사용된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// jQuery 예시
$("#button")
  .addClass("active")
  .text("활성화됨")
  .on("click", function() {
    console.log("버튼이 클릭되었습니다");
  });

// 자체 구현 예시
class Calculator {
  constructor(value = 0) {
    this.value = value;
  }
  
  add(x) {
    this.value += x;
    return this;
  }
  
  multiply(x) {
    this.value *= x;
    return this;
  }
  
  done(callback) {
    callback(this.value);
    return this;
  }
}

new Calculator(5)
  .add(3)
  .multiply(2)
  .done(function(result) {
    console.log("결과:", result); // 결과: 16
  });

콜백의 고급 패턴

  1. 커링(Currying)과 콜백
    커링은 여러 인자를 받는 함수를 인자를 하나씩 받는 함수의 체인으로 변환하는 기법이다.
    이를 통해 콜백 함수를 더 유연하게 구성할 수 있다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    // 일반 함수
    function add(x, y) {
      return x + y;
    }
    
    // 커링된 함수
    function curriedAdd(x) {
      return function(y) {
        return x + y;
      };
    }
    
    // 콜백으로 사용
    const numbers = [1, 2, 3, 4, 5];
    const addFive = curriedAdd(5);
    const result = numbers.map(addFive);
    console.log(result); // [6, 7, 8, 9, 10]
    
  2. 부분 적용(Partial Application)
    일부 인자를 미리 고정한 새로운 함수를 만드는 기법.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    function partial(fn, ...args) {
      return function(...moreArgs) {
        return fn(...args, ...moreArgs);
      };
    }
    
    function logger(level, message) {
      console.log(`[${level}] ${message}`);
    }
    
    const errorLogger = partial(logger, "ERROR");
    const warnLogger = partial(logger, "WARN");
    
    errorLogger("서버 연결 실패");  // [ERROR] 서버 연결 실패
    warnLogger("메모리 사용량 높음");  // [WARN] 메모리 사용량 높음
    
  3. 이벤트 이미터(EventEmitter) 패턴
    Node.js의 EventEmitter 패턴은 여러 콜백을 이벤트에 연결할 수 있게 해준다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const EventEmitter = require('events');

class DataProcessor extends EventEmitter {
  processData(data) {
    // 데이터 처리 시작
    this.emit('start', data);
    
    // 데이터 처리
    const result = data.map(item => item * 2);
    this.emit('progress', 50, result);
    
    // 추가 처리
    const finalResult = result.filter(item => item > 10);
    this.emit('progress', 100, finalResult);
    
    // 처리 완료
    this.emit('end', finalResult);
    
    return finalResult;
  }
}

const processor = new DataProcessor();

processor.on('start', (data) => {
  console.log('처리 시작:', data);
});

processor.on('progress', (percent, data) => {
  console.log(`진행률: ${percent}%`, data);
});

processor.on('end', (result) => {
  console.log('처리 완료:', result);
});

processor.processData([1, 3, 5, 7, 9]);

콜백 함수의 실행 컨텍스트와 This

자바스크립트에서 콜백 함수 내부의 this 값은 호출 방식에 따라 달라진다.
이는 종종 혼란을 일으키는 요소이다.

일반 함수에서의 This

일반 함수로 사용되는 콜백에서 this는 기본적으로 전역 객체(브라우저에서는 window, Node.js에서는 global)를 가리킨다.
단, strict mode에서는 undefined가 된다.

1
2
3
4
5
function onClick() {
  console.log(this); // window 또는 strict mode에서는 undefined
}

document.querySelector("button").addEventListener("click", onClick);

메서드로 전달된 콜백에서의 This

객체의 메서드를 콜백으로 전달하면, this 바인딩이 손실된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const user = {
  name: "홍길동",
  greet() {
    console.log(`안녕하세요, ${this.name}입니다`);
  }
};

// 직접 호출 - 정상 작동
user.greet(); // "안녕하세요, 홍길동입니다"

// 콜백으로 전달 - this 바인딩 손실
setTimeout(user.greet, 1000); // "안녕하세요, undefined입니다"

This 바인딩 해결 방법

bind() 메서드 사용
1
2
3
4
5
6
7
8
9
const user = {
  name: "홍길동",
  greet() {
    console.log(`안녕하세요, ${this.name}입니다`);
  }
};

// bind()로 this 고정
setTimeout(user.greet.bind(user), 1000); // "안녕하세요, 홍길동입니다"
화살표 함수 사용

화살표 함수는 자신만의 this를 가지지 않고, 외부 스코프의 this를 상속받는다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const user = {
  name: "홍길동",
  greet() {
    // 화살표 함수는 외부 스코프(greet)의 this를 유지
    setTimeout(() => {
      console.log(`안녕하세요, ${this.name}입니다`);
    }, 1000);
  }
};

user.greet(); // "안녕하세요, 홍길동입니다"
클로저를 사용한 값 캡처
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const user = {
  name: "홍길동",
  greet() {
    const self = this; // this 값 캡처
    setTimeout(function() {
      console.log(`안녕하세요, ${self.name}입니다`);
    }, 1000);
  }
};

user.greet(); // "안녕하세요, 홍길동입니다"

콜백과 클로저

콜백 함수는 종종 클로저를 형성하여 외부 함수의 변수에 접근한다.

클로저 기본

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function outer() {
  const message = "외부 함수의 메시지";
  
  return function() {
    console.log(message); // 외부 함수의 변수에 접근
  };
}

const callback = outer();
callback(); // "외부 함수의 메시지"

실용적인 클로저 예제

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function createCounter() {
  let count = 0;
  
  return {
    increment: function() {
      count += 1;
      return count;
    },
    decrement: function() {
      count -= 1;
      return count;
    },
    getCount: function() {
      return count;
    }
  };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.getCount());  // 1

비동기 작업에서의 클로저 문제

루프 내에서 비동기 콜백을 생성할 때 발생할 수 있는 문제:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 문제 있는 코드
for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i); // 모두 5를 출력
  }, 1000);
}

// 해결책 1: let 사용 (블록 스코프)
for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i); // 0, 1, 2, 3, 4 출력
  }, 1000);
}

// 해결책 2: 즉시 실행 함수로 값 캡처
for (var i = 0; i < 5; i++) {
  (function(index) {
    setTimeout(function() {
      console.log(index); // 0, 1, 2, 3, 4 출력
    }, 1000);
  })(i);
}

콜백 함수 최적화

  1. 디바운싱(Debouncing)
    연속적인 이벤트를 그룹화하여 마지막 이벤트 후 일정 시간이 지난 후에만 콜백을 실행한다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    function debounce(func, delay) {
      let timeoutId;
    
      return function(...args) {
        const context = this;
    
        clearTimeout(timeoutId);
    
        timeoutId = setTimeout(() => {
          func.apply(context, args);
        }, delay);
      };
    }
    
    // 사용 예: 검색 입력 최적화
    const searchInput = document.querySelector('#search');
    
    searchInput.addEventListener('input', debounce(function(e) {
      console.log('검색 쿼리:', e.target.value);
      // API 호출 등의 무거운 작업
    }, 300));
    
  2. 쓰로틀링(Throttling)
    일정 시간 간격으로 콜백 함수를 최대 한 번만 실행하도록 제한한다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    function throttle(func, limit) {
      let inThrottle;
    
      return function(...args) {
        const context = this;
    
        if (!inThrottle) {
          func.apply(context, args);
          inThrottle = true;
    
          setTimeout(() => {
            inThrottle = false;
          }, limit);
        }
      };
    }
    
    // 사용 예: 스크롤 이벤트 최적화
    window.addEventListener('scroll', throttle(function() {
      console.log('스크롤 위치:', window.scrollY);
      // 무거운 시각화 업데이트 등
    }, 100));
    
  3. 메모이제이션(Memoization)
    이전 결과를 캐싱하여 동일한 입력에 대해 콜백 함수의 재계산을 방지한다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
    function memoize(func) {
      const cache = {};
    
      return function(...args) {
        const key = JSON.stringify(args);
    
        if (cache[key]) {
          console.log('캐시에서 결과 반환');
          return cache[key];
        }
    
        console.log('함수 실행 및 결과 캐싱');
        const result = func.apply(this, args);
        cache[key] = result;
    
        return result;
      };
    }
    
    // 사용 예: 피보나치 수열 계산
    const fibonacci = memoize(function(n) {
      if (n <= 1) return n;
      return fibonacci(n - 1) + fibonacci(n - 2);
    });
    
    console.log(fibonacci(40)); // 첫 번째 호출: 계산
    console.log(fibonacci(40)); // 두 번째 호출: 캐시에서 반환
    

프론트엔드와 백엔드에서의 콜백 활용

프론트엔드 콜백 패턴

  1. DOM 이벤트 리스너

    1
    2
    3
    4
    
    document.querySelector('#myForm').addEventListener('submit', function(event) {
      event.preventDefault();
      // 폼 처리 로직
    });
    
  2. AJAX 요청

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    function fetchData(url, callback) {
      fetch(url)
        .then(response => response.json())
        .then(data => callback(null, data))
        .catch(error => callback(error));
    }
    
    fetchData('https://api.example.com/data', function(error, data) {
      if (error) {
        console.error('에러:', error);
        return;
      }
    
      console.log('데이터:', data);
    });
    
  3. 애니메이션

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    function animate(element, from, to, duration, callback) {
      const start = performance.now();
    
      function step(timestamp) {
        const elapsed = timestamp - start;
        const progress = Math.min(elapsed / duration, 1);
        const current = from + (to - from) * progress;
    
        element.style.opacity = current;
    
        if (progress < 1) {
          window.requestAnimationFrame(step);
        } else if (callback) {
          callback();
        }
      }
    
      window.requestAnimationFrame(step);
    }
    
    const element = document.querySelector('.fade');
    animate(element, 0, 1, 1000, function() {
      console.log('애니메이션 완료');
    });
    

백엔드(Node.js) 콜백 패턴

  1. 파일 시스템 작업

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    const fs = require('fs');
    
    fs.readFile('config.json', 'utf8', function(err, data) {
      if (err) {
        console.error('파일 읽기 실패:', err);
        return;
      }
    
      try {
        const config = JSON.parse(data);
        console.log('설정 로드 완료:', config);
      } catch (parseErr) {
        console.error('JSON 파싱 실패:', parseErr);
      }
    });
    
  2. 데이터베이스 쿼리

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    const mysql = require('mysql');
    
    const connection = mysql.createConnection({
      host: 'localhost',
      user: 'user',
      password: 'password',
      database: 'mydb'
    });
    
    connection.connect();
    
    connection.query('SELECT * FROM users WHERE status = ?', ['active'], function(error, results, fields) {
      if (error) {
        console.error('쿼리 실패:', error);
        return;
      }
    
      console.log('활성 사용자 수:', results.length);
      console.log('사용자 데이터:', results);
    });
    
    connection.end();
    
  3. HTTP 서버

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    
    const http = require('http');
    
    const server = http.createServer((req, res) => {
      if (req.url === '/api/data') {
        getData(function(err, data) {
          if (err) {
            res.statusCode = 500;
            res.end(JSON.stringify({ error: err.message }));
            return;
          }
    
          res.setHeader('Content-Type', 'application/json');
          res.end(JSON.stringify(data));
        });
      } else {
        res.statusCode = 404;
        res.end('Not Found');
      }
    });
    
    server.listen(3000, () => {
      console.log('서버가 포트 3000에서 실행 중입니다');
    });
    
    function getData(callback) {
      // 데이터 조회 로직
      setTimeout(() => {
        callback(null, { message: '데이터 응답' });
      }, 100);
    }
    

콜백에서 Promise, Async/Await로의 전환

  1. 콜백 기반 함수를 Promise로 변환

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    
    // 콜백 기반 함수
    function getDataWithCallback(id, callback) {
      setTimeout(() => {
        if (id < 0) {
          callback(new Error('잘못된 ID'));
          return;
        }
    
        callback(null, { id, name: `항목 ${id}` });
      }, 1000);
    }
    
    // Promise 기반으로 변환
    function getDataWithPromise(id) {
      return new Promise((resolve, reject) => {
        getDataWithCallback(id, (error, data) => {
          if (error) {
            reject(error);
            return;
          }
    
          resolve(data);
        });
      });
    }
    
    // 사용 방법
    getDataWithPromise(123)
      .then(data => console.log('데이터:', data))
      .catch(error => console.error('에러:', error));
    
  2. 유틸리티 함수로 변환 자동화

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    function promisify(fn) {
      return function(...args) {
        return new Promise((resolve, reject) => {
          fn(...args, (error, result) => {
            if (error) {
              reject(error);
              return;
            }
    
            resolve(result);
          });
        });
      };
    }
    
    // 사용 예
    const fs = require('fs');
    const readFilePromise = promisify(fs.readFile);
    
    readFilePromise('config.json', 'utf8')
      .then(data => console.log('파일 내용:', data))
      .catch(error => console.error('읽기 실패:', error));
    
  3. Async/Await 사용

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    async function processData(id) {
      try {
        const data = await getDataWithPromise(id);
        console.log('데이터:', data);
    
        const processedData = await processDataWithPromise(data);
        console.log('처리된 데이터:', processedData);
    
        return processedData;
      } catch (error) {
        console.error('처리 중 에러:', error);
        throw error;
      }
    }
    
    // 사용 방법
    processData(123)
      .then(result => console.log('최종 결과:', result))
      .catch(error => console.error('실패:', error));
    

주요 자바스크립트 라이브러리에서의 콜백 사용

  1. jQuery의 콜백

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    // AJAX 요청
    $.ajax({
      url: 'https://api.example.com/data',
      method: 'GET',
      success: function(data) {
        console.log('성공:', data);
      },
      error: function(xhr, status, error) {
        console.error('에러:', error);
      },
      complete: function() {
        console.log('요청 완료');
      }
    });
    
    // 애니메이션
    $('#element').fadeIn(500, function() {
      console.log('페이드인 완료');
    });
    
  2. Express.js의 미들웨어 콜백

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    
    const express = require('express');
    const app = express();
    
    // 미들웨어 함수
    function logger(req, res, next) {
      console.log(`${req.method} ${req.url}`);
      next();
    }
    
    function authenticate(req, res, next) {
      const isAuthenticated = checkAuth(req);
    
      if (isAuthenticated) {
        next();
      } else {
        res.status(401).send('인증 실패');
      }
    }
    
    // 미들웨어 등록
    app.use(logger);
    app.use(authenticate);
    
    // 라우트 핸들러
    app.get('/api/users', function(req, res) {
      // 사용자 목록 반환
      fetchUsers(function(err, users) {
        if (err) {
          return res.status(500).json({ error: err.message });
        }
        res.json(users);
      });
    });
    
    // 오류 처리 미들웨어
    app.use(function(err, req, res, next) {
      console.error(err.stack);
      res.status(500).send('서버 오류 발생');
    });
    
    app.listen(3000, function() {
      console.log('서버가 3000 포트에서 실행 중입니다');
    });
    
  3. React의 콜백 패턴

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    
    // 이벤트 핸들러 콜백
    function Button({ onClick, text }) {
      return (
        <button onClick={onClick}>
          {text}
        </button>
      );
    }
    
    function App() {
      const handleClick = () => {
        console.log('버튼이 클릭되었습니다');
      };
    
      return (
        <div>
          <Button onClick={handleClick} text="클릭하세요" />
        </div>
      );
    }
    
    // 콜백 ref
    function TextInputWithFocusButton() {
      const inputRef = React.useRef(null);
    
      const handleClick = () => {
        // 인풋에 포커스
        inputRef.current.focus();
      };
    
      return (
        <>
          <input ref={inputRef} type="text" />
          <button onClick={handleClick}>포커스</button>
        </>
      );
    }
    
  4. Node.js의 스트림 콜백

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    
    const fs = require('fs');
    
    // 파일 스트림 생성
    const readStream = fs.createReadStream('input.txt');
    const writeStream = fs.createWriteStream('output.txt');
    
    // 데이터 이벤트 리스너
    readStream.on('data', (chunk) => {
      console.log(`${chunk.length} 바이트 읽음`);
      writeStream.write(chunk);
    });
    
    // 종료 이벤트 리스너
    readStream.on('end', () => {
      console.log('읽기 완료');
      writeStream.end();
    });
    
    // 에러 이벤트 리스너
    readStream.on('error', (err) => {
      console.error('읽기 오류:', err);
    });
    
    writeStream.on('finish', () => {
      console.log('쓰기 완료');
    });
    
    writeStream.on('error', (err) => {
      console.error('쓰기 오류:', err);
    });
    

테스트와 디버깅에서의 콜백

  1. 비동기 코드 테스트

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    
    // Jest를 사용한 테스트 예제
    const fetchData = require('./fetchData');
    
    // 콜백 방식 테스트
    test('데이터를 올바르게 가져오는지 테스트 (콜백)', (done) => {
      function callback(error, data) {
        if (error) {
          done(error);
          return;
        }
    
        try {
          expect(data).toHaveProperty('id');
          expect(data.name).toBe('테스트 항목');
          done();
        } catch (err) {
          done(err);
        }
      }
    
      fetchData(123, callback);
    });
    
    // Promise 방식 테스트
    test('데이터를 올바르게 가져오는지 테스트 (Promise)', () => {
      return fetchDataPromise(123)
        .then(data => {
          expect(data).toHaveProperty('id');
          expect(data.name).toBe('테스트 항목');
        });
    });
    
    // Async/Await 방식 테스트
    test('데이터를 올바르게 가져오는지 테스트 (Async/Await)', async () => {
      const data = await fetchDataPromise(123);
      expect(data).toHaveProperty('id');
      expect(data.name).toBe('테스트 항목');
    });
    
  2. 모킹과 스파이를 사용한 콜백 테스트

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    // Jest를 사용한 콜백 모킹
    test('콜백이 올바른 인자로 호출되는지 테스트', () => {
      // 모의 콜백 함수 생성
      const mockCallback = jest.fn();
    
      // 테스트할 함수
      function processItems(items, callback) {
        for (const item of items) {
          callback(item);
        }
      }
    
      // 함수 실행
      const items = [1, 2, 3];
      processItems(items, mockCallback);
    
      // 검증
      expect(mockCallback.mock.calls.length).toBe(3);
      expect(mockCallback.mock.calls[0][0]).toBe(1);
      expect(mockCallback.mock.calls[1][0]).toBe(2);
      expect(mockCallback.mock.calls[2][0]).toBe(3);
    });
    
  3. 콜백 디버깅

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    function processData(data, callback) {
      console.log('데이터 처리 시작:', data);
    
      setTimeout(() => {
        try {
          const result = data.map(item => item * 2);
          console.log('처리된 결과:', result);
          callback(null, result);
        } catch (error) {
          console.error('처리 중 오류:', error);
          callback(error);
        }
      }, 1000);
    }
    
    // 디버깅을 위한 로그 추가
    function debugCallback(error, result) {
      console.log('콜백 실행됨');
      console.log('오류:', error);
      console.log('결과:', result);
    }
    
    processData([1, 2, 3], debugCallback);
    

실제 애플리케이션에서의 콜백 사용 사례

  1. 인증 시스템

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    
    function authenticateUser(username, password, callback) {
      // 사용자 조회
      findUser(username, (err, user) => {
        if (err) {
          return callback(err);
        }
    
        if (!user) {
          return callback(new Error('사용자가 존재하지 않습니다'));
        }
    
        // 비밀번호 검증
        verifyPassword(user, password, (err, isValid) => {
          if (err) {
            return callback(err);
          }
    
          if (!isValid) {
            return callback(new Error('비밀번호가 일치하지 않습니다'));
          }
    
          // 세션 생성
          createSession(user, (err, session) => {
            if (err) {
              return callback(err);
            }
    
            callback(null, { user, session });
          });
        });
      });
    }
    
    // Promise 버전
    function authenticateUserPromise(username, password) {
      return findUserPromise(username)
        .then(user => {
          if (!user) {
            throw new Error('사용자가 존재하지 않습니다');
          }
          return verifyPasswordPromise(user, password).then(isValid => ({ user, isValid }));
        })
        .then(({ user, isValid }) => {
          if (!isValid) {
            throw new Error('비밀번호가 일치하지 않습니다');
          }
          return createSessionPromise(user).then(session => ({ user, session }));
        });
    }
    
    // Async/Await 버전
    async function authenticateUserAsync(username, password) {
      const user = await findUserPromise(username);
    
      if (!user) {
        throw new Error('사용자가 존재하지 않습니다');
      }
    
      const isValid = await verifyPasswordPromise(user, password);
    
      if (!isValid) {
        throw new Error('비밀번호가 일치하지 않습니다');
      }
    
      const session = await createSessionPromise(user);
    
      return { user, session };
    }
    
  2. 결제 시스템

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    
    function processPayment(orderData, callback) {
      // 주문 유효성 검사
      validateOrder(orderData, (err, validatedOrder) => {
        if (err) {
          return callback(err);
        }
    
        // 재고 확인
        checkInventory(validatedOrder, (err, isAvailable) => {
          if (err) {
            return callback(err);
          }
    
          if (!isAvailable) {
            return callback(new Error('재고가 부족합니다'));
          }
    
          // 결제 처리
          chargeCustomer(validatedOrder, (err, paymentResult) => {
            if (err) {
              return callback(err);
            }
    
            // 주문 생성
            createOrder(validatedOrder, paymentResult, (err, order) => {
              if (err) {
                // 결제 취소 (롤백)
                refundPayment(paymentResult, () => {
                  callback(err);
                });
                return;
              }
    
              // 영수증 발송
              sendReceipt(order, (err) => {
                if (err) {
                  console.error('영수증 발송 실패:', err);
                  // 실패해도 주문은 완료됨
                }
    
                callback(null, { order, paymentResult });
              });
            });
          });
        });
      });
    }
    
  3. 파일 업로드 관리자

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    
    function uploadManager(file, options, callback) {
      // 파일 유효성 검사
      validateFile(file, options, (err, validatedFile) => {
        if (err) {
          return callback(err);
        }
    
        // 파일 압축
        compressFile(validatedFile, options.compression, (err, compressedFile) => {
          if (err) {
            return callback(err);
          }
    
          // 썸네일 생성 (이미지인 경우)
          if (isImage(compressedFile)) {
            createThumbnail(compressedFile, (err, thumbnail) => {
              if (err) {
                console.error('썸네일 생성 실패:', err);
                // 썸네일 없이 계속 진행
                uploadFile(compressedFile, null, callback);
              } else {
                uploadFile(compressedFile, thumbnail, callback);
              }
            });
          } else {
            uploadFile(compressedFile, null, callback);
          }
        });
      });
    
      function uploadFile(file, thumbnail, callback) {
        // 스토리지에 업로드
        storage.upload(file, (err, fileUrl) => {
          if (err) {
            return callback(err);
          }
    
          let result = { fileUrl };
    
          if (thumbnail) {
            // 썸네일 업로드
            storage.upload(thumbnail, (err, thumbnailUrl) => {
              if (err) {
                console.error('썸네일 업로드 실패:', err);
                // 썸네일 없이 계속 진행
                saveToDatabase(file, fileUrl, null, callback);
              } else {
                result.thumbnailUrl = thumbnailUrl;
                saveToDatabase(file, fileUrl, thumbnailUrl, callback);
              }
            });
          } else {
            saveToDatabase(file, fileUrl, null, callback);
          }
        });
      }
    
      function saveToDatabase(file, fileUrl, thumbnailUrl, callback) {
        // 데이터베이스에 파일 정보 저장
        database.saveFile({
          originalName: file.name,
          url: fileUrl,
          thumbnailUrl: thumbnailUrl,
          size: file.size,
          type: file.type,
          uploadedAt: new Date()
        }, (err, fileRecord) => {
          if (err) {
            return callback(err);
          }
    
          callback(null, {
            id: fileRecord.id,
            url: fileUrl,
            thumbnailUrl: thumbnailUrl
          });
        });
      }
    }
    

콜백 설계 모범 사례

  1. 일관된 콜백 시그니처 유지

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    // 잘못된 예: 일관성 없는 콜백 시그니처
    function getData(id, callback) {
      // 성공 시 (data, null) 반환
      callback(data, null);
    }
    
    function saveData(data, callback) {
      // 성공 시 true 반환
      callback(true);
    }
    
    // 좋은 예: 일관된 콜백 시그니처
    function getData(id, callback) {
      // 에러 우선 콜백 패턴
      if (error) {
        callback(error);
        return;
      }
      callback(null, data);
    }
    
    function saveData(data, callback) {
      // 동일한 패턴 유지
      if (error) {
        callback(error);
        return;
      }
      callback(null, true);
    }
    
  2. 오류 처리 일관성

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    
    function processTask(task, callback) {
      try {
        // 동기적 오류가 발생할 수 있는 코드
        if (!task || typeof task !== 'object') {
          throw new Error('유효하지 않은 태스크');
        }
    
        // 비동기 처리
        performAsyncOperation(task, (err, result) => {
          if (err) {
            callback(err);
            return;
          }
    
          try {
            // 또 다른 동기적 오류가 발생할 수 있는 코드
            const processedResult = processResult(result);
            callback(null, processedResult);
          } catch (error) {
            callback(error);
          }
        });
      } catch (error) {
        // 동기적 오류를 비동기 콜백으로 전달
        // 즉시 호출하지 않고 다음 틱으로 지연
        setTimeout(() => {
          callback(error);
        }, 0);
      }
    }
    
  3. 콜백 검증 및 기본값

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    
    function fetchData(options, callback) {
      // 콜백이 함수인지 확인
      if (typeof callback !== 'function') {
        throw new Error('콜백은 함수여야 합니다');
      }
    
      // 옵션 객체 기본값 설정
      options = Object.assign({
        url: 'https://api.example.com/data',
        method: 'GET',
        timeout: 5000
      }, options);
    
      // 비동기 작업 수행
      const xhr = new XMLHttpRequest();
      xhr.open(options.method, options.url);
      xhr.timeout = options.timeout;
    
      xhr.onload = function() {
        if (xhr.status >= 200 && xhr.status < 300) {
          try {
            const data = JSON.parse(xhr.responseText);
            callback(null, data);
          } catch (error) {
            callback(new Error('응답 파싱 실패: ' + error.message));
          }
        } else {
          callback(new Error('HTTP 오류: ' + xhr.status));
        }
      };
    
      xhr.onerror = function() {
        callback(new Error('네트워크 오류'));
      };
    
      xhr.ontimeout = function() {
        callback(new Error('요청 시간 초과'));
      };
    
      xhr.send();
    }
    
  4. 반복 호출 방지

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    function safeCallback(callback) {
      let called = false;
    
      return function(...args) {
        if (called) return;
        called = true;
        callback.apply(this, args);
      };
    }
    
    function processData(data, callback) {
      // 콜백을 한 번만 호출되도록 보장
      const cb = safeCallback(callback);
    
      try {
        validateData(data);
    
        asyncOperation(data, (err, result) => {
          if (err) {
            cb(err);
            return;
          }
    
          cb(null, result);
        });
      } catch (error) {
        cb(error);
      }
    }
    

콜백의 미래와 진화

  1. 콜백의 지속적인 관련성
    콜백은 새로운 비동기 패턴(Promise, Async/Await)이 등장했음에도 여전히 관련성이 높다:

    1. 이벤트 기반 프로그래밍: DOM 이벤트, Node.js 이벤트 등에서 콜백은 여전히 핵심적인 역할을 한다.
    2. 하위 수준 API: 많은 브라우저 및 Node.js API는 콜백을 기반으로 한다.
    3. 오래된 코드베이스: 기존의 많은 프로젝트와 라이브러리는 콜백 패턴을 사용한다.
    4. 간단한 사용 사례: 복잡하지 않은 비동기 작업의 경우 콜백이 여전히 적합할 수 있다.
  2. 함수형 프로그래밍과 콜백
    함수형 프로그래밍에서 콜백은 고차 함수(higher-order functions)의 형태로 중요한 역할을 한다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    // 함수형 프로그래밍 스타일의 콜백 사용
    const numbers = [1, 2, 3, 4, 5];
    
    // 데이터 변환 파이프라인
    const result = numbers
      .filter(n => n % 2 === 0)  // 짝수만 필터링
      .map(n => n * n)           // 제곱
      .reduce((acc, n) => acc + n, 0); // 합계
    
    console.log(result); // 20 (2² + 4²)
    
    // 함수 합성
    const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
    
    const double = x => x * 2;
    const square = x => x * x;
    const addOne = x => x + 1;
    
    const compute = compose(addOne, square, double);
    
    console.log(compute(3)); // ((3 * 2)² + 1) = 37
    
  3. 리액티브 프로그래밍과 콜백
    리액티브 프로그래밍 라이브러리(RxJS 등)는 콜백 패턴을 발전시켜 더 강력한 스트림 처리를 가능하게 한다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    // RxJS 예제
    const { fromEvent } = rxjs;
    const { map, debounceTime, distinctUntilChanged } = rxjs.operators;
    
    const searchInput = document.querySelector('#search');
    
    // 이벤트 스트림 생성
    const searchTerms = fromEvent(searchInput, 'input').pipe(
      map(event => event.target.value),
      debounceTime(300),
      distinctUntilChanged()
    );
    
    // 스트림 구독 (콜백과 유사)
    const subscription = searchTerms.subscribe(
      term => {
        console.log('검색어:', term);
        // API 호출 등
      },
      error => {
        console.error('에러:', error);
      },
      () => {
        console.log('스트림 완료');
      }
    );
    
    // 나중에 구독 취소
    // subscription.unsubscribe();
    
  4. 웹 워커와 서비스 워커에서의 콜백
    병렬 처리와 오프라인 기능을 위한 웹 워커와 서비스 워커에서도 콜백은 중요한 역할을 한다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    // 웹 워커 예제
    const worker = new Worker('worker.js');
    
    worker.onmessage = function(event) {
      console.log('워커로부터 메시지 받음:', event.data);
    };
    
    worker.onerror = function(error) {
      console.error('워커 오류:', error);
    };
    
    worker.postMessage({ action: 'process', data: [1, 2, 3, 4, 5] });
    
    // worker.js
    self.onmessage = function(event) {
      const { action, data } = event.data;
    
      if (action === 'process') {
        // 무거운 계산 수행
        const result = data.map(x => x * x);
    
        // 결과 반환
        self.postMessage({ result });
      }
    };
    

참고 및 출처