프로그래밍/Node.js

Node.js로 웹 애플리케이션 개발하기: 기본부터 실전까지

shimdh 2025. 2. 18. 09:22
728x90

1. Node.js 웹 서버 기초: Hello, World!

Node.js 개발의 첫걸음으로, 간단한 웹 서버를 만들어 보겠습니다. 이를 통해 Node.js의 기본 동작 원리와 서버 생성 과정을 이해할 수 있습니다.

1.1. 기본 개념: 웹 서버의 역할

웹 서버는 클라이언트(예: 웹 브라우저)의 HTTP 요청을 받아 적절한 응답을 반환하는 프로그램입니다. Node.js는 비동기 I/O 모델을 사용하여 높은 성능과 확장성을 제공하며, 많은 동시 연결을 효율적으로 처리할 수 있습니다.

1.2. http 모듈: 기본 웹 서버 만들기

Node.js는 내장 http 모듈을 제공하여 웹 서버를 쉽게 구축할 수 있도록 지원합니다. 가장 기본적인 "Hello, World!" 웹 서버를 만들어 보겠습니다.

// http 모듈 불러오기
const http = require('http');

// 포트 번호 설정
const PORT = 3000;

// HTTP 서버 생성
const server = http.createServer((req, res) => {
  // 응답 헤더 설정 (상태 코드 200, Content-Type: text/plain)
  res.writeHead(200, { 'Content-Type': 'text/plain' });

  // 클라이언트에게 보낼 메시지 작성
  res.end('안녕하세요! 이것은 Node.js로 만든 기본 웹 서버입니다.\n');
});

// 서버 실행 및 리스닝 시작
server.listen(PORT, () => {
  console.log(`서버가 http://localhost:${PORT} 에서 실행 중입니다.`);
});

코드 해설:

  1. require('http'): Node.js의 내장 http 모듈을 불러옵니다.
  2. http.createServer((req, res) => { ... }): HTTP 서버 객체를 생성합니다. createServer 메서드는 콜백 함수를 인자로 받으며, 이 콜백 함수는 클라이언트의 요청이 있을 때마다 호출됩니다.
    • req: 클라이언트의 요청(request) 정보를 담고 있는 객체입니다.
    • res: 클라이언트에게 보낼 응답(response)을 설정하는 객체입니다.
  3. res.writeHead(200, { 'Content-Type': 'text/plain' }): 응답 헤더를 설정합니다. 상태 코드 200 (OK)과 콘텐츠 타입을 "text/plain"으로 지정합니다.
  4. res.end('안녕하세요! ...'): 클라이언트에게 보낼 메시지를 작성하고 응답을 종료합니다.
  5. server.listen(PORT, () => { ... }): 서버가 지정된 포트(3000)에서 요청을 수신하도록 설정합니다. 서버가 시작되면 콜백 함수가 실행되어 콘솔에 메시지를 출력합니다.

1.3. http 모듈 활용: 다양한 응답 처리

단순한 텍스트 응답 외에도 HTML, URL에 따른 분기 처리 등 다양한 응답을 처리하는 예제를 살펴봅니다.

예제 1: HTML 응답하기

const http = require('http');
const PORT = 3000;

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/html' }); // HTML 컨텐츠 타입
  res.end('<h1>안녕하세요!</h1><p>Node.js 웹 서버입니다.</p>');
});

server.listen(PORT, () => {
  console.log(`서버가 http://localhost:${PORT} 에서 실행 중입니다.`);
});

예제 2: 요청 URL에 따라 다른 응답하기

const http = require('http');
const PORT = 3000;

const server = http.createServer((req, res) => {
  if (req.url === '/') {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('홈페이지입니다.');
  } else if (req.url === '/about') {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('About 페이지입니다.');
  } else {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('페이지를 찾을 수 없습니다.');
  }
});

server.listen(PORT, () => {
  console.log(`서버가 http://localhost:${PORT} 에서 실행 중입니다.`);
});

코드 해설:

  • res.writeHead(200, { 'Content-Type': 'text/html' }): 응답의 Content-Type을 'text/html'로 설정하여 브라우저가 HTML로 렌더링하도록 합니다.
  • req.url: 요청 URL을 확인하여 조건부로 다른 응답을 제공합니다. 이를 통해 간단한 라우팅을 구현할 수 있습니다.

1.4. 서버 테스트 및 확인

위 코드를 server.js와 같은 파일로 저장한 후, 터미널에서 node server.js 명령어를 실행하여 서버를 구동합니다. 웹 브라우저에서 http://localhost:3000, http://localhost:3000/about 등에 접속하여 결과를 확인해 봅니다.

2. Express.js로 RESTful API 구축하기

이번에는 Node.js의 대표적인 웹 프레임워크인 Express.js를 사용하여 RESTful API를 구축하는 방법을 단계별로 살펴보겠습니다.

2.1. RESTful API: 개념 및 특징

REST(Representational State Transfer)는 웹 서비스 설계를 위한 아키텍처 스타일입니다. RESTful API는 HTTP 프로토콜을 기반으로 하며, 자원(Resource)을 URI로 표현하고, HTTP 메서드(GET, POST, PUT, DELETE 등)를 사용하여 자원을 조작합니다.

RESTful API의 주요 특징:

  • 자원(Resource) 중심: 모든 것을 자원으로 간주하고 URI를 통해 자원을 식별합니다. (예: /users, /users/1)
  • HTTP 메서드 활용: HTTP 메서드를 사용하여 자원에 대한 CRUD(Create, Read, Update, Delete) 작업을 수행합니다.
  • 상태 비저장성(Stateless): 서버는 클라이언트의 상태 정보를 저장하지 않습니다. 각 요청은 독립적이며, 필요한 모든 정보는 요청에 포함되어야 합니다.

2.2. Express.js: Node.js 웹 프레임워크

Express.js는 Node.js를 위한 간결하고 유연한 웹 애플리케이션 프레임워크입니다. 라우팅, 미들웨어, 템플릿 엔진 등 웹 애플리케이션 개발에 필요한 다양한 기능을 제공하여 개발 생산성을 높여줍니다.

2.3. Express.js로 RESTful API 개발: 단계별 가이드

본격적으로 Express.js를 사용하여 CRUD(Create, Read, Update, Delete) 작업을 수행하는 RESTful API를 구축해 보겠습니다.

2.3.1. 프로젝트 초기화 및 패키지 설치

먼저 프로젝트 폴더를 생성하고, npm init -y 명령어를 사용하여 package.json 파일을 생성합니다.

mkdir my-restful-api
cd my-restful-api
npm init -y

이어서 필요한 패키지(Express.js, body-parser, cors)를 설치합니다.

npm install express body-parser cors
  • express: Express.js 프레임워크
  • body-parser: 요청 본문(request body)을 파싱하여 req.body 객체로 만들어주는 미들웨어 (JSON 요청 처리에 필수)
  • cors: CORS(Cross-Origin Resource Sharing)를 활성화하는 미들웨어 (다른 도메인에서의 요청을 허용할 때 필요)

2.3.2. 기본 서버 구조 작성 (server.js)

server.js 파일을 생성하고 아래와 같이 Express.js를 사용한 기본 서버 코드를 작성합니다.

const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');

const app = express();

// body-parser를 사용하여 JSON 요청 본문을 파싱
app.use(bodyParser.json());

// CORS 활성화
app.use(cors());

// 임시 데이터 저장소 (실제 애플리케이션에서는 데이터베이스 사용)
let items = [];

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

코드 해설:

  • require(...): 필요한 모듈들을 불러옵니다.
  • app = express(): Express 애플리케이션 객체를 생성합니다.
  • app.use(...): 미들웨어를 애플리케이션에 추가합니다.
    • bodyParser.json(): JSON 형식의 요청 본문을 파싱하여 req.body 객체에 저장합니다.
    • cors(): CORS를 활성화합니다.
  • let items = [];: 데이터를 임시로 저장할 배열을 선언합니다. (실제 서비스에서는 데이터베이스를 사용합니다.)
  • app.listen(PORT, ...): 서버를 시작하고 지정된 포트에서 요청을 수신합니다.

2.3.3. 라우트 정의: CRUD 기능 구현

이제 RESTful API의 핵심인 CRUD 기능을 위한 라우트를 정의합니다.

// ... (기존 코드) ...

// GET /items: 모든 아이템 조회
app.get('/items', (req, res) => {
  res.json(items);
});

// POST /items: 새로운 아이템 추가
app.post('/items', (req, res) => {
  const newItem = req.body; // 요청 본문에서 새 아이템 데이터 가져오기
  // 간단한 유효성 검사: 이름 필수
  if (!newItem.name) {
    return res.status(400).json({ error: 'Name is required' });
  }
  items.push(newItem);
  res.status(201).json(newItem); // 201 Created 상태 코드와 함께 새 아이템 반환
});

// PUT /items/:id: 특정 아이템 수정
app.put('/items/:id', (req, res) => {
  const id = parseInt(req.params.id); // URL 파라미터에서 ID 가져오기
  const updatedItem = req.body;
  // 아이템 존재 여부 확인
  if (!items[id]) {
    return res.status(404).json({ error: 'Item not found' });
  }
  items[id] = updatedItem;
  res.json(updatedItem);
});

// DELETE /items/:id: 특정 아이템 삭제
app.delete('/items/:id', (req, res) => {
  const id = parseInt(req.params.id);
  // 아이템 존재 여부 확인
  if (!items[id]) {
    return res.status(404).json({ error: 'Item not found' });
  }
  items.splice(id, 1); // 해당 인덱스의 아이템 제거
  res.status(204).send(); // 204 No Content (삭제 성공)
});

코드 해설:

  • app.get('/items', ...): GET /items 요청을 처리하는 라우트를 정의합니다. 모든 아이템 목록을 JSON 형식으로 응답합니다.
  • app.post('/items', ...): POST /items 요청을 처리합니다. 요청 본문에서 새 아이템 정보를 가져와 items 배열에 추가하고, 성공 응답(201 Created)과 함께 추가된 아이템을 반환합니다.
  • app.put('/items/:id', ...): PUT /items/:id 요청을 처리합니다. URL 파라미터 :id를 통해 수정할 아이템의 인덱스를 식별하고, 요청 본문의 데이터로 아이템 정보를 업데이트합니다.
  • app.delete('/items/:id', ...): DELETE /items/:id 요청을 처리합니다. URL 파라미터 :id를 통해 삭제할 아이템의 인덱스를 식별하고, splice() 메서드로 해당 아이템을 제거합니다.
  • req.params.id: URL 경로에서 :id에 해당하는 파라미터 값을 가져옵니다.
  • req.body: 요청 본문에 포함된 데이터(JSON)를 담고 있는 객체입니다.
  • res.json(...): JSON 형식의 응답을 전송합니다.
  • res.status(...): HTTP 상태 코드를 설정합니다.

2.3.4. 추가 라우트: 검색 및 상세 조회

기본 CRUD 기능 외에도 검색, 상세 조회와 같은 유용한 라우트를 추가해 보겠습니다.

// ... (기존 코드) ...

// GET /items/search?name=:name - 이름으로 아이템 검색
app.get('/items/search', (req, res) => {
  const name = req.query.name;
  const filteredItems = items.filter(item => item.name.includes(name));
  res.json(filteredItems);
});

// GET /items/:id - ID로 특정 아이템 조회
app.get('/items/:id', (req, res) => {
  const id = parseInt(req.params.id);
  if (!items[id]) {
    return res.status(404).json({ error: 'Item not found' });
  }
  res.json(items[id]);
});

// 예시 데이터에 카테고리 속성 추가 (테스트용)
items = [
  { name: "사과", category: "과일" },
  { name: "바나나", category: "과일" },
  { name: "당근", category: "채소" },
];

// GET /items/category/:category - 카테고리별 아이템 조회
app.get('/items/category/:category', (req, res) => {
  const category = req.params.category;
  const filteredItems = items.filter(item => item.category === category);
  res.json(filteredItems);
});

코드 해설:

  • app.get('/items/search?name=...', ...): GET /items/search?name=검색어 요청을 처리합니다. req.query.name을 통해 쿼리 파라미터 name의 값을 가져오고, filter() 메서드를 사용하여 해당 이름을 포함하는 아이템만 필터링하여 반환합니다.
  • app.get('/items/:id', ...): GET /items/:id 요청을 처리합니다. URL 파라미터 :id를 통해 조회할 아이템의 인덱스를 식별하고, 해당 아이템 정보를 JSON 형식으로 반환합니다.
  • app.get('/items/category/:category', ...): GET /items/category/:category 요청을 처리합니다. URL 파라미터 :category를 통해 조회할 아이템의 카테고리를 식별하고, filter() 메서드를 사용하여 해당 카테고리에 속하는 아이템만 필터링하여 반환합니다.

2.3.5. API 테스트 및 검증

node server.js 명령어로 서버를 시작하고, Postman, Insomnia, curl과 같은 도구를 사용하여 API를 테스트합니다. 다양한 요청(GET, POST, PUT, DELETE, 검색, 상세 조회)을 보내고, 응답 코드와 데이터가 예상대로 반환되는지 확인합니다.

3. Node.js와 데이터베이스 연동: 데이터 영속성 확보

지금까지는 items 배열에 임시로 데이터를 저장했습니다. 실제 애플리케이션에서는 데이터베이스를 사용하여 데이터를 영구적으로 저장하고 관리해야 합니다. 이번 섹션에서는 Node.js 애플리케이션에서 데이터베이스를 사용하는 방법을 살펴보겠습니다.

3.1. 데이터베이스 종류: 관계형(RDBMS) vs. NoSQL

Node.js는 다양한 관계형 데이터베이스(RDBMS)와 NoSQL 데이터베이스를 지원합니다.

  • 관계형 데이터베이스 (RDBMS): MySQL, PostgreSQL, SQL Server 등. 데이터를 테이블 형태로 저장하고 SQL을 사용하여 데이터를 쿼리합니다. 정형화된 데이터, 복잡한 쿼리, 트랜잭션 처리에 적합합니다.
  • NoSQL 데이터베이스: MongoDB, Redis, Cassandra 등. 스키마가 유연하고, 대규모 데이터 처리에 적합합니다. 비정형 데이터, 빠른 쓰기/읽기, 수평적 확장이 필요한 경우에 유용합니다.

3.2. MySQL 연동: RDBMS 활용

MySQL을 예로 들어 Node.js에서 관계형 데이터베이스를 사용하는 방법을 알아보겠습니다.

3.2.1. mysql 패키지 설치

npm install mysql

3.2.2. MySQL 연결 및 쿼리 실행

const mysql = require('mysql');

// MySQL 연결 설정
const connection = mysql.createConnection({
  host: 'localhost', // 데이터베이스 호스트
  user: 'your_mysql_username', // MySQL 사용자 이름
  password: 'your_mysql_password', // MySQL 비밀번호
  database: 'your_database_name' // 데이터베이스 이름
});

// 연결
connection.connect((err) => {
  if (err) {
    console.error('Error connecting to MySQL:', err);
    return;
  }
  console.log('Connected to MySQL database!');
});

// 사용자 추가 쿼리
const newUser = { name: 'John Doe', email: 'john.doe@example.com' };
connection.query('INSERT INTO users SET ?', newUser, (error, results) => {
  if (error) {
    console.error('Error inserting user:', error);
    return;
  }
  console.log('User inserted successfully! ID:', results.insertId);
});

// 모든 사용자 조회 쿼리
connection.query('SELECT * FROM users', (error, results) => {
  if (error) {
    console.error('Error fetching users:', error);
    return;
  }
  console.log('Users:', results);
});

// 특정 사용자 조회
const userId = 1;
connection.query('SELECT * FROM users WHERE id = ?', [userId], (error, results) => {
  if (error) {
    console.error('Error fetching user:', error);
    return;
  }
  if (results.length > 0) {
    console.log('User found:', results[0]);
  } else {
    console.log('User not found.');
  }
});

// 사용자 정보 업데이트
const userIdToUpdate = 1;
const updatedUser = { name: 'Updated Name', email: 'updated.email@example.com' };
connection.query('UPDATE users SET ? WHERE id = ?', [updatedUser, userIdToUpdate], (error, results) => {
  if (error) {
    console.error('Error updating user:', error);
    return;
  }
  console.log('User updated successfully!');
});

// 사용자 삭제
const userIdToDelete = 1;
connection.query('DELETE FROM users WHERE id = ?', [userIdToDelete], (error, results) => {
  if (error) {
    console.error('Error deleting user:', error);
    return;
  }
  console.log('User deleted successfully!');
});

// 연결 종료 (더 이상 쿼리를 실행하지 않을 때)
// connection.end();

코드 해설:

  • mysql.createConnection(...): MySQL 연결 객체를 생성합니다. 연결 정보를 인자로 전달합니다.
  • connection.connect(...): 데이터베이스에 연결합니다.
  • connection.query(...): SQL 쿼리를 실행합니다.
    • 첫 번째 인자: 실행할 SQL 쿼리 문자열
    • 두 번째 인자 (선택): 쿼리에 사용할 파라미터 값 (예: newUser, userId). Prepared Statement를 사용하여 SQL Injection 공격을 방지합니다.
    • 세 번째 인자: 콜백 함수. 쿼리 실행 후 호출됩니다.
      • error: 에러 객체 (쿼리 실행 중 에러가 발생한 경우)
      • results: 쿼리 실행 결과
  • connection.end(): 데이터베이스 연결을 종료합니다.

3.3. MongoDB 연동: NoSQL 활용

이번에는 NoSQL 데이터베이스 중 가장 널리 사용되는 MongoDB를 Node.js와 연동하는 방법을 살펴보겠습니다.

3.3.1. mongoose 패키지 설치

MongoDB를 편리하게 사용하기 위해 ODM(Object Document Mapper) 라이브러리인 mongoose를 설치합니다.

npm install mongoose

3.3.2. MongoDB 연결, 스키마 정의, CRUD 작업

const mongoose = require('mongoose');

// MongoDB 연결
mongoose.connect('mongodb://localhost:27017/your_database_name', {
  useNewUrlParser: true,
  useUnifiedTopology: true
})
  .then(() => console.log('Connected to MongoDB!'))
  .catch(err => console.error('Error connecting to MongoDB:', err));

// 사용자 스키마 정의
const userSchema = new mongoose.Schema({
  name: { type: String, required: true }, // 이름 (필수)
  email: String,
  age: Number
});

// 사용자 모델 생성
const User = mongoose.model('User', userSchema);

// 새로운 사용자 생성 및 저장
const newUser = new User({ name: 'Jane Smith', email: 'jane.smith@example.com', age: 30 });
newUser.save()
  .then(user => console.log('User saved:', user))
  .catch(err => console.error('Error saving user:', err));

// 모든 사용자 조회
User.find({})
  .then(users => console.log('Users:', users))
  .catch(err => console.error('Error fetching users:', err));

// 특정 조건 사용자 조회 (나이가 25세 이상)
User.find({ age: { $gte: 25 } })
  .then(users => console.log('Users (age >= 25):', users))
  .catch(err => console.error('Error fetching users:', err));

// 사용자 업데이트 (이름으로 검색 후 이메일 변경)
User.findOneAndUpdate({ name: 'Jane Smith' }, { email: 'updated.email@example.com' }, { new: true })
  .then(updatedUser => {
    if (updatedUser) {
      console.log('User updated:', updatedUser);
    } else {
      console.log('User not found.');
    }
  })
  .catch(err => console.error('Error updating user:', err));

// 사용자 삭제 (이름으로 검색 후 삭제)
User.findOneAndDelete({ name: 'Jane Smith' })
  .then(deletedUser => {
    if (deletedUser) {
      console.log('User deleted:', deletedUser);
    } else {
      console.log('User not found.');
    }
  })
  .catch(err => console.error('Error deleting user:', err));

코드 해설:

  • mongoose.connect(...): MongoDB에 연결합니다. 연결 문자열을 인자로 전달합니다.
  • mongoose.Schema(...): 데이터의 구조를 정의하는 스키마를 생성합니다.
  • mongoose.model('User', userSchema): 스키마를 기반으로 모델을 생성합니다. 모델은 MongoDB 컬렉션과 상호 작용하는 데 사용됩니다.
  • new User(...): 모델 인스턴스(문서)를 생성합니다.
  • newUser.save(): 문서를 데이터베이스에 저장합니다.
  • User.find(...): 쿼리를 실행하여 문서를 조회합니다.
    • User.find({}): 모든 문서를 조회합니다.
    • User.find({ age: { $gte: 25 } }): age 필드가 25 이상인 문서를 조회합니다. $gte는 "greater than or equal to"를 의미합니다.
  • User.findOneAndUpdate(...): 조건에 맞는 문서를 찾아 업데이트합니다. { new: true } 옵션은 업데이트된 문서를 반환하도록 합니다.
  • User.findOneAndDelete(...): 조건에 맞는 문서를 찾아 삭제합니다.

3.4. 데이터베이스 성능 최적화: 핵심 전략

데이터베이스를 사용할 때는 성능 최적화를 고려하는 것이 중요합니다.

  • 커넥션 풀링(Connection Pooling): 데이터베이스 연결을 생성하고 닫는 작업은 비용이 많이 듭니다. 커넥션 풀링은 연결을 재사용하여 성능을 향상시킵니다. 대부분의 데이터베이스 드라이버는 커넥션 풀링을 지원합니다.
  • 인덱싱(Indexing): 자주 사용되는 쿼리의 성능을 향상시키기 위해 적절한 인덱스를 생성해야 합니다. 예를 들어, 사용자 이름으로 사용자를 자주 조회하는 경우, name 필드에 인덱스를 생성하는 것이 좋습니다.
  • 쿼리 최적화: 비효율적인 쿼리는 성능 저하의 원인이 됩니다. 쿼리를 분석하고 최적화하여 성능을 개선해야 합니다.

결론: Node.js로 효율적인 웹 애플리케이션 개발

본 가이드를 통해 Node.js를 사용하여 웹 서버를 구축하고, Express.js를 활용하여 RESTful API를 개발하고, 데이터베이스를 연동하는 방법을 단계별로 살펴보았습니다. 이러한 기본 지식을 바탕으로 더 복잡하고 기능이 풍부한 웹 애플리케이션을 개발할 수 있습니다. Node.js의 강력한 기능과 생태계를 활용하여 효율적이고 확장 가능한 웹 애플리케이션을 구축해 보시기 바랍니다.

728x90