REST API 서버 구축 단계별 튜토리얼 – 완성까지 한번에!

REST API 서버 구축 단계별 튜토리얼 – 완성까지 한번에!

REST API 서버 구축 단계별 튜토리얼에서는 Node.js와 Express를 활용하여 실무에서 바로 사용할 수 있는 RESTful API 서버를 처음부터 끝까지 완성해보겠습니다. 초보자도 따라할 수 있도록 각 단계를 상세히 설명하며, 데이터베이스 연동부터 인증, 에러 핸들링까지 실전 개발에 필요한 모든 요소를 다룹니다. 이 튜토리얼을 완료하면 포트폴리오에 추가할 수 있는 완성도 높은 프로젝트를 갖게 될 것입니다.

1. 프로젝트 소개 및 목표

이번 프로젝트에서는 사용자 관리와 게시글 CRUD 기능을 제공하는 RESTful API 서버를 구축합니다. 주요 목표는 다음과 같습니다:

  • RESTful 원칙을 준수하는 API 엔드포인트 설계
  • JWT 기반 인증 및 권한 관리 시스템 구현
  • MongoDB를 활용한 데이터베이스 연동
  • 체계적인 에러 핸들링 및 검증 로직
  • 실무 환경을 고려한 보안 및 최적화

완성된 서버는 프론트엔드 애플리케이션과 연동하거나 모바일 앱의 백엔드로 활용할 수 있으며, 실제 서비스 배포까지 경험할 수 있습니다.

2. 필요한 기술 스택

REST API 서버 구축 단계별 튜토리얼을 진행하기 위해 다음 기술 스택이 필요합니다:

  • Node.js (v16 이상) – JavaScript 런타임 환경
  • Express.js – 웹 프레임워크
  • MongoDB – NoSQL 데이터베이스
  • Mongoose – MongoDB ODM (Object Data Modeling)
  • JWT (jsonwebtoken) – 인증 토큰 관리
  • bcrypt – 비밀번호 암호화
  • express-validator – 입력 데이터 검증
  • dotenv – 환경 변수 관리

개발 도구로는 Postman 또는 Thunder Client를 사용하여 API 테스트를 진행합니다.

3. 프로젝트 셋업

먼저 프로젝트 디렉토리를 생성하고 필요한 패키지를 설치합니다.

3.1 초기 설정

# 프로젝트 디렉토리 생성
mkdir rest-api-tutorial
cd rest-api-tutorial

# package.json 생성
npm init -y

# 필수 패키지 설치
npm install express mongoose dotenv jsonwebtoken bcryptjs express-validator cors helmet

# 개발 의존성 설치
npm install --save-dev nodemon

3.2 프로젝트 구조

rest-api-tutorial/
├── src/
│   ├── config/
│   │   └── database.js
│   ├── models/
│   │   ├── User.js
│   │   └── Post.js
│   ├── routes/
│   │   ├── auth.js
│   │   └── posts.js
│   ├── middleware/
│   │   ├── auth.js
│   │   └── errorHandler.js
│   ├── controllers/
│   │   ├── authController.js
│   │   └── postController.js
│   └── validators/
│       └── postValidator.js
├── .env
├── .gitignore
├── server.js
└── package.json

3.3 환경 변수 설정

.env 파일을 생성하고 다음 내용을 추가합니다:

PORT=5000
MONGODB_URI=mongodb://localhost:27017/rest-api-tutorial
JWT_SECRET=your-super-secret-jwt-key-change-this
NODE_ENV=development

4. 단계별 구현 과정

이제 본격적으로 REST API 서버 구축 단계별 튜토리얼의 핵심 구현 단계를 진행하겠습니다.

4.1 데이터베이스 연결 설정

src/config/database.js 파일을 생성하여 MongoDB 연결을 설정합니다:

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;

4.2 User 모델 생성

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
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

// 비밀번호 암호화 미들웨어
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  this.password = await bcrypt.hash(this.password, 12);
  next();
});

// 비밀번호 검증 메서드
userSchema.methods.comparePassword = async function(candidatePassword) {
  return await bcrypt.compare(candidatePassword, this.password);
};

module.exports = mongoose.model('User', userSchema);

4.3 Post 모델 생성

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, '내용은 필수입니다'],
    minlength: [10, '내용은 최소 10자 이상이어야 합니다']
  },
  author: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true
  },
  tags: [{
    type: String,
    trim: true
  }],
  status: {
    type: String,
    enum: ['draft', 'published'],
    default: 'draft'
  },
  createdAt: {
    type: Date,
    default: Date.now
  },
  updatedAt: {
    type: Date,
    default: Date.now
  }
});

postSchema.pre('save', function(next) {
  this.updatedAt = Date.now();
  next();
});

module.exports = mongoose.model('Post', postSchema);

4.4 인증 미들웨어 구현

src/middleware/auth.js 파일에서 JWT 검증 미들웨어를 작성합니다:

const jwt = require('jsonwebtoken');
const User = require('../models/User');

const protect = async (req, res, next) => {
  try {
    let token;
    
    if (req.headers.authorization && 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) {
    res.status(401).json({
      success: false,
      message: '토큰 인증에 실패했습니다'
    });
  }
};

module.exports = { protect };

4.5 인증 컨트롤러 구현

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: '7d'
  });
};

// 회원가입
exports.register = async (req, res) => {
  try {
    const { username, email, password } = req.body;
    
    const userExists = await User.findOne({ $or: [{ email }, { username }] });
    if (userExists) {
      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: {
        id: user._id,
        username: user.username,
        email: user.email,
        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: {
        id: user._id,
        username: user.username,
        email: user.email,
        token
      }
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      message: error.message
    });
  }
};

4.6 게시글 컨트롤러 구현

src/controllers/postController.js 파일에서 CRUD 기능을 구현합니다:

const Post = require('../models/Post');

// 모든 게시글 조회
exports.getPosts = async (req, res) => {
  try {
    const { page = 1, limit = 10, status } = req.query;
    const query = status ? { status } : {};
    
    const posts = await Post.find(query)
      .populate('author', 'username email')
      .limit(limit * 1)
      .skip((page - 1) * limit)
      .sort({ createdAt: -1 });
    
    const count = await Post.countDocuments(query);
    
    res.json({
      success: true,
      data: posts,
      pagination: {
        currentPage: parseInt(page),
        totalPages: Math.ceil(count / limit),
        totalItems: count
      }
    });
  } 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 { title, content, tags, status } = req.body;
    
    const post = await Post.create({
      title,
      content,
      tags,
      status,
      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 {
    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()) {
      return res.status(403).json({
        success: false,
        message: '수정 권한이 없습니다'
      });
    }
    
    const updatedPost = await Post.findByIdAndUpdate(
      req.params.id,
      req.body,
      { new: true, runValidators: true }
    );
    
    res.json({ success: true, data: updatedPost });
  } 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()) {
      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.7 라우트 설정

src/routes/auth.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/posts.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;

4.8 서버 메인 파일 구성

server.js 파일에서 모든 설정을 통합합니다:

require('dotenv').config();
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const connectDB = require('./src/config/database');
const authRoutes = require('./src/routes/auth');
const postRoutes = require('./src/routes/posts');

const app = express();

// 데이터베이스 연결
connectDB();

// 미들웨어
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 라우트
app.use('/api/auth', authRoutes);
app.use('/api/posts', postRoutes);

// 에러 핸들링
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({
    success: false,
    message: err.message || '서버 오류가 발생했습니다'
  });
});

// 404 처리
app.use((req, res) => {
  res.status(404).json({
    success: false,
    message: '요청한 리소스를 찾을 수 없습니다'
  });
});

const PORT = process.env.PORT || 5000;

app.listen(PORT, () => {
  console.log(`서버가 포트 ${PORT}에서 실행 중입니다`);
});

5. 테스트 및 배포

5.1 로컬 테스트

package.json에 스크립트를 추가합니다:

"scripts": {
  "start": "node server.js",
  "dev": "nodemon server.js"
}

서버를 실행하고 Postman으로 API를 테스트합니다:

# 개발 모드로 서버 실행
npm run dev

# 회원가입 테스트
POST http://localhost:5000/api/auth/register
Body: { "username": "testuser", "email": "[email protected]", "password": "password123" }

# 로그인 테스트
POST http://localhost:5000/api/auth/login
Body: { "email": "[email protected]", "password": "password123" }

# 게시글 생성 (인증 필요)
POST http://localhost:5000/api/posts
Headers: Authorization: Bearer [토큰]
Body: { "title": "첫 게시글", "content": "내용입니다", "status": "published" }

5.2 배포 준비

프로덕션 환경을 위한 .gitignore 파일을 생성합니다:

node_modules/
.env
*.log
.DS_Store

5.3 Heroku 배포

# Heroku CLI로 배포
heroku create your-app-name
heroku config:set MONGODB_URI=your-mongodb-uri
heroku config:set JWT_SECRET=your-jwt-secret
git push heroku main

또는 Vercel, Railway, Render 등의 플랫폼을 사용하여 간편하게 배포할 수 있습니다.

6. 마무리 및 확장 아이디어

REST API 서버 구축 단계별 튜토리얼을 통해 완전한 기능을 갖춘 RESTful API 서버를 구축했습니다. 다음 단계로 확장할 수 있는 아이디어는:

  • 파일 업로드 – Multer를 사용한 이미지 업로드 기능
  • 댓글 시스템 – 게시글에 댓글 추가 기능
  • 좋아요 기능 – 게시글 좋아요 및 북마크
  • 검색 및 필터링 – 전체 텍스트 검색 구현
  • Rate Limiting – API 요청 제한으로 보안 강화
  • API 문서화 – Swagger로 자동 문서 생성
  • WebSocket – 실시간 알림 기능 추가
  • 캐싱 – Redis를 활용한 성능 최적화

이제 직접 기능을 추가하며 서버를 발전시켜보세요. 이 프로젝트는 포트폴리오에 추가하기 좋은 실무 중심의 완성도 높은 작품이 될 것입니다!

📚 함께 읽으면 좋은 글

1

REST API 서버 구축 단계별 튜토리얼 – 완성까지 한번에!

📂 프로젝트 아이디어
📅 2025. 10. 19.
🎯 REST API 서버 구축 단계별 튜토리얼

2

MongoDB와 Express.js로 블로그 만들기 – 완성까지 한번에!

📂 프로젝트 아이디어
📅 2025. 10. 31.
🎯 MongoDB와 Express.js로 블로그 만들기

3

MongoDB와 Express.js로 블로그 만들기 – 완성까지 한번에!

📂 프로젝트 아이디어
📅 2025. 10. 30.
🎯 MongoDB와 Express.js로 블로그 만들기

4

실시간 채팅 앱 만들기 with Socket.io – 완성까지 한번에!

📂 프로젝트 아이디어
📅 2025. 10. 29.
🎯 실시간 채팅 앱 만들기 with Socket.io

5

30분만에 만드는 Todo App 완성 가이드 – 완성까지 한번에!

📂 프로젝트 아이디어
📅 2025. 10. 28.
🎯 30분만에 만드는 Todo App 완성 가이드

💡 위 글들을 통해 더 깊이 있는 정보를 얻어보세요!

📢 이 글이 도움되셨나요? 공유해주세요!

여러분의 공유 한 번이 더 많은 사람들에게 도움이 됩니다 ✨

🔥 공유할 때마다 블로그 성장에 큰 힘이 됩니다! 감사합니다 🙏

💬 여러분의 소중한 의견을 들려주세요!

REST API 서버 구축 단계별 튜토리얼에 대한 여러분만의 경험이나 노하우가 있으시나요?

💡
유용한 정보 공유

궁금한 점 질문

🤝
경험담 나누기

👍
의견 표현하기

⭐ 모든 댓글은 24시간 내에 답변드리며, 여러분의 의견이 다른 독자들에게 큰 도움이 됩니다!
🎯 건설적인 의견과 경험 공유를 환영합니다 ✨

🔔 블로그 구독하고 최신 글을 받아보세요!

📚
다양한 주제
17개 카테고리

정기 업데이트
하루 3회 발행

🎯
실용적 정보
바로 적용 가능

💡
최신 트렌드
2025년 기준

🌟 프로젝트 아이디어부터 다양한 실생활 정보까지!
매일 새로운 유용한 콘텐츠를 만나보세요 ✨

📧 RSS 구독 | 🔖 북마크 추가 | 📱 모바일 앱 알림 설정
지금 구독하고 놓치는 정보 없이 업데이트 받아보세요!

답글 남기기