REST API 서버 구축 단계별 튜토리얼 – 완성까지 한번에!
이 REST API 서버 구축 단계별 튜토리얼은 백엔드 개발의 핵심인 RESTful API 서버를 처음부터 끝까지 직접 만들어보는 실습 가이드입니다. Node.js와 Express를 활용하여 데이터베이스 연동, 인증, CRUD 기능을 모두 구현하며, 실제 프로덕션 환경에 배포할 수 있는 수준의 서버를 완성합니다. 초보자도 따라할 수 있도록 각 단계를 상세히 설명하며, 포트폴리오로 활용할 수 있는 완성도 높은 프로젝트를 목표로 합니다.
1. 프로젝트 소개 및 목표
🔗 관련 에러 해결 가이드
이번 튜토리얼에서는 사용자 관리와 게시글 CRUD 기능을 제공하는 REST API 서버를 구축합니다. 주요 목표는 다음과 같습니다:
- RESTful 설계 원칙 이해: HTTP 메서드(GET, POST, PUT, DELETE)와 적절한 엔드포인트 설계
- 데이터베이스 연동: MongoDB를 활용한 데이터 영속성 관리
- 인증 및 보안: JWT 기반 사용자 인증 시스템 구현
- 에러 핸들링: 체계적인 오류 처리 및 HTTP 상태 코드 관리
- API 문서화: Swagger를 통한 자동 API 문서 생성
완성된 프로젝트는 실제 서비스의 백엔드로 활용 가능하며, 프론트엔드 애플리케이션과 연동하여 풀스택 프로젝트로 확장할 수 있습니다.
2. 필요한 기술 스택
이 REST API 서버 구축 단계별 튜토리얼에서 사용할 주요 기술 스택은 다음과 같습니다:
- Node.js (v18 이상): JavaScript 런타임 환경
- Express.js (v4.18+): 웹 프레임워크
- MongoDB: NoSQL 데이터베이스
- Mongoose: MongoDB ODM(Object Data Modeling)
- JWT (jsonwebtoken): 인증 토큰 관리
- bcrypt: 비밀번호 암호화
- dotenv: 환경 변수 관리
- express-validator: 입력 데이터 검증
- Swagger UI: API 문서화
- Jest & Supertest: 테스트 프레임워크
개발 환경으로는 VS Code, Postman(API 테스트), MongoDB Compass(데이터베이스 GUI) 사용을 권장합니다.
3. 프로젝트 셋업
먼저 프로젝트 디렉토리를 생성하고 초기 설정을 진행합니다.
3.1 프로젝트 초기화
# 프로젝트 디렉토리 생성
mkdir rest-api-server
cd rest-api-server
# package.json 생성
npm init -y
# 필수 패키지 설치
npm install express mongoose jsonwebtoken bcryptjs dotenv express-validator cors helmet morgan
# 개발 도구 설치
npm install -D nodemon jest supertest eslint
3.2 프로젝트 구조 설정
rest-api-server/
├── src/
│ ├── config/ # 설정 파일
│ ├── models/ # 데이터베이스 모델
│ ├── controllers/ # 비즈니스 로직
│ ├── routes/ # API 라우트
│ ├── middleware/ # 커스텀 미들웨어
│ ├── utils/ # 유틸리티 함수
│ └── app.js # Express 앱 설정
├── tests/ # 테스트 파일
├── .env # 환경 변수
├── .gitignore
└── server.js # 서버 엔트리 포인트
3.3 환경 변수 설정
# .env 파일 생성
PORT=5000
MONGODB_URI=mongodb://localhost:27017/rest-api-db
JWT_SECRET=your_jwt_secret_key_here
JWT_EXPIRE=7d
NODE_ENV=development
package.json에 실행 스크립트를 추가합니다:
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "jest --watchAll --verbose"
}
4. 단계별 구현 과정
4.1 서버 기본 설정
먼저 Express 서버와 데이터베이스 연결을 설정합니다.
// src/config/database.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
});
console.log('MongoDB 연결 성공');
} catch (error) {
console.error('MongoDB 연결 실패:', error.message);
process.exit(1);
}
};
module.exports = connectDB;
// src/app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const app = express();
// 미들웨어 설정
app.use(helmet()); // 보안 헤더 설정
app.use(cors()); // CORS 활성화
app.use(morgan('dev')); // 로깅
app.use(express.json()); // JSON 파싱
app.use(express.urlencoded({ extended: true }));
// 기본 라우트
app.get('/', (req, res) => {
res.json({ message: 'REST API 서버가 실행 중입니다!' });
});
// 에러 핸들러
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
success: false,
message: err.message || '서버 오류가 발생했습니다'
});
});
module.exports = app;
// server.js
require('dotenv').config();
const app = require('./src/app');
const connectDB = require('./src/config/database');
const PORT = process.env.PORT || 5000;
// 데이터베이스 연결 후 서버 시작
connectDB().then(() => {
app.listen(PORT, () => {
console.log(`서버가 포트 ${PORT}에서 실행 중입니다`);
});
});
4.2 데이터베이스 모델 정의
사용자와 게시글 모델을 생성합니다.
// src/models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
username: {
type: String,
required: [true, '사용자명을 입력해주세요'],
unique: true,
trim: true,
minlength: [3, '사용자명은 최소 3자 이상이어야 합니다']
},
email: {
type: String,
required: [true, '이메일을 입력해주세요'],
unique: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, '유효한 이메일 주소를 입력해주세요']
},
password: {
type: String,
required: [true, '비밀번호를 입력해주세요'],
minlength: [6, '비밀번호는 최소 6자 이상이어야 합니다'],
select: false // 조회 시 기본적으로 제외
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
}
}, {
timestamps: true
});
// 비밀번호 해싱 미들웨어
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// 비밀번호 검증 메서드
userSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
module.exports = mongoose.model('User', userSchema);
// src/models/Post.js
const mongoose = require('mongoose');
const postSchema = new mongoose.Schema({
title: {
type: String,
required: [true, '제목을 입력해주세요'],
trim: true,
maxlength: [100, '제목은 100자를 초과할 수 없습니다']
},
content: {
type: String,
required: [true, '내용을 입력해주세요']
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
tags: [String],
status: {
type: String,
enum: ['draft', 'published'],
default: 'draft'
}
}, {
timestamps: true
});
module.exports = mongoose.model('Post', postSchema);
4.3 인증 시스템 구현
// src/middleware/auth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');
exports.protect = async (req, res, next) => {
try {
let token;
// Authorization 헤더에서 토큰 추출
if (req.headers.authorization?.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}
if (!token) {
return res.status(401).json({
success: false,
message: '인증 토큰이 필요합니다'
});
}
// 토큰 검증
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decoded.id);
if (!req.user) {
return res.status(401).json({
success: false,
message: '유효하지 않은 토큰입니다'
});
}
next();
} catch (error) {
return res.status(401).json({
success: false,
message: '인증에 실패했습니다'
});
}
};
// 관리자 권한 체크
exports.authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
message: '권한이 없습니다'
});
}
next();
};
};
// src/controllers/authController.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');
// JWT 토큰 생성
const generateToken = (id) => {
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRE
});
};
// 회원가입
exports.register = async (req, res) => {
try {
const { username, email, password } = req.body;
// 중복 확인
const existingUser = await User.findOne({ $or: [{ email }, { username }] });
if (existingUser) {
return res.status(400).json({
success: false,
message: '이미 존재하는 사용자입니다'
});
}
const user = await User.create({ username, email, password });
const token = generateToken(user._id);
res.status(201).json({
success: true,
data: {
user: {
id: user._id,
username: user.username,
email: user.email,
role: user.role
},
token
}
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
};
// 로그인
exports.login = async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({
success: false,
message: '이메일과 비밀번호를 입력해주세요'
});
}
const user = await User.findOne({ email }).select('+password');
if (!user || !(await user.comparePassword(password))) {
return res.status(401).json({
success: false,
message: '이메일 또는 비밀번호가 올바르지 않습니다'
});
}
const token = generateToken(user._id);
res.json({
success: true,
data: {
user: {
id: user._id,
username: user.username,
email: user.email,
role: user.role
},
token
}
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
};
4.4 CRUD API 구현
// src/controllers/postController.js
const Post = require('../models/Post');
// 게시글 목록 조회 (페이지네이션)
exports.getPosts = async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const posts = await Post.find({ status: 'published' })
.populate('author', 'username email')
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit);
const total = await Post.countDocuments({ status: 'published' });
res.json({
success: true,
data: {
posts,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
}
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
};
// 게시글 단일 조회
exports.getPost = async (req, res) => {
try {
const post = await Post.findById(req.params.id)
.populate('author', 'username email');
if (!post) {
return res.status(404).json({
success: false,
message: '게시글을 찾을 수 없습니다'
});
}
res.json({
success: true,
data: post
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
};
// 게시글 생성
exports.createPost = async (req, res) => {
try {
const post = await Post.create({
...req.body,
author: req.user._id
});
res.status(201).json({
success: true,
data: post
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
};
// 게시글 수정
exports.updatePost = async (req, res) => {
try {
let post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({
success: false,
message: '게시글을 찾을 수 없습니다'
});
}
// 작성자 확인
if (post.author.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
return res.status(403).json({
success: false,
message: '수정 권한이 없습니다'
});
}
post = await Post.findByIdAndUpdate(req.params.id, req.body, {
new: true,
runValidators: true
});
res.json({
success: true,
data: post
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
};
// 게시글 삭제
exports.deletePost = async (req, res) => {
try {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({
success: false,
message: '게시글을 찾을 수 없습니다'
});
}
if (post.author.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
return res.status(403).json({
success: false,
message: '삭제 권한이 없습니다'
});
}
await post.deleteOne();
res.json({
success: true,
message: '게시글이 삭제되었습니다'
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
};
4.5 라우트 설정
// src/routes/authRoutes.js
const express = require('express');
const { register, login } = require('../controllers/authController');
const router = express.Router();
router.post('/register', register);
router.post('/login', login);
module.exports = router;
// src/routes/postRoutes.js
const express = require('express');
const {
getPosts,
getPost,
createPost,
updatePost,
deletePost
} = require('../controllers/postController');
const { protect } = require('../middleware/auth');
const router = express.Router();
router.route('/')
.get(getPosts)
.post(protect, createPost);
router.route('/:id')
.get(getPost)
.put(protect, updatePost)
.delete(protect, deletePost);
module.exports = router;
// src/app.js에 라우트 추가
const authRoutes = require('./routes/authRoutes');
const postRoutes = require('./routes/postRoutes');
app.use('/api/v1/auth', authRoutes);
app.use('/api/v1/posts', postRoutes);
5. 테스트 및 배포
5.1 API 테스트
Jest와 Supertest를 활용한 통합 테스트 작성:
// tests/auth.test.js
const request = require('supertest');
const app = require('../src/app');
const mongoose = require('mongoose');
const User = require('../src/models/User');
beforeAll(async () => {
await mongoose.connect(process.env.MONGODB_URI_TEST);
});
afterAll(async () => {
await User.deleteMany({});
await mongoose.connection.close();
});
describe('인증 API 테스트', () => {
test('회원가입 성공', async () => {
const response = await request(app)
.post('/api/v1/auth/register')
.send({
username: 'testuser',
email: '[email protected]',
password: 'password123'
});
expect(response.status).toBe(201);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('token');
});
test('로그인 성공', async () => {
const response = await request(app)
.post('/api/v1/auth/login')
.send({
email: '[email protected]',
password: 'password123'
});
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('token');
});
});
5.2 배포 준비
프로덕션 환경을 위한 설정을 추가합니다:
// src/config/production.js
module.exports = {
mongodb: {
options: {
useNewUrlParser: true,
useUnifiedTopology: true,
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000
}
},
cors: {
origin: process.env.ALLOWED_ORIGINS?.split(',') || [],
credentials: true
},
rateLimit: {
windowMs: 15 * 60 * 1000,
max: 100
}
};
Docker를 활용한 배포:
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 5000
CMD ["node", "server.js"]
# docker-compose.yml
version: '3.8'
services:
api:
build: .
ports:
- "5000:5000"
environment:
- MONGODB_URI=mongodb://mongo:27017/rest-api-db
- JWT_SECRET=${JWT_SECRET}
depends_on:
- mongo
mongo:
image: mongo:6
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
volumes:
mongo-data:
6. 마무리 및 확장 아이디어
이 REST API 서버 구축 단계별 튜토리얼을 통해 완성한 프로젝트는 실제 프로덕션 환경에서 사용 가능한 수준의 백엔드 시스템입니다. 핵심 기능인 인증, CRUD, 에러 핸들링을 모두 포함하고 있으며, 확장 가능한 구조로 설계되었습니다.
추가 확장 아이디어:
- 파일 업로드: Multer와 AWS S3를 활용한 이미지 업로드 기능
- 실시간 기능: Socket.io를 통한 실시간 알림 및 채팅
- 검색 기능: Elasticsearch 연동으로 고급 검색 구현
- 캐싱: Redis를 활용한 성능 최적화
- GraphQL: Apollo Server로 GraphQL API 추가
- 마이크로서비스: 기능별로 서비스 분리 및 API Gateway 구축
완성된 코드는 GitHub에 업로드하여 포트폴리오로 활용하고, Swagger 문서를 통해 API 명세를 공유할 수 있습니다. 이 튜토리얼을 기반으로 자신만의 서비스를 개발해보세요!
📚 함께 읽으면 좋은 글
REST API 서버 구축 단계별 튜토리얼 – 완성까지 한번에!
📅 2025. 10. 1.
🎯 REST API 서버 구축 단계별 튜토리얼
REST API 서버 구축 단계별 튜토리얼 – 완성까지 한번에!
📅 2025. 10. 1.
🎯 REST API 서버 구축 단계별 튜토리얼
실시간 채팅 앱 만들기 with Socket.io – 완성까지 한번에!
📅 2025. 10. 1.
🎯 실시간 채팅 앱 만들기 with Socket.io
undefined 완벽 해결법 – 원인부터 예방까지
📅 2025. 9. 29.
🎯 undefined
FastAPI로 REST API 만들기 – 초보자도 쉽게 따라하는 완벽 가이드
📅 2025. 10. 1.
🎯 FastAPI로 REST API 만들기
💡 위 글들을 통해 더 깊이 있는 정보를 얻어보세요!
📢 이 글이 도움되셨나요? 공유해주세요!
여러분의 공유 한 번이 더 많은 사람들에게 도움이 됩니다 ✨
🔥 공유할 때마다 블로그 성장에 큰 힘이 됩니다! 감사합니다 🙏
💬 여러분의 소중한 의견을 들려주세요!
REST API 서버 구축 단계별 튜토리얼 관련해서 궁금한 점이 더 있으시다면 언제든 물어보세요!
⭐ 모든 댓글은 24시간 내에 답변드리며, 여러분의 의견이 다른 독자들에게 큰 도움이 됩니다!
🎯 건설적인 의견과 경험 공유를 환영합니다 ✨
🔔 블로그 구독하고 최신 글을 받아보세요!
🌟 프로젝트 아이디어부터 다양한 실생활 정보까지!
매일 새로운 유용한 콘텐츠를 만나보세요 ✨
📧 RSS 구독 | 🔖 북마크 추가 | 📱 모바일 앱 알림 설정
지금 구독하고 놓치는 정보 없이 업데이트 받아보세요!