[2026] Node.js 비동기 프로그래밍 | Callback, Promise, Async/Await
이 글의 핵심
Node.js 비동기 프로그래밍: Callback, Promise, Async/Await. Callback (콜백)·Promise.
들어가며
비동기 프로그래밍이란?
비동기(Asynchronous) 프로그래밍은 작업이 완료될 때까지 기다리지 않고 다음 코드를 실행하는 방식입니다. 이 글에서 자주 쓰는 비유는 다음과 같습니다. 콜백은 “일이 끝나면 이 번호로 연락 주세요”처럼 전화번호를 남기는 것과 같습니다. Promise는 나중에 결과를 받을 수 있는 약속 쿠폰·영수증에 가깝고, 성공·실패 상태가 적혀 있습니다. async/await는 실제로는 비동기인데 위에서 아래로 읽히는 동기 코드처럼 보이게 정리한 문법입니다. 이벤트 루프는 식당에서 주문만 받고 주방·배달은 다른 곳에서 처리하는 흐름, 또는 우체국 창구처럼 순번이 돌아올 때까지 기다렸다가 처리하는 시스템으로 이해하시면 됩니다. 동기 vs 비동기:
// 동기 (Synchronous)
console.log('1');
console.log('2');
console.log('3');
// 출력: 1 → 2 → 3 (순서대로)
// 비동기 (Asynchronous)
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
// 출력: 1 → 3 → 2 (2는 나중에)
Node.js가 비동기를 사용하는 이유:
- ✅ Non-blocking I/O: 파일, 네트워크 작업 중에도 다른 요청 처리
- ✅ 높은 동시성: 단일 스레드로 수천 개의 동시 연결 처리
- ✅ 효율성: CPU 대기 시간 최소화
같은 자바스크립트 계열이라도 브라우저·JS 엔진의 Promise·async/await는 이 모델과 맞닿아 있고, C++의
std::async처럼 별도 스레드에 작업을 넘기는 방식과는 출발점이 다릅니다. 경량 동시성 모델은 Go 고루틴·Kotlin 코루틴·Rust async 글과 비교해 보면 이해가 빨라집니다.
실무에서 마주한 현실
개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.
1. Callback (콜백)
기본 개념
콜백은 다른 함수에 인자로 전달되는 함수입니다. “작업이 끝나면 이 함수를 실행해 주세요”라고 연락처를 넘기는 것과 같아서, Node.js의 비동기 API는 대부분 이런 형태로 결과를 돌려줍니다. 다음은 javascript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 동기 콜백
function greet(name, callback) {
const message = `안녕하세요, ${name}님!`;
callback(message);
}
greet('홍길동', (msg) => {
console.log(msg);
});
// 안녕하세요, 홍길동님!
// 비동기 콜백
setTimeout(() => {
console.log('1초 후 실행');
}, 1000);
에러 우선 콜백 (Error-First Callback)
Node.js의 표준 콜백 패턴: 아래 코드는 javascript를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
const fs = require('fs');
// 첫 번째 인자: 에러
// 두 번째 인자: 결과
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) {
console.error('에러 발생:', err.message);
return;
}
console.log('파일 내용:', data);
});
Callback Hell (콜백 지옥)
비동기 작업을 순차적으로 실행하다 보면 콜백이 중첩되어 코드가 읽기 어려워집니다: 문제 상황: 다음은 javascript를 활용한 상세한 구현 코드입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
const fs = require('fs');
// 파일 3개를 순서대로 읽기 (각 파일을 읽은 후 다음 파일 읽기)
fs.readFile('file1.txt', 'utf8', (err1, data1) => {
// 첫 번째 콜백 - 1단계 들여쓰기
if (err1) {
console.error(err1);
return;
}
console.log('파일 1:', data1);
// 두 번째 파일 읽기 (첫 번째가 끝난 후)
fs.readFile('file2.txt', 'utf8', (err2, data2) => {
// 두 번째 콜백 - 2단계 들여쓰기
if (err2) {
console.error(err2);
return;
}
console.log('파일 2:', data2);
// 세 번째 파일 읽기 (두 번째가 끝난 후)
fs.readFile('file3.txt', 'utf8', (err3, data3) => {
// 세 번째 콜백 - 3단계 들여쓰기
if (err3) {
console.error(err3);
return;
}
console.log('파일 3:', data3);
// 더 깊어질 수 있음....(4단계, 5단계...)
// 이런 구조를 "피라미드 오브 둠(Pyramid of Doom)"이라고 부름
});
});
});
문제점:
- ❌ 가독성 저하: 피라미드 구조로 코드가 오른쪽으로 계속 들여쓰기됨
- ❌ 에러 처리 중복: 각 콜백마다 if (err) 체크 반복
- ❌ 유지보수 어려움: 코드 수정이나 디버깅이 매우 어려움
- ❌ 로직 파악 어려움: 실행 흐름을 따라가기 힘듦 실제 프로젝트에서는:
- 5~10단계 이상 중첩되는 경우도 있음
- 각 단계마다 에러 처리, 로깅, 유효성 검사 추가
- 코드가 수백 줄로 늘어나 관리 불가능
2. Promise
기본 개념
Promise는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체입니다. 나중에 찾아가면 결과를 주겠다는 약속 쿠폰처럼 생각하시면 됩니다. 아직 대기 중(pending)이거나, 성공(fulfilled)으로 찍혔거나, 거절(rejected)된 상태 중 하나입니다. 상태:
- Pending: 대기 중
- Fulfilled: 성공 (
.then()실행) - Rejected: 실패 (
.catch()실행)
Promise 생성
Promise를 직접 만들어서 비동기 작업을 캡슐화할 수 있습니다: 다음은 javascript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
function delay(ms) {
// new Promise: Promise 객체 생성
// executor 함수: (resolve, reject) => { ....}
// resolve: 성공 시 호출할 함수
// reject: 실패 시 호출할 함수
return new Promise((resolve, reject) => {
// 입력 검증
if (ms < 0) {
// reject 호출: Promise를 실패 상태로 만듦
// Error 객체를 전달하면 .catch()에서 받을 수 있음
reject(new Error('시간은 양수여야 합니다'));
return;
}
// setTimeout: ms 밀리초 후에 콜백 실행
setTimeout(() => {
// resolve 호출: Promise를 성공 상태로 만듦
// 전달한 값은 .then()에서 받을 수 있음
resolve(`${ms}ms 대기 완료`);
}, ms);
});
}
// Promise 사용
delay(1000)
// then: Promise가 성공하면 실행
// result: resolve에 전달한 값
.then((result) => {
console.log(result); // 1000ms 대기 완료
})
// catch: Promise가 실패하면 실행
// err: reject에 전달한 Error 객체
.catch((err) => {
console.error('에러:', err.message);
});
Promise 생성자의 동작:
new Promise()호출 시 executor 함수가 즉시 실행됨- 비동기 작업 수행 (예: setTimeout, 파일 읽기)
- 작업 성공 시
resolve(value)호출 →.then()으로 이동 - 작업 실패 시
reject(error)호출 →.catch()로 이동 Promise의 장점:
- 콜백보다 읽기 쉬운 체이닝 구조
- 에러 처리를 한 곳에서 관리 (
.catch()) - 여러 Promise를 조합 가능 (
.all(),.race()등)
Promise 체이닝
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
const fs = require('fs').promises;
// Callback Hell 해결
fs.readFile('file1.txt', 'utf8')
.then((data1) => {
console.log('파일 1:', data1);
return fs.readFile('file2.txt', 'utf8');
})
.then((data2) => {
console.log('파일 2:', data2);
return fs.readFile('file3.txt', 'utf8');
})
.then((data3) => {
console.log('파일 3:', data3);
})
.catch((err) => {
console.error('에러:', err.message);
})
.finally(() => {
console.log('작업 완료');
});
Promise 정적 메서드
Promise.all (모두 성공해야 함): 다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
const fs = require('fs').promises;
Promise.all([
fs.readFile('file1.txt', 'utf8'),
fs.readFile('file2.txt', 'utf8'),
fs.readFile('file3.txt', 'utf8')
])
.then(([data1, data2, data3]) => {
console.log('파일 1:', data1);
console.log('파일 2:', data2);
console.log('파일 3:', data3);
})
.catch((err) => {
console.error('에러:', err.message);
// 하나라도 실패하면 catch 실행
});
Promise.allSettled (모든 결과 확인): 아래 코드는 javascript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
Promise.allSettled([
fs.readFile('file1.txt', 'utf8'),
fs.readFile('file2.txt', 'utf8'),
fs.readFile('nonexistent.txt', 'utf8')
])
.then((results) => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`파일 ${index + 1} 성공:`, result.value);
} else {
console.log(`파일 ${index + 1} 실패:`, result.reason.message);
}
});
});
Promise.race (가장 빠른 것): 아래 코드는 javascript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
Promise.race([
delay(1000).then(() => '1초'),
delay(2000).then(() => '2초'),
delay(500).then(() => '0.5초')
])
.then((result) => {
console.log('가장 빠른 것:', result); // 0.5초
});
Promise.any (하나라도 성공): 아래 코드는 javascript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
Promise.any([
Promise.reject('에러 1'),
Promise.reject('에러 2'),
Promise.resolve('성공!')
])
.then((result) => {
console.log(result); // 성공!
})
.catch((err) => {
console.error('모두 실패:', err);
});
3. Async/Await
기본 사용법
async/await는 Promise를 더 읽기 쉽게 만드는 문법적 설탕(Syntactic Sugar)입니다:
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
const fs = require('fs').promises;
// async 키워드: 이 함수가 비동기 함수임을 선언
// async 함수는 항상 Promise를 반환
async function readFiles() {
try {
// await: Promise가 완료될 때까지 대기
// 코드는 동기처럼 보이지만 실제로는 비동기로 동작
// await는 async 함수 안에서만 사용 가능
// 첫 번째 파일 읽기 (완료될 때까지 대기)
const data1 = await fs.readFile('file1.txt', 'utf8');
console.log('파일 1:', data1);
// 첫 번째가 끝난 후 두 번째 파일 읽기
const data2 = await fs.readFile('file2.txt', 'utf8');
console.log('파일 2:', data2);
// 두 번째가 끝난 후 세 번째 파일 읽기
const data3 = await fs.readFile('file3.txt', 'utf8');
console.log('파일 3:', data3);
// 모든 작업이 성공하면 이 값을 resolve
return '모든 파일 읽기 완료';
} catch (err) {
// await 중 에러가 발생하면 catch 블록으로 이동
// Promise의 .catch()와 동일한 역할
console.error('에러:', err.message);
throw err; // 에러를 다시 던져서 호출자에게 전달
}
}
// async 함수 호출
readFiles()
.then((result) => {
console.log(result); // 모든 파일 읽기 완료
})
.catch((err) => {
console.error('최종 에러:', err.message);
});
async/await의 동작 원리:
async function은 내부적으로 Promise를 반환await는 Promise가 완료될 때까지 함수 실행을 일시 중지- 다른 코드는 계속 실행됨 (블로킹하지 않음)
- Promise가 완료되면 함수 실행 재개 Callback Hell → Promise → async/await 비교: 다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// Callback Hell (읽기 어려움)
fs.readFile('file1.txt', (err, data) => {
fs.readFile('file2.txt', (err, data) => {
fs.readFile('file3.txt', (err, data) => {
// ...
});
});
});
// Promise (나아짐)
fs.promises.readFile('file1.txt')
.then(() => fs.promises.readFile('file2.txt'))
.then(() => fs.promises.readFile('file3.txt'));
// async/await (가장 읽기 쉬움)
const data1 = await fs.promises.readFile('file1.txt');
const data2 = await fs.promises.readFile('file2.txt');
const data3 = await fs.promises.readFile('file3.txt');
병렬 처리
순차 실행 (느림): 아래 코드는 javascript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
async function sequential() {
const start = Date.now();
const data1 = await delay(1000); // 1초 대기
const data2 = await delay(1000); // 1초 대기
const data3 = await delay(1000); // 1초 대기
const elapsed = Date.now() - start;
console.log(`총 ${elapsed}ms`); // 약 3000ms
}
병렬 실행 (빠름): 다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
async function parallel() {
const start = Date.now();
// 동시에 시작
const promise1 = delay(1000);
const promise2 = delay(1000);
const promise3 = delay(1000);
// 모두 완료 대기
const [data1, data2, data3] = await Promise.all([
promise1,
promise2,
promise3
]);
const elapsed = Date.now() - start;
console.log(`총 ${elapsed}ms`); // 약 1000ms
}
Promise.all 사용: 아래 코드는 javascript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 실행 예제
async function parallelWithAll() {
const files = ['file1.txt', 'file2.txt', 'file3.txt'];
const results = await Promise.all(
files.map(file => fs.readFile(file, 'utf8'))
);
results.forEach((data, index) => {
console.log(`파일 ${index + 1}:`, data);
});
}
에러 처리
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
async function handleErrors() {
try {
const data = await fs.readFile('nonexistent.txt', 'utf8');
console.log(data);
} catch (err) {
if (err.code === 'ENOENT') {
console.error('파일을 찾을 수 없습니다');
} else {
console.error('에러:', err.message);
}
} finally {
console.log('작업 완료');
}
}
// 또는 .catch() 사용
async function handleErrorsWithCatch() {
const data = await fs.readFile('file.txt', 'utf8')
.catch((err) => {
console.error('파일 읽기 실패:', err.message);
return '기본값'; // 기본값 반환
});
console.log(data);
}
4. 이벤트 루프 (Event Loop)
동작 원리
JavaScript 엔진 한 줄로만 코드를 실행하는 동안, 타이머·파일·네트워크 완료 같은 일은 libuv 등이 맡고, 끝나면 “이제 이 콜백 실행해 주세요”가 이벤트 루프 큐에 쌓입니다. 식당으로 치면 홀 직원은 주문과 서빙 순서만 관리하고, 요리는 주방에서 돌아가는 구조에 가깝습니다. 다음은 code를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
┌───────────────────────────┐
┌─>│ timers │ setTimeout, setInterval
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ I/O 콜백
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │ 내부용
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ poll │ I/O 이벤트 대기
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ check │ setImmediate
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──│ close callbacks │ 소켓 종료 등
└───────────────────────────┘
실행 순서
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
console.log('1. 동기 코드');
setTimeout(() => {
console.log('4. setTimeout (0ms)');
}, 0);
setImmediate(() => {
console.log('5. setImmediate');
});
Promise.resolve().then(() => {
console.log('3. Promise (Microtask)');
});
console.log('2. 동기 코드');
// 출력 순서:
// 1. 동기 코드
// 2. 동기 코드
// 3. Promise (Microtask)
// 4. setTimeout (0ms)
// 5. setImmediate
실행 순서:
- 동기 코드 (Call Stack)
- Microtasks (Promise, process.nextTick)
- Macrotasks (setTimeout, setInterval, setImmediate)
process.nextTick
console.log('1');
process.nextTick(() => {
console.log('3. nextTick');
});
Promise.resolve().then(() => {
console.log('4. Promise');
});
console.log('2');
// 출력:
// 1
// 2
// 3. nextTick (Microtask 중 가장 먼저)
// 4. Promise
주의: process.nextTick을 남용하면 이벤트 루프가 블로킹될 수 있음.
5. Callback → Promise 변환
util.promisify
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
const fs = require('fs');
const util = require('util');
// Callback 기반 함수를 Promise로 변환
const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);
async function main() {
try {
const data = await readFile('file.txt', 'utf8');
console.log(data);
await writeFile('output.txt', data, 'utf8');
console.log('파일 쓰기 완료');
} catch (err) {
console.error('에러:', err.message);
}
}
main();
수동 변환
다음은 javascript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// Callback 기반 함수
function fetchData(url, callback) {
setTimeout(() => {
if (url) {
callback(null, { data: 'result' });
} else {
callback(new Error('URL이 필요합니다'));
}
}, 1000);
}
// Promise로 변환
function fetchDataPromise(url) {
return new Promise((resolve, reject) => {
fetchData(url, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
// 사용
async function main() {
try {
const data = await fetchDataPromise('https://api.example.com');
console.log(data);
} catch (err) {
console.error('에러:', err.message);
}
}
6. 실전 예제
예제 1: 파일 처리 파이프라인
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
const fs = require('fs').promises;
const path = require('path');
async function processFiles(inputDir, outputDir) {
try {
// 1. 디렉토리 읽기
const files = await fs.readdir(inputDir);
console.log(`${files.length}개 파일 발견`);
// 2. 출력 디렉토리 생성
await fs.mkdir(outputDir, { recursive: true });
// 3. 각 파일 처리 (병렬)
const results = await Promise.allSettled(
files.map(async (file) => {
const inputPath = path.join(inputDir, file);
const outputPath = path.join(outputDir, `processed-${file}`);
// 파일 읽기
const data = await fs.readFile(inputPath, 'utf8');
// 처리 (예: 대문자 변환)
const processed = data.toUpperCase();
// 파일 쓰기
await fs.writeFile(outputPath, processed, 'utf8');
return { file, success: true };
})
);
// 4. 결과 요약
const succeeded = results.filter(r => r.status === 'fulfilled').length;
const failed = results.filter(r => r.status === 'rejected').length;
console.log(`성공: ${succeeded}개, 실패: ${failed}개`);
// 실패한 파일 출력
results.forEach((result, index) => {
if (result.status === 'rejected') {
console.error(`파일 ${files[index]} 실패:`, result.reason.message);
}
});
} catch (err) {
console.error('치명적 에러:', err.message);
}
}
processFiles('./input', './output');
예제 2: API 요청 재시도
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
async function fetchWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
console.log(`시도 ${i + 1}/${maxRetries}`);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
return data;
} catch (err) {
console.error(`시도 ${i + 1} 실패:`, err.message);
if (i === maxRetries - 1) {
throw new Error(`${maxRetries}번 시도 후 실패`);
}
// 지수 백오프 (Exponential Backoff)
const delay = Math.pow(2, i) * 1000;
console.log(`${delay}ms 후 재시도...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// 사용
async function main() {
try {
const data = await fetchWithRetry('https://api.example.com/data');
console.log('데이터:', data);
} catch (err) {
console.error('최종 실패:', err.message);
}
}
main();
예제 3: 동시 실행 제한
async function limitConcurrency(tasks, limit) {
const results = [];
const executing = [];
for (const task of tasks) {
const promise = task().then((result) => {
// 완료된 작업 제거
executing.splice(executing.indexOf(promise), 1);
return result;
});
results.push(promise);
executing.push(promise);
// 동시 실행 수 제한
if (executing.length >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
// 사용 예제
async function main() {
const urls = [
'https://api.example.com/1',
'https://api.example.com/2',
'https://api.example.com/3',
'https://api.example.com/4',
'https://api.example.com/5'
];
// 최대 2개씩만 동시 실행
const tasks = urls.map(url => () => fetch(url).then(r => r.json()));
const results = await limitConcurrency(tasks, 2);
console.log('결과:', results);
}
예제 4: 타임아웃 처리
다음은 javascript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
function timeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`${ms}ms 타임아웃`));
}, ms);
});
}
async function fetchWithTimeout(url, timeoutMs = 5000) {
try {
const result = await Promise.race([
fetch(url).then(r => r.json()),
timeout(timeoutMs)
]);
return result;
} catch (err) {
if (err.message.includes('타임아웃')) {
console.error('요청 시간 초과');
}
throw err;
}
}
// 사용
async function main() {
try {
const data = await fetchWithTimeout('https://slow-api.com/data', 3000);
console.log(data);
} catch (err) {
console.error('에러:', err.message);
}
}
7. 스트림 (Stream)
스트림이란?
스트림은 데이터를 청크(chunk) 단위로 처리하는 방식입니다. 통째로 컵에 붓는 대신 호스로 조금씩 흘려 보내는 것과 비슷해서, 수 GB 파일도 전부 메모리에 올리지 않고 처리할 수 있습니다. 장점:
- ✅ 메모리 효율: 전체 데이터를 메모리에 로드하지 않음
- ✅ 빠른 시작: 첫 청크부터 처리 시작
- ✅ 파이프라인: 여러 작업을 연결 스트림 종류:
- Readable: 읽기 (파일 읽기, HTTP 요청)
- Writable: 쓰기 (파일 쓰기, HTTP 응답)
- Duplex: 읽기/쓰기 (TCP 소켓)
- Transform: 변환 (압축, 암호화)
파일 스트림
다음은 javascript를 활용한 상세한 구현 코드입니다. 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
const fs = require('fs');
// 읽기 스트림
const readStream = fs.createReadStream('large-file.txt', {
encoding: 'utf8',
highWaterMark: 64 * 1024 // 64KB 청크
});
readStream.on('data', (chunk) => {
console.log(`청크 받음: ${chunk.length} bytes`);
});
readStream.on('end', () => {
console.log('파일 읽기 완료');
});
readStream.on('error', (err) => {
console.error('에러:', err.message);
});
// 쓰기 스트림
const writeStream = fs.createWriteStream('output.txt');
writeStream.write('첫 번째 줄\n');
writeStream.write('두 번째 줄\n');
writeStream.end('마지막 줄\n');
writeStream.on('finish', () => {
console.log('파일 쓰기 완료');
});
파이프 (Pipe)
다음은 javascript를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
const fs = require('fs');
const zlib = require('zlib');
// 파일 복사
fs.createReadStream('input.txt')
.pipe(fs.createWriteStream('output.txt'));
// 파일 압축
fs.createReadStream('input.txt')
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream('input.txt.gz'));
// 파일 압축 해제
fs.createReadStream('input.txt.gz')
.pipe(zlib.createGunzip())
.pipe(fs.createWriteStream('output.txt'));
// 체이닝
fs.createReadStream('input.txt')
.pipe(transformStream) // 변환
.pipe(zlib.createGzip()) // 압축
.pipe(fs.createWriteStream('output.txt.gz')); // 저장
Transform 스트림
다음은 javascript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
const { Transform } = require('stream');
// 대문자 변환 스트림
class UpperCaseTransform extends Transform {
_transform(chunk, encoding, callback) {
const upperChunk = chunk.toString().toUpperCase();
this.push(upperChunk);
callback();
}
}
// 사용
fs.createReadStream('input.txt')
.pipe(new UpperCaseTransform())
.pipe(fs.createWriteStream('output.txt'));
8. 실전 프로젝트
프로젝트 1: 파일 다운로더
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// downloader.js
const https = require('https');
const fs = require('fs');
const path = require('path');
async function downloadFile(url, outputPath) {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(outputPath);
https.get(url, (response) => {
if (response.statusCode !== 200) {
reject(new Error(`HTTP ${response.statusCode}`));
return;
}
const totalSize = parseInt(response.headers['content-length'], 10);
let downloadedSize = 0;
response.on('data', (chunk) => {
downloadedSize += chunk.length;
const progress = ((downloadedSize / totalSize) * 100).toFixed(2);
process.stdout.write(`\r다운로드: ${progress}%`);
});
response.pipe(file);
file.on('finish', () => {
file.close();
console.log('\n다운로드 완료');
resolve(outputPath);
});
}).on('error', (err) => {
fs.unlink(outputPath, () => {});
reject(err);
});
});
}
// 사용
async function main() {
try {
const url = 'https://nodejs.org/dist/latest/node-v20.11.0.tar.gz';
const output = path.join(__dirname, 'node.tar.gz');
await downloadFile(url, output);
console.log('저장 위치:', output);
} catch (err) {
console.error('다운로드 실패:', err.message);
}
}
main();
프로젝트 2: 배치 작업 처리
다음은 javascript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// batch-processor.js
const fs = require('fs').promises;
const path = require('path');
class BatchProcessor {
constructor(concurrency = 3) {
this.concurrency = concurrency;
}
async processFiles(inputDir, processor) {
try {
// 파일 목록
const files = await fs.readdir(inputDir);
console.log(`총 ${files.length}개 파일`);
// 배치 단위로 처리
const results = [];
for (let i = 0; i < files.length; i += this.concurrency) {
const batch = files.slice(i, i + this.concurrency);
console.log(`\n배치 ${Math.floor(i / this.concurrency) + 1} 처리 중...`);
const batchResults = await Promise.allSettled(
batch.map(async (file) => {
const filePath = path.join(inputDir, file);
const data = await fs.readFile(filePath, 'utf8');
const result = await processor(file, data);
return { file, result };
})
);
results.push(...batchResults);
// 진행률
const progress = ((i + batch.length) / files.length * 100).toFixed(1);
console.log(`진행률: ${progress}%`);
}
// 결과 요약
const succeeded = results.filter(r => r.status === 'fulfilled').length;
const failed = results.filter(r => r.status === 'rejected').length;
console.log(`\n완료: ${succeeded}개, 실패: ${failed}개`);
return results;
} catch (err) {
console.error('배치 처리 실패:', err.message);
throw err;
}
}
}
// 사용
async function main() {
const processor = new BatchProcessor(3);
await processor.processFiles('./data', async (filename, content) => {
// 파일 처리 로직
console.log(`처리 중: ${filename}`);
await new Promise(resolve => setTimeout(resolve, 1000));
return content.length;
});
}
main();
프로젝트 3: 웹 크롤러
다음은 javascript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// crawler.js
const https = require('https');
class WebCrawler {
constructor() {
this.visited = new Set();
}
async fetch(url) {
return new Promise((resolve, reject) => {
https.get(url, (res) => {
let data = ';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
resolve(data);
});
}).on('error', reject);
});
}
async crawl(url, maxDepth = 2, currentDepth = 0) {
if (currentDepth > maxDepth || this.visited.has(url)) {
return;
}
this.visited.add(url);
console.log(`크롤링 [깊이 ${currentDepth}]: ${url}`);
try {
const html = await this.fetch(url);
// 링크 추출 (간단한 정규식)
const linkRegex = /href="(https?:\/\/[^"]+)"/g;
const links = [];
let match;
while ((match = linkRegex.exec(html)) !== null) {
links.push(match[1]);
}
console.log(`${links.length}개 링크 발견`);
// 재귀적으로 크롤링 (병렬)
await Promise.allSettled(
links.slice(0, 5).map(link =>
this.crawl(link, maxDepth, currentDepth + 1)
)
);
} catch (err) {
console.error(`크롤링 실패 ${url}:`, err.message);
}
}
}
// 사용
async function main() {
const crawler = new WebCrawler();
await crawler.crawl('https://example.com', 1);
console.log(`총 ${crawler.visited.size}개 페이지 방문`);
}
main();
9. 자주 발생하는 문제
문제 1: Unhandled Promise Rejection
에러:
UnhandledPromiseRejectionWarning: Error: Something went wrong
원인: Promise의 .catch()를 빠뜨림
해결:
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 에러 처리 없음
async function bad() {
await someAsyncFunction(); // 에러 발생 시 처리 안 됨
}
// ✅ try-catch 사용
async function good() {
try {
await someAsyncFunction();
} catch (err) {
console.error('에러:', err.message);
}
}
// ✅ 전역 핸들러
process.on('unhandledRejection', (reason, promise) => {
console.error('처리되지 않은 Promise 거부:', reason);
process.exit(1);
});
문제 2: async 함수를 기다리지 않음
아래 코드는 javascript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ await 누락
async function bad() {
const data = fetchData(); // Promise 객체 반환
console.log(data); // Promise { <pending> }
}
// ✅ await 사용
async function good() {
const data = await fetchData();
console.log(data); // 실제 데이터
}
문제 3: 루프에서 await
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 순차 실행 (느림)
async function sequential(urls) {
const results = [];
for (const url of urls) {
const data = await fetch(url); // 하나씩 대기
results.push(data);
}
return results;
}
// ✅ 병렬 실행 (빠름)
async function parallel(urls) {
const promises = urls.map(url => fetch(url));
return Promise.all(promises);
}
문제 4: forEach와 async/await
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ forEach는 async를 기다리지 않음
async function bad(files) {
files.forEach(async (file) => {
const data = await fs.readFile(file, 'utf8');
console.log(data);
});
console.log('완료'); // 파일 읽기 전에 출력됨!
}
// ✅ for...of 사용
async function good(files) {
for (const file of files) {
const data = await fs.readFile(file, 'utf8');
console.log(data);
}
console.log('완료'); // 모든 파일 읽은 후 출력
}
// ✅ Promise.all 사용 (병렬)
async function better(files) {
await Promise.all(
files.map(async (file) => {
const data = await fs.readFile(file, 'utf8');
console.log(data);
})
);
console.log('완료');
}
10. 실전 팁
에러 처리 패턴
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 패턴 1: try-catch
async function pattern1() {
try {
const data = await fetchData();
return data;
} catch (err) {
console.error('에러:', err.message);
return null;
}
}
// 패턴 2: .catch()
async function pattern2() {
const data = await fetchData().catch((err) => {
console.error('에러:', err.message);
return null;
});
return data;
}
// 패턴 3: 래퍼 함수
async function safeAsync(fn) {
try {
return [null, await fn()];
} catch (err) {
return [err, null];
}
}
// 사용
const [err, data] = await safeAsync(() => fetchData());
if (err) {
console.error('에러:', err.message);
} else {
console.log('데이터:', data);
}
성능 최적화
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ✅ 병렬 처리
async function optimized() {
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
return { users, posts, comments };
}
// ✅ 조기 반환
async function findUser(users, id) {
for (const user of users) {
if (user.id === id) {
return user; // 찾으면 즉시 반환
}
}
return null;
}
// ✅ 캐싱
const cache = new Map();
async function fetchWithCache(url) {
if (cache.has(url)) {
console.log('캐시 히트');
return cache.get(url);
}
const data = await fetch(url).then(r => r.json());
cache.set(url, data);
return data;
}
디버깅
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 비동기 스택 트레이스
async function a() {
await b();
}
async function b() {
await c();
}
async function c() {
throw new Error('에러 발생!');
}
a().catch((err) => {
console.error(err.stack);
// 전체 호출 스택 확인 가능
});
// 타이밍 측정
async function measure() {
console.time('작업');
await someAsyncTask();
console.timeEnd('작업');
// 작업: 1234.567ms
}
정리
핵심 요약
- Callback: 에러 우선 콜백, Callback Hell 주의
- Promise:
.then(),.catch(),.finally()체이닝 - async/await: 동기 코드처럼 작성,
try-catch로 에러 처리 - 병렬 처리:
Promise.all(),Promise.allSettled() - 이벤트 루프: 동기 → Microtask → Macrotask
- 스트림: 대용량 데이터 처리, 파이프라인
비교: Callback vs Promise vs Async/Await
| 특징 | Callback | Promise | Async/Await |
|---|---|---|---|
| 가독성 | ❌ (중첩) | ⭕ (체이닝) | ✅ (동기 스타일) |
| 에러 처리 | 각 콜백마다 | .catch() | try-catch |
| 디버깅 | 어려움 | 보통 | 쉬움 |
| 병렬 처리 | 복잡 | Promise.all() | Promise.all() + await |
| 사용 권장 | ❌ | ⭕ | ✅ |
선택 가이드
Callback 사용:
- 기존 API와 호환성 필요
- 간단한 비동기 작업 Promise 사용:
- 여러 비동기 작업 체이닝
Promise.all(),Promise.race()활용 Async/Await 사용 (권장):- 복잡한 비동기 로직
- 순차적 작업
- 가독성 중요
다음 단계
추천 학습 자료
공식 문서:
- Node.js Async Hooks
- MDN Promise 도구:
- async - 비동기 유틸리티
- p-limit - 동시 실행 제한