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

개발 에러 해결 가이드 - FixLog 노트

프로젝트 소개 및 목표

JWT 인증 시스템 구현하기는 현대 웹 애플리케이션의 핵심 보안 메커니즘을 직접 구축해보는 실전 프로젝트입니다. 이 가이드를 통해 JSON Web Token을 활용한 사용자 인증 및 권한 관리 시스템을 처음부터 끝까지 완성할 수 있습니다. 프로젝트의 주요 목표는 회원가입, 로그인, 토큰 발급 및 검증, 보호된 라우트 접근 제어를 구현하는 것입니다. 실무에서 바로 활용 가능한 보안 베스트 프랙티스를 적용하며, Access Token과 Refresh Token을 활용한 이중 토큰 전략까지 학습합니다. 완성된 시스템은 포트폴리오에 추가하거나 다른 프로젝트의 기반으로 활용할 수 있습니다.

필요한 기술 스택

이 프로젝트를 진행하기 위해 필요한 기술 스택은 다음과 같습니다. 백엔드로는 Node.js와 Express.js를 사용하며, JWT 생성 및 검증을 위해 jsonwebtoken 라이브러리를 활용합니다. 데이터베이스는 MongoDB와 Mongoose를 사용하고, 비밀번호 암호화를 위해 bcrypt를 적용합니다. 프론트엔드는 React 또는 Vue.js를 사용하며, HTTP 클라이언트로 axios를 활용합니다. 개발 환경으로는 VS Code, Postman(API 테스트), Git(버전 관리)이 필요합니다. 추가로 dotenv를 통한 환경변수 관리와 CORS 설정을 위한 cors 패키지도 사용합니다.

프로젝트 셋업

먼저 프로젝트 디렉토리를 생성하고 초기 설정을 진행합니다.

mkdir jwt-auth-system
cd jwt-auth-system
npm init -y

필요한 패키지들을 설치합니다:

npm install express mongoose jsonwebtoken bcryptjs dotenv cors
npm install --save-dev nodemon

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

jwt-auth-system/
├── src/
│   ├── models/
│   │   └── User.js
│   ├── middleware/
│   │   └── auth.js
│   ├── routes/
│   │   └── auth.js
│   ├── controllers/
│   │   └── authController.js
│   └── server.js
├── .env
└── package.json

.env 파일을 생성하고 환경변수를 설정합니다:

PORT=5000
MONGODB_URI=mongodb://localhost:27017/jwt-auth
JWT_SECRET=your_jwt_secret_key_here
JWT_REFRESH_SECRET=your_refresh_secret_key_here
ACCESS_TOKEN_EXPIRE=15m
REFRESH_TOKEN_EXPIRE=7d

단계별 구현 과정

1단계: 서버 기본 설정 및 데이터베이스 연결

먼저 Express 서버를 설정하고 MongoDB에 연결합니다. src/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.MONGODB_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true
})
.then(() => console.log('MongoDB 연결 성공'))
.catch((err) => console.error('MongoDB 연결 실패:', err));

// 라우트
const authRoutes = require('./routes/auth');
app.use('/api/auth', authRoutes);

// 서버 시작
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,
    trim: true
  },
  password: {
    type: String,
    required: true,
    minlength: 6
  },
  refreshToken: {
    type: String,
    default: null
  }
}, {
  timestamps: true
});

// 비밀번호 암호화
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  
  try {
    const salt = await bcrypt.genSalt(10);
    this.password = await bcrypt.hash(this.password, salt);
    next();
  } catch (error) {
    next(error);
  }
});

// 비밀번호 검증 메서드
userSchema.methods.comparePassword = async function(candidatePassword) {
  return await bcrypt.compare(candidatePassword, this.password);
};

module.exports = mongoose.model('User', userSchema);

3단계: 인증 컨트롤러 구현

JWT 인증 시스템 구현하기의 핵심인 토큰 생성 및 검증 로직을 작성합니다. src/controllers/authController.js:

const jwt = require('jsonwebtoken');
const User = require('../models/User');

// Access Token 생성
const generateAccessToken = (userId) => {
  return jwt.sign(
    { userId },
    process.env.JWT_SECRET,
    { expiresIn: process.env.ACCESS_TOKEN_EXPIRE }
  );
};

// Refresh Token 생성
const generateRefreshToken = (userId) => {
  return jwt.sign(
    { userId },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: process.env.REFRESH_TOKEN_EXPIRE }
  );
};

// 회원가입
exports.register = async (req, res) => {
  try {
    const { username, email, password } = req.body;

    // 유효성 검사
    if (!username || !email || !password) {
      return res.status(400).json({
        success: false,
        message: '모든 필드를 입력해주세요'
      });
    }

    // 중복 확인
    const existingUser = await User.findOne({ $or: [{ email }, { username }] });
    if (existingUser) {
      return res.status(409).json({
        success: false,
        message: '이미 존재하는 사용자입니다'
      });
    }

    // 사용자 생성
    const user = new User({ username, email, password });
    await user.save();

    // 토큰 생성
    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
        },
        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;

    // 유효성 검사
    if (!email || !password) {
      return res.status(400).json({
        success: false,
        message: '이메일과 비밀번호를 입력해주세요'
      });
    }

    // 사용자 찾기
    const user = await User.findOne({ email });
    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
        },
        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(400).json({
        success: false,
        message: 'Refresh Token이 필요합니다'
      });
    }

    // Refresh Token 검증
    const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
    const user = await User.findById(decoded.userId);

    if (!user || user.refreshToken !== refreshToken) {
      return res.status(403).json({
        success: false,
        message: '유효하지 않은 Refresh Token입니다'
      });
    }

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

    res.status(200).json({
      success: true,
      message: '토큰 갱신 성공',
      data: {
        accessToken: newAccessToken
      }
    });
  } catch (error) {
    res.status(403).json({
      success: false,
      message: '토큰 검증 실패',
      error: error.message
    });
  }
};

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

    res.status(200).json({
      success: true,
      message: '로그아웃 성공'
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      message: '서버 오류',
      error: error.message
    });
  }
};

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

보호된 라우트를 위한 인증 미들웨어를 구현합니다. src/middleware/auth.js:

const jwt = require('jsonwebtoken');

const authMiddleware = async (req, res, next) => {
  try {
    // 헤더에서 토큰 추출
    const authHeader = req.headers.authorization;
    
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return res.status(401).json({
        success: false,
        message: '인증 토큰이 필요합니다'
      });
    }

    const token = authHeader.split(' ')[1];

    // 토큰 검증
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.userId = decoded.userId;
    
    next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({
        success: false,
        message: '토큰이 만료되었습니다'
      });
    }
    
    return res.status(403).json({
      success: false,
      message: '유효하지 않은 토큰입니다'
    });
  }
};

module.exports = authMiddleware;

5단계: 라우트 설정

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

const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');
const authMiddleware = require('../middleware/auth');

// 공개 라우트
router.post('/register', authController.register);
router.post('/login', authController.login);
router.post('/refresh', authController.refreshToken);

// 보호된 라우트
router.post('/logout', authMiddleware, authController.logout);
router.get('/profile', authMiddleware, async (req, res) => {
  try {
    const User = require('../models/User');
    const user = await User.findById(req.userId).select('-password -refreshToken');
    
    res.status(200).json({
      success: true,
      data: { user }
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      message: '서버 오류'
    });
  }
});

module.exports = router;

테스트 및 배포

구현이 완료되면 Postman이나 Thunder Client를 사용하여 API를 테스트합니다. 회원가입 테스트: POST /api/auth/register로 username, email, password를 전송합니다. 로그인 테스트: POST /api/auth/login으로 email과 password를 전송하고 토큰을 받습니다. 보호된 라우트 테스트: GET /api/auth/profile 요청 시 Authorization 헤더에 ‘Bearer [accessToken]’을 포함합니다. 토큰 갱신 테스트: POST /api/auth/refresh로 refreshToken을 전송하여 새로운 accessToken을 발급받습니다. 배포는 Heroku, AWS, DigitalOcean 등을 활용하며, 환경변수를 안전하게 설정하고 HTTPS를 반드시 사용합니다. MongoDB Atlas를 사용하여 클라우드 데이터베이스를 구성하는 것을 권장합니다.

마무리 및 확장 아이디어

이제 JWT 인증 시스템 구현하기 프로젝트가 완성되었습니다! 기본적인 인증 시스템을 넘어 다음과 같은 기능을 추가할 수 있습니다: 이메일 인증, 비밀번호 재설정, 소셜 로그인(Google, GitHub), 역할 기반 접근 제어(RBAC), Rate Limiting으로 브루트포스 공격 방지, 2단계 인증(2FA) 등입니다. 이 프로젝트는 포트폴리오에 추가하기 좋으며, 다른 웹 애플리케이션의 인증 시스템 기반으로 활용할 수 있습니다. 보안은 지속적으로 업데이트해야 하는 영역이므로, 최신 보안 베스트 프랙티스를 계속 학습하고 적용하세요!

📚 함께 읽으면 좋은 글

1

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

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

2

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

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

3

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

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

4

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

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

5

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

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

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

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

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


📘 페이스북


🐦 트위터


✈️ 텔레그램

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

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

이 글을 읽고 새롭게 알게 된 정보가 있다면 공유해주세요!

💡
유용한 정보 공유

궁금한 점 질문

🤝
경험담 나누기

👍
의견 표현하기

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

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

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

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

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

💡
최신 트렌드
2025년 기준

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

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

📱 전체 버전 보기