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

프로젝트 소개 및 목표

JWT 인증 시스템 구현하기는 현대 웹 애플리케이션에서 가장 널리 사용되는 인증 방식을 직접 구축해보는 실습 프로젝트입니다. JWT(JSON Web Token)는 서버와 클라이언트 간 안전한 정보 전송을 위한 토큰 기반 인증 시스템으로, RESTful API 개발의 핵심 기술입니다. 이 프로젝트를 통해 회원가입, 로그인, 토큰 발급 및 검증, 리프레시 토큰 관리 등 실무에서 필요한 인증 시스템의 모든 요소를 학습할 수 있습니다. 보안이 중요한 현대 웹 서비스에서 필수적인 기술을 익히고 포트폴리오에 추가할 수 있는 완벽한 실습 프로젝트입니다.

필요한 기술 스택

이 프로젝트를 진행하기 위해서는 다음과 같은 기술 스택이 필요합니다. 백엔드는 Node.js와 Express.js 프레임워크를 사용하며, JWT 토큰 생성과 검증을 위해 jsonwebtoken 라이브러리를 활용합니다. 데이터베이스는 MongoDB와 Mongoose ORM을 사용하여 사용자 정보를 저장하고, 비밀번호 암호화를 위해 bcryptjs를 적용합니다. 프론트엔드는 React.js로 구성하며, Axios를 통해 API 통신을 처리합니다. 개발 환경은 VS Code를 권장하며, Postman으로 API 테스트를 진행합니다.

프로젝트 셋업

먼저 프로젝트 디렉토리를 생성하고 필요한 패키지들을 설치합니다. 터미널에서 다음 명령어를 실행하세요:

mkdir jwt-auth-system
cd jwt-auth-system
npm init -y
npm install express mongoose jsonwebtoken bcryptjs dotenv cors
npm install --save-dev nodemon

package.json 파일을 열어 scripts 섹션에 개발 서버 실행 스크립트를 추가합니다:

"scripts": {
  "start": "node server.js",
  "dev": "nodemon server.js"
}

프로젝트 루트에 .env 파일을 생성하여 환경 변수를 설정합니다. 여기에는 MongoDB 연결 문자열, JWT 시크릿 키, 포트 번호 등을 포함합니다. 보안을 위해 .gitignore 파일에 .env를 반드시 추가해야 합니다.

단계별 구현 과정

1단계: 서버 기본 설정

server.js 파일을 생성하고 Express 서버의 기본 구조를 작성합니다:

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

const app = express();

// 미들웨어 설정
app.use(express.json());
app.use(cors());

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

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

2단계: 사용자 모델 생성

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
  },
  password: {
    type: String,
    required: true,
    minlength: 6
  },
  refreshToken: {
    type: String
  }
}, { timestamps: true });

// 비밀번호 해싱 미들웨어
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  this.password = await bcrypt.hash(this.password, 10);
  next();
});

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

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

3단계: JWT 유틸리티 함수 작성

utils/jwt.js 파일을 생성하여 토큰 생성 및 검증 함수를 구현합니다:

const jwt = require('jsonwebtoken');

// Access Token 생성 (15분 유효)
const generateAccessToken = (userId) => {
  return jwt.sign(
    { userId },
    process.env.JWT_ACCESS_SECRET,
    { expiresIn: '15m' }
  );
};

// Refresh Token 생성 (7일 유효)
const generateRefreshToken = (userId) => {
  return jwt.sign(
    { userId },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: '7d' }
  );
};

// 토큰 검증
const verifyToken = (token, secret) => {
  try {
    return jwt.verify(token, secret);
  } catch (error) {
    return null;
  }
};

module.exports = {
  generateAccessToken,
  generateRefreshToken,
  verifyToken
};

4단계: 인증 미들웨어 구현

middleware/auth.js 파일을 생성하여 요청 인증을 처리합니다:

const { verifyToken } = require('../utils/jwt');

const authMiddleware = (req, res, next) => {
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ message: '인증 토큰이 필요합니다' });
  }

  const token = authHeader.split(' ')[1];
  const decoded = verifyToken(token, process.env.JWT_ACCESS_SECRET);

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

  req.userId = decoded.userId;
  next();
};

module.exports = authMiddleware;

5단계: 인증 라우터 구현

routes/auth.js 파일을 생성하여 회원가입, 로그인, 토큰 갱신 API를 구현합니다:

const express = require('express');
const router = express.Router();
const User = require('../models/User');
const { generateAccessToken, generateRefreshToken, verifyToken } = require('../utils/jwt');

// 회원가입
router.post('/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 = new User({ username, email, password });
    await user.save();

    res.status(201).json({ message: '회원가입 성공', userId: user._id });
  } catch (error) {
    res.status(500).json({ message: '서버 오류', error: error.message });
  }
});

// 로그인
router.post('/login', async (req, res) => {
  try {
    const { email, password } = req.body;

    // 사용자 찾기
    const user = await User.findOne({ email });
    if (!user) {
      return res.status(401).json({ message: '이메일 또는 비밀번호가 잘못되었습니다' });
    }

    // 비밀번호 확인
    const isValidPassword = await user.comparePassword(password);
    if (!isValidPassword) {
      return res.status(401).json({ message: '이메일 또는 비밀번호가 잘못되었습니다' });
    }

    // 토큰 생성
    const accessToken = generateAccessToken(user._id);
    const refreshToken = generateRefreshToken(user._id);

    // Refresh Token을 DB에 저장
    user.refreshToken = refreshToken;
    await user.save();

    res.json({
      message: '로그인 성공',
      accessToken,
      refreshToken,
      user: { id: user._id, username: user.username, email: user.email }
    });
  } catch (error) {
    res.status(500).json({ message: '서버 오류', error: error.message });
  }
});

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

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

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

    // DB에서 사용자 및 토큰 확인
    const user = await User.findById(decoded.userId);
    if (!user || user.refreshToken !== refreshToken) {
      return res.status(401).json({ message: '유효하지 않은 Refresh Token입니다' });
    }

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

    res.json({ accessToken: newAccessToken });
  } catch (error) {
    res.status(500).json({ message: '서버 오류', error: error.message });
  }
});

// 로그아웃
router.post('/logout', async (req, res) => {
  try {
    const { userId } = req.body;
    await User.findByIdAndUpdate(userId, { refreshToken: null });
    res.json({ message: '로그아웃 성공' });
  } catch (error) {
    res.status(500).json({ message: '서버 오류', error: error.message });
  }
});

module.exports = router;

server.js 파일에 라우터를 추가합니다:

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

6단계: 보호된 라우트 예시

routes/user.js 파일을 생성하여 인증이 필요한 API를 구현합니다:

const express = require('express');
const router = express.Router();
const authMiddleware = require('../middleware/auth');
const User = require('../models/User');

// 사용자 프로필 조회 (보호된 라우트)
router.get('/profile', authMiddleware, async (req, res) => {
  try {
    const user = await User.findById(req.userId).select('-password -refreshToken');
    if (!user) {
      return res.status(404).json({ message: '사용자를 찾을 수 없습니다' });
    }
    res.json({ user });
  } catch (error) {
    res.status(500).json({ message: '서버 오류', error: error.message });
  }
});

module.exports = router;

테스트 및 배포

Postman을 사용한 API 테스트를 진행합니다. 먼저 회원가입 엔드포인트(POST /api/auth/register)에 username, email, password를 포함한 JSON 데이터를 전송하여 사용자를 생성합니다. 다음으로 로그인 엔드포인트(POST /api/auth/login)를 테스트하여 Access Token과 Refresh Token이 정상적으로 발급되는지 확인합니다. 보호된 라우트를 테스트할 때는 Authorization 헤더에 ‘Bearer {accessToken}’ 형식으로 토큰을 포함시킵니다. 토큰 갱신 기능도 Refresh Token을 사용하여 새로운 Access Token을 받아오는 과정을 검증합니다. 배포는 Heroku, AWS EC2, 또는 Vercel 등의 플랫폼을 활용할 수 있으며, 환경 변수 설정을 반드시 확인해야 합니다. MongoDB는 MongoDB Atlas를 사용하여 클라우드 데이터베이스로 연결합니다.

마무리 및 확장 아이디어

JWT 인증 시스템 구현하기 프로젝트를 완성했다면, 다음 단계로 기능을 확장할 수 있습니다. 이메일 인증 기능을 추가하여 회원가입 시 이메일 확인 절차를 구현하거나, OAuth 2.0을 통한 소셜 로그인(Google, GitHub 등)을 통합할 수 있습니다. 비밀번호 재설정 기능, 역할 기반 접근 제어(RBAC), 토큰 블랙리스트 관리, Rate Limiting을 통한 보안 강화 등도 고려해볼 만합니다. 이 프로젝트는 실무에서 바로 사용 가능한 수준의 인증 시스템으로, 포트폴리오에 추가하면 높은 평가를 받을 수 있습니다.

📚 함께 읽으면 좋은 글

1

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

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

2

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

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

3

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

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

4

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

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

5

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

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

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

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

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

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

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

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

💡
유용한 정보 공유

궁금한 점 질문

🤝
경험담 나누기

👍
의견 표현하기

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

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

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

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

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

💡
최신 트렌드
2025년 기준

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

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

답글 남기기