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

프로젝트 소개 및 목표

JWT 인증 시스템 구현하기는 현대 웹 애플리케이션에서 가장 널리 사용되는 보안 인증 방식을 직접 구축해보는 실습 프로젝트입니다. 이 프로젝트를 통해 사용자 회원가입, 로그인, 토큰 기반 인증, 권한 관리까지 전체 인증 흐름을 이해하고 구현할 수 있습니다. JWT(JSON Web Token)는 서버의 부담을 줄이면서도 안전한 인증을 제공하는 stateless 방식으로, RESTful API와 마이크로서비스 아키텍처에 최적화되어 있습니다. 본 가이드를 따라하면 실무에서 바로 활용할 수 있는 인증 시스템을 완성할 수 있으며, 포트폴리오에 추가할 수 있는 완성도 높은 프로젝트를 만들 수 있습니다.

필요한 기술 스택

이 프로젝트를 진행하기 위해 필요한 기술 스택은 다음과 같습니다:

  • 백엔드: Node.js (v16 이상), Express.js 프레임워크
  • 데이터베이스: MongoDB (Mongoose ODM) 또는 PostgreSQL (Sequelize ORM)
  • 인증 라이브러리: jsonwebtoken, bcryptjs (비밀번호 암호화)
  • 검증: express-validator (입력 데이터 검증)
  • 환경 변수: dotenv
  • 프론트엔드: React.js 또는 Vue.js (선택사항)
  • 테스트 도구: Jest, Supertest

프로젝트 셋업

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

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 jest supertest

프로젝트 구조를 다음과 같이 구성합니다:

jwt-auth-system/
├── src/
│   ├── config/
│   │   └── database.js
│   ├── models/
│   │   └── User.js
│   ├── controllers/
│   │   └── authController.js
│   ├── middleware/
│   │   └── authMiddleware.js
│   ├── routes/
│   │   └── authRoutes.js
│   └── utils/
│       └── tokenUtils.js
├── .env
├── server.js
└── package.json

.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

단계별 구현 과정

1단계: 데이터베이스 연결 설정

MongoDB 연결을 위한 설정 파일을 작성합니다 (src/config/database.js):

const mongoose = require('mongoose');

const connectDB = async () => {
  try {
    await mongoose.connect(process.env.MONGODB_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    console.log('MongoDB 연결 성공');
  } catch (error) {
    console.error('MongoDB 연결 실패:', error.message);
    process.exit(1);
  }
};

module.exports = connectDB;

2단계: 사용자 모델 정의

JWT 인증 시스템 구현하기의 핵심인 사용자 모델을 생성합니다 (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, '사용자명은 최소 3자 이상이어야 합니다']
  },
  email: {
    type: String,
    required: [true, '이메일을 입력해주세요'],
    unique: true,
    lowercase: true,
    match: [/^\S+@\S+\.\S+$/, '유효한 이메일 주소를 입력해주세요']
  },
  password: {
    type: String,
    required: [true, '비밀번호를 입력해주세요'],
    minlength: [6, '비밀번호는 최소 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/utils/tokenUtils.js):

const jwt = require('jsonwebtoken');

const generateAccessToken = (userId) => {
  return jwt.sign(
    { id: userId },
    process.env.JWT_SECRET,
    { expiresIn: process.env.JWT_EXPIRE }
  );
};

const generateRefreshToken = (userId) => {
  return jwt.sign(
    { id: userId },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: process.env.JWT_REFRESH_EXPIRE }
  );
};

const verifyAccessToken = (token) => {
  try {
    return jwt.verify(token, process.env.JWT_SECRET);
  } catch (error) {
    return null;
  }
};

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/controllers/authController.js):

const User = require('../models/User');
const {
  generateAccessToken,
  generateRefreshToken,
  verifyRefreshToken
} = require('../utils/tokenUtils');

// 회원가입
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이 필요합니다'
      });
    }

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

    // 사용자 확인
    const user = await User.findById(decoded.id).select('+refreshToken');
    if (!user || user.refreshToken !== refreshToken) {
      return res.status(401).json({
        success: false,
        message: '인증에 실패했습니다'
      });
    }

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

    res.status(200).json({
      success: true,
      message: '토큰이 갱신되었습니다',
      data: {
        accessToken: newAccessToken
      }
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      message: '서버 오류가 발생했습니다',
      error: error.message
    });
  }
};

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

    res.status(200).json({
      success: true,
      message: '로그아웃되었습니다'
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      message: '서버 오류가 발생했습니다',
      error: error.message
    });
  }
};

5단계: 인증 미들웨어 작성

JWT 인증 시스템 구현하기에서 가장 중요한 보호 미들웨어를 작성합니다 (src/middleware/authMiddleware.js):

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

exports.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);
    if (!decoded) {
      return res.status(401).json({
        success: false,
        message: '유효하지 않은 토큰입니다'
      });
    }

    // 사용자 확인
    const user = await User.findById(decoded.id);
    if (!user) {
      return res.status(401).json({
        success: false,
        message: '사용자를 찾을 수 없습니다'
      });
    }

    req.user = user;
    next();
  } catch (error) {
    res.status(500).json({
      success: false,
      message: '인증 처리 중 오류가 발생했습니다',
      error: error.message
    });
  }
};

// 권한 확인 미들웨어
exports.authorize = (...roles) => {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({
        success: false,
        message: '접근 권한이 없습니다'
      });
    }
    next();
  };
};

6단계: 라우트 설정

인증 관련 라우트를 정의합니다 (src/routes/authRoutes.js):

const express = require('express');
const router = express.Router();
const { body } = require('express-validator');
const authController = require('../controllers/authController');
const { protect, authorize } = require('../middleware/authMiddleware');

// 회원가입
router.post('/register', [
  body('username').trim().isLength({ min: 3 }).withMessage('사용자명은 최소 3자 이상이어야 합니다'),
  body('email').isEmail().normalizeEmail().withMessage('유효한 이메일을 입력해주세요'),
  body('password').isLength({ min: 6 }).withMessage('비밀번호는 최소 6자 이상이어야 합니다')
], authController.register);

// 로그인
router.post('/login', [
  body('email').isEmail().normalizeEmail(),
  body('password').notEmpty()
], authController.login);

// 토큰 갱신
router.post('/refresh', authController.refreshToken);

// 로그아웃 (보호된 라우트)
router.post('/logout', protect, authController.logout);

// 사용자 프로필 조회 (보호된 라우트)
router.get('/profile', protect, (req, res) => {
  res.status(200).json({
    success: true,
    data: {
      user: {
        id: req.user._id,
        username: req.user.username,
        email: req.user.email,
        role: req.user.role
      }
    }
  });
});

// 관리자 전용 라우트 예시
router.get('/admin', protect, authorize('admin'), (req, res) => {
  res.status(200).json({
    success: true,
    message: '관리자 페이지에 접근했습니다'
  });
});

module.exports = router;

7단계: 서버 설정

메인 서버 파일을 작성합니다 (server.js):

require('dotenv').config();
const express = require('express');
const cors = require('cors');
const connectDB = require('./src/config/database');
const authRoutes = require('./src/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: '서버 오류가 발생했습니다'
  });
});

const PORT = process.env.PORT || 5000;

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

테스트 및 배포

API 테스트

Postman이나 Thunder Client를 사용하여 API를 테스트합니다:

1. 회원가입:

POST http://localhost:5000/api/auth/register
Content-Type: application/json

{
  "username": "testuser",
  "email": "[email protected]",
  "password": "password123"
}

2. 로그인:

POST http://localhost:5000/api/auth/login
Content-Type: application/json

{
  "email": "[email protected]",
  "password": "password123"
}

3. 프로필 조회 (인증 필요):

GET http://localhost:5000/api/auth/profile
Authorization: Bearer YOUR_ACCESS_TOKEN

유닛 테스트 작성

Jest를 사용한 테스트 코드 예시 (tests/auth.test.js):

const request = require('supertest');
const app = require('../server');

describe('인증 API 테스트', () => {
  it('회원가입이 정상적으로 동작해야 함', async () => {
    const res = await request(app)
      .post('/api/auth/register')
      .send({
        username: 'testuser',
        email: '[email protected]',
        password: 'password123'
      });
    
    expect(res.statusCode).toEqual(201);
    expect(res.body).toHaveProperty('data.accessToken');
  });

  it('로그인이 정상적으로 동작해야 함', async () => {
    const res = await request(app)
      .post('/api/auth/login')
      .send({
        email: '[email protected]',
        password: 'password123'
      });
    
    expect(res.statusCode).toEqual(200);
    expect(res.body).toHaveProperty('data.accessToken');
  });
});

배포

Heroku, AWS, 또는 Vercel을 통해 배포할 수 있습니다. 배포 전 체크리스트:

  • 환경 변수가 프로덕션 환경에 올바르게 설정되었는지 확인
  • JWT_SECRET은 강력한 랜덤 문자열로 변경
  • CORS 설정을 프로덕션 도메인으로 제한
  • HTTPS 사용 (SSL 인증서 설정)
  • Rate limiting 미들웨어 추가 (express-rate-limit)
  • Helmet.js로 보안 헤더 설정

마무리 및 확장 아이디어

JWT 인증 시스템 구현하기 프로젝트를 성공적으로 완성했습니다! 이제 이 시스템을 기반으로 다양한 기능을 추가할 수 있습니다:

  • 이메일 인증: Nodemailer를 사용한 회원가입 이메일 인증
  • 비밀번호 재설정: 이메일을 통한 비밀번호 찾기 기능
  • 소셜 로그인: Passport.js를 활용한 Google, Facebook 로그인
  • 2단계 인증 (2FA): TOTP 기반 추가 보안 레이어
  • 프로필 관리: 사용자 정보 수정, 프로필 이미지 업로드
  • 역할 기반 접근 제어 (RBAC): 더 세밀한 권한 관리 시스템
  • 토큰 블랙리스트: Redis를 활용한 로그아웃 토큰 무효화
  • API Rate Limiting: 브루트 포스 공격 방지

이 프로젝트는 포트폴리오에 추가하기에 매우 적합하며, 실무에서 요구되는 인증 시스템의 기초를 모두 담고 있습니다. 지속적으로 보안 업데이트를 확인하고, 최신 보안 모범 사례를 적용하여 시스템을 개선해 나가세요!

📚 함께 읽으면 좋은 글

1

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

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

2

30분만에 만드는 Todo App 완성 가이드 – 완성까지 한번에!

📂 프로젝트 아이디어
📅 2025. 10. 21.
🎯 30분만에 만드는 Todo App 완성 가이드

3

30분만에 만드는 Todo App 완성 가이드 – 완성까지 한번에!

📂 프로젝트 아이디어
📅 2025. 10. 20.
🎯 30분만에 만드는 Todo App 완성 가이드

4

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

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

5

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

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

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

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

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

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

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

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

💡
유용한 정보 공유

궁금한 점 질문

🤝
경험담 나누기

👍
의견 표현하기

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

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

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

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

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

💡
최신 트렌드
2025년 기준

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

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

답글 남기기