Callback
자바스크립트에서 콜백(Callback)은 다른 함수에 인자로 전달되는 함수를 의미한다.
이 함수는 특정 작업이 완료된 후에 실행되도록 설계되어 있다.
콜백은 자바스크립트의 비동기 프로그래밍의 기초가 되는 개념이다.
자바스크립트에서 콜백은 여전히 중요한 개념이며, 현대적인 비동기 패턴(Promise, Async/Await)의 기초가 되었다.
콜백은 다음과 같은 핵심 영역에서 여전히 중요한 역할을 한다:
- 이벤트 처리: 사용자 상호작용, 타이머, 네트워크 이벤트 등
- 함수형 프로그래밍: 고차 함수와 데이터 변환 파이프라인
- API 설계: 유연한 확장성과 컴포지션을 가능하게 함
- 비동기 흐름 제어: 기본적인 비동기 작업 조정
비록 콜백 지옥과 같은 문제로 인해 Promise와 Async/Await가 더 선호되는 경우가 많지만, 적절한 패턴과 함께 사용되면 콜백은 여전히 강력하고 유용한 도구이다. 현대적인 자바스크립트 개발자는 콜백을 효과적으로 사용하는 방법과 더 고급 비동기 패턴으로 전환하는 방법을 모두 이해하는 것이 중요하다.
콜백의 정의
콜백 함수는 다음과 같은 특징을 가진다:
콜백의 작동 원리
자바스크립트는 단일 스레드 언어이지만, 브라우저 환경에서는 Web API를 통해 비동기 작업을 수행할 수 있다.
콜백은 이러한 비동기 작업의 완료 시점을 처리하기 위한 방법이다.
위 예제에서 볼 수 있듯이, setTimeout
의 콜백은 동기 코드가 모두 실행된 후에 실행된다.
콜백의 종류
동기 콜백(Synchronous Callbacks)
동기 콜백은 함수가 즉시 실행되고 결과를 바로 반환한다.
주로 배열 메서드와 같은 데이터 처리에 사용된다.
|
|
비동기 콜백(Asynchronous Callbacks)
비동기 콜백은 즉시 실행되지 않고, 특정 이벤트가 발생하거나 작업이 완료된 후에 실행된다.
|
|
콜백 패턴
에러 처리 패턴 (Error-First Callbacks)
Node.js에서 널리 사용되는 콜백 패턴으로, 첫 번째 매개변수는 항상 에러 객체이다.
에러가 없으면 이 값은 null이 된다.
|
|
옵션 객체 패턴 (Options Object Pattern)
콜백을 포함한 여러 옵션을 객체로 전달하는 패턴.
|
|
메서드 체이닝 (Method Chaining)
콜백을 메서드 체인의 일부로 사용하는 패턴.
jQuery나 Promise에서 자주 사용된다.
|
|
콜백의 고급 패턴
커링(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]
부분 적용(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] 메모리 사용량 높음
이벤트 이미터(EventEmitter) 패턴
Node.js의 EventEmitter 패턴은 여러 콜백을 이벤트에 연결할 수 있게 해준다.
|
|
콜백 함수의 실행 컨텍스트와 This
자바스크립트에서 콜백 함수 내부의 this
값은 호출 방식에 따라 달라진다.
이는 종종 혼란을 일으키는 요소이다.
일반 함수에서의 This
일반 함수로 사용되는 콜백에서 this
는 기본적으로 전역 객체(브라우저에서는 window
, Node.js에서는 global
)를 가리킨다.
단, strict mode에서는 undefined
가 된다.
메서드로 전달된 콜백에서의 This
객체의 메서드를 콜백으로 전달하면, this
바인딩이 손실된다.
This 바인딩 해결 방법
bind() 메서드 사용
화살표 함수 사용
화살표 함수는 자신만의 this
를 가지지 않고, 외부 스코프의 this
를 상속받는다.
클로저를 사용한 값 캡처
콜백과 클로저
콜백 함수는 종종 클로저를 형성하여 외부 함수의 변수에 접근한다.
클로저 기본
실용적인 클로저 예제
|
|
비동기 작업에서의 클로저 문제
루프 내에서 비동기 콜백을 생성할 때 발생할 수 있는 문제:
|
|
콜백 함수 최적화
디바운싱(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));
쓰로틀링(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));
메모이제이션(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)); // 두 번째 호출: 캐시에서 반환
프론트엔드와 백엔드에서의 콜백 활용
프론트엔드 콜백 패턴
DOM 이벤트 리스너
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); });
애니메이션
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 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); } });
데이터베이스 쿼리
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();
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로의 전환
콜백 기반 함수를 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));
유틸리티 함수로 변환 자동화
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));
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));
주요 자바스크립트 라이브러리에서의 콜백 사용
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('페이드인 완료'); });
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 포트에서 실행 중입니다'); });
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> </> ); }
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 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('테스트 항목'); });
모킹과 스파이를 사용한 콜백 테스트
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); });
콜백 디버깅
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 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 }; }
결제 시스템
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 }); }); }); }); }); }); }
파일 업로드 관리자
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 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); }
오류 처리 일관성
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); } }
콜백 검증 및 기본값
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(); }
반복 호출 방지
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); } }
콜백의 미래와 진화
콜백의 지속적인 관련성
콜백은 새로운 비동기 패턴(Promise, Async/Await)이 등장했음에도 여전히 관련성이 높다:- 이벤트 기반 프로그래밍: DOM 이벤트, Node.js 이벤트 등에서 콜백은 여전히 핵심적인 역할을 한다.
- 하위 수준 API: 많은 브라우저 및 Node.js API는 콜백을 기반으로 한다.
- 오래된 코드베이스: 기존의 많은 프로젝트와 라이브러리는 콜백 패턴을 사용한다.
- 간단한 사용 사례: 복잡하지 않은 비동기 작업의 경우 콜백이 여전히 적합할 수 있다.
함수형 프로그래밍과 콜백
함수형 프로그래밍에서 콜백은 고차 함수(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
리액티브 프로그래밍과 콜백
리액티브 프로그래밍 라이브러리(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();
웹 워커와 서비스 워커에서의 콜백
병렬 처리와 오프라인 기능을 위한 웹 워커와 서비스 워커에서도 콜백은 중요한 역할을 한다: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 }); } };