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

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

프로젝트 소개 및 목표

REST API 서버 구축 단계별 튜토리얼에서는 Node.js와 Express를 활용하여 실무에서 사용 가능한 RESTful API 서버를 처음부터 끝까지 만들어봅니다. 데이터베이스 연동, 인증 시스템, 에러 핸들링, 그리고 배포까지 모든 과정을 단계별로 학습하게 됩니다. 초보 개발자도 따라할 수 있도록 각 단계마다 상세한 설명과 함께 실제 코드 예제를 제공합니다. 이 튜토리얼을 완료하면 사용자 관리 시스템을 갖춘 완전한 REST API 서버를 포트폴리오에 추가할 수 있으며, 실무 프로젝트에도 바로 적용할 수 있는 실전 경험을 얻게 됩니다.

필요한 기술 스택

이 프로젝트를 시작하기 전에 다음 기술들에 대한 기본적인 이해가 필요합니다:

  • Node.js (v18 이상): 서버 사이드 JavaScript 런타임
  • Express.js: 웹 애플리케이션 프레임워크
  • MongoDB: NoSQL 데이터베이스
  • Mongoose: MongoDB ODM (Object Data Modeling)
  • JWT: 인증을 위한 JSON Web Token
  • Postman: API 테스트 도구
  • Git: 버전 관리 시스템

프로젝트 셋업

먼저 개발 환경을 구성하겠습니다. 터미널을 열고 다음 명령어를 순서대로 실행하세요:

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

# package.json 초기화
npm init -y

# 필요한 패키지 설치
npm install express mongoose dotenv bcryptjs jsonwebtoken cors
npm install --save-dev nodemon

package.json 파일을 열어 scripts 섹션을 다음과 같이 수정합니다:

"scripts": {
  "start": "node server.js",
  "dev": "nodemon server.js",
  "test": "echo \"Error: no test specified\" && exit 1"
}

프로젝트 루트에 .env 파일을 생성하여 환경 변수를 설정합니다:

PORT=5000
MONGODB_URI=mongodb://localhost:27017/rest-api-tutorial
JWT_SECRET=your_super_secret_key_change_this_in_production
NODE_ENV=development

단계별 구현 과정

1단계: 서버 기본 구조 설정

프로젝트의 기본 구조를 만들어봅시다. server.js 파일을 생성하고 다음 코드를 작성합니다:

const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
require('dotenv').config();

const app = express();
const PORT = process.env.PORT || 5000;

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

// 기본 라우트
app.get('/', (req, res) => {
  res.json({ message: 'REST API 서버가 정상적으로 작동 중입니다!' });
});

// MongoDB 연결
mongoose.connect(process.env.MONGODB_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
})
.then(() => console.log('MongoDB 연결 성공'))
.catch((err) => console.error('MongoDB 연결 실패:', err));

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

2단계: 데이터 모델 정의

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자 이상이어야 합니다']
  },
  role: {
    type: String,
    enum: ['user', 'admin'],
    default: 'user'
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

// 비밀번호 해싱 미들웨어
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  
  try {
    const salt = await bcrypt.genSalt(10);
    this.password = await bcrypt.hash(this.password, salt);
    next();
  } catch (error) {
    next(error);
  }
});

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

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

3단계: 인증 미들웨어 구현

middleware 폴더를 생성하고 auth.js 파일을 만듭니다:

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

const authenticateToken = async (req, res, next) => {
  try {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];

    if (!token) {
      return res.status(401).json({ message: '인증 토큰이 필요합니다' });
    }

    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    const user = await User.findById(decoded.userId).select('-password');

    if (!user) {
      return res.status(404).json({ message: '사용자를 찾을 수 없습니다' });
    }

    req.user = user;
    next();
  } catch (error) {
    return res.status(403).json({ message: '유효하지 않은 토큰입니다' });
  }
};

const authorizeRoles = (...roles) => {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ 
        message: '이 작업을 수행할 권한이 없습니다' 
      });
    }
    next();
  };
};

module.exports = { authenticateToken, authorizeRoles };

4단계: 컨트롤러 구현

controllers 폴더를 만들고 authController.js를 생성합니다:

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

// JWT 토큰 생성 함수
const generateToken = (userId) => {
  return jwt.sign({ userId }, process.env.JWT_SECRET, {
    expiresIn: '7d'
  });
};

// 회원가입
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({ 
        message: '이미 존재하는 이메일 또는 사용자 이름입니다' 
      });
    }

    // 새 사용자 생성
    const user = new User({ username, email, password });
    await user.save();

    // 토큰 생성
    const token = generateToken(user._id);

    res.status(201).json({
      message: '회원가입이 완료되었습니다',
      token,
      user: {
        id: user._id,
        username: user.username,
        email: user.email,
        role: user.role
      }
    });
  } catch (error) {
    res.status(500).json({ 
      message: '서버 오류가 발생했습니다', 
      error: error.message 
    });
  }
};

// 로그인
exports.login = async (req, res) => {
  try {
    const { email, password } = req.body;

    // 사용자 찾기
    const user = await User.findOne({ email });
    if (!user) {
      return res.status(401).json({ 
        message: '이메일 또는 비밀번호가 올바르지 않습니다' 
      });
    }

    // 비밀번호 확인
    const isPasswordValid = await user.comparePassword(password);
    if (!isPasswordValid) {
      return res.status(401).json({ 
        message: '이메일 또는 비밀번호가 올바르지 않습니다' 
      });
    }

    // 토큰 생성
    const token = generateToken(user._id);

    res.json({
      message: '로그인 성공',
      token,
      user: {
        id: user._id,
        username: user.username,
        email: user.email,
        role: user.role
      }
    });
  } catch (error) {
    res.status(500).json({ 
      message: '서버 오류가 발생했습니다', 
      error: error.message 
    });
  }
};

// 현재 사용자 정보 조회
exports.getCurrentUser = async (req, res) => {
  try {
    res.json({
      user: req.user
    });
  } catch (error) {
    res.status(500).json({ 
      message: '서버 오류가 발생했습니다', 
      error: error.message 
    });
  }
};

5단계: 라우트 설정

routes 폴더를 생성하고 authRoutes.js 파일을 만듭니다:

const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');
const { authenticateToken } = require('../middleware/auth');

// 공개 라우트
router.post('/register', authController.register);
router.post('/login', authController.login);

// 보호된 라우트
router.get('/me', authenticateToken, authController.getCurrentUser);

module.exports = router;

userRoutes.js 파일도 생성하여 사용자 CRUD 작업을 처리합니다:

const express = require('express');
const router = express.Router();
const User = require('../models/User');
const { authenticateToken, authorizeRoles } = require('../middleware/auth');

// 모든 사용자 조회 (관리자만)
router.get('/', authenticateToken, authorizeRoles('admin'), async (req, res) => {
  try {
    const users = await User.find().select('-password');
    res.json({ users });
  } catch (error) {
    res.status(500).json({ message: '서버 오류', error: error.message });
  }
});

// 특정 사용자 조회
router.get('/:id', authenticateToken, async (req, res) => {
  try {
    const user = await User.findById(req.params.id).select('-password');
    if (!user) {
      return res.status(404).json({ message: '사용자를 찾을 수 없습니다' });
    }
    res.json({ user });
  } catch (error) {
    res.status(500).json({ message: '서버 오류', error: error.message });
  }
});

// 사용자 정보 수정
router.put('/:id', authenticateToken, async (req, res) => {
  try {
    // 자신의 정보만 수정 가능
    if (req.user._id.toString() !== req.params.id && req.user.role !== 'admin') {
      return res.status(403).json({ message: '권한이 없습니다' });
    }

    const { username, email } = req.body;
    const user = await User.findByIdAndUpdate(
      req.params.id,
      { username, email },
      { new: true, runValidators: true }
    ).select('-password');

    if (!user) {
      return res.status(404).json({ message: '사용자를 찾을 수 없습니다' });
    }

    res.json({ message: '정보가 수정되었습니다', user });
  } catch (error) {
    res.status(500).json({ message: '서버 오류', error: error.message });
  }
});

// 사용자 삭제 (관리자만)
router.delete('/:id', authenticateToken, authorizeRoles('admin'), async (req, res) => {
  try {
    const user = await User.findByIdAndDelete(req.params.id);
    if (!user) {
      return res.status(404).json({ message: '사용자를 찾을 수 없습니다' });
    }
    res.json({ message: '사용자가 삭제되었습니다' });
  } catch (error) {
    res.status(500).json({ message: '서버 오류', error: error.message });
  }
});

module.exports = router;

6단계: 라우트 통합 및 에러 핸들링

server.js 파일을 업데이트하여 라우트를 연결하고 전역 에러 핸들러를 추가합니다:

const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
require('dotenv').config();

const app = express();
const PORT = process.env.PORT || 5000;

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

// 라우트 import
const authRoutes = require('./routes/authRoutes');
const userRoutes = require('./routes/userRoutes');

// 라우트 연결
app.get('/', (req, res) => {
  res.json({ 
    message: 'REST API 서버가 정상적으로 작동 중입니다!',
    version: '1.0.0',
    endpoints: {
      auth: '/api/auth',
      users: '/api/users'
    }
  });
});

app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);

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

// 전역 에러 핸들러
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({
    message: err.message || '서버 내부 오류가 발생했습니다',
    ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
  });
});

// MongoDB 연결
mongoose.connect(process.env.MONGODB_URI)
.then(() => {
  console.log('MongoDB 연결 성공');
  // 서버 시작
  app.listen(PORT, () => {
    console.log(`서버가 포트 ${PORT}에서 실행 중입니다.`);
  });
})
.catch((err) => {
  console.error('MongoDB 연결 실패:', err);
  process.exit(1);
});

테스트 및 배포

로컬 테스트

서버를 실행하고 Postman으로 API를 테스트해봅시다:

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

테스트 시나리오:

  1. 회원가입: POST http://localhost:5000/api/auth/register
    {
      "username": "testuser",
      "email": "[email protected]",
      "password": "password123"
    }
    
  2. 로그인: POST http://localhost:5000/api/auth/login
    {
      "email": "[email protected]",
      "password": "password123"
    }
    
  3. 현재 사용자 정보 조회: GET http://localhost:5000/api/auth/me
    Headers: Authorization: Bearer {받은_토큰}
  4. 모든 사용자 조회: GET http://localhost:5000/api/users
    Headers: Authorization: Bearer {관리자_토큰}

배포 준비

프로덕션 환경을 위한 설정을 추가합니다. .gitignore 파일을 생성하세요:

node_modules/
.env
.DS_Store
logs/
*.log

Heroku나 Railway, Render 등의 플랫폼에 배포할 수 있습니다. 배포 전 체크리스트:

  • 환경 변수가 올바르게 설정되었는지 확인
  • JWT_SECRET을 강력한 값으로 변경
  • MongoDB Atlas 등 클라우드 데이터베이스 연결
  • CORS 설정을 프로덕션 도메인으로 제한
  • rate limiting 미들웨어 추가 (선택사항)

마무리 및 확장 아이디어

축하합니다! 이 REST API 서버 구축 단계별 튜토리얼을 통해 완전한 기능을 갖춘 RESTful API를 만들었습니다. 이제 다음과 같은 기능을 추가하여 프로젝트를 확장할 수 있습니다:

  • 비밀번호 재설정: 이메일 인증을 통한 비밀번호 복구 기능
  • 파일 업로드: multer를 이용한 프로필 이미지 업로드
  • 페이지네이션: 대량의 데이터를 효율적으로 조회
  • 검색 및 필터링: 쿼리 파라미터를 활용한 데이터 검색
  • API 문서화: Swagger를 이용한 자동 문서 생성
  • 로깅: Winston이나 Morgan을 이용한 로그 관리
  • 테스트 코드: Jest와 Supertest로 단위 테스트 작성
  • 실시간 기능: Socket.io를 추가하여 실시간 통신 구현

이 튜토리얼에서 배운 내용은 다양한 웹 애플리케이션의 백엔드 기반이 될 수 있습니다. 계속해서 새로운 기능을 추가하며 실력을 향상시켜보세요!

📚 함께 읽으면 좋은 글

1

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

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

2

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

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

3

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

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

4

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

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

5

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

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

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

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

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

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

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

이 글에서 가장 도움이 된 부분은 어떤 것인가요?

💡
유용한 정보 공유

궁금한 점 질문

🤝
경험담 나누기

👍
의견 표현하기

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

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

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

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

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

💡
최신 트렌드
2025년 기준

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

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

답글 남기기