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

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를 작성하여 당신의 개발 역량을 어필해보세요!

📚 함께 읽으면 좋은 글

1

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

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

2

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

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

3

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

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

4

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

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

5

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

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

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

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

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

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

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

JWT 인증 시스템 구현하기 관련해서 궁금한 점이 더 있으시다면 언제든 물어보세요!

💡
유용한 정보 공유

궁금한 점 질문

🤝
경험담 나누기

👍
의견 표현하기

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

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

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

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

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

💡
최신 트렌드
2025년 기준

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

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

답글 남기기