📌 소개
안녕하세요!
이번 포스팅에서는 Node.js와 Express 프레임워크를 사용하여 완전한 RESTful API를 구축하는 방법을 알아보겠습니다.
백엔드 개발에 관심이 있거나 Node.js 생태계에 입문하고자 하는 분들에게 유용한 내용이 될 것입니다.
오늘 만들어볼 프로젝트는 Todo List API로, CRUD(Create, Read, Update, Delete) 작업을 지원하는 기본적인 API 서버입니다.
MongoDB를 데이터베이스로 사용하고, 프론트엔드는 간단한 HTML, CSS, JavaScript로 구현하여
Full-Stack 애플리케이션을 완성해볼 것입니다.
🛠 기술 스택 소개
이 프로젝트에서 사용되는 주요 기술 스택은 다음과 같습니다:
- Backend: Node.js, Express.js
- Database: MongoDB
- Frontend: HTML, CSS, JavaScript (Vanilla)
- Tools: npm, Git
각 기술의 역할을 간략히 살펴보겠습니다:
- Node.js: 서버 측 JavaScript 런타임 환경
- Express.js: Node.js 웹 애플리케이션 프레임워크
- MongoDB: NoSQL 문서 데이터베이스
- Mongoose: MongoDB 객체 모델링 도구 (ODM)
🚀 프로젝트 설정
프로젝트를 시작하기 전, 필요한 도구들을 설치해야 합니다.
사전 요구사항
- Node.js (v12.0.0 이상)
- MongoDB
프로젝트 초기화
# 프로젝트 디렉토리 생성 및 초기화
mkdir todo-rest-api
cd todo-rest-api
npm init -y
# 필요한 패키지 설치
npm install express mongoose body-parser
💾 MongoDB 설정
MongoDB는 NoSQL 데이터베이스로, JSON과 유사한 문서를 저장하는 데 적합합니다.
운영체제별 설치 방법을 알아보겠습니다.
macOS
Homebrew를 사용한 MongoDB 설치:
brew tap mongodb/brew
brew install mongodb-community
MongoDB 서비스 시작:
brew services start mongodb/brew/mongodb-community
Windows
- MongoDB 다운로드 센터에서 MongoDB Community Server 다운로드
- 다운로드한 설치 파일 실행
- "Complete" 설치 옵션 선택
- "MongoDB Compass" 설치 옵션 체크 (선택사항)
- 설치 완료 후 MongoDB 서비스가 자동으로 시작됨
🖥 서버 구현하기
이제 Express.js를 사용하여 RESTful API 서버를 구현해보겠습니다.
server.js 파일 생성
const express = require('express');
const mongoose = require('mongoose');
const bodyParser = require('body-parser');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(bodyParser.json());
// 정적 파일 제공
app.use(express.static('public'));
// MongoDB 연결
mongoose.connect('mongodb://localhost:27017/todo', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
// Todo 모델 정의
const Todo = mongoose.model('Todo', new mongoose.Schema({
title: { type: String, required: true },
completed: { type: Boolean, default: false },
}));
// CRUD API 엔드포인트
// 1. 모든 Todo 가져오기
app.get('/todos', async (req, res) => {
const todos = await Todo.find();
res.json(todos);
});
// 2. Todo 추가하기
app.post('/todos', async (req, res) => {
const todo = new Todo(req.body);
await todo.save();
res.status(201).json(todo);
});
// 3. Todo 수정하기
app.put('/todos/:id', async (req, res) => {
const todo = await Todo.findByIdAndUpdate(req.params.id, req.body, { new: true });
res.json(todo);
});
// 4. Todo 삭제하기
app.delete('/todos/:id', async (req, res) => {
await Todo.findByIdAndDelete(req.params.id);
res.status(204).send();
});
// 기본 경로에 대한 라우트 추가
app.get('/', (req, res) => {
res.sendFile(__dirname + '/public/index.html');
});
// 서버 시작
app.listen(PORT, () => {
console.log(`서버가 http://localhost:${PORT}에서 실행 중입니다.`);
});
이 서버 코드에서는:
- Express 애플리케이션을 생성하고 필요한 미들웨어를 설정합니다.
- MongoDB에 연결하고 Todo 모델을 정의합니다.
- CRUD 작업을 위한 RESTful API 엔드포인트를 구현합니다.
- 서버가 시작될 때 포트 번호를 콘솔에 출력합니다.
🎨 프론트엔드 구현하기
이제 사용자가 API와 상호작용할 수 있는 간단한 프론트엔드를 구현해보겠습니다.
public/index.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo List</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<h1>Todo List</h1>
<input type="text" id="todoInput" placeholder="할 일을 입력하세요...">
<button id="addTodo">추가하기</button>
<ul id="todoList"></ul>
</div>
<script src="scripts.js"></script>
</body>
</html>
public/styles.css
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 20px;
}
.container {
max-width: 600px;
margin: auto;
background: white;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
}
input[type="text"] {
width: calc(100% - 100px);
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
button {
padding: 10px;
border: none;
background-color: #28a745;
color: white;
border-radius: 5px;
cursor: pointer;
}
button:hover {
background-color: #218838;
}
ul {
list-style-type: none;
padding: 0;
}
.button-container {
float: right;
}
.edit-btn, .delete-btn {
margin-left: 5px;
padding: 5px 10px;
}
.edit-btn {
background-color: #ffc107;
}
.edit-btn:hover {
background-color: #e0a800;
}
.delete-btn {
background-color: #dc3545;
}
.delete-btn:hover {
background-color: #c82333;
}
li {
padding: 10px;
border-bottom: 1px solid #ccc;
display: flex;
justify-content: space-between;
align-items: center;
}
li span {
flex-grow: 1;
}
public/scripts.js
document.addEventListener('DOMContentLoaded', () => {
const todoInput = document.getElementById('todoInput');
const addTodoButton = document.getElementById('addTodo');
const todoList = document.getElementById('todoList');
// 할 일 추가 (버튼 클릭)
addTodoButton.addEventListener('click', () => addTodo());
// 할 일 추가 (엔터키)
todoInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
addTodo();
}
});
// 할 일 추가 함수
async function addTodo() {
const title = todoInput.value.trim();
if (!title) return;
try {
const response = await fetch('/todos', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ title }),
});
if (!response.ok) throw new Error('할 일 추가에 실패했습니다.');
const newTodo = await response.json();
addTodoToList(newTodo);
todoInput.value = '';
} catch (error) {
console.error('Error:', error);
alert(error.message);
}
}
// 할 일 목록에 추가하기
function addTodoToList(todo) {
const li = document.createElement('li');
li.dataset.id = todo._id;
const todoText = document.createElement('span');
todoText.textContent = todo.title;
li.appendChild(todoText);
const buttonContainer = document.createElement('div');
buttonContainer.className = 'button-container';
// 수정 버튼
const editButton = createButton('수정', async () => {
const newTitle = prompt('새 제목을 입력하세요:', todo.title);
if (!newTitle || newTitle === todo.title) return;
try {
const updatedTodo = await updateTodo(todo._id, newTitle);
todoText.textContent = updatedTodo.title;
} catch (error) {
console.error('Error:', error);
alert('수정에 실패했습니다.');
}
});
// 삭제 버튼
const deleteButton = createButton('삭제', async () => {
if (!confirm('정말 삭제하시겠습니까?')) return;
try {
await deleteTodo(todo._id);
li.remove();
} catch (error) {
console.error('Error:', error);
alert('삭제에 실패했습니다.');
}
});
buttonContainer.appendChild(editButton);
buttonContainer.appendChild(deleteButton);
li.appendChild(buttonContainer);
todoList.appendChild(li);
}
// 버튼 생성 함수
function createButton(text, onClick) {
const button = document.createElement('button');
button.textContent = text;
button.className = text === '수정' ? 'edit-btn' : 'delete-btn';
button.onclick = onClick;
return button;
}
// Todo 수정 함수
async function updateTodo(id, title) {
const response = await fetch(`/todos/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ title }),
});
if (!response.ok) throw new Error('수정에 실패했습니다.');
return await response.json();
}
// Todo 삭제 함수
async function deleteTodo(id) {
const response = await fetch(`/todos/${id}`, {
method: 'DELETE',
});
if (!response.ok) throw new Error('삭제에 실패했습니다.');
}
// 초기 로드 시 할 일 목록 가져오기
async function loadTodos() {
try {
const response = await fetch('/todos');
if (!response.ok) throw new Error('할 일 목록을 불러오는데 실패했습니다.');
const todos = await response.json();
todos.forEach(addTodoToList);
} catch (error) {
console.error('Error:', error);
alert(error.message);
}
}
loadTodos();
});
🧪 API 테스트
RESTful API를 테스트하는 방법을 알아보겠습니다.
요청/응답 예시
Todo 생성
POST /todos
Content-Type: application/json
{
"title": "새로운 할 일"
}
응답:
{
"_id": "60f1234567890",
"title": "새로운 할 일",
"completed": false
}
Todo 목록 조회
GET /todos
응답:
[
{
"_id": "60f1234567890",
"title": "새로운 할 일",
"completed": false
}
]
Todo 수정
PUT /todos/60f1234567890
Content-Type: application/json
{
"title": "수정된 할 일"
}
응답:
{
"_id": "60f1234567890",
"title": "수정된 할 일",
"completed": false
}
Todo 삭제
DELETE /todos/60f1234567890
응답: 204 No Content
📁 프로젝트 구조
최종 프로젝트 구조는 다음과 같습니다:
/todo-rest-api
│
├── public
│ ├── index.html # 프론트엔드 HTML
│ ├── styles.css # CSS 스타일
│ └── scripts.js # 프론트엔드 JavaScript
│
├── server.js # 서버 설정 및 API 엔드포인트
├── package.json # 프로젝트 의존성
└── README.md # 프로젝트 문서
📐 RESTful API 디자인 원칙
RESTful API 설계 시 고려해야 할 주요 원칙들은 다음과 같습니다:
- 자원 기반 URL: 명사를 사용하여 리소스를 표현 (예:
/todos
) - HTTP 메서드 활용:
- GET: 리소스 조회
- POST: 새 리소스 생성
- PUT/PATCH: 리소스 수정
- DELETE: 리소스 삭제
- 상태 코드 적절히 사용:
- 200: 성공
- 201: 생성 성공
- 204: 콘텐츠 없음
- 400: 잘못된 요청
- 404: 리소스 없음
- 500: 서버 오류
- 응답 포맷: JSON 형식 사용
- 버전 관리: API 버전을 URL에 포함 (예:
/v1/todos
)
🔒 보안 고려사항
실제 프로덕션 환경에서는 다음과 같은 보안 조치를 추가하는 것이 좋습니다:
- 인증 및 권한 부여: JWT(JSON Web Token)를 사용한 사용자 인증
- 입력 유효성 검사: 모든 클라이언트 입력 데이터 검증
- HTTPS 사용: 모든 API 통신에 HTTPS 적용
- 속도 제한: 특정 IP에서 과도한 요청 제한
- CORS(Cross-Origin Resource Sharing) 설정: 허용된 도메인만 API 접근 가능
이러한 보안 기능을 구현하기 위해서는 다음과 같은 패키지들을 활용할 수 있습니다:
helmet
: HTTP 헤더 보안express-rate-limit
: 요청 속도 제한jsonwebtoken
: JWT 구현express-validator
: 입력 유효성 검사
🎯 마무리 및 추가 학습 자료
이 튜토리얼에서는 Node.js와 Express를 사용하여 간단한 RESTful API를 구현해보았습니다.
이러한 기본 지식을 바탕으로 더 복잡한 애플리케이션을 개발할 수 있습니다.
더 깊이 학습하고 싶다면 다음 주제들을 살펴보세요:
- Express 미들웨어: 요청 처리 파이프라인 구성
- MongoDB 고급 기능: 인덱싱, 집계, 트랜잭션
- 인증 및 권한 부여: OAuth, JWT, 세션 관리
- 마이크로서비스 아키텍처: API 게이트웨이, 서비스 디스커버리
- 테스트 자동화: 단위 테스트, 통합 테스트, E2E 테스트
참고 자료
이 튜토리얼이 여러분의 RESTful API 개발 여정에 도움이 되었기를 바랍니다.
질문이나 피드백이 있으시면 댓글로 남겨주세요!
🚀 Github 코드
이 튜토리얼의 전체 소스 코드는 GitHub 저장소에서 확인할 수 있습니다.