프로젝트 소개 및 목표
🔗 관련 에러 해결 가이드
JWT 인증 시스템 구현하기는 현대 웹 애플리케이션에서 가장 널리 사용되는 인증 방식을 직접 구축해보는 실습 프로젝트입니다. JWT(JSON Web Token)는 사용자 인증 정보를 안전하게 전달하는 표준 방식으로, RESTful API와 마이크로서비스 아키텍처에서 필수적인 기술입니다. 이 프로젝트를 통해 회원가입, 로그인, 토큰 발급 및 검증, 리프레시 토큰 관리까지 완전한 인증 시스템을 구현할 수 있습니다. 보안의 기본 원칙을 이해하고 실무에서 바로 적용 가능한 인증 로직을 배우게 됩니다. 완성된 프로젝트는 포트폴리오로 활용하기에 매우 적합하며, 다른 프로젝트에 재사용할 수 있는 모듈로도 활용 가능합니다.
필요한 기술 스택
이 프로젝트를 구현하기 위해서는 다음과 같은 기술 스택이 필요합니다. 백엔드는 Node.js와 Express.js 프레임워크를 사용하며, JWT 생성 및 검증을 위해 jsonwebtoken 라이브러리를 활용합니다. 데이터베이스는 MongoDB와 Mongoose를 사용하여 사용자 정보를 저장하고, 비밀번호 암호화를 위해 bcrypt를 사용합니다. 프론트엔드는 React.js로 구성하며, HTTP 요청을 위해 Axios를 사용합니다. 개발 환경은 Node.js 18 이상, npm 또는 yarn 패키지 매니저가 설치되어 있어야 합니다.
프로젝트 셋업
프로젝트를 시작하기 위한 초기 설정 단계입니다. 먼저 프로젝트 디렉토리를 생성하고 npm을 초기화합니다.
mkdir jwt-auth-system
cd jwt-auth-system
npm init -y
필요한 패키지들을 설치합니다:
npm install express mongoose jsonwebtoken bcryptjs dotenv cors
npm install -D nodemon
프로젝트 구조를 다음과 같이 설정합니다:
jwt-auth-system/
├── server/
│ ├── models/
│ │ └── User.js
│ ├── routes/
│ │ └── auth.js
│ ├── middleware/
│ │ └── auth.js
│ ├── config/
│ │ └── db.js
│ └── server.js
├── client/
│ └── (React 앱)
├── .env
└── package.json
.env 파일을 생성하여 환경변수를 설정합니다:
MONGO_URI=mongodb://localhost:27017/jwt-auth
JWT_SECRET=your_jwt_secret_key_here
JWT_REFRESH_SECRET=your_refresh_secret_key_here
PORT=5000
단계별 구현 과정
1단계: 데이터베이스 연결 및 사용자 모델 생성
먼저 MongoDB 연결을 설정합니다. server/config/db.js 파일을 생성합니다:
const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
});
console.log('MongoDB 연결 성공');
} catch (error) {
console.error('MongoDB 연결 실패:', error.message);
process.exit(1);
}
};
module.exports = connectDB;
사용자 모델을 생성합니다. server/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
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true
},
password: {
type: String,
required: true,
minlength: 6
},
refreshToken: {
type: String,
default: null
},
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, 10);
next();
});
// 비밀번호 검증 메서드
userSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
module.exports = mongoose.model('User', userSchema);
2단계: JWT 토큰 생성 및 검증 로직
JWT 인증 시스템 구현하기의 핵심은 토큰 생성과 검증입니다. 인증 라우트를 생성합니다. server/routes/auth.js:
const express = require('express');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const router = express.Router();
// Access Token 생성 함수
const generateAccessToken = (userId) => {
return jwt.sign(
{ userId },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
};
// Refresh Token 생성 함수
const generateRefreshToken = (userId) => {
return jwt.sign(
{ userId },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
};
// 회원가입 엔드포인트
router.post('/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 accessToken = generateAccessToken(user._id);
const refreshToken = generateRefreshToken(user._id);
// Refresh Token을 DB에 저장
user.refreshToken = refreshToken;
await user.save();
res.status(201).json({
message: '회원가입 성공',
accessToken,
refreshToken,
user: {
id: user._id,
username: user.username,
email: user.email
}
});
} catch (error) {
res.status(500).json({ message: '서버 오류', error: error.message });
}
});
// 로그인 엔드포인트
router.post('/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 accessToken = generateAccessToken(user._id);
const refreshToken = generateRefreshToken(user._id);
// Refresh Token 저장
user.refreshToken = refreshToken;
await user.save();
res.json({
message: '로그인 성공',
accessToken,
refreshToken,
user: {
id: user._id,
username: user.username,
email: user.email
}
});
} catch (error) {
res.status(500).json({ message: '서버 오류', error: error.message });
}
});
// 토큰 갱신 엔드포인트
router.post('/refresh', async (req, res) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ message: 'Refresh Token이 필요합니다.' });
}
// Refresh Token 검증
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
const user = await User.findById(decoded.userId);
if (!user || user.refreshToken !== refreshToken) {
return res.status(403).json({ message: '유효하지 않은 Refresh Token입니다.' });
}
// 새로운 Access Token 생성
const newAccessToken = generateAccessToken(user._id);
res.json({
accessToken: newAccessToken
});
} catch (error) {
res.status(403).json({ message: '토큰 갱신 실패', error: error.message });
}
});
// 로그아웃 엔드포인트
router.post('/logout', async (req, res) => {
try {
const { userId } = req.body;
await User.findByIdAndUpdate(userId, { refreshToken: null });
res.json({ message: '로그아웃 성공' });
} catch (error) {
res.status(500).json({ message: '서버 오류', error: error.message });
}
});
module.exports = router;
3단계: 인증 미들웨어 구현
보호된 라우트에 사용할 인증 미들웨어를 생성합니다. server/middleware/auth.js:
const jwt = require('jsonwebtoken');
const authMiddleware = (req, res, next) => {
try {
// Authorization 헤더에서 토큰 추출
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: '인증 토큰이 없습니다.' });
}
const token = authHeader.split(' ')[1];
// 토큰 검증
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.userId = decoded.userId;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ message: '토큰이 만료되었습니다.' });
}
return res.status(403).json({ message: '유효하지 않은 토큰입니다.' });
}
};
module.exports = authMiddleware;
4단계: Express 서버 설정
메인 서버 파일을 구성합니다. server/server.js:
const express = require('express');
const cors = require('cors');
const dotenv = require('dotenv');
const connectDB = require('./config/db');
const authRoutes = require('./routes/auth');
const authMiddleware = require('./middleware/auth');
const User = require('./models/User');
dotenv.config();
const app = express();
// 미들웨어
app.use(cors());
app.use(express.json());
// 데이터베이스 연결
connectDB();
// 라우트
app.use('/api/auth', authRoutes);
// 보호된 라우트 예시
app.get('/api/profile', authMiddleware, async (req, res) => {
try {
const user = await User.findById(req.userId).select('-password -refreshToken');
res.json({ user });
} catch (error) {
res.status(500).json({ message: '서버 오류' });
}
});
// 서버 시작
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`서버가 포트 ${PORT}에서 실행 중입니다.`);
});
5단계: React 프론트엔드 구현
클라이언트 측에서 토큰을 관리하는 간단한 예시입니다:
// src/services/authService.js
import axios from 'axios';
const API_URL = 'http://localhost:5000/api/auth';
class AuthService {
async register(username, email, password) {
const response = await axios.post(`${API_URL}/register`, {
username,
email,
password
});
if (response.data.accessToken) {
localStorage.setItem('accessToken', response.data.accessToken);
localStorage.setItem('refreshToken', response.data.refreshToken);
}
return response.data;
}
async login(email, password) {
const response = await axios.post(`${API_URL}/login`, {
email,
password
});
if (response.data.accessToken) {
localStorage.setItem('accessToken', response.data.accessToken);
localStorage.setItem('refreshToken', response.data.refreshToken);
}
return response.data;
}
logout() {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
}
async refreshToken() {
const refreshToken = localStorage.getItem('refreshToken');
const response = await axios.post(`${API_URL}/refresh`, {
refreshToken
});
if (response.data.accessToken) {
localStorage.setItem('accessToken', response.data.accessToken);
}
return response.data.accessToken;
}
getAccessToken() {
return localStorage.getItem('accessToken');
}
}
export default new AuthService();
Axios 인터셉터를 사용하여 자동으로 토큰을 갱신합니다:
// src/services/api.js
import axios from 'axios';
import authService from './authService';
const api = axios.create({
baseURL: 'http://localhost:5000/api'
});
// 요청 인터셉터
api.interceptors.request.use(
(config) => {
const token = authService.getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// 응답 인터셉터
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const newAccessToken = await authService.refreshToken();
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
return api(originalRequest);
} catch (refreshError) {
authService.logout();
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
export default api;
테스트 및 배포
프로젝트를 테스트하기 위해 Postman이나 Thunder Client를 사용합니다. 회원가입, 로그인, 토큰 갱신, 보호된 라우트 접근 등을 테스트합니다.
// 테스트 스크립트 예시
npm install -D jest supertest
// tests/auth.test.js
const request = require('supertest');
const app = require('../server/server');
describe('Auth API', () => {
it('회원가입 테스트', async () => {
const res = await request(app)
.post('/api/auth/register')
.send({
username: 'testuser',
email: '[email protected]',
password: 'password123'
});
expect(res.statusCode).toBe(201);
expect(res.body).toHaveProperty('accessToken');
});
});
배포는 백엔드를 Heroku나 Railway에, 프론트엔드를 Vercel이나 Netlify에 배포할 수 있습니다. 환경변수를 반드시 설정하고, HTTPS를 사용하여 보안을 강화합니다. MongoDB는 MongoDB Atlas를 사용하여 클라우드 데이터베이스로 전환합니다.
마무리 및 확장 아이디어
JWT 인증 시스템 구현하기 프로젝트를 완성했습니다! 이제 완전히 동작하는 인증 시스템을 구축했으며, 이를 다양한 프로젝트에 적용할 수 있습니다. 확장 아이디어로는 OAuth2.0 소셜 로그인 추가, 이메일 인증 기능, 비밀번호 재설정 기능, 역할 기반 접근 제어(RBAC), 다중 디바이스 로그인 관리, Redis를 사용한 토큰 블랙리스트 구현 등이 있습니다. 이 프로젝트는 포트폴리오에 추가하기 좋으며, 실무에서 요구되는 보안 개념을 실습할 수 있는 훌륭한 기회입니다.
📚 함께 읽으면 좋은 글
MongoDB와 Express.js로 블로그 만들기 – 완성까지 한번에!
📅 2025. 10. 14.
🎯 MongoDB와 Express.js로 블로그 만들기
30분만에 만드는 Todo App 완성 가이드 – 완성까지 한번에!
📅 2025. 10. 10.
🎯 30분만에 만드는 Todo App 완성 가이드
JWT 인증 시스템 구현하기 – 완성까지 한번에!
📅 2025. 10. 9.
🎯 JWT 인증 시스템 구현하기
실시간 채팅 앱 만들기 with Socket.io – 완성까지 한번에!
📅 2025. 10. 9.
🎯 실시간 채팅 앱 만들기 with Socket.io
React + Node.js 풀스택 앱 배포하기 – 완성까지 한번에!
📅 2025. 10. 9.
🎯 React + Node.js 풀스택 앱 배포하기
💡 위 글들을 통해 더 깊이 있는 정보를 얻어보세요!
📢 이 글이 도움되셨나요? 공유해주세요!
여러분의 공유 한 번이 더 많은 사람들에게 도움이 됩니다 ✨
🔥 공유할 때마다 블로그 성장에 큰 힘이 됩니다! 감사합니다 🙏
💬 여러분의 소중한 의견을 들려주세요!
JWT 인증 시스템 구현하기 관련해서 궁금한 점이 더 있으시다면 언제든 물어보세요!
⭐ 모든 댓글은 24시간 내에 답변드리며, 여러분의 의견이 다른 독자들에게 큰 도움이 됩니다!
🎯 건설적인 의견과 경험 공유를 환영합니다 ✨
🔔 블로그 구독하고 최신 글을 받아보세요!
🌟 프로젝트 아이디어부터 다양한 실생활 정보까지!
매일 새로운 유용한 콘텐츠를 만나보세요 ✨
📧 RSS 구독 | 🔖 북마크 추가 | 📱 모바일 앱 알림 설정
지금 구독하고 놓치는 정보 없이 업데이트 받아보세요!