프로젝트 소개 및 목표
🔗 관련 에러 해결 가이드
JWT 인증 시스템 구현하기는 현대 웹 애플리케이션에서 가장 널리 사용되는 인증 방식을 직접 구축해보는 실습 프로젝트입니다. 이 프로젝트를 통해 사용자 회원가입, 로그인, 토큰 기반 인증, 권한 관리까지 전체 인증 플로우를 경험할 수 있습니다. JWT(JSON Web Token)는 stateless한 특성으로 확장성이 뛰어나며, 마이크로서비스 아키텍처에서도 유용하게 활용됩니다. 본 가이드를 따라하면 실무에서 바로 적용 가능한 보안 인증 시스템을 구현할 수 있으며, 포트폴리오에 추가할 수 있는 완성도 높은 프로젝트를 완성하게 됩니다.
필요한 기술 스택
이 프로젝트를 진행하기 위해 필요한 기술 스택은 다음과 같습니다:
- 백엔드: Node.js, Express.js – 서버 프레임워크
- 데이터베이스: MongoDB + Mongoose 또는 PostgreSQL + Sequelize
- 인증 라이브러리: jsonwebtoken, bcryptjs – 토큰 생성 및 비밀번호 암호화
- 검증: express-validator – 입력값 검증
- 프론트엔드: React.js, Axios – API 통신
- 개발 도구: Postman 또는 Thunder Client – API 테스트
프로젝트 셋업
먼저 프로젝트 디렉토리를 생성하고 필요한 패키지를 설치합니다:
// 프로젝트 초기화
mkdir jwt-auth-system
cd jwt-auth-system
npm init -y
// 필수 패키지 설치
npm install express mongoose jsonwebtoken bcryptjs dotenv express-validator cors
npm install --save-dev nodemon
// 프로젝트 구조 생성
mkdir src
mkdir src/models src/routes src/controllers src/middleware src/config
.env 파일을 생성하여 환경 변수를 설정합니다:
PORT=5000
MONGO_URI=mongodb://localhost:27017/jwt-auth
JWT_SECRET=your_super_secret_key_change_this_in_production
JWT_EXPIRE=7d
JWT_REFRESH_SECRET=your_refresh_token_secret
JWT_REFRESH_EXPIRE=30d
package.json의 scripts 섹션을 수정합니다:
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js"
}
단계별 구현 과정
1단계: 데이터베이스 연결 설정
src/config/db.js 파일을 생성하여 MongoDB 연결을 설정합니다:
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;
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
},
email: {
type: String,
required: [true, '이메일을 입력해주세요'],
unique: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, '유효한 이메일을 입력해주세요']
},
password: {
type: String,
required: [true, '비밀번호를 입력해주세요'],
minlength: 6,
select: false
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
refreshToken: {
type: String,
select: false
},
createdAt: {
type: Date,
default: Date.now
}
});
// 비밀번호 암호화 미들웨어
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);
3단계: JWT 유틸리티 함수 생성
src/config/jwt.js에서 토큰 생성 및 검증 함수를 작성합니다:
const jwt = require('jsonwebtoken');
// Access Token 생성
const generateAccessToken = (userId) => {
return jwt.sign(
{ id: userId },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRE }
);
};
// Refresh Token 생성
const generateRefreshToken = (userId) => {
return jwt.sign(
{ id: userId },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: process.env.JWT_REFRESH_EXPIRE }
);
};
// Access Token 검증
const verifyAccessToken = (token) => {
try {
return jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
throw new Error('유효하지 않은 토큰입니다');
}
};
// Refresh Token 검증
const verifyRefreshToken = (token) => {
try {
return jwt.verify(token, process.env.JWT_REFRESH_SECRET);
} catch (error) {
throw new Error('유효하지 않은 리프레시 토큰입니다');
}
};
module.exports = {
generateAccessToken,
generateRefreshToken,
verifyAccessToken,
verifyRefreshToken
};
4단계: 인증 미들웨어 구현
src/middleware/auth.js에서 보호된 라우트를 위한 미들웨어를 작성합니다:
const { verifyAccessToken } = require('../config/jwt');
const User = require('../models/User');
const protect = async (req, res, next) => {
try {
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: '인증 토큰이 없습니다'
});
}
// 토큰 검증
const decoded = verifyAccessToken(token);
// 사용자 조회
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: '인증에 실패했습니다',
error: error.message
});
}
};
// 권한 체크 미들웨어
const authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
message: '권한이 없습니다'
});
}
next();
};
};
module.exports = { protect, authorize };
5단계: 인증 컨트롤러 구현
src/controllers/authController.js에서 회원가입, 로그인, 토큰 갱신 로직을 구현합니다:
const User = require('../models/User');
const { generateAccessToken, generateRefreshToken, verifyRefreshToken } = require('../config/jwt');
// 회원가입
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 accessToken = generateAccessToken(user._id);
const refreshToken = generateRefreshToken(user._id);
// Refresh Token 저장
user.refreshToken = refreshToken;
await user.save();
res.status(201).json({
success: true,
message: '회원가입 성공',
data: {
user: {
id: user._id,
username: user.username,
email: user.email,
role: user.role
},
accessToken,
refreshToken
}
});
} catch (error) {
res.status(500).json({
success: false,
message: '회원가입 실패',
error: error.message
});
}
};
// 로그인
exports.login = async (req, res) => {
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 isPasswordValid = await user.comparePassword(password);
if (!isPasswordValid) {
return res.status(401).json({
success: false,
message: '이메일 또는 비밀번호가 올바르지 않습니다'
});
}
// 토큰 생성
const accessToken = generateAccessToken(user._id);
const refreshToken = generateRefreshToken(user._id);
// Refresh Token 저장
user.refreshToken = refreshToken;
await user.save();
res.status(200).json({
success: true,
message: '로그인 성공',
data: {
user: {
id: user._id,
username: user.username,
email: user.email,
role: user.role
},
accessToken,
refreshToken
}
});
} catch (error) {
res.status(500).json({
success: false,
message: '로그인 실패',
error: error.message
});
}
};
// 토큰 갱신
exports.refreshToken = async (req, res) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({
success: false,
message: '리프레시 토큰이 없습니다'
});
}
// Refresh Token 검증
const decoded = verifyRefreshToken(refreshToken);
// 사용자 조회
const user = await User.findById(decoded.id).select('+refreshToken');
if (!user || user.refreshToken !== refreshToken) {
return res.status(401).json({
success: false,
message: '유효하지 않은 리프레시 토큰입니다'
});
}
// 새로운 토큰 생성
const newAccessToken = generateAccessToken(user._id);
const newRefreshToken = generateRefreshToken(user._id);
// 새로운 Refresh Token 저장
user.refreshToken = newRefreshToken;
await user.save();
res.status(200).json({
success: true,
message: '토큰 갱신 성공',
data: {
accessToken: newAccessToken,
refreshToken: newRefreshToken
}
});
} catch (error) {
res.status(401).json({
success: false,
message: '토큰 갱신 실패',
error: error.message
});
}
};
// 로그아웃
exports.logout = async (req, res) => {
try {
// Refresh Token 삭제
req.user.refreshToken = undefined;
await req.user.save();
res.status(200).json({
success: true,
message: '로그아웃 성공'
});
} catch (error) {
res.status(500).json({
success: false,
message: '로그아웃 실패',
error: error.message
});
}
};
// 현재 사용자 정보 조회
exports.getMe = async (req, res) => {
res.status(200).json({
success: true,
data: {
user: req.user
}
});
};
6단계: 라우트 설정
src/routes/authRoutes.js에서 인증 관련 라우트를 정의합니다:
const express = require('express');
const router = express.Router();
const { body } = require('express-validator');
const { register, login, refreshToken, logout, getMe } = require('../controllers/authController');
const { protect } = require('../middleware/auth');
// 유효성 검사 규칙
const registerValidation = [
body('username').trim().isLength({ min: 3 }).withMessage('사용자명은 최소 3자 이상이어야 합니다'),
body('email').isEmail().normalizeEmail().withMessage('유효한 이메일을 입력해주세요'),
body('password').isLength({ min: 6 }).withMessage('비밀번호는 최소 6자 이상이어야 합니다')
];
const loginValidation = [
body('email').isEmail().normalizeEmail().withMessage('유효한 이메일을 입력해주세요'),
body('password').notEmpty().withMessage('비밀번호를 입력해주세요')
];
// 공개 라우트
router.post('/register', registerValidation, register);
router.post('/login', loginValidation, login);
router.post('/refresh', refreshToken);
// 보호된 라우트 (인증 필요)
router.get('/me', protect, getMe);
router.post('/logout', protect, logout);
module.exports = router;
7단계: 서버 설정
src/server.js에서 Express 서버를 설정합니다:
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const connectDB = require('./config/db');
const authRoutes = require('./routes/authRoutes');
const app = express();
// 데이터베이스 연결
connectDB();
// 미들웨어
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 라우트
app.use('/api/auth', authRoutes);
// 에러 핸들링
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
success: false,
message: '서버 오류가 발생했습니다',
error: process.env.NODE_ENV === 'development' ? err.message : undefined
});
});
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`서버가 포트 ${PORT}에서 실행 중입니다`);
});
테스트 및 배포
로컬 테스트: Postman이나 Thunder Client를 사용하여 API 엔드포인트를 테스트합니다. 회원가입(POST /api/auth/register), 로그인(POST /api/auth/login), 사용자 정보 조회(GET /api/auth/me) 등의 기능을 순차적으로 테스트합니다. 반환된 accessToken을 Authorization 헤더에 Bearer {token} 형식으로 포함시켜 보호된 라우트를 테스트합니다.
단위 테스트: Jest와 Supertest를 사용하여 자동화된 테스트를 작성합니다:
npm install --save-dev jest supertest
// tests/auth.test.js
const request = require('supertest');
const app = require('../src/server');
describe('인증 API 테스트', () => {
test('회원가입 성공', async () => {
const response = await request(app)
.post('/api/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('accessToken');
});
});
배포: Heroku, AWS, 또는 DigitalOcean에 배포할 수 있습니다. 환경 변수를 안전하게 설정하고, HTTPS를 사용하며, JWT_SECRET은 반드시 강력한 랜덤 문자열로 변경해야 합니다. MongoDB Atlas를 사용하면 클라우드 데이터베이스를 쉽게 연결할 수 있습니다.
마무리 및 확장 아이디어
이제 JWT 인증 시스템 구현하기 프로젝트의 기본 구조가 완성되었습니다. 이 시스템을 확장하여 더욱 강력하게 만들 수 있습니다:
- 이메일 인증: nodemailer를 사용한 회원가입 이메일 확인
- 비밀번호 재설정: 임시 토큰 기반 비밀번호 복구 기능
- OAuth 2.0: Google, GitHub 등 소셜 로그인 통합
- Rate Limiting: express-rate-limit으로 무차별 대입 공격 방지
- 2FA (Two-Factor Authentication): speakeasy를 활용한 2단계 인증
- 프론트엔드 연동: React에서 Axios Interceptor로 자동 토큰 갱신 구현
이 프로젝트를 통해 실무 수준의 JWT 인증 시스템 구현하기를 경험하고 포트폴리오에 추가할 수 있습니다!
📚 함께 읽으면 좋은 글
실시간 채팅 앱 만들기 with Socket.io – 완성까지 한번에!
📅 2025. 11. 5.
🎯 실시간 채팅 앱 만들기 with Socket.io
REST API 서버 구축 단계별 튜토리얼 – 완성까지 한번에!
📅 2025. 11. 5.
🎯 REST API 서버 구축 단계별 튜토리얼
실시간 채팅 앱 만들기 with Socket.io – 완성까지 한번에!
📅 2025. 11. 4.
🎯 실시간 채팅 앱 만들기 with Socket.io
React + Node.js 풀스택 앱 배포하기 – 완성까지 한번에!
📅 2025. 11. 3.
🎯 React + Node.js 풀스택 앱 배포하기
30분만에 만드는 Todo App 완성 가이드 – 완성까지 한번에!
📅 2025. 11. 2.
🎯 30분만에 만드는 Todo App 완성 가이드
💡 위 글들을 통해 더 깊이 있는 정보를 얻어보세요!
📢 이 글이 도움되셨나요? 공유해주세요!
여러분의 공유 한 번이 더 많은 사람들에게 도움이 됩니다 ✨
🔥 공유할 때마다 블로그 성장에 큰 힘이 됩니다! 감사합니다 🙏
💬 여러분의 소중한 의견을 들려주세요!
JWT 인증 시스템 구현하기 관련해서 궁금한 점이 더 있으시다면 언제든 물어보세요!
⭐ 모든 댓글은 24시간 내에 답변드리며, 여러분의 의견이 다른 독자들에게 큰 도움이 됩니다!
🎯 건설적인 의견과 경험 공유를 환영합니다 ✨
🔔 블로그 구독하고 최신 글을 받아보세요!
🌟 프로젝트 아이디어부터 다양한 실생활 정보까지!
매일 새로운 유용한 콘텐츠를 만나보세요 ✨
📧 RSS 구독 | 🔖 북마크 추가 | 📱 모바일 앱 알림 설정
지금 구독하고 놓치는 정보 없이 업데이트 받아보세요!