์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ” JWT ์ธ์ฆ์„ ํ™œ์šฉํ•œ ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ ์‹œ์Šคํ…œ ์™„๋ฒฝ ๊ตฌํ˜„ ๊ฐ€์ด๋“œ

๐Ÿ” JWT ์ธ์ฆ์„ ํ™œ์šฉํ•œ ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ ์‹œ์Šคํ…œ ์™„๋ฒฝ ๊ตฌํ˜„ ๊ฐ€์ด๋“œ

์‹ค์ „ ์†Œ์Šค์ฝ”๋“œ์™€ ํ•จ๊ป˜ ๋ฐฐ์šฐ๋Š” ํ˜„๋Œ€์ ์ธ ์ธ์ฆ ์‹œ์Šคํ…œ ๊ตฌ์ถ•๋ฒ•

๐ŸŽฏ ์ด ๊ธ€์—์„œ ๋ฐฐ์šธ ๋‚ด์šฉ ์š”์ฆ˜ ์›น ๊ฐœ๋ฐœ์—์„œ JWT๋Š” ๊ฑฐ์˜ ํ•„์ˆ˜๊ฐ€ ๋˜์–ด๋ฒ„๋ ธ์ฃ ! ๐Ÿ˜Ž ์ด ๊ธ€์—์„œ๋Š” JWT๋ฅผ ํ™œ์šฉํ•œ ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ ์‹œ์Šคํ…œ์„ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ์‹ค์ „ ์†Œ์Šค์ฝ”๋“œ์™€ ํ•จ๊ป˜ ์•Œ์•„๋ณผ ๊ฑฐ์˜ˆ์š”. ๋ณต์žกํ•ด ๋ณด์ด์ง€๋งŒ ํ•˜๋‚˜์”ฉ ๋œฏ์–ด๋ณด๋ฉด ์ •๋ง ์žฌ๋ฏธ์žˆ๋‹ต๋‹ˆ๋‹ค. ํ”„๋กœ๊ทธ๋ž˜๋ฐ์„ ๋ฐฐ์šฐ๊ณ  ์‹ถ๊ฑฐ๋‚˜ ์‹ค๋ ฅ์„ ํ‚ค์šฐ๊ณ  ์‹ถ๋‹ค๋ฉด ์žฌ๋Šฅ๋„ท์—์„œ ๋‹ค์–‘ํ•œ ๊ฐœ๋ฐœ ์ „๋ฌธ๊ฐ€๋“ค์˜ ์žฌ๋Šฅ๋„ ์ฐพ์•„๋ณผ ์ˆ˜ ์žˆ์œผ๋‹ˆ ์ฐธ๊ณ ํ•ด๋ณด์„ธ์š”! ๐Ÿš€

๐Ÿค” JWT๊ฐ€ ๋ญ๊ธธ๋ž˜ ๋‹ค๋“ค ๋‚œ๋ฆฌ์•ผ?

JWT๋Š” JSON Web Token์˜ ์•ฝ์ž์ธ๋ฐ์š”, ์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๋‹ด์€ ๋””์ง€ํ„ธ ์‹ ๋ถ„์ฆ ๊ฐ™์€ ๊ฑฐ์˜ˆ์š”. ์ „ํ†ต์ ์ธ ์„ธ์…˜ ๋ฐฉ์‹๊ณผ ๋‹ฌ๋ฆฌ ์„œ๋ฒ„์— ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜์ง€ ์•Š์•„๋„ ๋˜๊ธฐ ๋•Œ๋ฌธ์— ํ™•์žฅ์„ฑ์ด ๋›ฐ์–ด๋‚˜์ฃ .

๐Ÿ’ก ์™œ JWT๋ฅผ ์“ธ๊นŒ? โ€ข Stateless: ์„œ๋ฒ„๊ฐ€ ์„ธ์…˜ ์ •๋ณด๋ฅผ ์ €์žฅํ•  ํ•„์š”๊ฐ€ ์—†์–ด์š”
โ€ข ํ™•์žฅ์„ฑ: ์—ฌ๋Ÿฌ ์„œ๋ฒ„๋กœ ํ™•์žฅํ•˜๊ธฐ ์‰ฌ์›Œ์š”
โ€ข ๋ชจ๋ฐ”์ผ ์นœํ™”์ : ์•ฑ์—์„œ๋„ ์‰ฝ๊ฒŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ด์š”
โ€ข ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค: ์„œ๋น„์Šค ๊ฐ„ ์ธ์ฆ ๊ณต์œ ๊ฐ€ ํŽธ๋ฆฌํ•ด์š”

JWT๋Š” ์„ธ ๋ถ€๋ถ„์œผ๋กœ ๋‚˜๋‰˜์–ด์ ธ ์žˆ์–ด์š”. Header(ํ—ค๋”), Payload(ํŽ˜์ด๋กœ๋“œ), Signature(์„œ๋ช…)๋กœ ๊ตฌ์„ฑ๋˜๊ณ , ์ (.)์œผ๋กœ ๊ตฌ๋ถ„๋ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค๋ฉด ์ด๋Ÿฐ ์‹์ด์ฃ :

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0NSIsIm5hbWUiOiLtmY3quLjrj5kifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

๋ณต์žกํ•ด ๋ณด์ด์ง€๋งŒ ๊ฑฑ์ • ๋งˆ์„ธ์š”! ํ•˜๋‚˜์”ฉ ํŒŒํ—ค์ณ๋ณผ๊ฒŒ์š”. ๐Ÿ˜Š

JWT ์ธ์ฆ ํ๋ฆ„๋„ ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ ์š”์ฒญ ID: hong@email.com POST /login ์„œ๋ฒ„ ์ธ์ฆ ์ฒ˜๋ฆฌ DB ๊ฒ€์ฆ JWT ์ƒ์„ฑ JWT ํ† ํฐ eyJhbGci... ์œ ํšจ๊ธฐ๊ฐ„: 1์‹œ๊ฐ„ ํ† ํฐ ์ €์žฅ ํด๋ผ์ด์–ธํŠธ LocalStorage ๋˜๋Š” Cookie API ์š”์ฒญ + JWT ํ† ํฐ API ์„œ๋ฒ„ ํ† ํฐ ๊ฒ€์ฆ ๋ฐ์ดํ„ฐ ์‘๋‹ต โœ“ ์ธ์ฆ ์™„๋ฃŒ

๐Ÿ› ๏ธ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์ค€๋น„ํ•˜๊ธฐ

๋ณธ๊ฒฉ์ ์œผ๋กœ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์ „์— ํ•„์š”ํ•œ ๊ฒƒ๋“ค์„ ์ค€๋น„ํ•ด๋ณผ๊นŒ์š”? ์ด๋ฒˆ ์˜ˆ์ œ์—์„œ๋Š” Node.js์™€ Express๋ฅผ ์‚ฌ์šฉํ•  ๊ฑฐ์˜ˆ์š”. ๊ฐ€์žฅ ๋Œ€์ค‘์ ์ด๊ณ  ๋ฐฐ์šฐ๊ธฐ ์‰ฌ์šด ์กฐํ•ฉ์ด๊ฑฐ๋“ ์š”! ๐ŸŽจ

๐Ÿ“ฆ ํ•„์š”ํ•œ ํŒจํ‚ค์ง€๋“ค

npm init -y
npm install express jsonwebtoken bcryptjs dotenv
npm install --save-dev nodemon

๊ฐ ํŒจํ‚ค์ง€๊ฐ€ ํ•˜๋Š” ์ผ์„ ์•Œ์•„๋ณผ๊ฒŒ์š”:


โ€ข express: ์›น ์„œ๋ฒ„ ํ”„๋ ˆ์ž„์›Œํฌ
โ€ข jsonwebtoken: JWT ์ƒ์„ฑ๊ณผ ๊ฒ€์ฆ์„ ๋‹ด๋‹น
โ€ข bcryptjs: ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”์šฉ
โ€ข dotenv: ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ด€๋ฆฌ
โ€ข nodemon: ๊ฐœ๋ฐœ ์‹œ ์ž๋™ ์žฌ์‹œ์ž‘
๐Ÿ” ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

์‹ค๋ฌด์—์„œ๋Š” ์ฝ”๋“œ๋ฅผ ๊น”๋”ํ•˜๊ฒŒ ์ •๋ฆฌํ•˜๋Š” ๊ฒŒ ์ •๋ง ์ค‘์š”ํ•ด์š”. ์ด๋Ÿฐ ๊ตฌ์กฐ๋ฅผ ์ถ”์ฒœ๋“œ๋ ค์š”:

project/
โ”œโ”€โ”€ config/
โ”‚   โ””โ”€โ”€ db.js
โ”œโ”€โ”€ controllers/
โ”‚   โ””โ”€โ”€ authController.js
โ”œโ”€โ”€ middleware/
โ”‚   โ””โ”€โ”€ authMiddleware.js
โ”œโ”€โ”€ models/
โ”‚   โ””โ”€โ”€ User.js
โ”œโ”€โ”€ routes/
โ”‚   โ””โ”€โ”€ authRoutes.js
โ”œโ”€โ”€ .env
โ”œโ”€โ”€ server.js
โ””โ”€โ”€ package.json

๐Ÿ”‘ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •ํ•˜๊ธฐ

๋ณด์•ˆ์— ๋ฏผ๊ฐํ•œ ์ •๋ณด๋“ค์€ ์ ˆ๋Œ€ ์ฝ”๋“œ์— ์ง์ ‘ ๋„ฃ์œผ๋ฉด ์•ˆ ๋ผ์š”! .env ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์„œ ๊ด€๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฑด ์ •๋ง์ •๋ง ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด์—์š”. ๐Ÿ˜ฑ

# .env ํŒŒ์ผ
PORT=5000
JWT_SECRET=your_super_secret_key_change_this_in_production_12345
JWT_EXPIRE=1h
JWT_REFRESH_SECRET=your_refresh_token_secret_key_67890
JWT_REFRESH_EXPIRE=7d
MONGODB_URI=mongodb://localhost:27017/jwt_auth_demo
โš ๏ธ ๋ณด์•ˆ ์ฃผ์˜์‚ฌํ•ญ โ€ข JWT_SECRET์€ ์ตœ์†Œ 32์ž ์ด์ƒ์˜ ๋žœ๋ค ๋ฌธ์ž์—ด์„ ์‚ฌ์šฉํ•˜์„ธ์š”
โ€ข .env ํŒŒ์ผ์€ ์ ˆ๋Œ€ Git์— ์ปค๋ฐ‹ํ•˜์ง€ ๋งˆ์„ธ์š” (.gitignore์— ์ถ”๊ฐ€!)
โ€ข ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋” ๊ฐ•๋ ฅํ•œ ์‹œํฌ๋ฆฟ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”
โ€ข ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ๊ฐ„์€ ๋ณด์•ˆ๊ณผ ์‚ฌ์šฉ์„ฑ์˜ ๊ท ํ˜•์„ ๊ณ ๋ คํ•ด์„œ ์„ค์ •ํ•˜์„ธ์š”

๐Ÿ’พ ์‚ฌ์šฉ์ž ๋ชจ๋ธ ๋งŒ๋“ค๊ธฐ

๋จผ์ € ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋–ป๊ฒŒ ์ €์žฅํ• ์ง€ ์ •์˜ํ•ด์•ผ๊ฒ ์ฃ ? MongoDB์™€ Mongoose๋ฅผ ์‚ฌ์šฉํ•œ ์˜ˆ์ œ๋ฅผ ๋ณด์—ฌ๋“œ๋ฆด๊ฒŒ์š”. ์‹ค์ œ๋กœ๋Š” ์—ฌ๋Ÿฌ๋ถ„์ด ์„ ํ˜ธํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค! ๐Ÿ—„๏ธ

// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema({
  email: {
    type: String,
    required: [true, '์ด๋ฉ”์ผ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'],
    unique: true,
    lowercase: true,
    trim: true,
    match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, '์œ ํšจํ•œ ์ด๋ฉ”์ผ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”']
  },
  password: {
    type: String,
    required: [true, '๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'],
    minlength: [6, '๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์ตœ์†Œ 6์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค'],
    select: false // ๊ธฐ๋ณธ ์ฟผ๋ฆฌ์—์„œ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ œ์™ธ
  },
  name: {
    type: String,
    required: [true, '์ด๋ฆ„์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'],
    trim: true
  },
  createdAt: {
    type: Date,
    default: Date.now
  },
  refreshToken: {
    type: String,
    select: false
  }
});

// ๋น„๋ฐ€๋ฒˆํ˜ธ ์ €์žฅ ์ „ ์•”ํ˜ธํ™”
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);

์ด ์ฝ”๋“œ์—์„œ ์ฃผ๋ชฉํ•  ์ ๋“ค์ด ์žˆ์–ด์š”:


๐ŸŽฏ ํ•ต์‹ฌ ํฌ์ธํŠธ 1. ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”: bcrypt๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ํ•ด์‹ฑํ•ด์š”. ์ ˆ๋Œ€ ํ‰๋ฌธ์œผ๋กœ ์ €์žฅํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค!

2. select: false: ์ผ๋ฐ˜ ์ฟผ๋ฆฌ์—์„œ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ž๋™์œผ๋กœ ์ œ์™ธ๋ผ์š”. ๋ณด์•ˆ์„ ์œ„ํ•ด ํ•„์ˆ˜!

3. ์œ ํšจ์„ฑ ๊ฒ€์ฆ: ์ด๋ฉ”์ผ ํ˜•์‹, ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ธธ์ด ๋“ฑ์„ ์Šคํ‚ค๋งˆ ๋ ˆ๋ฒจ์—์„œ ๊ฒ€์ฆํ•ด์š”

4. ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€: comparePassword๋กœ ๋กœ๊ทธ์ธ ์‹œ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ์„ ์‰ฝ๊ฒŒ ํ•  ์ˆ˜ ์žˆ์–ด์š”

๐ŸŽซ JWT ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ ๋งŒ๋“ค๊ธฐ

JWT๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š” ํ•จ์ˆ˜๋“ค์„ ๋ฏธ๋ฆฌ ๋งŒ๋“ค์–ด๋‘๋ฉด ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ์ด ํŽธํ•ด์ ธ์š”. ๊น”๋”ํ•˜๊ฒŒ ์ •๋ฆฌํ•ด๋ณผ๊นŒ์š”? โœจ

// utils/jwtUtils.js
const jwt = require('jsonwebtoken');

// Access Token ์ƒ์„ฑ
const generateAccessToken = (userId) => {
  return jwt.sign(
    { userId }, // payload
    process.env.JWT_SECRET,
    { 
      expiresIn: process.env.JWT_EXPIRE,
      issuer: 'your-app-name' // ๋ฐœ๊ธ‰์ž ์ •๋ณด
    }
  );
};

// Refresh Token ์ƒ์„ฑ
const generateRefreshToken = (userId) => {
  return jwt.sign(
    { userId },
    process.env.JWT_REFRESH_SECRET,
    { 
      expiresIn: process.env.JWT_REFRESH_EXPIRE,
      issuer: 'your-app-name'
    }
  );
};

// Access Token ๊ฒ€์ฆ
const verifyAccessToken = (token) => {
  try {
    return jwt.verify(token, process.env.JWT_SECRET);
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      throw new Error('ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค');
    }
    if (error.name === 'JsonWebTokenError') {
      throw new Error('์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค');
    }
    throw error;
  }
};

// Refresh Token ๊ฒ€์ฆ
const verifyRefreshToken = (token) => {
  try {
    return jwt.verify(token, process.env.JWT_REFRESH_SECRET);
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      throw new Error('๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค');
    }
    if (error.name === 'JsonWebTokenError') {
      throw new Error('์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์ž…๋‹ˆ๋‹ค');
    }
    throw error;
  }
};

module.exports = {
  generateAccessToken,
  generateRefreshToken,
  verifyAccessToken,
  verifyRefreshToken
};
๐Ÿ”„ Access Token vs Refresh Token Access Token: ์งง์€ ์ˆ˜๋ช…(๋ณดํ†ต 15๋ถ„~1์‹œ๊ฐ„), API ์š”์ฒญ ์‹œ ์‚ฌ์šฉ
Refresh Token: ๊ธด ์ˆ˜๋ช…(๋ณดํ†ต 7์ผ~30์ผ), Access Token ์žฌ๋ฐœ๊ธ‰์šฉ

์ด๋ ‡๊ฒŒ ๋‘ ๊ฐœ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ ๋Š” ๋ณด์•ˆ๊ณผ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์˜ ๊ท ํ˜•์„ ๋งž์ถ”๊ธฐ ์œ„ํ•ด์„œ์˜ˆ์š”. Access Token์ด ํƒˆ์ทจ๋˜๋”๋ผ๋„ ์งง์€ ์‹œ๊ฐ„ ๋‚ด์— ๋งŒ๋ฃŒ๋˜๊ณ , Refresh Token์œผ๋กœ ์ƒˆ๋กœ์šด Access Token์„ ๋ฐœ๊ธ‰๋ฐ›์„ ์ˆ˜ ์žˆ์ฃ ! ๐Ÿ›ก๏ธ

๐Ÿšช ํšŒ์›๊ฐ€์ž… API ๊ตฌํ˜„ํ•˜๊ธฐ

์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ํšŒ์›๊ฐ€์ž… ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. ์‚ฌ์šฉ์ž๊ฐ€ ๊ณ„์ •์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด์•ผ ๋กœ๊ทธ์ธ๋„ ํ•  ์ˆ˜ ์žˆ๊ฒ ์ฃ ? ๐Ÿ˜„

// controllers/authController.js
const User = require('../models/User');
const { generateAccessToken, generateRefreshToken } = require('../utils/jwtUtils');

// ํšŒ์›๊ฐ€์ž…
exports.register = async (req, res) => {
  try {
    const { email, password, name } = req.body;
    
    // ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ
    if (!email || !password || !name) {
      return res.status(400).json({
        success: false,
        message: '๋ชจ๋“  ํ•„๋“œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'
      });
    }
    
    // ์ด๋ฉ”์ผ ์ค‘๋ณต ์ฒดํฌ
    const existingUser = await User.findOne({ email });
    if (existingUser) {
      return res.status(409).json({
        success: false,
        message: '์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค'
      });
    }
    
    // ์‚ฌ์šฉ์ž ์ƒ์„ฑ
    const user = await User.create({
      email,
      password,
      name
    });
    
    // ํ† ํฐ ์ƒ์„ฑ
    const accessToken = generateAccessToken(user._id);
    const refreshToken = generateRefreshToken(user._id);
    
    // Refresh Token์„ DB์— ์ €์žฅ
    user.refreshToken = refreshToken;
    await user.save();
    
    // ์‘๋‹ต (๋น„๋ฐ€๋ฒˆํ˜ธ ์ œ์™ธ)
    res.status(201).json({
      success: true,
      message: 'ํšŒ์›๊ฐ€์ž…์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค',
      data: {
        user: {
          id: user._id,
          email: user.email,
          name: user.name
        },
        accessToken,
        refreshToken
      }
    });
    
  } catch (error) {
    console.error('ํšŒ์›๊ฐ€์ž… ์—๋Ÿฌ:', error);
    res.status(500).json({
      success: false,
      message: '์„œ๋ฒ„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค',
      error: error.message
    });
  }
};

ํšŒ์›๊ฐ€์ž… ๋กœ์ง์—์„œ ์ค‘์š”ํ•œ ๋ถ€๋ถ„๋“ค์„ ์งš์–ด๋ณผ๊ฒŒ์š”:


โœ… ํšŒ์›๊ฐ€์ž… ์ฒดํฌ๋ฆฌ์ŠคํŠธ

1๏ธโƒฃ ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ: ํ•„์ˆ˜ ํ•„๋“œ๊ฐ€ ๋ชจ๋‘ ์žˆ๋Š”์ง€ ํ™•์ธ
2๏ธโƒฃ ์ค‘๋ณต ์ฒดํฌ: ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์ด๋ฉ”์ผ์ธ์ง€ ํ™•์ธ
3๏ธโƒฃ ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”: User ๋ชจ๋ธ์˜ pre-save ํ›…์—์„œ ์ž๋™ ์ฒ˜๋ฆฌ
4๏ธโƒฃ ํ† ํฐ ์ƒ์„ฑ: Access Token๊ณผ Refresh Token ๋ชจ๋‘ ์ƒ์„ฑ
5๏ธโƒฃ Refresh Token ์ €์žฅ: DB์— ์ €์žฅํ•ด์„œ ๋‚˜์ค‘์— ๊ฒ€์ฆ
6๏ธโƒฃ ์•ˆ์ „ํ•œ ์‘๋‹ต: ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์ ˆ๋Œ€ ์‘๋‹ต์— ํฌํ•จํ•˜์ง€ ์•Š๊ธฐ

๐Ÿ”“ ๋กœ๊ทธ์ธ API ๊ตฌํ˜„ํ•˜๊ธฐ

๋“œ๋””์–ด ํ•ต์‹ฌ์ธ ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์ด์—์š”! ์ด๊ฒŒ ๋ฐ”๋กœ JWT ์ธ์ฆ์˜ ์‹œ์ž‘์ ์ด์ฃ . ์‚ฌ์šฉ์ž๊ฐ€ ์ด๋ฉ”์ผ๊ณผ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด JWT ํ† ํฐ์„ ๋ฐœ๊ธ‰ํ•ด์ฃผ๋Š” ๊ฑฐ์˜ˆ์š”. ๐ŸŽ‰

// controllers/authController.js (๊ณ„์†)

// ๋กœ๊ทธ์ธ
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 }).select('+password');
    
    if (!user) {
      return res.status(401).json({
        success: false,
        message: '์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค'
      });
    }
    
    // ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ
    const isPasswordCorrect = await user.comparePassword(password);
    
    if (!isPasswordCorrect) {
      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,
          email: user.email,
          name: user.name
        },
        accessToken,
        refreshToken
      }
    });
    
  } catch (error) {
    console.error('๋กœ๊ทธ์ธ ์—๋Ÿฌ:', error);
    res.status(500).json({
      success: false,
      message: '์„œ๋ฒ„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค',
      error: error.message
    });
  }
};
๐Ÿ”’ ๋ณด์•ˆ ํŒ ๋กœ๊ทธ์ธ ์‹คํŒจ ์‹œ "์ด๋ฉ”์ผ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค" ๋˜๋Š” "๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ํ‹€๋ ธ์Šต๋‹ˆ๋‹ค"๋ผ๊ณ  ๊ตฌ์ฒด์ ์œผ๋กœ ์•Œ๋ ค์ฃผ๋ฉด ์•ˆ ๋ผ์š”! ๊ณต๊ฒฉ์ž๊ฐ€ ์–ด๋–ค ์ด๋ฉ”์ผ์ด ์‹œ์Šคํ…œ์— ๋“ฑ๋ก๋˜์–ด ์žˆ๋Š”์ง€ ์•Œ ์ˆ˜ ์žˆ๊ฑฐ๋“ ์š”. ๊ทธ๋ž˜์„œ ํ•ญ์ƒ "์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค"๋ผ๊ณ  ํ†ตํ•ฉ๋œ ๋ฉ”์‹œ์ง€๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๐Ÿ›ก๏ธ

๐Ÿ›ก๏ธ ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด ๋งŒ๋“ค๊ธฐ

์ด์ œ ๋ณดํ˜ธ๋œ API ์—”๋“œํฌ์ธํŠธ๋ฅผ ๋งŒ๋“ค ์ฐจ๋ก€์˜ˆ์š”. ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๋งŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ๋“ค ๋ง์ด์ฃ . ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ฝ”๋“œ ์ค‘๋ณต ์—†์ด ๊น”๋”ํ•˜๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด์š”! ๐Ÿ’ช

// middleware/authMiddleware.js
const { verifyAccessToken } = require('../utils/jwtUtils');
const User = require('../models/User');

// ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด
exports.authenticate = async (req, res, next) => {
  try {
    // ํ—ค๋”์—์„œ ํ† ํฐ ์ถ”์ถœ
    const authHeader = req.headers.authorization;
    
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return res.status(401).json({
        success: false,
        message: '์ธ์ฆ ํ† ํฐ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค'
      });
    }
    
    // Bearer ์ œ๊ฑฐํ•˜๊ณ  ํ† ํฐ๋งŒ ์ถ”์ถœ
    const token = authHeader.substring(7);
    
    // ํ† ํฐ ๊ฒ€์ฆ
    const decoded = verifyAccessToken(token);
    
    // ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ
    const user = await User.findById(decoded.userId);
    
    if (!user) {
      return res.status(401).json({
        success: false,
        message: '์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค'
      });
    }
    
    // req ๊ฐ์ฒด์— ์‚ฌ์šฉ์ž ์ •๋ณด ์ถ”๊ฐ€
    req.user = user;
    req.userId = user._id;
    
    next();
    
  } catch (error) {
    console.error('์ธ์ฆ ์—๋Ÿฌ:', error);
    
    if (error.message.includes('ํ† ํฐ')) {
      return res.status(401).json({
        success: false,
        message: error.message
      });
    }
    
    res.status(500).json({
      success: false,
      message: '์ธ์ฆ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค'
    });
  }
};

// ์„ ํƒ์  ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด (๋กœ๊ทธ์ธ ์•ˆ ํ•ด๋„ ๋˜์ง€๋งŒ, ํ–ˆ์œผ๋ฉด ์ •๋ณด ์ œ๊ณต)
exports.optionalAuthenticate = async (req, res, next) => {
  try {
    const authHeader = req.headers.authorization;
    
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return next(); // ํ† ํฐ ์—†์–ด๋„ ๊ณ„์† ์ง„ํ–‰
    }
    
    const token = authHeader.substring(7);
    const decoded = verifyAccessToken(token);
    const user = await User.findById(decoded.userId);
    
    if (user) {
      req.user = user;
      req.userId = user._id;
    }
    
    next();
    
  } catch (error) {
    // ์—๋Ÿฌ๊ฐ€ ์žˆ์–ด๋„ ๊ณ„์† ์ง„ํ–‰
    next();
  }
};

์ด ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์ •๋ง ๊ฐ„๋‹จํ•ด์š”:


// routes/protectedRoutes.js
const express = require('express');
const router = express.Router();
const { authenticate } = require('../middleware/authMiddleware');

// ๋ณดํ˜ธ๋œ ๋ผ์šฐํŠธ - ๋กœ๊ทธ์ธ ํ•„์ˆ˜
router.get('/profile', authenticate, (req, res) => {
  res.json({
    success: true,
    data: {
      user: req.user
    }
  });
});

// ๋ณดํ˜ธ๋œ ๋ผ์šฐํŠธ - ์‚ฌ์šฉ์ž ์ •๋ณด ์ˆ˜์ •
router.put('/profile', authenticate, async (req, res) => {
  try {
    const { name } = req.body;
    
    req.user.name = name;
    await req.user.save();
    
    res.json({
      success: true,
      message: 'ํ”„๋กœํ•„์ด ์—…๋ฐ์ดํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค',
      data: {
        user: req.user
      }
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      message: 'ํ”„๋กœํ•„ ์—…๋ฐ์ดํŠธ ์‹คํŒจ'
    });
  }
});

module.exports = router;

๐Ÿ”„ ํ† ํฐ ๊ฐฑ์‹  API ๊ตฌํ˜„ํ•˜๊ธฐ

Access Token์€ ๋ณด์•ˆ์„ ์œ„ํ•ด ์งง์€ ์ˆ˜๋ช…์„ ๊ฐ€์ง€๊ณ  ์žˆ์–ด์š”. ๊ทธ๋ž˜์„œ ๋งŒ๋ฃŒ๋˜๋ฉด Refresh Token์„ ์‚ฌ์šฉํ•ด์„œ ์ƒˆ๋กœ์šด Access Token์„ ๋ฐœ๊ธ‰๋ฐ›์•„์•ผ ํ•ด์š”. ์ด ๊ณผ์ •์„ ๊ตฌํ˜„ํ•ด๋ณผ๊ฒŒ์š”! ๐Ÿ”

// controllers/authController.js (๊ณ„์†)

// ํ† ํฐ ๊ฐฑ์‹ 
exports.refreshToken = async (req, res) => {
  try {
    const { refreshToken } = req.body;
    
    if (!refreshToken) {
      return res.status(400).json({
        success: false,
        message: '๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค'
      });
    }
    
    // Refresh Token ๊ฒ€์ฆ
    const decoded = verifyRefreshToken(refreshToken);
    
    // ์‚ฌ์šฉ์ž ์ฐพ๊ธฐ
    const user = await User.findById(decoded.userId).select('+refreshToken');
    
    if (!user) {
      return res.status(401).json({
        success: false,
        message: '์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค'
      });
    }
    
    // DB์— ์ €์žฅ๋œ Refresh Token๊ณผ ๋น„๊ต
    if (user.refreshToken !== refreshToken) {
      return res.status(401).json({
        success: false,
        message: '์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์ž…๋‹ˆ๋‹ค'
      });
    }
    
    // ์ƒˆ๋กœ์šด ํ† ํฐ ์ƒ์„ฑ
    const newAccessToken = generateAccessToken(user._id);
    const newRefreshToken = generateRefreshToken(user._id);
    
    // ์ƒˆ๋กœ์šด Refresh Token ์ €์žฅ
    user.refreshToken = newRefreshToken;
    await user.save();
    
    res.json({
      success: true,
      message: 'ํ† ํฐ์ด ๊ฐฑ์‹ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค',
      data: {
        accessToken: newAccessToken,
        refreshToken: newRefreshToken
      }
    });
    
  } catch (error) {
    console.error('ํ† ํฐ ๊ฐฑ์‹  ์—๋Ÿฌ:', error);
    
    if (error.message.includes('ํ† ํฐ')) {
      return res.status(401).json({
        success: false,
        message: error.message
      });
    }
    
    res.status(500).json({
      success: false,
      message: 'ํ† ํฐ ๊ฐฑ์‹  ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค'
    });
  }
};
๐Ÿ’ก ํ† ํฐ ๊ฐฑ์‹  ์ „๋žต ํด๋ผ์ด์–ธํŠธ์—์„œ๋Š” API ์š”์ฒญ ์‹œ 401 ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์ž๋™์œผ๋กœ ํ† ํฐ ๊ฐฑ์‹ ์„ ์‹œ๋„ํ•˜๋„๋ก ๊ตฌํ˜„ํ•˜๋Š” ๊ฒŒ ์ข‹์•„์š”. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์‚ฌ์šฉ์ž๋Š” ๋กœ๊ทธ์ธ ์ƒํƒœ๊ฐ€ ๋Š๊ธฐ๋Š” ๊ฑธ ๋А๋ผ์ง€ ๋ชปํ•˜๊ณ  ๊ณ„์† ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์žˆ์ฃ ! ๐Ÿ˜Š

์˜ˆ๋ฅผ ๋“ค์–ด Axios ์ธํ„ฐ์…‰ํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ด๋Ÿฐ ๋กœ์ง์„ ๊น”๋”ํ•˜๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด์š”.

๐Ÿšช ๋กœ๊ทธ์•„์›ƒ API ๊ตฌํ˜„ํ•˜๊ธฐ

๋กœ๊ทธ์•„์›ƒ์€ ๊ฐ„๋‹จํ•ด ๋ณด์ด์ง€๋งŒ ์ œ๋Œ€๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด์š”. Refresh Token์„ ๋ฌดํšจํ™”ํ•ด์„œ ๋ณด์•ˆ์„ ๊ฐ•ํ™”ํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ ์š”! ๐Ÿ”

// controllers/authController.js (๊ณ„์†)

// ๋กœ๊ทธ์•„์›ƒ
exports.logout = async (req, res) => {
  try {
    const userId = req.userId; // ๋ฏธ๋“ค์›จ์–ด์—์„œ ์„ค์ •๋จ
    
    // Refresh Token ์ œ๊ฑฐ
    await User.findByIdAndUpdate(userId, {
      refreshToken: null
    });
    
    res.json({
      success: true,
      message: '๋กœ๊ทธ์•„์›ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค'
    });
    
  } catch (error) {
    console.error('๋กœ๊ทธ์•„์›ƒ ์—๋Ÿฌ:', error);
    res.status(500).json({
      success: false,
      message: '๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค'
    });
  }
};

// ๋ชจ๋“  ๊ธฐ๊ธฐ์—์„œ ๋กœ๊ทธ์•„์›ƒ (๋ณด์•ˆ ๊ฐ•ํ™”)
exports.logoutAll = async (req, res) => {
  try {
    const userId = req.userId;
    
    // ๋ชจ๋“  Refresh Token ์ œ๊ฑฐ
    await User.findByIdAndUpdate(userId, {
      refreshToken: null
    });
    
    res.json({
      success: true,
      message: '๋ชจ๋“  ๊ธฐ๊ธฐ์—์„œ ๋กœ๊ทธ์•„์›ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค'
    });
    
  } catch (error) {
    console.error('์ „์ฒด ๋กœ๊ทธ์•„์›ƒ ์—๋Ÿฌ:', error);
    res.status(500).json({
      success: false,
      message: '๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค'
    });
  }
};

๐Ÿ—บ๏ธ ๋ผ์šฐํŠธ ์„ค์ •ํ•˜๊ธฐ

์ด์ œ ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ๋ผ์šฐํŠธ๋กœ ์—ฐ๊ฒฐํ•ด๋ณผ๊ฒŒ์š”. Express ๋ผ์šฐํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊น”๋”ํ•˜๊ฒŒ ์ •๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์š”! ๐ŸŽฏ

// routes/authRoutes.js
const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');
const { authenticate } = require('../middleware/authMiddleware');

// ๊ณต๊ฐœ ๋ผ์šฐํŠธ
router.post('/register', authController.register);
router.post('/login', authController.login);
router.post('/refresh-token', authController.refreshToken);

// ๋ณดํ˜ธ๋œ ๋ผ์šฐํŠธ (์ธ์ฆ ํ•„์š”)
router.post('/logout', authenticate, authController.logout);
router.post('/logout-all', authenticate, authController.logoutAll);

module.exports = router;

๐Ÿš€ ์„œ๋ฒ„ ์„ค์ • ๋ฐ ์‹คํ–‰

๋งˆ์ง€๋ง‰์œผ๋กœ ๋ชจ๋“  ๊ฑธ ํ•˜๋‚˜๋กœ ํ•ฉ์ณ์„œ ์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•ด๋ณผ๊ฒŒ์š”! ์ด์ œ ๊ฑฐ์˜ ๋‹ค ์™”์–ด์š”. ๐ŸŽŠ

// server.js
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const authRoutes = require('./routes/authRoutes');
const protectedRoutes = require('./routes/protectedRoutes');

const app = express();

// ๋ฏธ๋“ค์›จ์–ด
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// CORS ์„ค์ • (ํ•„์š”ํ•œ ๊ฒฝ์šฐ)
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  next();
});

// MongoDB ์—ฐ๊ฒฐ
mongoose.connect(process.env.MONGODB_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true
})
.then(() => console.log('โœ… MongoDB ์—ฐ๊ฒฐ ์„ฑ๊ณต'))
.catch(err => console.error('โŒ MongoDB ์—ฐ๊ฒฐ ์‹คํŒจ:', err));

// ๋ผ์šฐํŠธ ๋“ฑ๋ก
app.use('/api/auth', authRoutes);
app.use('/api', protectedRoutes);

// ๊ธฐ๋ณธ ๋ผ์šฐํŠธ
app.get('/', (req, res) => {
  res.json({
    message: 'JWT ์ธ์ฆ API ์„œ๋ฒ„',
    version: '1.0.0',
    endpoints: {
      auth: '/api/auth',
      protected: '/api'
    }
  });
});

// 404 ์—๋Ÿฌ ํ•ธ๋“ค๋ง
app.use((req, res) => {
  res.status(404).json({
    success: false,
    message: '์š”์ฒญํ•œ ๋ฆฌ์†Œ์Šค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค'
  });
});

// ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๋ฏธ๋“ค์›จ์–ด
app.use((err, req, res, next) => {
  console.error('์„œ๋ฒ„ ์—๋Ÿฌ:', err);
  res.status(500).json({
    success: false,
    message: '์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค',
    error: process.env.NODE_ENV === 'development' ? err.message : undefined
  });
});

// ์„œ๋ฒ„ ์‹œ์ž‘
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
  console.log(`๐Ÿš€ ์„œ๋ฒ„๊ฐ€ ํฌํŠธ ${PORT}์—์„œ ์‹คํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค`);
});

์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•˜๋ ค๋ฉด ํ„ฐ๋ฏธ๋„์—์„œ ์ด๋ ‡๊ฒŒ ์ž…๋ ฅํ•˜์„ธ์š”:


npm run dev

package.json์— ์ด ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ด๋‘๋ฉด ํŽธํ•ด์š”:


"scripts": {
  "start": "node server.js",
  "dev": "nodemon server.js"
}
ํด๋ผ์ด์–ธํŠธ ์ธก JWT ์ฒ˜๋ฆฌ ํ๋ฆ„ 1. ๋กœ๊ทธ์ธ ์š”์ฒญ POST /api/auth/login { email, password } 2. ํ† ํฐ ์ˆ˜์‹  accessToken refreshToken 3. ํ† ํฐ ์ €์žฅ localStorage.setItem() 4. API ์š”์ฒญ Authorization: Bearer {token} ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ โœ“ HTTPS ์‚ฌ์šฉ ํ•„์ˆ˜ โœ“ XSS ๋ฐฉ์–ด โœ“ CSRF ํ† ํฐ ์‚ฌ์šฉ โœ“ ํ† ํฐ ๋งŒ๋ฃŒ ์ฒ˜๋ฆฌ โœ“ ๋ฏผ๊ฐ์ •๋ณด ์•”ํ˜ธํ™” โœ“ Rate Limiting โœ“ ๋กœ๊ทธ ๋ชจ๋‹ˆํ„ฐ๋ง ๐Ÿ”’ ์•ˆ์ „ํ•œ ์ธ์ฆ ์™„๋ฃŒ

๐Ÿ’ป ํด๋ผ์ด์–ธํŠธ ์ธก ๊ตฌํ˜„ ์˜ˆ์ œ

์„œ๋ฒ„๋Š” ์™„์„ฑํ–ˆ์œผ๋‹ˆ ์ด์ œ ํด๋ผ์ด์–ธํŠธ์—์„œ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•˜๋Š”์ง€ ๋ณผ๊นŒ์š”? JavaScript์™€ Axios๋ฅผ ์‚ฌ์šฉํ•œ ์˜ˆ์ œ๋ฅผ ๋ณด์—ฌ๋“œ๋ฆด๊ฒŒ์š”! ๐ŸŽจ

// auth.js - ํด๋ผ์ด์–ธํŠธ ์ธก ์ธ์ฆ ์œ ํ‹ธ๋ฆฌํ‹ฐ
class AuthService {
  constructor() {
    this.API_URL = 'http://localhost:5000/api';
    this.accessToken = localStorage.getItem('accessToken');
    this.refreshToken = localStorage.getItem('refreshToken');
  }
  
  // ํšŒ์›๊ฐ€์ž…
  async register(email, password, name) {
    try {
      const response = await fetch(`${this.API_URL}/auth/register`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ email, password, name })
      });
      
      const data = await response.json();
      
      if (data.success) {
        this.setTokens(data.data.accessToken, data.data.refreshToken);
        return { success: true, user: data.data.user };
      }
      
      return { success: false, message: data.message };
    } catch (error) {
      console.error('ํšŒ์›๊ฐ€์ž… ์—๋Ÿฌ:', error);
      return { success: false, message: '๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค' };
    }
  }
  
  // ๋กœ๊ทธ์ธ
  async login(email, password) {
    try {
      const response = await fetch(`${this.API_URL}/auth/login`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ email, password })
      });
      
      const data = await response.json();
      
      if (data.success) {
        this.setTokens(data.data.accessToken, data.data.refreshToken);
        return { success: true, user: data.data.user };
      }
      
      return { success: false, message: data.message };
    } catch (error) {
      console.error('๋กœ๊ทธ์ธ ์—๋Ÿฌ:', error);
      return { success: false, message: '๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค' };
    }
  }
  
  // ํ† ํฐ ์ €์žฅ
  setTokens(accessToken, refreshToken) {
    this.accessToken = accessToken;
    this.refreshToken = refreshToken;
    localStorage.setItem('accessToken', accessToken);
    localStorage.setItem('refreshToken', refreshToken);
  }
  
  // ํ† ํฐ ๊ฐฑ์‹ 
  async refreshAccessToken() {
    try {
      const response = await fetch(`${this.API_URL}/auth/refresh-token`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ refreshToken: this.refreshToken })
      });
      
      const data = await response.json();
      
      if (data.success) {
        this.setTokens(data.data.accessToken, data.data.refreshToken);
        return true;
      }
      
      // Refresh Token๋„ ๋งŒ๋ฃŒ๋จ - ์žฌ๋กœ๊ทธ์ธ ํ•„์š”
      this.logout();
      return false;
    } catch (error) {
      console.error('ํ† ํฐ ๊ฐฑ์‹  ์—๋Ÿฌ:', error);
      return false;
    }
  }
  
  // ์ธ์ฆ๋œ API ์š”์ฒญ
  async authenticatedRequest(url, options = {}) {
    try {
      // ์ฒซ ๋ฒˆ์งธ ์‹œ๋„
      let response = await fetch(url, {
        ...options,
        headers: {
          ...options.headers,
          'Authorization': `Bearer ${this.accessToken}`
        }
      });
      
      // 401 ์—๋Ÿฌ๋ฉด ํ† ํฐ ๊ฐฑ์‹  ํ›„ ์žฌ์‹œ๋„
      if (response.status === 401) {
        const refreshed = await this.refreshAccessToken();
        
        if (refreshed) {
          // ํ† ํฐ ๊ฐฑ์‹  ์„ฑ๊ณต - ์žฌ์‹œ๋„
          response = await fetch(url, {
            ...options,
            headers: {
              ...options.headers,
              'Authorization': `Bearer ${this.accessToken}`
            }
          });
        } else {
          // ํ† ํฐ ๊ฐฑ์‹  ์‹คํŒจ - ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ
          window.location.href = '/login';
          return null;
        }
      }
      
      return await response.json();
    } catch (error) {
      console.error('API ์š”์ฒญ ์—๋Ÿฌ:', error);
      return null;
    }
  }
  
  // ๋กœ๊ทธ์•„์›ƒ
  async logout() {
    try {
      await fetch(`${this.API_URL}/auth/logout`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${this.accessToken}`
        }
      });
    } catch (error) {
      console.error('๋กœ๊ทธ์•„์›ƒ ์—๋Ÿฌ:', error);
    } finally {
      // ๋กœ์ปฌ ํ† ํฐ ์ œ๊ฑฐ
      this.accessToken = null;
      this.refreshToken = null;
      localStorage.removeItem('accessToken');
      localStorage.removeItem('refreshToken');
      window.location.href = '/login';
    }
  }
  
  // ๋กœ๊ทธ์ธ ์ƒํƒœ ํ™•์ธ
  isAuthenticated() {
    return !!this.accessToken;
  }
}

// ์ „์—ญ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
const authService = new AuthService();

์ด์ œ ์‹ค์ œ๋กœ ์‚ฌ์šฉํ•˜๋Š” ์˜ˆ์ œ๋ฅผ ๋ณผ๊นŒ์š”:


// ์‚ฌ์šฉ ์˜ˆ์ œ

// ๋กœ๊ทธ์ธ ํผ ์ฒ˜๋ฆฌ
document.getElementById('loginForm').addEventListener('submit', async (e) => {
  e.preventDefault();
  
  const email = document.getElementById('email').value;
  const password = document.getElementById('password').value;
  
  const result = await authService.login(email, password);
  
  if (result.success) {
    alert('๋กœ๊ทธ์ธ ์„ฑ๊ณต!');
    window.location.href = '/dashboard';
  } else {
    alert(result.message);
  }
});

// ํ”„๋กœํ•„ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
async function loadProfile() {
  const data = await authService.authenticatedRequest(
    'http://localhost:5000/api/profile',
    { method: 'GET' }
  );
  
  if (data && data.success) {
    document.getElementById('userName').textContent = data.data.user.name;
    document.getElementById('userEmail').textContent = data.data.user.email;
  }
}

// ๋กœ๊ทธ์•„์›ƒ ๋ฒ„ํŠผ
document.getElementById('logoutBtn').addEventListener('click', () => {
  authService.logout();
});

๐Ÿ” ๋ณด์•ˆ ๊ฐ•ํ™” ํŒ

๊ธฐ๋ณธ ๊ตฌํ˜„์€ ์™„๋ฃŒํ–ˆ์ง€๋งŒ, ์‹ค์ œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋” ๋งŽ์€ ๋ณด์•ˆ ์กฐ์น˜๊ฐ€ ํ•„์š”ํ•ด์š”. ์ค‘์š”ํ•œ ๋ณด์•ˆ ํŒ๋“ค์„ ์•Œ๋ ค๋“œ๋ฆด๊ฒŒ์š”! ๐Ÿ›ก๏ธ

โš ๏ธ ํ•„์ˆ˜ ๋ณด์•ˆ ์กฐ์น˜ 1. HTTPS ์‚ฌ์šฉ
ํ† ํฐ์ด ํ‰๋ฌธ์œผ๋กœ ์ „์†ก๋˜๊ธฐ ๋•Œ๋ฌธ์— HTTPS๋Š” ํ•„์ˆ˜์˜ˆ์š”. HTTP๋กœ๋Š” ์ ˆ๋Œ€ ์šด์˜ํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค!

2. XSS ๋ฐฉ์–ด
์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ๊ทธ๋Œ€๋กœ ๋ Œ๋”๋งํ•˜์ง€ ๋ง๊ณ  ํ•ญ์ƒ ์ด์Šค์ผ€์ดํ”„ ์ฒ˜๋ฆฌํ•˜์„ธ์š”. React๋‚˜ Vue ๊ฐ™์€ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ณดํ˜ธ๋ฉ๋‹ˆ๋‹ค.

3. CSRF ๋ฐฉ์–ด
์ฟ ํ‚ค์— ํ† ํฐ์„ ์ €์žฅํ•œ๋‹ค๋ฉด CSRF ํ† ํฐ๋„ ํ•จ๊ป˜ ์‚ฌ์šฉํ•ด์•ผ ํ•ด์š”.

4. Rate Limiting
๋ฌด์ฐจ๋ณ„ ๋Œ€์ž… ๊ณต๊ฒฉ์„ ๋ง‰๊ธฐ ์œ„ํ•ด ๋กœ๊ทธ์ธ ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ์ œํ•œํ•˜์„ธ์š”.
// Rate Limiting ์˜ˆ์ œ (express-rate-limit ์‚ฌ์šฉ)
const rateLimit = require('express-rate-limit');

// ๋กœ๊ทธ์ธ API์— Rate Limiting ์ ์šฉ
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15๋ถ„
  max: 5, // ์ตœ๋Œ€ 5๋ฒˆ ์‹œ๋„
  message: {
    success: false,
    message: '๋„ˆ๋ฌด ๋งŽ์€ ๋กœ๊ทธ์ธ ์‹œ๋„๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. 15๋ถ„ ํ›„์— ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.'
  },
  standardHeaders: true,
  legacyHeaders: false,
});

// ๋ผ์šฐํŠธ์— ์ ์šฉ
router.post('/login', loginLimiter, authController.login);
๐Ÿ’ก ์ถ”๊ฐ€ ๋ณด์•ˆ ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•˜๊ธฐ IP ์ฃผ์†Œ ๊ธฐ๋ก
๋กœ๊ทธ์ธ ์‹œ IP ์ฃผ์†Œ๋ฅผ ๊ธฐ๋กํ•ด์„œ ์ด์ƒํ•œ ์ ‘์†์„ ๊ฐ์ง€ํ•  ์ˆ˜ ์žˆ์–ด์š”.

๋””๋ฐ”์ด์Šค ํ•‘๊ฑฐํ”„๋ฆฐํŒ…
์‚ฌ์šฉ์ž์˜ ๋ธŒ๋ผ์šฐ์ €, OS ์ •๋ณด๋ฅผ ๊ธฐ๋กํ•ด์„œ ์ƒˆ๋กœ์šด ๊ธฐ๊ธฐ์—์„œ ๋กœ๊ทธ์ธ ์‹œ ์•Œ๋ฆผ์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ์–ด์š”.

2๋‹จ๊ณ„ ์ธ์ฆ (2FA)
SMS๋‚˜ OTP ์•ฑ์„ ํ†ตํ•œ ์ถ”๊ฐ€ ์ธ์ฆ์„ ๊ตฌํ˜„ํ•˜๋ฉด ๋ณด์•ˆ์ด ํฌ๊ฒŒ ๊ฐ•ํ™”๋ฉ๋‹ˆ๋‹ค.

๋น„๋ฐ€๋ฒˆํ˜ธ ์ •์ฑ…
์ตœ์†Œ ๊ธธ์ด, ํŠน์ˆ˜๋ฌธ์ž ํฌํ•จ ๋“ฑ์˜ ๊ทœ์น™์„ ์ ์šฉํ•˜์„ธ์š”.

๐Ÿ› ์ผ๋ฐ˜์ ์ธ ๋ฌธ์ œ์™€ ํ•ด๊ฒฐ๋ฒ•

JWT ์ธ์ฆ์„ ๊ตฌํ˜„ํ•˜๋‹ค ๋ณด๋ฉด ์ž์ฃผ ๋งˆ์ฃผ์น˜๋Š” ๋ฌธ์ œ๋“ค์ด ์žˆ์–ด์š”. ๋ฏธ๋ฆฌ ์•Œ์•„๋‘๋ฉด ์‹œ๊ฐ„์„ ๋งŽ์ด ์ ˆ์•ฝํ•  ์ˆ˜ ์žˆ์ฃ ! ๐Ÿ˜Š

โŒ ๋ฌธ์ œ 1: "JsonWebTokenError: invalid signature"

์›์ธ: JWT_SECRET์ด ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ์—์„œ ๋‹ค๋ฅด๊ฑฐ๋‚˜, ํ† ํฐ์ด ๋ณ€์กฐ๋˜์—ˆ์–ด์š”.
ํ•ด๊ฒฐ: .env ํŒŒ์ผ์˜ JWT_SECRET์„ ํ™•์ธํ•˜๊ณ , ์„œ๋ฒ„ ์žฌ์‹œ์ž‘ ํ›„ ์ƒˆ๋กœ์šด ํ† ํฐ์„ ๋ฐœ๊ธ‰๋ฐ›์œผ์„ธ์š”.
โŒ ๋ฌธ์ œ 2: "TokenExpiredError: jwt expired"

์›์ธ: Access Token์ด ๋งŒ๋ฃŒ๋˜์—ˆ์–ด์š”.
ํ•ด๊ฒฐ: Refresh Token์„ ์‚ฌ์šฉํ•ด์„œ ์ƒˆ๋กœ์šด Access Token์„ ๋ฐœ๊ธ‰๋ฐ›์œผ์„ธ์š”. ํด๋ผ์ด์–ธํŠธ์—์„œ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋„๋ก ๊ตฌํ˜„ํ•˜๋Š” ๊ฒŒ ์ข‹์•„์š”.
โŒ ๋ฌธ์ œ 3: CORS ์—๋Ÿฌ

์›์ธ: ์„œ๋ฒ„์—์„œ CORS ์„ค์ •์ด ์•ˆ ๋˜์–ด ์žˆ์–ด์š”.
ํ•ด๊ฒฐ: cors ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์„ค์น˜ํ•˜๊ณ  ์„ค์ •ํ•˜์„ธ์š”:

npm install cors

// server.js
const cors = require('cors');
app.use(cors({
  origin: 'http://localhost:3000', // ํด๋ผ์ด์–ธํŠธ ์ฃผ์†Œ
  credentials: true
}));
โŒ ๋ฌธ์ œ 4: ํ† ํฐ์ด ๋„ˆ๋ฌด ์ปค์š”

์›์ธ: Payload์— ๋„ˆ๋ฌด ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋„ฃ์—ˆ์–ด์š”.
ํ•ด๊ฒฐ: JWT์—๋Š” ์ตœ์†Œํ•œ์˜ ์ •๋ณด๋งŒ ๋„ฃ์œผ์„ธ์š”. ์‚ฌ์šฉ์ž ID ์ •๋„๋งŒ ๋„ฃ๊ณ , ๋‚˜๋จธ์ง€๋Š” ์„œ๋ฒ„์—์„œ ์กฐํšŒํ•˜๋Š” ๊ฒŒ ์ข‹์•„์š”.

๐Ÿ“Š JWT vs ์„ธ์…˜ ๋น„๊ต

JWT๋ฅผ ์‚ฌ์šฉํ• ์ง€ ์ „ํ†ต์ ์ธ ์„ธ์…˜์„ ์‚ฌ์šฉํ• ์ง€ ๊ณ ๋ฏผ๋˜์‹œ๋‚˜์š”? ๊ฐ๊ฐ์˜ ์žฅ๋‹จ์ ์„ ๋น„๊ตํ•ด๋ณผ๊ฒŒ์š”! ๐Ÿค”

ํŠน์ง• JWT ์„ธ์…˜
์ €์žฅ ์œ„์น˜ ํด๋ผ์ด์–ธํŠธ (LocalStorage, Cookie) ์„œ๋ฒ„ (๋ฉ”๋ชจ๋ฆฌ, Redis, DB)
ํ™•์žฅ์„ฑ โญโญโญโญโญ ๋งค์šฐ ์ข‹์Œ โญโญโญ ๋ณดํ†ต
๋ณด์•ˆ โญโญโญ ๋ณดํ†ต (XSS ์ทจ์•ฝ) โญโญโญโญ ์ข‹์Œ
์„œ๋ฒ„ ๋ถ€ํ•˜ โญโญโญโญโญ ๋งค์šฐ ๋‚ฎ์Œ โญโญโญ ๋ณดํ†ต
์ฆ‰์‹œ ๋ฌดํšจํ™” โญโญ ์–ด๋ ค์›€ โญโญโญโญโญ ์‰ฌ์›€
๋ชจ๋ฐ”์ผ ์•ฑ โญโญโญโญโญ ๋งค์šฐ ์ ํ•ฉ โญโญโญ ๋ณดํ†ต
๊ตฌํ˜„ ๋ณต์žก๋„ โญโญโญ ๋ณดํ†ต โญโญ ์‰ฌ์›€
๐ŸŽฏ ์–ธ์ œ JWT๋ฅผ ์‚ฌ์šฉํ• ๊นŒ? โ€ข ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜
โ€ข ๋ชจ๋ฐ”์ผ ์•ฑ ๋ฐฑ์—”๋“œ
โ€ข ์„œ๋ฒ„ ํ™•์žฅ์ด ์ž์ฃผ ํ•„์š”ํ•œ ๊ฒฝ์šฐ
โ€ข API ์„œ๋ฒ„ (RESTful API)
โ€ข ์„œ๋ฒ„๋ฆฌ์Šค ํ™˜๊ฒฝ

๐ŸŽฏ ์–ธ์ œ ์„ธ์…˜์„ ์‚ฌ์šฉํ• ๊นŒ?

โ€ข ์ „ํ†ต์ ์ธ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜
โ€ข ์ฆ‰์‹œ ๋กœ๊ทธ์•„์›ƒ์ด ์ค‘์š”ํ•œ ๊ฒฝ์šฐ
โ€ข ์„œ๋ฒ„ ๋ฆฌ์†Œ์Šค๊ฐ€ ์ถฉ๋ถ„ํ•œ ๊ฒฝ์šฐ
โ€ข ๋ณด์•ˆ์ด ์ตœ์šฐ์„ ์ธ ๊ฒฝ์šฐ

๐Ÿš€ ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ

JWT ์ธ์ฆ ์‹œ์Šคํ…œ์˜ ์„ฑ๋Šฅ์„ ๋” ๋Œ์–ด์˜ฌ๋ฆฌ๋Š” ๋ฐฉ๋ฒ•๋“ค์„ ์•Œ์•„๋ณผ๊ฒŒ์š”! ์‹ค๋ฌด์—์„œ ์ •๋ง ์œ ์šฉํ•œ ํŒ๋“ค์ด์—์š”. โšก

1. Redis๋ฅผ ํ™œ์šฉํ•œ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ ๊ด€๋ฆฌ

๋กœ๊ทธ์•„์›ƒํ•œ ํ† ํฐ์„ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ํ•ด์„œ ์ฆ‰์‹œ ๋ฌดํšจํ™”ํ•  ์ˆ˜ ์žˆ์–ด์š”. Redis๋Š” ๋น ๋ฅด๊ณ  ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์„ค์ •์ด ์‰ฌ์›Œ์„œ ์™„๋ฒฝํ•ด์š”!

const redis = require('redis');
const client = redis.createClient();

// ํ† ํฐ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€
async function blacklistToken(token, expiresIn) {
  await client.setEx(`blacklist:${token}`, expiresIn, 'true');
}

// ํ† ํฐ์ด ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ์— ์žˆ๋Š”์ง€ ํ™•์ธ
async function isTokenBlacklisted(token) {
  const result = await client.get(`blacklist:${token}`);
  return result !== null;
}
2. ํ† ํฐ ํŽ˜์ด๋กœ๋“œ ์ตœ์†Œํ™”

JWT๋Š” Base64๋กœ ์ธ์ฝ”๋”ฉ๋˜์–ด ๋งค ์š”์ฒญ๋งˆ๋‹ค ์ „์†ก๋˜๋ฏ€๋กœ, ํฌ๊ธฐ๊ฐ€ ์ž‘์„์ˆ˜๋ก ์ข‹์•„์š”. ํ•„์š”ํ•œ ์ตœ์†Œํ•œ์˜ ์ •๋ณด๋งŒ ๋„ฃ์œผ์„ธ์š”!

โŒ ๋‚˜์œ ์˜ˆ:
{
  userId: "12345",
  email: "user@example.com",
  name: "ํ™๊ธธ๋™",
  address: "์„œ์šธ์‹œ...",
  phoneNumber: "010-1234-5678",
  preferences: {...}
}
โœ… ์ข‹์€ ์˜ˆ:
{
  userId: "12345",
  role: "user"
}
3. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ์ตœ์ ํ™”

๋งค ์š”์ฒญ๋งˆ๋‹ค ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๋ฉด DB ๋ถ€ํ•˜๊ฐ€ ์ปค์ ธ์š”. ์บ์‹ฑ์„ ํ™œ์šฉํ•˜์„ธ์š”!

// ๊ฐ„๋‹จํ•œ ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ ์˜ˆ์ œ
const userCache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5๋ถ„

async function getUserById(userId) {
  // ์บ์‹œ ํ™•์ธ
  const cached = userCache.get(userId);
  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
    return cached.user;
  }
  
  // DB ์กฐํšŒ
  const user = await User.findById(userId);
  
  // ์บ์‹œ ์ €์žฅ
  userCache.set(userId, {
    user,
    timestamp: Date.now()
  });
  
  return user;
}

๐Ÿงช ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑํ•˜๊ธฐ

์ข‹์€ ์ฝ”๋“œ๋Š” ํ…Œ์ŠคํŠธ๊ฐ€ ์žˆ๋Š” ์ฝ”๋“œ์ฃ ! JWT ์ธ์ฆ ์‹œ์Šคํ…œ์„ ํ…Œ์ŠคํŠธํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณผ๊ฒŒ์š”. Jest์™€ Supertest๋ฅผ ์‚ฌ์šฉํ•  ๊ฑฐ์˜ˆ์š”. ๐Ÿงช

// __tests__/auth.test.js
const request = require('supertest');
const app = require('../server');
const User = require('../models/User');
const mongoose = require('mongoose');

describe('์ธ์ฆ API ํ…Œ์ŠคํŠธ', () => {
  let accessToken;
  let refreshToken;
  
  // ํ…Œ์ŠคํŠธ ์ „ DB ์—ฐ๊ฒฐ
  beforeAll(async () => {
    await mongoose.connect(process.env.TEST_MONGODB_URI);
  });
  
  // ๊ฐ ํ…Œ์ŠคํŠธ ํ›„ DB ์ •๋ฆฌ
  afterEach(async () => {
    await User.deleteMany({});
  });
  
  // ํ…Œ์ŠคํŠธ ํ›„ DB ์—ฐ๊ฒฐ ์ข…๋ฃŒ
  afterAll(async () => {
    await mongoose.connection.close();
  });
  
  describe('POST /api/auth/register', () => {
    it('ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณต', async () => {
      const response = await request(app)
        .post('/api/auth/register')
        .send({
          email: 'test@example.com',
          password: 'password123',
          name: 'ํ…Œ์ŠคํŠธ์œ ์ €'
        });
      
      expect(response.status).toBe(201);
      expect(response.body.success).toBe(true);
      expect(response.body.data).toHaveProperty('accessToken');
      expect(response.body.data).toHaveProperty('refreshToken');
      expect(response.body.data.user.email).toBe('test@example.com');
    });
    
    it('์ค‘๋ณต ์ด๋ฉ”์ผ๋กœ ํšŒ์›๊ฐ€์ž… ์‹คํŒจ', async () => {
      // ์ฒซ ๋ฒˆ์งธ ํšŒ์›๊ฐ€์ž…
      await request(app)
        .post('/api/auth/register')
        .send({
          email: 'test@example.com',
          password: 'password123',
          name: 'ํ…Œ์ŠคํŠธ์œ ์ €'
        });
      
      // ๊ฐ™์€ ์ด๋ฉ”์ผ๋กœ ๋‘ ๋ฒˆ์งธ ์‹œ๋„
      const response = await request(app)
        .post('/api/auth/register')
        .send({
          email: 'test@example.com',
          password: 'password456',
          name: 'ํ…Œ์ŠคํŠธ์œ ์ €2'
        });
      
      expect(response.status).toBe(409);
      expect(response.body.success).toBe(false);
    });
  });
  
  describe('POST /api/auth/login', () => {
    beforeEach(async () => {
      // ํ…Œ์ŠคํŠธ์šฉ ์‚ฌ์šฉ์ž ์ƒ์„ฑ
      await request(app)
        .post('/api/auth/register')
        .send({
          email: 'test@example.com',
          password: 'password123',
          name: 'ํ…Œ์ŠคํŠธ์œ ์ €'
        });
    });
    
    it('๋กœ๊ทธ์ธ ์„ฑ๊ณต', async () => {
      const response = await request(app)
        .post('/api/auth/login')
        .send({
          email: 'test@example.com',
          password: 'password123'
        });
      
      expect(response.status).toBe(200);
      expect(response.body.success).toBe(true);
      expect(response.body.data).toHaveProperty('accessToken');
      
      accessToken = response.body.data.accessToken;
      refreshToken = response.body.data.refreshToken;
    });
    
    it('์ž˜๋ชป๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ๋กœ๊ทธ์ธ ์‹คํŒจ', async () => {
      const response = await request(app)
        .post('/api/auth/login')
        .send({
          email: 'test@example.com',
          password: 'wrongpassword'
        });
      
      expect(response.status).toBe(401);
      expect(response.body.success).toBe(false);
    });
  });
  
  describe('GET /api/profile', () => {
    beforeEach(async () => {
      // ๋กœ๊ทธ์ธํ•ด์„œ ํ† ํฐ ๋ฐ›๊ธฐ
      const response = await request(app)
        .post('/api/auth/register')
        .send({
          email: 'test@example.com',
          password: 'password123',
          name: 'ํ…Œ์ŠคํŠธ์œ ์ €'
        });
      
      accessToken = response.body.data.accessToken;
    });
    
    it('์ธ์ฆ๋œ ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์กฐํšŒ ์„ฑ๊ณต', async () => {
      const response = await request(app)
        .get('/api/profile')
        .set('Authorization', `Bearer ${accessToken}`);
      
      expect(response.status).toBe(200);
      expect(response.body.success).toBe(true);
      expect(response.body.data.user.email).toBe('test@example.com');
    });
    
    it('ํ† ํฐ ์—†์ด ํ”„๋กœํ•„ ์กฐํšŒ ์‹คํŒจ', async () => {
      const response = await request(app)
        .get('/api/profile');
      
      expect(response.status).toBe(401);
      expect(response.body.success).toBe(false);
    });
  });
});

ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๋ ค๋ฉด:


npm install --save-dev jest supertest
npm test

๐ŸŒŸ ์‹ค์ „ ํ™œ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค

์ด์ œ ๋ฐฐ์šด ๋‚ด์šฉ์„ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— ์–ด๋–ป๊ฒŒ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์•Œ์•„๋ณผ๊ฒŒ์š”! ๋‹ค์–‘ํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ค€๋น„ํ–ˆ์–ด์š”. ๐ŸŽฏ

๐Ÿ“ฑ ์‹œ๋‚˜๋ฆฌ์˜ค 1: ๋ชจ๋ฐ”์ผ ์•ฑ + ์›น ๋™์‹œ ์ง€์› ๊ฐ™์€ ๊ณ„์ •์œผ๋กœ ์—ฌ๋Ÿฌ ๊ธฐ๊ธฐ์—์„œ ๋กœ๊ทธ์ธํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ•˜๊ธฐ
// ๋””๋ฐ”์ด์Šค๋ณ„ Refresh Token ๊ด€๋ฆฌ
const userSchema = new mongoose.Schema({
  email: String,
  password: String,
  name: String,
  devices: [{
    deviceId: String,
    deviceName: String,
    refreshToken: String,
    lastLogin: Date
  }]
});

// ๋กœ๊ทธ์ธ ์‹œ ๋””๋ฐ”์ด์Šค ์ •๋ณด ์ €์žฅ
exports.login = async (req, res) => {
  const { email, password, deviceId, deviceName } = req.body;
  
  // ... ์ธ์ฆ ๋กœ์ง ...
  
  const accessToken = generateAccessToken(user._id);
  const refreshToken = generateRefreshToken(user._id);
  
  // ๋””๋ฐ”์ด์Šค ์ •๋ณด ์—…๋ฐ์ดํŠธ
  const deviceIndex = user.devices.findIndex(d => d.deviceId === deviceId);
  
  if (deviceIndex >= 0) {
    user.devices[deviceIndex].refreshToken = refreshToken;
    user.devices[deviceIndex].lastLogin = new Date();
  } else {
    user.devices.push({
      deviceId,
      deviceName,
      refreshToken,
      lastLogin: new Date()
    });
  }
  
  await user.save();
  
  res.json({ accessToken, refreshToken });
};
๐Ÿ”„ ์‹œ๋‚˜๋ฆฌ์˜ค 2: ์†Œ์…œ ๋กœ๊ทธ์ธ ํ†ตํ•ฉ Google, Facebook ๋“ฑ ์†Œ์…œ ๋กœ๊ทธ์ธ๊ณผ JWT ์—ฐ๋™ํ•˜๊ธฐ
// Google OAuth ๋กœ๊ทธ์ธ ์˜ˆ์ œ
const { OAuth2Client } = require('google-auth-library');
const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);

exports.googleLogin = async (req, res) => {
  try {
    const { token } = req.body;
    
    // Google ํ† ํฐ ๊ฒ€์ฆ
    const ticket = await client.verifyIdToken({
      idToken: token,
      audience: process.env.GOOGLE_CLIENT_ID
    });
    
    const payload = ticket.getPayload();
    const { email, name, picture } = payload;
    
    // ์‚ฌ์šฉ์ž ์ฐพ๊ธฐ ๋˜๋Š” ์ƒ์„ฑ
    let user = await User.findOne({ email });
    
    if (!user) {
      user = await User.create({
        email,
        name,
        profilePicture: picture,
        authProvider: 'google'
      });
    }
    
    // JWT ํ† ํฐ ์ƒ์„ฑ
    const accessToken = generateAccessToken(user._id);
    const refreshToken = generateRefreshToken(user._id);
    
    res.json({
      success: true,
      data: { user, accessToken, refreshToken }
    });
    
  } catch (error) {
    res.status(401).json({
      success: false,
      message: 'Google ๋กœ๊ทธ์ธ ์‹คํŒจ'
    });
  }
};
๐Ÿ‘ฅ ์‹œ๋‚˜๋ฆฌ์˜ค 3: ๊ถŒํ•œ ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ์ œ์–ด (RBAC) ์‚ฌ์šฉ์ž ์—ญํ• ์— ๋”ฐ๋ผ API ์ ‘๊ทผ ์ œ์–ดํ•˜๊ธฐ
// ์—ญํ•  ๊ธฐ๋ฐ˜ ๋ฏธ๋“ค์›จ์–ด
exports.authorize = (...roles) => {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({
        success: false,
        message: '์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค'
      });
    }
    
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({
        success: false,
        message: '๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค'
      });
    }
    
    next();
  };
};

// ์‚ฌ์šฉ ์˜ˆ์ œ
router.delete('/users/:id', 
  authenticate, 
  authorize('admin'), 
  deleteUser
);

router.post('/posts', 
  authenticate, 
  authorize('user', 'admin'), 
  createPost
);

๐Ÿ“š ์ถ”๊ฐ€ ํ•™์Šต ์ž๋ฃŒ

JWT ์ธ์ฆ์„ ๋” ๊นŠ์ด ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์ด๋Ÿฐ ๊ฒƒ๋“ค์„ ์•Œ์•„๋ณด๋ฉด ์ข‹์•„์š”! ๊ณ„์† ๋ฐœ์ „ํ•˜๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ๋˜๋ ค๋ฉด ํ•™์Šต์ด ์ค‘์š”ํ•˜์ฃ . ๐Ÿ“–

๐ŸŽ“ ์ถ”์ฒœ ํ•™์Šต ์ฃผ์ œ 1. OAuth 2.0๊ณผ OpenID Connect
๋” ๋ณต์žกํ•œ ์ธ์ฆ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์œ„ํ•œ ํ‘œ์ค€ ํ”„๋กœํ† ์ฝœ์ด์—์š”.

2. Passport.js
Node.js์—์„œ ๊ฐ€์žฅ ์ธ๊ธฐ ์žˆ๋Š” ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด์˜ˆ์š”. ๋‹ค์–‘ํ•œ ์ „๋žต์„ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด์š”.

3. JWT ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€
OWASP ๊ฐ€์ด๋“œ๋ผ์ธ์„ ์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์•„์š”.

4. ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์ธ์ฆ
์—ฌ๋Ÿฌ ์„œ๋น„์Šค ๊ฐ„ ์ธ์ฆ์„ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ• ์ง€ ์•Œ์•„๋ณด์„ธ์š”.

5. WebAuthn
๋น„๋ฐ€๋ฒˆํ˜ธ ์—†๋Š” ์ธ์ฆ์˜ ๋ฏธ๋ž˜์˜ˆ์š”!

ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์‹ค๋ ฅ์„ ๋” ํ‚ค์šฐ๊ณ  ์‹ถ๊ฑฐ๋‚˜ ์‹ค์ „ ํ”„๋กœ์ ํŠธ ๊ฒฝํ—˜์ด ํ•„์š”ํ•˜๋‹ค๋ฉด, ์žฌ๋Šฅ๋„ท์—์„œ ๋‹ค์–‘ํ•œ ๊ฐœ๋ฐœ ๋ฉ˜ํ† ๋“ค์„ ๋งŒ๋‚˜๋ณผ ์ˆ˜ ์žˆ์–ด์š”. ์‹ค๋ฌด ๊ฒฝํ—˜์ด ํ’๋ถ€ํ•œ ์ „๋ฌธ๊ฐ€๋“ค์ด ์—ฌ๋Ÿฌ๋ถ„์˜ ์„ฑ์žฅ์„ ๋„์™€์ค„ ๊ฑฐ์˜ˆ์š”! ๐Ÿš€

๐ŸŽฏ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€! ์—ฌ๊ธฐ๊นŒ์ง€ ์˜ค๋А๋ผ ์ •๋ง ์ˆ˜๊ณ  ๋งŽ์œผ์…จ์–ด์š”! ๐ŸŽ‰ JWT ์ธ์ฆ ์‹œ์Šคํ…œ์„ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ๊ตฌํ˜„ํ•ด๋ดค๋Š”๋ฐ, ์–ด๋– ์…จ๋‚˜์š”?


์ฒ˜์Œ์—๋Š” ๋ณต์žกํ•ด ๋ณด์˜€์ง€๋งŒ ํ•˜๋‚˜์”ฉ ๋œฏ์–ด๋ณด๋‹ˆ ๊ทธ๋ ‡๊ฒŒ ์–ด๋ ต์ง€ ์•Š์ฃ ? ํ•ต์‹ฌ์€ ์ด๊ฑฐ์˜ˆ์š”:


1๏ธโƒฃ ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธํ•˜๋ฉด JWT ํ† ํฐ์„ ๋ฐœ๊ธ‰
2๏ธโƒฃ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ํ† ํฐ์„ ์ €์žฅํ•˜๊ณ  API ์š”์ฒญ ์‹œ ํ•จ๊ป˜ ์ „์†ก
3๏ธโƒฃ ์„œ๋ฒ„๊ฐ€ ํ† ํฐ์„ ๊ฒ€์ฆํ•˜๊ณ  ์š”์ฒญ ์ฒ˜๋ฆฌ
4๏ธโƒฃ ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜๋ฉด Refresh Token์œผ๋กœ ๊ฐฑ์‹ 

์ด ๊ธ€์—์„œ ๋‹ค๋ฃฌ ๋‚ด์šฉ๋“ค์„ ๋ฐ”ํƒ•์œผ๋กœ ์—ฌ๋Ÿฌ๋ถ„๋งŒ์˜ ๋ฉ‹์ง„ ์ธ์ฆ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”! ์‹ค์ œ๋กœ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด์„œ ๋ถ€๋”ชํžˆ๋Š” ๋ฌธ์ œ๋“ค์„ ํ•ด๊ฒฐํ•˜๋‹ค ๋ณด๋ฉด ์‹ค๋ ฅ์ด ์‘ฅ์‘ฅ ๋Š˜ ๊ฑฐ์˜ˆ์š”. ๐Ÿ’ช


๊ธฐ์–ตํ•˜์„ธ์š”:


โœจ ๊ฐœ๋ฐœ์ž๋กœ์„œ ์„ฑ์žฅํ•˜๋Š” ๋ฒ• โ€ข ์ง์ ‘ ๋งŒ๋“ค์–ด๋ณด๊ธฐ: ํŠœํ† ๋ฆฌ์–ผ๋งŒ ๋”ฐ๋ผํ•˜์ง€ ๋ง๊ณ  ์ง์ ‘ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”
โ€ข ์—๋Ÿฌ๋ฅผ ๋‘๋ ค์›Œํ•˜์ง€ ์•Š๊ธฐ: ์—๋Ÿฌ๋Š” ๋ฐฐ์›€์˜ ๊ธฐํšŒ์˜ˆ์š”
โ€ข ์ฝ”๋“œ ๋ฆฌ๋ทฐ ๋ฐ›๊ธฐ: ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž์˜ ํ”ผ๋“œ๋ฐฑ์€ ์ •๋ง ์†Œ์ค‘ํ•ด์š”
โ€ข ๋ณด์•ˆ์„ ํ•ญ์ƒ ์—ผ๋‘์— ๋‘๊ธฐ: ํŠนํžˆ ์ธ์ฆ ์‹œ์Šคํ…œ์€ ๋ณด์•ˆ์ด ์ƒ๋ช…์ด์—์š”
โ€ข ๊ณ„์† ํ•™์Šตํ•˜๊ธฐ: ๊ธฐ์ˆ ์€ ๊ณ„์† ๋ฐœ์ „ํ•˜๋‹ˆ๊นŒ์š”!

์ด ๊ธ€์ด ์—ฌ๋Ÿฌ๋ถ„์˜ ๊ฐœ๋ฐœ ์—ฌ์ •์— ๋„์›€์ด ๋˜์—ˆ๊ธฐ๋ฅผ ๋ฐ”๋ผ์š”. ๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๊ฑฐ๋‚˜ ๋” ๋ฐฐ์šฐ๊ณ  ์‹ถ์€ ๊ฒŒ ์žˆ๋‹ค๋ฉด ์ฃผ์ €ํ•˜์ง€ ๋ง๊ณ  ์ฐพ์•„๋ณด๊ณ , ๋ฌผ์–ด๋ณด๊ณ , ์‹œ๋„ํ•ด๋ณด์„ธ์š”! ๐ŸŒŸ


ํ•ดํ”ผ ์ฝ”๋”ฉ! ๐Ÿš€โœจ

๐ŸŽ“ ๋” ๋งŽ์€ ๊ฐœ๋ฐœ ์ง€์‹์ด ํ•„์š”ํ•˜์‹ ๊ฐ€์š”?

์žฌ๋Šฅ๋„ท์—์„œ ๋‹ค์–‘ํ•œ ๊ฐœ๋ฐœ ์ „๋ฌธ๊ฐ€๋“ค์˜ ์žฌ๋Šฅ์„ ๋งŒ๋‚˜๋ณด์„ธ์š”!

ํ”„๋กœ๊ทธ๋ž˜๋ฐ, ์›น ๊ฐœ๋ฐœ, ์•ฑ ๊ฐœ๋ฐœ ๋“ฑ ๋‹ค์–‘ํ•œ ๋ถ„์•ผ์˜ ๋ฉ˜ํ† ๋ง๊ณผ ๊ฐ•์˜๊ฐ€ ์ค€๋น„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.