JWT 인증 시스템 구현하기 – 완성까지 한번에!

프로젝트 소개 및 목표

JWT 인증 시스템 구현하기는 현대 웹 애플리케이션에서 필수적인 사용자 인증 및 권한 관리 시스템을 직접 구축하는 프로젝트입니다. JWT(JSON Web Token)는 stateless한 인증 방식으로, 서버의 세션 부담을 줄이면서도 안전한 사용자 인증을 제공합니다. 이 프로젝트를 통해 회원가입, 로그인, 토큰 발급 및 검증, 그리고 보호된 라우트 구현까지 실무에서 바로 활용할 수 있는 인증 시스템을 완성하게 됩니다. 백엔드와 프론트엔드를 모두 다루며, 보안 개념과 토큰 기반 인증의 핵심 원리를 체득할 수 있는 포트폴리오 프로젝트입니다.

필요한 기술 스택

이 프로젝트는 Node.js 환경에서 진행되며, 다음 기술들이 필요합니다:

  • 백엔드: Node.js, Express.js, jsonwebtoken 라이브러리, bcrypt(비밀번호 해싱)
  • 데이터베이스: MongoDB + Mongoose 또는 PostgreSQL + Sequelize
  • 프론트엔드: React.js 또는 Vue.js, Axios(HTTP 클라이언트)
  • 개발 도구: Postman(API 테스트), dotenv(환경변수 관리)

JWT 인증 시스템 구현하기에서는 이러한 스택을 조합하여 완전한 인증 플로우를 구축합니다.

프로젝트 셋업

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

// 프로젝트 초기화
mkdir jwt-auth-system && cd jwt-auth-system
npm init -y

// 필수 패키지 설치
npm install express mongoose jsonwebtoken bcryptjs dotenv cors
npm install --save-dev nodemon

// 프로젝트 구조 생성
mkdir src
mkdir src/models src/routes src/middleware src/controllers
touch src/server.js .env

.env 파일에 환경변수를 설정합니다:

PORT=5000
MONGODB_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": {
  "start": "node src/server.js",
  "dev": "nodemon src/server.js"
}

단계별 구현 과정

1단계: 서버 기본 설정 (src/server.js)

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

const app = express();

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

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

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

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

// 에러 핸들링
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ message: '서버 에러 발생' });
});

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

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
  },
  email: {
    type: String,
    required: [true, '이메일을 입력하세요'],
    unique: true,
    lowercase: true,
    match: [/^\S+@\S+\.\S+$/, '유효한 이메일을 입력하세요']
  },
  password: {
    type: String,
    required: [true, '비밀번호를 입력하세요'],
    minlength: 6,
    select: false // 기본 조회 시 비밀번호 제외
  },
  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/utils/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) {
    return null;
  }
};

// Refresh Token 검증
const verifyRefreshToken = (token) => {
  try {
    return jwt.verify(token, process.env.JWT_REFRESH_SECRET);
  } catch (error) {
    return null;
  }
};

module.exports = {
  generateAccessToken,
  generateRefreshToken,
  verifyAccessToken,
  verifyRefreshToken
};

4단계: 인증 미들웨어 (src/middleware/auth.js)

const { verifyAccessToken } = require('../utils/jwt');
const User = require('../models/User');

const protect = async (req, res, next) => {
  let token;

  // 헤더에서 토큰 추출
  if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
    token = req.headers.authorization.split(' ')[1];
  }

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

  // 토큰 검증
  const decoded = verifyAccessToken(token);
  
  if (!decoded) {
    return res.status(401).json({ message: '유효하지 않은 토큰입니다' });
  }

  // 사용자 정보 조회
  const user = await User.findById(decoded.id).select('-password');
  
  if (!user) {
    return res.status(404).json({ message: '사용자를 찾을 수 없습니다' });
  }

  req.user = user;
  next();
};

module.exports = { protect };

5단계: 인증 컨트롤러 (src/controllers/authController.js)

const User = require('../models/User');
const { generateAccessToken, generateRefreshToken, verifyRefreshToken } = require('../utils/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({ 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({
      message: '회원가입 성공',
      user: {
        id: user._id,
        username: user.username,
        email: user.email
      },
      accessToken,
      refreshToken
    });
  } 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 }).select('+password');
    
    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.status(200).json({
      message: '로그인 성공',
      user: {
        id: user._id,
        username: user.username,
        email: user.email
      },
      accessToken,
      refreshToken
    });
  } catch (error) {
    res.status(500).json({ message: '로그인 실패', error: error.message });
  }
};

// 토큰 갱신
exports.refreshToken = async (req, res) => {
  try {
    const { refreshToken } = req.body;

    if (!refreshToken) {
      return res.status(401).json({ message: 'Refresh Token이 필요합니다' });
    }

    // Refresh Token 검증
    const decoded = verifyRefreshToken(refreshToken);
    
    if (!decoded) {
      return res.status(401).json({ message: '유효하지 않은 Refresh Token입니다' });
    }

    // 사용자 조회 및 저장된 토큰 확인
    const user = await User.findById(decoded.id).select('+refreshToken');
    
    if (!user || user.refreshToken !== refreshToken) {
      return res.status(401).json({ message: '유효하지 않은 Refresh Token입니다' });
    }

    // 새 Access Token 생성
    const newAccessToken = generateAccessToken(user._id);

    res.status(200).json({
      accessToken: newAccessToken
    });
  } catch (error) {
    res.status(500).json({ message: '토큰 갱신 실패', error: error.message });
  }
};

// 로그아웃
exports.logout = async (req, res) => {
  try {
    const user = await User.findById(req.user.id);
    user.refreshToken = null;
    await user.save();

    res.status(200).json({ message: '로그아웃 성공' });
  } catch (error) {
    res.status(500).json({ message: '로그아웃 실패', error: error.message });
  }
};

6단계: 라우트 설정 (src/routes/auth.js)

const express = require('express');
const router = express.Router();
const { register, login, refreshToken, logout } = require('../controllers/authController');
const { protect } = require('../middleware/auth');

router.post('/register', register);
router.post('/login', login);
router.post('/refresh', refreshToken);
router.post('/logout', protect, logout);

module.exports = router;

7단계: 보호된 라우트 예시 (src/routes/user.js)

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

// 보호된 라우트 - 인증된 사용자만 접근 가능
router.get('/profile', protect, async (req, res) => {
  res.status(200).json({
    user: req.user
  });
});

router.put('/profile', protect, async (req, res) => {
  try {
    const { username } = req.body;
    const user = req.user;

    if (username) user.username = username;
    await user.save();

    res.status(200).json({
      message: '프로필 업데이트 성공',
      user
    });
  } catch (error) {
    res.status(500).json({ message: '프로필 업데이트 실패', error: error.message });
  }
});

module.exports = router;

테스트 및 배포

JWT 인증 시스템 구현하기의 테스트는 Postman을 활용하여 진행합니다:

테스트 시나리오

  1. 회원가입 테스트: POST /api/auth/register로 새 사용자 생성
  2. 로그인 테스트: POST /api/auth/login으로 토큰 획득
  3. 보호된 라우트 접근: GET /api/user/profile에 Authorization 헤더로 토큰 전달
  4. 토큰 갱신 테스트: POST /api/auth/refresh로 새 Access Token 획득
  5. 로그아웃 테스트: POST /api/auth/logout으로 토큰 무효화

배포 준비사항

// 프로덕션 환경 설정
// .env.production
JWT_SECRET=강력한_시크릿_키_사용
JWT_REFRESH_SECRET=다른_강력한_시크릿_키
MONGODB_URI=프로덕션_데이터베이스_URI
NODE_ENV=production

배포는 Heroku, AWS EC2, DigitalOcean 등을 활용할 수 있으며, 환경변수를 반드시 안전하게 설정해야 합니다. HTTPS를 적용하고, CORS 설정을 프로덕션 도메인으로 제한하는 것이 중요합니다.

마무리 및 확장 아이디어

JWT 인증 시스템 구현하기 프로젝트를 통해 현대적인 토큰 기반 인증 시스템의 핵심을 구축했습니다. 추가로 다음 기능을 확장할 수 있습니다:

  • 이메일 인증: 회원가입 시 이메일 인증 링크 발송 (nodemailer 활용)
  • 비밀번호 재설정: 비밀번호 찾기 및 재설정 기능
  • OAuth 통합: Google, Facebook 등 소셜 로그인 추가
  • 역할 기반 권한: 관리자, 일반 사용자 등 역할별 접근 제어
  • 토큰 블랙리스트: Redis를 활용한 로그아웃된 토큰 관리
  • Rate Limiting: express-rate-limit으로 무차별 대입 공격 방지

이 프로젝트는 실무에서 바로 적용 가능한 인증 시스템의 기초를 제공하며, 포트폴리오에 훌륭한 자산이 될 것입니다.

📚 함께 읽으면 좋은 글

1

JWT 인증 시스템 구현하기 – 완성까지 한번에!

📂 프로젝트 아이디어
📅 2025. 11. 8.
🎯 JWT 인증 시스템 구현하기

2

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

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

3

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

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

4

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

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

5

React + Node.js 풀스택 앱 배포하기 – 완성까지 한번에!

📂 프로젝트 아이디어
📅 2025. 11. 3.
🎯 React + Node.js 풀스택 앱 배포하기

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

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

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

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

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

JWT 인증 시스템 구현하기에 대한 여러분만의 경험이나 노하우가 있으시나요?

💡
유용한 정보 공유

궁금한 점 질문

🤝
경험담 나누기

👍
의견 표현하기

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

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

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

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

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

💡
최신 트렌드
2025년 기준

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

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

답글 남기기