1. 비동기 프로그래밍의 필요성: 왜 비동기여야 하는가?
1.1 동기 vs. 비동기: 근본적인 차이 이해
전통적인 동기(Synchronous) 프로그래밍 모델에서는 특정 작업이 완료될 때까지 프로그램의 실행이 멈춥니다. 예를 들어, 파일 읽기나 네트워크 요청과 같이 시간이 오래 걸리는 작업이 있을 경우, 해당 작업이 끝날 때까지 프로그램은 다른 작업을 수행할 수 없습니다. 이는 특히 서버 환경에서 큰 성능 저하를 초래할 수 있습니다. 사용자가 많아질수록 대기 시간은 기하급수적으로 늘어나게 되고, 결국 서비스 불능 상태에 빠질 위험이 있습니다.
반면, 비동기(Asynchronous) 프로그래밍은 이러한 긴 작업을 기다리지 않고 다른 코드를 실행할 수 있게 해줍니다. 즉, 하나의 작업이 완료되기를 기다리는 동안 다른 작업을 병렬적으로 처리할 수 있어, 자원을 효율적으로 사용하고 응답 속도를 향상시킬 수 있습니다. 이는 사용자 경험을 개선하고 서버의 확장성을 높이는 데 매우 중요합니다.
1.2 동기와 비동기 예제로 비교하기
예제 1: 동기적 파일 읽기
const fs = require('fs');
console.log('동기적 파일 읽기 시작');
const data = fs.readFileSync('example.txt', 'utf8'); // 파일을 동기적으로 읽음
console.log('동기적 파일 읽기 완료:', data);
console.log('동기적 작업 종료');
예제 2: 비동기적 파일 읽기
const fs = require('fs');
console.log('비동기적 파일 읽기 시작');
fs.readFile('example.txt', 'utf8', (err, data) => {
if (err) {
console.error('비동기적 파일 읽기 에러:', err);
return;
}
console.log('비동기적 파일 읽기 완료:', data);
});
console.log('비동기적 작업 진행 중...');
동기적 예제에서는 fs.readFileSync
가 파일 읽기를 완료할 때까지 "동기적 작업 종료"가 출력되지 않습니다. 반면, 비동기적 예제에서는 "비동기적 작업 진행 중..."이 먼저 출력되고, 파일 읽기가 완료된 후에 "비동기적 파일 읽기 완료: ..."가 출력됩니다.
예제 3: 네트워크 요청 비교 (가상의 예제)
동기적 네트워크 요청 (가상)
// 가상의 동기적 네트워크 요청 함수
function synchronousFetch(url) {
// ... 실제로는 브라우저에서만 동기적 요청이 제한적으로 가능하며, Node.js에서는 불가능합니다.
// 이 예제는 개념 이해를 위한 가상의 예제입니다.
const response = /* ... url에 대한 동기적 요청을 보내고 응답을 받음 ... */;
return response;
}
console.log('동기적 요청 시작');
const responseData = synchronousFetch('https://example.com/api/data');
console.log('동기적 요청 응답:', responseData);
console.log('동기적 작업 종료');
비동기적 네트워크 요청
const https = require('https');
console.log('비동기적 요청 시작');
https.get('https://example.com/api/data', (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
console.log('비동기적 요청 응답:', data);
});
}).on('error', (err) => {
console.error('비동기적 요청 에러:', err);
});
console.log('비동기적 작업 진행 중...');
이 가상의 동기적 예제에서는 synchronousFetch
가 완료될 때까지 "동기적 작업 종료"가 출력되지 않습니다. 반면 비동기 예제에서는 "비동기적 작업 진행 중..."이 먼저 출력되고, 요청이 완료된 후에 "비동기적 요청 응답: ..."이 출력됩니다.
2. 비동기 프로그래밍의 핵심 메커니즘: 이벤트 루프와 비동기 I/O
2.1 이벤트 루프: Node.js 비동기 처리의 심장
Node.js가 비동기적으로 작동하도록 하는 핵심 메커니즘은 바로 이벤트 루프(Event Loop)입니다. JavaScript는 기본적으로 싱글 스레드(Single Thread)로 동작합니다. 즉, 한 번에 하나의 작업만 처리할 수 있습니다. 하지만 Node.js는 이벤트 루프 덕분에 마치 여러 작업을 동시에 처리하는 것처럼 보이게 할 수 있습니다. 이벤트 루프는 Node.js의 심장과 같으며, 비동기 작업의 실행 흐름을 관리하는 주체입니다.
이벤트 루프의 구성 요소
- 콜 스택(Call Stack): 현재 실행 중인 함수들을 관리하는 자료구조입니다. 함수가 호출되면 콜 스택에 쌓이고, 실행이 완료되면 스택에서 제거됩니다.
- 태스크 큐(Task Queue): 비동기 작업이 완료된 후 실행되어야 할 콜백 함수들이 대기하는 큐(Queue)입니다. 파일 읽기, 네트워크 요청, 타이머 이벤트 등이 완료되면 해당 콜백 함수가 태스크 큐에 추가됩니다.
- 이벤트 루프(Event Loop): 콜 스택이 비어있는지 지속적으로 확인하고, 비어있다면 태스크 큐에서 가장 오래된 콜백 함수를 꺼내 콜 스택에 넣고 실행합니다.
이벤트 루프의 동작 원리: 단계별 해설
- Node.js 프로그램이 실행되면, 코드가 순차적으로 실행되면서 함수 호출이 발생합니다.
- 호출된 함수는 콜 스택에 쌓입니다.
- 함수 실행 중 비동기 함수(예:
fs.readFile
,setTimeout
)가 호출되면, Node.js는 해당 작업을 백그라운드 스레드(libuv 라이브러리에서 제공)에 위임하고, 콜 스택에서 해당 함수를 제거합니다. 즉, 비동기 함수의 실행을 기다리지 않고 다음 코드를 계속 실행합니다. - 비동기 작업이 완료되면, 해당 콜백 함수가 태스크 큐에 추가됩니다.
- 이벤트 루프는 콜 스택이 비어있는지 지속적으로 감시합니다.
- 콜 스택이 비어있으면, 이벤트 루프는 태스크 큐에서 가장 먼저 들어온 콜백 함수를 꺼내 콜 스택에 넣고 실행합니다.
- 콜 스택에 있는 함수가 실행을 완료하면, 콜 스택에서 제거됩니다.
- 이벤트 루프는 이 과정을 반복합니다.
예제 4: 이벤트 루프 동작 이해하기
console.log('Start');
setTimeout(() => {
console.log('Timeout 2 seconds');
}, 2000);
console.log('End');
위 코드를 실행하면 다음과 같은 결과가 출력됩니다.
Start
End
Timeout 2 seconds
setTimeout
은 비동기 함수이기 때문에, console.log('End')
가 먼저 실행되고, 2초 후에 console.log('Timeout 2 seconds')
가 실행됩니다. 이것이 가능한 이유는 setTimeout
의 콜백 함수가 태스크 큐에 추가되고, 이벤트 루프가 콜 스택이 비워진 후에 태스크 큐에서 해당 콜백 함수를 꺼내 실행하기 때문입니다.
예제 5: 다양한 비동기 함수 혼합 사용
const fs = require('fs');
console.log('Start');
setTimeout(() => {
console.log('Timeout 1 second');
}, 1000);
fs.readFile('example.txt', 'utf8', (err, data) => {
if (!err) {
console.log('File read complete');
}
});
setTimeout(() => {
console.log('Timeout 0 second');
}, 0);
console.log('End');
이 코드 실행 시, example.txt
파일의 크기와 읽기 속도에 따라 출력 순서가 달라질 수 있지만, "Timeout 0 second"는 일반적으로 "File read complete"보다 먼저 출력됩니다. 이는 setTimeout
의 지연 시간이 0초라도, 여전히 비동기 작업으로 태스크 큐에 들어가기 때문입니다. 파일 읽기는 상대적으로 더 오래 걸리는 작업이므로, 태스크 큐에 더 늦게 추가될 가능성이 높습니다.
예제 6: setImmediate 활용
console.log('Start');
setTimeout(() => {
console.log('Timeout 0 second');
}, 0);
setImmediate(() => {
console.log('Immediate');
});
console.log('End');
setImmediate
는 setTimeout(..., 0)
보다 일반적으로 먼저 실행됩니다. setImmediate
는 현재 폴링(polling) 페이즈가 완료된 직후에 실행되도록 설계되었기 때문입니다. 반면에 setTimeout(..., 0)
은 최소 지연 시간 후에 실행되도록 예약하지만, 정확한 실행 시점은 현재 이벤트 루프의 상태에 따라 달라질 수 있습니다.
2.2 비동기 I/O: 논블로킹 I/O로 성능 극대화
Node.js는 파일 읽기/쓰기, 네트워크 요청, 데이터베이스 쿼리 등 다양한 I/O(Input/Output) 연산을 비동기적으로 수행합니다. 즉, I/O 작업이 완료될 때까지 기다리지 않고 다른 작업을 계속 처리할 수 있습니다. 이를 통해 서버는 여러 요청을 동시에 처리할 수 있는 능력을 갖추게 됩니다.
예제 7: 비동기 파일 읽기
const fs = require('fs');
console.log("파일 읽기를 시작합니다.");
fs.readFile('example.txt', 'utf8', (err, data) => {
if (err) {
console.error("파일 읽기 에러:", err);
return;
}
console.log("파일 내용:", data);
});
console.log("다른 처리를 계속합니다.");
이 코드를 실행하면 다음과 같은 결과가 출력될 수 있습니다(파일 읽기 속도에 따라 순서가 달라질 수 있음).
파일 읽기를 시작합니다.
다른 처리를 계속합니다.
파일 내용: ... // example.txt 파일의 내용
fs.readFile
함수는 비동기적으로 파일을 읽기 때문에, "다른 처리를 계속합니다."가 먼저 출력되고, 파일 읽기가 완료된 후에 "파일 내용: ..."이 출력됩니다. fs.readFile
의 세 번째 인자로 전달된 함수는 파일 읽기가 완료된 후에 실행되는 콜백 함수입니다.
예제 8: 비동기 파일 쓰기
const fs = require('fs');
const content = '비동기적으로 파일에 쓰기!';
console.log('파일 쓰기 시작');
fs.writeFile('output.txt', content, 'utf8', (err) => {
if (err) {
console.error('파일 쓰기 에러:', err);
return;
}
console.log('파일 쓰기 완료');
});
console.log('다른 작업 수행 중...');
이 코드는 output.txt
파일에 비동기적으로 데이터를 씁니다. "다른 작업 수행 중..."이 먼저 출력되고, 파일 쓰기가 완료된 후에 "파일 쓰기 완료"가 출력됩니다.
예제 9: 여러 파일 비동기적으로 읽기
const fs = require('fs');
const files = ['file1.txt', 'file2.txt', 'file3.txt'];
console.log('여러 파일 읽기 시작');
files.forEach(file => {
fs.readFile(file, 'utf8', (err, data) => {
if (err) {
console.error(`${file} 읽기 에러:`, err);
return;
}
console.log(`${file} 내용:`, data);
});
});
console.log('모든 파일 읽기 요청 완료');
이 코드는 file1.txt
, file2.txt
, file3.txt
를 비동기적으로 읽습니다. "모든 파일 읽기 요청 완료"가 먼저 출력되고, 각 파일의 읽기가 완료될 때마다 해당 파일의 내용이 출력됩니다. 파일 읽기 순서는 파일 크기나 시스템 상황에 따라 달라질 수 있습니다.
3. 콜백 함수: 비동기 처리의 기본
3.1 콜백 함수의 정의와 작동 원리
콜백 함수는 비동기 프로그래밍에서 핵심적인 역할을 합니다. 특정 이벤트가 발생했을 때 호출되는 함수를 의미하며, 주로 비동기 작업의 완료를 처리하는 데 사용됩니다.
콜백 함수의 작동 원리
- 작업 요청: 프로그램이 비동기 작업을 요청합니다. (예: 파일 읽기, 네트워크 요청)
- 콜백 등록: 해당 작업이 완료되면 호출될 콜백 함수를 등록합니다.
- 다른 코드 실행: 요청한 작업이 백그라운드에서 진행되는 동안 프로그램은 다른 코드를 계속 실행합니다.
- 완료 시 콜백 호출: 비동기 작업이 완료되면, 등록된 콜백 함수가 호출되어 결과를 처리합니다.
3.2 콜백 함수 예제
예제 10: 파일 읽기와 콜백 함수
const fs = require('fs');
fs.readFile('example.txt', 'utf8', (err, data) => {
if (err) {
console.error('파일 읽기 에러:', err);
return;
}
console.log('파일 내용:', data);
});
console.log('파일을 읽는 중...');
위 예제에서 fs.readFile
의 세 번째 인자로 전달된 익명 함수가 콜백 함수입니다. 이 콜백 함수는 파일 읽기가 완료된 후에 실행되며, 에러가 발생했을 경우 에러를 출력하고, 성공했을 경우 파일 내용을 출력합니다.
예제 11: 타이머와 콜백 함수
function delayedGreeting(name, delay, callback) {
setTimeout(() => {
const message = `안녕하세요, ${name}!`;
callback(message);
}, delay);
}
delayedGreeting('홍길동', 2000, (message) => {
console.log(message);
});
console.log('인사말 기다리는 중...');
이 코드는 delayedGreeting
함수를 사용하여 2초 후에 "안녕하세요, 홍길동!"을 출력합니다. setTimeout
에 전달된 익명 함수가 지정된 지연 시간 후에 콜백 함수를 호출합니다.
예제 12: 사용자 정의 이벤트와 콜백
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on('myEvent', (data) => {
console.log('이벤트 발생:', data);
});
console.log('이벤트 발생 준비 완료');
myEmitter.emit('myEvent', '이벤트 데이터');
console.log('이벤트 발생 후 작업 진행');
이 예제는 Node.js의 EventEmitter
를 사용하여 사용자 정의 이벤트 'myEvent'를 생성하고, 이벤트가 발생했을 때 실행될 콜백 함수를 등록합니다. myEmitter.emit
을 통해 이벤트를 발생시키면 등록된 콜백 함수가 실행됩니다.
3.3 콜백 함수의 장단점
장점
- 비동기 프로그래밍 구현: 비동기 프로그래밍을 가능하게 하여, 응답성이 높은 애플리케이션을 개발할 수 있습니다.
- 자원 효율성: 여러 I/O 작업을 동시에 처리하여, 자원을 효율적으로 사용할 수 있습니다.
단점
- 콜백 헬(Callback Hell): 복잡한 비동기 로직을 처리하기 위해 여러 콜백 함수가 중첩되면, 코드가 매우 복잡해지고 가독성이 떨어지는 "콜백 헬" 문제가 발생할 수 있습니다.
- 어려운 에러 처리: 콜백 함수를 사용할 경우, 에러 처리가 복잡해질 수 있습니다. 각 콜백 함수에서 에러를 개별적으로 처리해야 하기 때문에, 에러 처리 코드가 중복되고 관리가 어려워집니다.
예제 13: 콜백 헬
fs.readFile('file1.txt', 'utf8', (err, data1) => {
if (err) {
console.error(err);
return;
}
fs.readFile('file2.txt', 'utf8', (err, data2) => {
if (err) {
console.error(err);
return;
}
fs.readFile('file3.txt', 'utf8', (err, data3) => {
if (err) {
console.error(err);
return;
}
console.log(data1, data2, data3);
});
});
});
위 예제는 세 개의 파일을 순차적으로 읽는 코드입니다. 콜백 함수가 계속 중첩되면서 코드가 매우 복잡해지고, 가독성이 떨어지는 것을 볼 수 있습니다.
예제 14: 중첩된 setTimeout을 사용한 콜백 헬
setTimeout(() => {
console.log('첫 번째 작업');
setTimeout(() => {
console.log('두 번째 작업');
setTimeout(() => {
console.log('세 번째 작업');
setTimeout(() => {
console.log('네 번째 작업');
}, 1000);
}, 1000);
}, 1000);
}, 1000);
이 예제는 setTimeout
을 여러 번 중첩하여 사용하는 전형적인 콜백 헬의 모습을 보여줍니다. 각 setTimeout
마다 새로운 콜백 함수가 생성되고, 코드는 점점 더 안쪽으로 들여쓰기 되어 가독성이 매우 나빠집니다.
예제 15: 데이터베이스 쿼리와 콜백 헬 (가상의 예제)
// 가상의 데이터베이스 클라이언트 라이브러리
const dbClient = {
query: (sql, params, callback) => {
// ... 데이터베이스에 쿼리를 실행하고 결과를 콜백 함수에 전달하는 가상의 함수
}
};
dbClient.query('SELECT * FROM users WHERE id = ?', [1], (err, user) => {
if (err) {
console.error('사용자 조회 에러:', err);
return;
}
dbClient.query('SELECT * FROM orders WHERE user_id = ?', [user.id], (err, orders) => {
if (err) {
console.error('주문 조회 에러:', err);
return;
}
dbClient.query('SELECT * FROM products WHERE id IN (?)', [orders.map(o => o.product_id)], (err, products) => {
if (err) {
console.error('상품 조회 에러:', err);
return;
}
console.log('사용자:', user);
console.log('주문:', orders);
console.log('상품:', products);
});
});
});
이 예제는 가상의 데이터베이스 클라이언트를 사용하여 사용자 정보, 주문 정보, 상품 정보를 순차적으로 조회하는 코드입니다. 콜백 함수가 계속 중첩되면서 코드가 복잡해지고, 에러 처리도 어려워지는 콜백 헬의 전형적인 모습을 보여줍니다.
4. 프로미스와 async/await: 현대적인 비동기 처리 방식
4.1 프로미스(Promise): 비동기 작업의 결과를 나타내는 객체
콜백 함수의 단점을 극복하고, 더 나은 비동기 처리를 위해 프로미스(Promise)가 등장했습니다. 프로미스는 비동기 작업의 최종 완료(또는 실패)를 나타내는 객체입니다. 프로미스를 사용하면 비동기 작업의 결과를 더 쉽게 처리하고, 콜백 헬 문제를 개선할 수 있습니다.
프로미스의 상태
- 대기(pending): 초기 상태. 비동기 작업이 아직 완료되지 않음.
- 이행(fulfilled): 비동기 작업이 성공적으로 완료됨. 결과 값을 가짐.
- 거부(rejected): 비동기 작업이 실패함. 실패 이유(에러)를 가짐.
프로미스의 메서드
then(onFulfilled, onRejected)
: 프로미스가 이행(fulfilled)되었을 때 호출될onFulfilled
함수와 거부(rejected)되었을 때 호출될onRejected
함수를 등록합니다.then
메서드는 새로운 프로미스를 반환하기 때문에, 체이닝(chaining)이 가능합니다.catch(onRejected)
: 프로미스가 거부(rejected)되었을 때 호출될onRejected
함수를 등록합니다.finally(onFinally)
: 프로미스의 상태와 상관없이 (이행 또는 거부) 항상 실행될onFinally
함수를 등록합니다. (ES2018 추가)
예제 16: 프로미스 생성 및 사용
const fetchData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const data = "데이터 로드 완료";
const error = null;
if (error) {
reject(error);
} else {
resolve(data);
}
}, 2000);
});
};
fetchData()
.then(result => console.log(result))
.catch(error => console.error("에러 발생:", error))
.finally(() => console.log("프로미스 완료"));
위 예제에서 fetchData
함수는 프로미스를 반환합니다. resolve
함수는 프로미스를 이행 상태로 만들고, reject
함수는 프로미스를 거부 상태로 만듭니다. then
메서드를 사용하여 성공 시 실행할 콜백 함수와 실패 시 실행할 콜백 함수를 등록할 수 있습니다.
예제 17: 프로미스 기반의 파일 읽기
const fs = require('fs').promises; // 프로미스 기반의 fs 모듈 사용 (Node.js 10 이상)
console.log('파일 읽기 시작');
fs.readFile('example.txt', 'utf8')
.then(data => console.log('파일 내용:', data))
.catch(err => console.error('파일 읽기 에러:', err))
.finally(() => console.log('파일 읽기 완료'));
console.log('파일 읽기 요청 후 작업 진행...');
이 코드는 프로미스 기반의 fs.readFile
을 사용하여 파일을 비동기적으로 읽습니다. then
은 파일 읽기 성공 시, catch
는 실패 시, finally
는 성공/실패 여부와 상관없이 항상 실행됩니다.
예제 18: 프로미스 체이닝(Chaining)
function step1() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('Step 1 완료');
resolve('Step 1 결과');
}, 1000);
});
}
function step2(data) {
return new Promise((resolve) => {
setTimeout(() => {
console.log('Step 2 완료:', data);
resolve('Step 2 결과');
}, 1000);
});
}
function step3(data) {
return new Promise((resolve) => {
setTimeout(() => {
console.log('Step 3 완료:', data);
resolve('Step 3 결과');
}, 1000);
});
}
step1()
.then(step2)
.then(step3)
.then(result => console.log('최종 결과:', result))
.catch(err => console.error('에러 발생:', err));
이 예제는 step1
, step2
, step3
함수가 순차적으로 실행되도록 프로미스를 연결합니다. 각 함수의 resolve
결과는 다음 함수의 then
으로 전달됩니다.
예제 19: Promise.all을 사용한 병렬 처리
const promise1 = new Promise((resolve) => {
setTimeout(() => resolve('작업 1 완료'), 2000);
});
const promise2 = new Promise((resolve) => {
setTimeout(() => resolve('작업 2 완료'), 1000);
});
const promise3 = fs.promises.readFile('example.txt', 'utf8');
Promise.all([promise1, promise2, promise3])
.then(results => {
console.log('모든 작업 완료:', results);
})
.catch(err => console.error('에러 발생:', err));
Promise.all
은 여러 프로미스를 배열로 받아, 모든 프로미스가 완료될 때까지 기다린 후 각 프로미스의 결과 배열을 then
에 전달합니다. 이 예제에서는 promise1
, promise2
, promise3
가 병렬로 실행되고, 모두 완료되면 결과가 출력됩니다.
4.2 Async/Await: 프로미스를 활용한 간결한 비동기 코드
async/await
는 ES2017에 도입된 문법으로, 프로미스를 기반으로 비동기 코드를 더욱 쉽게 작성하고 읽을 수 있도록 도와줍니다. async/await
를 사용하면 비동기 코드를 동기 코드처럼 작성할 수 있어, 가독성이 향상되고 에러 처리가 간편해집니다.
async와 await 키워드
- async: 함수 앞에
async
키워드를 붙이면, 해당 함수는 항상 프로미스를 반환합니다. - await:
await
키워드는async
함수 내에서만 사용할 수 있으며, 프로미스가 이행될 때까지 기다립니다.await
는 프로미스의 결과 값을 반환합니다.
예제 20: async/await 기본 사용법
const fetchData = () => {
return new Promise((resolve) => {
setTimeout(() => {
const data = "데이터 로드 완료";
resolve(data);
}, 2000);
});
};
const loadData = async () => {
try {
const result = await fetchData();
console.log(result);
} catch (error) {
console.error("에러 발생:", error);
}
};
loadData();
위 예제에서 loadData
함수는 async
함수로 선언되었습니다. await fetchData()
는 fetchData
프로미스가 이행될 때까지 기다리고, 그 결과 값을 result
변수에 할당합니다. 에러 처리는 try...catch
블록을 사용하여 간편하게 처리할 수 있습니다.
예제 21: async/await를 사용한 파일 읽기
const fs = require('fs').promises;
async function readFileContent(filename) {
try {
console.log(`${filename} 읽기 시작`);
const data = await fs.readFile(filename, 'utf8');
console.log(`${filename} 내용:`, data);
} catch (err) {
console.error(`${filename} 읽기 에러:`, err);
}
}
readFileContent('example.txt');
console.log('파일 읽기 요청 후 다른 작업 진행...');
이 코드는 async/await
를 사용하여 example.txt
파일을 비동기적으로 읽습니다. await fs.readFile
은 파일 읽기가 완료될 때까지 기다린 후, 결과를 data
변수에 할당합니다.
예제 22: async/await와 Promise.all 조합
const fs = require('fs').promises;
async function readMultipleFiles(files) {
try {
console.log('파일 여러 개 읽기 시작');
const filePromises = files.map(file => fs.readFile(file, 'utf8'));
const fileContents = await Promise.all(filePromises);
fileContents.forEach((content, index) => {
console.log(`${files[index]} 내용:`, content);
});
} catch (err) {
console.error('파일 읽기 에러:', err);
}
}
readMultipleFiles(['file1.txt', 'file2.txt', 'file3.txt']);
console.log('파일 읽기 요청 후 다른 작업 진행...');
이 코드는 async/await
와 Promise.all
을 함께 사용하여 여러 파일을 병렬로 읽습니다. files.map
은 각 파일 읽기에 대한 프로미스 배열을 생성하고, await Promise.all
은 모든 파일 읽기가 완료될 때까지 기다립니다.
예제 23: async/await를 사용한 순차적 처리
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function processSequentially() {
console.log('작업 시작');
await delay(1000);
console.log('1초 대기');
await delay(1000);
console.log('2초 대기');
await delay(1000);
console.log('3초 대기');
console.log('작업 완료');
}
processSequentially();
이 예제는 delay
함수를 사용하여 지정된 시간만큼 기다리는 프로미스를 생성하고, async/await
를 사용하여 각 delay
함수가 순차적으로 실행되도록 합니다.
5. 결론: 비동기 프로그래밍, Node.js의 강력한 무기
비동기 프로그래밍은 Node.js의 핵심이며, 효율적이고 확장 가능한 서버를 구축하는 데 필수적인 요소입니다. 이벤트 루프와 비동기 I/O를 이해하고, 콜백 함수, 프로미스, async/await를 적절히 활용하면, 높은 트래픽 상황에서도 안정적으로 동작하는 애플리케이션을 개발할 수 있습니다.
처음에는 비동기 프로그래밍이 낯설고 어렵게 느껴질 수 있지만, 연습을 통해 익숙해지면 Node.js의 강력한 기능을 제대로 활용할 수 있을 것입니다. 이 글이 Node.js 비동기 프로그래밍을 이해하는 데 도움이 되었기를 바라며, 더 나은 서버 개발자로 성장하는 데 밑거름이 되기를 기대합니다.
'프로그래밍 > Node.js' 카테고리의 다른 글
Node.js 파일 시스템 정복: 파일, 디렉토리, 스트림, 버퍼까지! (0) | 2025.02.19 |
---|---|
Node.js 모듈 시스템 정복 가이드: 개념부터 활용까지 (0) | 2025.02.19 |
Node.js 시작하기: 설치부터 첫 프로젝트까지 완벽 가이드 (0) | 2025.02.19 |
Node.js 완벽 가이드: 개념부터 역사, 장점, 활용까지 (0) | 2025.02.19 |
Node.js: 최신 트렌드 정복하기 - 개발자를 위한 종합 가이드 (0) | 2025.02.19 |