1. 이벤트 기반 프로그래밍: 흐름을 지배하는 이벤트
Node.js의 핵심 중 하나는 바로 이벤트 기반 프로그래밍입니다. 프로그램의 흐름을 이벤트에 따라 제어하는 이 방식은 비동기적이며, 여러 작업을 동시에 처리할 수 있도록 합니다.
1.1 이벤트와 리스너: 이벤트 발생과 그에 따른 반응
- 이벤트(Event): 특정한 일이 발생했음을 알리는 신호입니다. 사용자 입력, 파일 다운로드 완료, 네트워크 요청 수신 등이 모두 이벤트에 해당합니다.
- 리스너(Listener): 이벤트 발생 시 실행되는 함수입니다. 특정 이벤트에 반응하도록 설정된 코드 블록입니다.
예를 들어, 웹 애플리케이션에서 "버튼 클릭" 이벤트가 발생하면, 해당 버튼에 연결된 리스너 함수가 호출되어 데이터 전송과 같은 특정 작업을 수행합니다.
const EventEmitter = require('events');
const myEmitter = new EventEmitter();
// 리스너 등록: 'event' 이벤트에 대한 리스너 함수를 등록합니다.
myEmitter.on('event', () => {
console.log('An event occurred!');
});
// 이벤트 발생: 'event' 이벤트를 발생시켜 등록된 리스너 함수를 실행합니다.
myEmitter.emit('event'); // 콘솔 출력: An event occurred!
// 예제 추가: 여러 개의 리스너 등록
myEmitter.on('data', (data) => {
console.log('Received data:', data);
});
myEmitter.emit('data', 'Hello'); // 콘솔 출력: Received data: Hello
myEmitter.emit('data', { name: 'John' }); // 콘솔 출력: Received data: { name: 'John' }
// 예제 추가: 한번만 실행되는 리스너
myEmitter.once('init', () => {
console.log('Initialization');
});
myEmitter.emit('init'); // 콘솔 출력: Initialization
myEmitter.emit('init'); // 아무것도 출력되지 않음
위 코드는 EventEmitter
클래스를 사용하여 간단한 이벤트를 생성하고 처리하는 방법을 보여줍니다. myEmitter.on()
메서드는 'event'라는 이벤트에 대한 리스너를 등록하고, myEmitter.emit()
메서드는 'event' 이벤트를 발생시켜 등록된 리스너를 실행합니다. 추가된 예제에서는 myEmitter.on()
을 사용하여 여러 개의 리스너를 등록하고 myEmitter.once()
를 사용하여 한번만 실행되는 리스너를 등록하는 방법을 보여줍니다.
1.2 비동기 처리: 효율적인 자원 활용을 위한 핵심 전략
Node.js는 비동기적으로 작업을 수행하여 CPU 자원을 효율적으로 사용합니다. 특히 I/O 작업에서 성능 향상을 가져옵니다. 비동기 처리는 주로 콜백(callback)이나 프로미스(promise)를 통해 이루어집니다.
const fs = require('fs');
// fs.readFile() 함수는 'sample.txt' 파일을 비동기적으로 읽습니다.
fs.readFile('sample.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data); // 파일 읽기가 완료되면 data를 출력합니다.
});
// 이 문장은 'sample.txt' 파일 읽기 완료 여부와 관계없이 먼저 출력됩니다.
console.log("파일 읽기를 요청했습니다.");
// 예제 추가: 프로미스를 사용한 비동기 처리
function readFilePromise(filename) {
return new Promise((resolve, reject) => {
fs.readFile(filename, 'utf8', (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
readFilePromise('sample.txt')
.then(data => console.log("프로미스: ", data))
.catch(err => console.error("프로미스 에러: ", err));
console.log("프로미스를 사용한 파일 읽기 요청");
// 예제 추가: async/await를 사용한 비동기 처리
async function readData() {
try {
const data = await readFilePromise('sample.txt');
console.log("async/await: ", data);
} catch (err) {
console.error("async/await 에러: ", err);
}
}
readData();
console.log("async/await를 사용한 파일 읽기 요청");
위 코드는 파일 시스템에서 데이터를 비동기적으로 읽는 예제입니다. fs.readFile()
함수는 'sample.txt' 파일을 비동기적으로 읽고, 읽기가 완료되면 콜백 함수를 호출하여 결과를 처리합니다. 파일 읽기가 진행되는 동안에도 프로그램은 멈추지 않고 "파일 읽기를 요청했습니다."라는 메시지를 먼저 출력합니다. 즉, 파일 읽기 작업이 완료되기를 기다리지 않고 다음 코드를 실행합니다. 추가된 예제에서는 프로미스와 async/await를 사용해서 비동기 처리를 하는 방법을 보여줍니다.
1.3 실제 사례: 이벤트 기반 프로그래밍의 활용
- 웹 서버: HTTP 요청과 응답은 모두 이벤트로 처리됩니다. 클라이언트 요청(request)을 감지하고 적절한 응답(response)을 반환합니다.
- 채팅 애플리케이션: 사용자 메시지는 이벤트로 처리됩니다. 새 메시지가 도착하면 리스너가 작동하여 모든 클라이언트에게 실시간으로 업데이트합니다.
- 실시간 주식 정보: 주식 정보는 실시간으로 변동되기 때문에 이벤트를 사용하여 업데이트를 처리합니다. 새로운 주식 정보가 도착하면 해당 이벤트에 대한 리스너가 작동하여 화면을 업데이트합니다.
1.4 장점과 단점: 이벤트 기반 프로그래밍의 양면성
장점:
- 높은 성능과 확장성을 제공합니다.
- 많은 동시 연결을 지원합니다.
단점:
- 복잡한 에러 핸들링이 필요합니다.
- 콜백 지옥(callback hell)에 빠질 위험이 있습니다. 콜백 지옥은 중첩된 콜백 함수로 인해 코드가 복잡해지고 가독성이 떨어지는 현상입니다.
2. 비동기 I/O: 멈추지 않는 흐름
비동기 I/O는 Node.js의 또 다른 핵심 요소입니다. 효율적이고 빠른 웹 애플리케이션 개발을 가능하게 합니다.
2.1 동기식 vs 비동기식: 작업 처리 방식의 차이
- 동기식(Synchronous): 작업이 순차적으로 진행됩니다. 이전 작업이 완료되어야 다음 작업이 시작됩니다.
const fs = require('fs');
console.log("파일 읽기를 시작합니다.");
// 동기 방식: 'example.txt' 파일을 동기적으로 읽습니다.
const data = fs.readFileSync('example.txt', 'utf8');
// 파일 읽기가 완료될 때까지 다음 코드는 실행되지 않습니다.
console.log(data);
console.log("파일 읽기가 완료되었습니다.");
// 예제 추가: 여러 파일 동기적으로 읽기
const data1 = fs.readFileSync('file1.txt', 'utf8');
console.log("file1.txt: ", data1);
const data2 = fs.readFileSync('file2.txt', 'utf8');
console.log("file2.txt: ", data2);
- 비동기식(Asynchronous): 작업이 동시에 진행될 수 있습니다. 이전 작업의 결과를 기다리지 않고 다음 작업을 실행합니다.
2.2 비동기 I/O의 작동 원리: 이벤트 루프와 콜백 함수의 조화
Node.js는 이벤트 루프(Event Loop)와 콜백 함수(Callback Function)를 사용하여 비동기 I/O를 처리합니다. 이벤트 루프는 여러 작업을 관리하고, 각 작업이 완료되면 해당 콜백 함수를 호출하여 결과를 처리합니다.
const fs = require('fs');
console.log("파일 읽기를 시작합니다.");
// 비동기 방식: 'example.txt' 파일을 비동기적으로 읽습니다.
fs.readFile('example.txt', 'utf8', (err, data) => {
if (err) {
return console.error(err);
}
// 파일 읽기가 완료되면 data를 출력합니다.
console.log(data);
});
// 이 부분은 'example.txt' 파일 읽기 완료 여부와 관계없이 즉시 실행됩니다.
console.log("다음 코드를 실행합니다.");
// 예제 추가: 여러 파일 비동기적으로 읽기
fs.readFile('file1.txt', 'utf8', (err, data) => {
if (err) console.error(err);
else console.log('file1.txt:', data);
});
fs.readFile('file2.txt', 'utf8', (err, data) => {
if (err) console.error(err);
else console.log('file2.txt:', data);
});
// 이 메시지는 파일들이 모두 읽히기 전에 출력됩니다.
console.log("두 파일 읽기 요청 완료");
fs.readFile()
메서드는 파일을 비동기적으로 읽습니다. 프로그램은 "다음 코드를 실행합니다."라는 메시지를 즉시 출력하며 대기하지 않습니다. 파일 읽기가 완료되면 콜백 함수가 호출되어 결과가 출력됩니다. 즉, 파일 읽기 작업이 완료되기를 기다리지 않고 다음 코드를 실행하여 프로그램의 흐름이 멈추지 않습니다. 추가된 예제에서는 여러 개의 파일을 비동기적으로 읽는 예제를 보여줍니다.
2.3 왜 비동기 I/O를 사용할까요?: 성능, 자원 효율성, 확장성
- 성능 향상: I/O 작업(데이터베이스 쿼리, API 호출 등)을 기다리지 않고 다른 요청을 처리할 수 있습니다.
- 자원 효율성: CPU 자원을 효율적으로 활용하여 높은 성능을 유지하면서 많은 사용자 요청에 응답할 수 있습니다.
- 확장성: 더 많은 클라이언트-서버 간 연결을 동시에 처리할 수 있습니다.
2.4 실생활 예제: 비동기 I/O를 활용한 웹 서버
const http = require('http');
const fs = require('fs');
// HTTP 서버 생성
const server = http.createServer((req, res) => {
if (req.url === '/') { // 루트 경로 요청 처리
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('홈페이지입니다.');
} else if (req.url === '/data') { // /data 경로 요청 처리
// data.json 파일을 비동기적으로 읽습니다.
fs.readFile('data.json', 'utf8', (err, data) => {
if (err) {
res.writeHead(500);
return res.end('서버 오류');
}
res.writeHead(200, { 'Content-Type': 'application/json' });
// JSON 데이터를 응답합니다.
res.end(data);
});
} else {
res.writeHead(404);
res.end('페이지를 찾을 수 없습니다.');
}
});
// 3000번 포트에서 서버 실행
server.listen(3000, () => {
console.log("서버가 포트 3000에서 실행 중입니다.");
});
// 예제 추가: 클라이언트 요청에 따라 다른 파일 제공
const server2 = http.createServer((req, res) => {
if (req.url === '/image') {
fs.readFile('image.jpg', (err, data) => {
if (err) {
res.writeHead(500);
return res.end('이미지 로딩 오류');
}
res.writeHead(200, { 'Content-Type': 'image/jpeg' });
// 이미지 데이터를 응답
res.end(data);
});
} else if (req.url === '/text') {
fs.readFile('text.txt', 'utf8', (err, data) => {
if (err) {
res.writeHead(500);
return res.end('텍스트 로딩 오류');
}
res.writeHead(200, { 'Content-Type': 'text/plain' });
// 텍스트 데이터를 응답
res.end(data);
});
} else {
res.writeHead(404);
res.end('페이지를 찾을 수 없습니다.');
}
});
server2.listen(3001, () => {
console.log('두 번째 서버가 포트 3001에서 실행 중입니다.');
});
위 코드는 /data
경로에 대한 요청이 들어오면 data.json
파일을 비동기적으로 읽고 그 내용을 응답으로 전송합니다. 이 과정 동안 다른 경로에 대한 요청도 계속해서 받을 수 있습니다. 즉, data.json
파일을 읽는 동안에도 서버는 멈추지 않고 다른 요청을 처리할 수 있어 효율적입니다. 추가된 예제에서는 클라이언트의 요청에 따라서 다른 파일을 제공하는 예제를 보여줍니다.
3. 모듈 시스템: 코드 재사용과 관리의 핵심
Node.js의 모듈 시스템은 코드 재사용성과 유지 보수성을 높이는 데 중요한 역할을 합니다.
3.1 모듈이란?: 기능을 담은 코드의 단위
모듈은 특정 기능을 수행하는 코드 집합입니다. 파일 읽기/쓰기, HTTP 요청 처리와 같은 작업을 수행하는 독립적인 코드 블록입니다.
3.2 CommonJS와 ES6 모듈: 모듈 정의와 사용의 두 가지 방식
- CommonJS:
module.exports
로 객체나 함수를 공개하고,require()
로 모듈을 가져옵니다. Node.js에서 주로 사용됩니다.
// math.js - 간단한 수학 연산 모듈
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
// add와 subtract 함수를 외부로 공개합니다.
module.exports = { add, subtract };
// 예제 추가: 객체, 변수 내보내기
const PI = 3.14159;
const myObject = {
name: 'My Object',
value: 10
};
module.exports.PI = PI;
module.exports.myObject = myObject;
// app.js - math.js 모듈 사용
// math.js 모듈을 가져옵니다.
const math = require('./math');
console.log(math.add(5, 3)); // 8 출력
console.log(math.subtract(5, 3)); // 2 출력
// 예제 추가: 객체와 변수 사용
console.log(math.PI); // 3.14159 출력
console.log(math.myObject.name); // 'My Object' 출력
- ES6 모듈:
import
와export
키워드를 사용합니다. Node.js 환경에서 점차 사용이 늘고 있습니다.
// utils.mjs
export function greet(name) {
return `Hello, ${name}!`;
}
export const message = "This is a message from utils.mjs";
// main.mjs
import { greet, message } from './utils.mjs';
console.log(greet('World'));
console.log(message);
3.3 npm과 패키지 관리: 외부 라이브러리 활용의 핵심
npm(Node Package Manager)은 전 세계 개발자들이 만든 다양한 패키지를 설치하고 관리하는 도구입니다.
npm install express # express 웹 프레임워크 설치
npm install lodash # lodash 유틸리티 라이브러리 설치
// express 모듈을 가져옵니다.
const express = require('express');
const app = express();
// 루트 경로에 대한 GET 요청 처리
app.get('/', (req, res) => {
res.send('Hello World!');
});
// 3000번 포트에서 서버 실행
app.listen(3000, () => {
console.log('Server is running on http://localhost:3000');
});
// 예제 추가: lodash 사용
const _ = require('lodash');
const numbers = [1, 2, 3, 4, 5];
const doubled = _.map(numbers, num => num * 2); // [2, 4, 6, 8, 10]
console.log(doubled);
위 코드는 express를 사용하여 간단한 웹 서버를 생성합니다. 추가된 예제에서는 lodash 유틸리티 라이브러리를 사용하는 예제를 보여줍니다.
결론: Node.js, 강력한 도구를 위한 열쇠
이벤트 기반 프로그래밍, 비동기 I/O, 모듈 시스템은 Node.js를 이해하고 활용하는 데 필수적인 개념입니다. 이 세 가지 핵심 개념을 통해 효율적이고 반응성이 뛰어난 애플리케이션을 개발할 수 있습니다.
'프로그래밍 > Node.js' 카테고리의 다른 글
Node.js 웹 개발: Express.js와 Koa.js 프레임워크 비교 (0) | 2025.02.18 |
---|---|
Node.js로 웹 애플리케이션 개발하기: 기본부터 실전까지 (0) | 2025.02.18 |
Node.js 핵심 정복: 주요 모듈 완벽 가이드 (0) | 2025.02.18 |
Node.js 정복 가이드: 설치부터 NPM 활용까지, 서버 개발 첫걸음 (0) | 2025.02.17 |
Node.js: 서버사이드 JavaScript의 혁명 (0) | 2025.02.17 |