JWT 인증 시스템 구현하기 – 완성까지 한번에!
프로젝트 소개 및 목표
🔗 관련 에러 해결 가이드
JWT 인증 시스템 구현하기는 현대 웹 애플리케이션의 핵심 보안 기능을 직접 만들어보는 실전 프로젝트입니다. 이 가이드를 따라하면 사용자 회원가입, 로그인, 토큰 발급 및 검증, 보호된 라우트 접근 제어까지 완벽하게 구현할 수 있습니다. JWT(JSON Web Token)는 서버와 클라이언트 간의 안전한 정보 전달을 위한 산업 표준 방식으로, 이를 직접 구현해보면서 인증/인가의 핵심 개념을 체득할 수 있습니다. 완성된 프로젝트는 포트폴리오로 활용하기에도 최적이며, 실무에서 바로 적용 가능한 코드 구조를 학습할 수 있습니다.
필요한 기술 스택
이 프로젝트를 완성하기 위해 다음 기술들이 필요합니다:
- 백엔드: Node.js, Express.js, jsonwebtoken, bcrypt
- 데이터베이스: MongoDB (Mongoose) 또는 PostgreSQL (Sequelize)
- 프론트엔드: React.js, Axios, React Router
- 개발 도구: Postman (API 테스트), Git
- 배포: Heroku 또는 Vercel, MongoDB Atlas
Node.js와 JavaScript 기본 지식이 있다면 누구나 따라할 수 있도록 단계별로 상세히 설명하겠습니다.
프로젝트 셋업
먼저 프로젝트 디렉토리를 생성하고 필요한 패키지를 설치합니다:
# 프로젝트 폴더 생성
mkdir jwt-auth-system
cd jwt-auth-system
# 백엔드 셋업
mkdir server
cd server
npm init -y
npm install express mongoose jsonwebtoken bcryptjs dotenv cors express-validator
npm install --save-dev nodemon
# 프론트엔드 셋업
cd ..
npx create-react-app client
cd client
npm install axios react-router-dom
프로젝트 구조는 다음과 같이 구성됩니다:
jwt-auth-system/
├── server/
│ ├── models/
│ ├── routes/
│ ├── middleware/
│ ├── config/
│ └── server.js
└── client/
└── src/
├── components/
├── pages/
└── services/
단계별 구현 과정
1단계: 환경 변수 설정
server 폴더에 .env 파일을 생성하고 다음 내용을 추가합니다:
PORT=5000
MONGO_URI=mongodb://localhost:27017/jwt-auth
JWT_SECRET=your_super_secret_key_change_this_in_production
JWT_EXPIRE=7d
2단계: 사용자 모델 생성
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, '사용자명은 최소 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')) {
next();
}
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
});
// 비밀번호 검증 메서드
UserSchema.methods.matchPassword = async function(enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
module.exports = mongoose.model('User', UserSchema);
3단계: JWT 미들웨어 구현
server/middleware/auth.js 파일을 생성하여 토큰 검증 로직을 구현합니다:
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const protect = async (req, res, next) => {
let token;
// Authorization 헤더에서 토큰 추출
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: '인증 토큰이 없습니다. 로그인해주세요.'
});
}
try {
// 토큰 검증
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// 사용자 정보 조회 (비밀번호 제외)
req.user = await User.findById(decoded.id).select('-password');
if (!req.user) {
return res.status(401).json({
success: false,
message: '해당 사용자를 찾을 수 없습니다.'
});
}
next();
} catch (error) {
return res.status(401).json({
success: false,
message: '유효하지 않은 토큰입니다.'
});
}
};
module.exports = { protect };
4단계: 인증 라우트 구현
server/routes/auth.js 파일에 회원가입과 로그인 API를 작성합니다:
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const { body, validationResult } = require('express-validator');
const User = require('../models/User');
const { protect } = require('../middleware/auth');
// JWT 토큰 생성 함수
const generateToken = (id) => {
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRE
});
};
// 회원가입
router.post('/register', [
body('username').trim().isLength({ min: 3 }).withMessage('사용자명은 최소 3자 이상이어야 합니다'),
body('email').isEmail().withMessage('올바른 이메일을 입력해주세요'),
body('password').isLength({ min: 6 }).withMessage('비밀번호는 최소 6자 이상이어야 합니다')
], async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ success: false, errors: errors.array() });
}
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,
token,
user: {
id: user._id,
username: user.username,
email: user.email
}
});
} catch (error) {
res.status(500).json({
success: false,
message: '서버 오류가 발생했습니다.',
error: error.message
});
}
});
// 로그인
router.post('/login', [
body('email').isEmail().withMessage('올바른 이메일을 입력해주세요'),
body('password').notEmpty().withMessage('비밀번호를 입력해주세요')
], async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ success: false, errors: errors.array() });
}
try {
const { email, password } = req.body;
// 사용자 조회 (비밀번호 포함)
const user = await User.findOne({ email }).select('+password');
if (!user) {
return res.status(401).json({
success: false,
message: '이메일 또는 비밀번호가 일치하지 않습니다.'
});
}
// 비밀번호 검증
const isMatch = await user.matchPassword(password);
if (!isMatch) {
return res.status(401).json({
success: false,
message: '이메일 또는 비밀번호가 일치하지 않습니다.'
});
}
// 토큰 발급
const token = generateToken(user._id);
res.json({
success: true,
token,
user: {
id: user._id,
username: user.username,
email: user.email
}
});
} catch (error) {
res.status(500).json({
success: false,
message: '서버 오류가 발생했습니다.',
error: error.message
});
}
});
// 현재 사용자 정보 조회 (보호된 라우트)
router.get('/me', protect, async (req, res) => {
res.json({
success: true,
user: req.user
});
});
module.exports = router;
5단계: Express 서버 설정
server/server.js 파일을 작성합니다:
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const dotenv = require('dotenv');
// 환경 변수 로드
dotenv.config();
const app = express();
// 미들웨어
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// MongoDB 연결
mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => console.log('MongoDB 연결 성공'))
.catch((err) => console.error('MongoDB 연결 실패:', err));
// 라우트
app.use('/api/auth', require('./routes/auth'));
// 기본 라우트
app.get('/', (req, res) => {
res.json({ message: 'JWT 인증 시스템 API' });
});
// 에러 핸들링
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
success: false,
message: '서버 내부 오류가 발생했습니다.'
});
});
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`서버가 포트 ${PORT}에서 실행 중입니다.`);
});
6단계: React 프론트엔드 구현
client/src/services/authService.js에 API 호출 함수를 작성합니다:
import axios from 'axios';
const API_URL = 'http://localhost:5000/api/auth';
// Axios 인스턴스 생성
const api = axios.create({
baseURL: API_URL
});
// 요청 인터셉터: 토큰 자동 추가
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
const authService = {
// 회원가입
register: async (userData) => {
const response = await api.post('/register', userData);
if (response.data.token) {
localStorage.setItem('token', response.data.token);
localStorage.setItem('user', JSON.stringify(response.data.user));
}
return response.data;
},
// 로그인
login: async (credentials) => {
const response = await api.post('/login', credentials);
if (response.data.token) {
localStorage.setItem('token', response.data.token);
localStorage.setItem('user', JSON.stringify(response.data.user));
}
return response.data;
},
// 로그아웃
logout: () => {
localStorage.removeItem('token');
localStorage.removeItem('user');
},
// 현재 사용자 조회
getCurrentUser: async () => {
const response = await api.get('/me');
return response.data;
},
// 토큰 확인
getToken: () => localStorage.getItem('token')
};
export default authService;
client/src/pages/Login.js 컴포넌트를 작성합니다:
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import authService from '../services/authService';
const Login = () => {
const [formData, setFormData] = useState({ email: '', password: '' });
const [error, setError] = useState('');
const navigate = useNavigate();
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
try {
await authService.login(formData);
navigate('/dashboard');
} catch (err) {
setError(err.response?.data?.message || '로그인에 실패했습니다.');
}
};
return (
로그인
{error && {error}}
);
};
export default Login;
테스트 및 배포
로컬 테스트
먼저 MongoDB를 실행하고 서버를 시작합니다:
# 터미널 1: MongoDB 실행
mongod
# 터미널 2: 백엔드 서버 실행
cd server
npm run dev
# 터미널 3: 프론트엔드 실행
cd client
npm start
Postman으로 API를 테스트합니다:
- POST
http://localhost:5000/api/auth/register– 회원가입 테스트 - POST
http://localhost:5000/api/auth/login– 로그인 테스트 - GET
http://localhost:5000/api/auth/me– 인증 토큰으로 사용자 정보 조회
배포하기
백엔드는 Heroku에, 프론트엔드는 Vercel에 배포할 수 있습니다:
# Heroku 배포
heroku create jwt-auth-api
git push heroku main
# 환경 변수 설정
heroku config:set JWT_SECRET=your_production_secret
heroku config:set MONGO_URI=your_mongodb_atlas_uri
# Vercel 배포 (프론트엔드)
cd client
npm run build
vercel --prod
마무리 및 확장 아이디어
JWT 인증 시스템 구현하기 프로젝트를 완료했습니다! 이제 다음 기능들을 추가하여 프로젝트를 더욱 발전시킬 수 있습니다:
- Refresh Token: 액세스 토큰 만료 시 자동 갱신 기능
- 이메일 인증: 회원가입 시 이메일 인증 링크 발송
- 비밀번호 재설정: 비밀번호 찾기 기능 구현
- 소셜 로그인: Google, GitHub OAuth 연동
- 역할 기반 접근 제어(RBAC): 관리자/일반 사용자 권한 분리
- 2FA 인증: 이중 인증으로 보안 강화
이 프로젝트는 포트폴리오에 추가하기 좋으며, 실무에서도 바로 활용할 수 있는 코드 구조를 제공합니다. GitHub에 업로드하고 README를 작성하여 당신의 개발 역량을 어필해보세요!
📚 함께 읽으면 좋은 글
실시간 채팅 앱 만들기 with Socket.io – 완성까지 한번에!
📅 2025. 10. 9.
🎯 실시간 채팅 앱 만들기 with Socket.io
React + Node.js 풀스택 앱 배포하기 – 완성까지 한번에!
📅 2025. 10. 9.
🎯 React + Node.js 풀스택 앱 배포하기
React + Node.js 풀스택 앱 배포하기 – 완성까지 한번에!
📅 2025. 10. 7.
🎯 React + Node.js 풀스택 앱 배포하기
30분만에 만드는 Todo App 완성 가이드 – 완성까지 한번에!
📅 2025. 10. 5.
🎯 30분만에 만드는 Todo App 완성 가이드
React + Node.js 풀스택 앱 배포하기 – 완성까지 한번에!
📅 2025. 10. 5.
🎯 React + Node.js 풀스택 앱 배포하기
💡 위 글들을 통해 더 깊이 있는 정보를 얻어보세요!
📢 이 글이 도움되셨나요? 공유해주세요!
여러분의 공유 한 번이 더 많은 사람들에게 도움이 됩니다 ✨
🔥 공유할 때마다 블로그 성장에 큰 힘이 됩니다! 감사합니다 🙏
💬 여러분의 소중한 의견을 들려주세요!
JWT 인증 시스템 구현하기 관련해서 궁금한 점이 더 있으시다면 언제든 물어보세요!
⭐ 모든 댓글은 24시간 내에 답변드리며, 여러분의 의견이 다른 독자들에게 큰 도움이 됩니다!
🎯 건설적인 의견과 경험 공유를 환영합니다 ✨
🔔 블로그 구독하고 최신 글을 받아보세요!
🌟 프로젝트 아이디어부터 다양한 실생활 정보까지!
매일 새로운 유용한 콘텐츠를 만나보세요 ✨
📧 RSS 구독 | 🔖 북마크 추가 | 📱 모바일 앱 알림 설정
지금 구독하고 놓치는 정보 없이 업데이트 받아보세요!