Node.JS 공부하기 - 006

변우영·2024년 9월 10일

NodeJS

목록 보기
6/11

<최종 목표>

  • 회원가입, 로그인, 캐릭터 생성,조회,삭제, 아이템 관리(생성,조회,수정,삭제)
    API 구현
  • 미들웨어 구현(사용자 인증, 에러, 로거)

<오늘 목표>

  • 회원가입, 로그인, 캐릭터 생성
  • 미들웨어 구현(사용자 인증, 에러, 로거)

프로젝트를 열고 터미널에 yarn init -y.
package.json 에서 "type" = "module" 추가.

yarn add @prisma/client bcrypt dotenv express jsonwebtoken prisma winston 추가.

.prettiercc 파일 생성하고 아래와 같이 추가.

{
    "semi": true,
    "singleQuote": true,
    "tabWidth": 2,
    "trailingComma": "es5",
    "printWidth": 80,
    "bracketSpacing": true,
    "arrowParens": "always"
}  

파일 역할별 구분

src/
│
├── app.js                           # 애플리케이션 진입점
├── controllers/                     # 컨트롤러 파일
│   ├── authController.js            # 인증 관련 컨트롤러
│   └── characterController.js       # 캐릭터 관련 컨트롤러
│
├── middlewares/                     # 미들웨어 파일
│   ├── authenticateJWT.js           # JWT 인증 미들웨어
│   ├── errorHandler.js              # 에러 핸들링 미들웨어
│   └── logger.js                    # 로깅 미들웨어
│
├── routes/                          # 라우트 정의 파일
│   ├── authRoutes.js                # 인증 관련 라우트
│   └── characterRoutes.js           # 캐릭터 관련 라우트
│
└── utils/                           # 유틸리티 파일 및 폴더
    └── prisma/                      # Prisma ORM 관련 파일
        └── index.js                 # Prisma 인스턴스 초기화 및 설정

utils/prisma/index.js

  • prisma 인스턴스 생성
import { PrismaClient } from '@prisma/client';

export  const prisma = new PrismaClient({
  // Prisma를 이용해 데이터베이스를 접근할 때, SQL을 출력해줍니다.
  log: ['query', 'info', 'warn', 'error'],

  // 에러 메시지를 평문이 아닌, 개발자가 읽기 쉬운 형태로 출력해줍니다.
  errorFormat: 'pretty',
});

app.js

  • 애플리케이션 초기화, 라우팅, 미들웨어 설정, 서버 시작
import express from 'express';
import dotenv from 'dotenv';
import { authenticateJWT } from './middlewares/authenticateJWT.js';
import { errorHandler } from './middlewares/errorHandler.js';
import { logger } from './middlewares/logger.js';

import authRoutes from './routes/authRoutes.js'; // 인증 라우트
import characterRoutes from './routes/characterRoutes.js'; // 캐릭터 라우트

dotenv.config(); // 환경 변수 로드

const app = express();

app.use(express.json());

// 로그 미들웨어 적용
app.use(logger);

// 인증 관련 라우트
app.use('/auth', authRoutes);

// 캐릭터 생성 라우트 (JWT 인증 필요)
app.use('/character', characterRoutes);

// JWT 인증이 필요한 보호된 경로
app.get('/protected', authenticateJWT, (req, res) => {
  res.json({ message: 'This is a protected route', user: req.locals.user });
});

// 에러 처리 미들웨어
app.use(errorHandler);

// 서버 시작
app.listen(3000, () => {
  console.log('포트 3000 서버가 시작');
});

routes

/authRoutes.js

  • 사용자 관련 라우트
import express from 'express';
import { register, login } from '../controllers/authController.js';

const router = express.Router();

// 회원가입 라우트
router.post('/register', register);

// 로그인 라우트
router.post('/login', login);

export default router;

/characterRoutes.js

  • 캐릭터 관련 라우트
import express from 'express';
import { createCharacter } from '../controllers/characterController.js';
import { authenticateJWT } from '../middlewares/authenticateJWT.js';

const router = express.Router();

// 캐릭터 생성 라우트 (JWT 인증 필요)
router.post('/create', authenticateJWT, createCharacter);

export default router;

controllers

authController.js

  • 회원가입 및 로그인 처리 구현
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { prisma } from '../utils/prisma/index.js'; 

// 정규식으로 영어 소문자와 숫자만 허용하는 패턴
const usernameRegex = /^[a-z0-9]+$/;

// 회원가입 처리
export const register = async (req, res, next) => {
  const { userId, password, confirmPassword } = req.body;

  try {
    // 아이디 중복 확인
    const existingUsername = await prisma.account.findUnique({
      where: { userId },
    });

    if (existingUsername) {
      return res.status(400).json({ error: '해당 아이디가 이미 존재합니다' });
    }

    // 아이디 형식 검증 (영어 소문자 + 숫자 조합인지 확인)
    if (!usernameRegex.test(userId)) {
      return res.status(400).json({ error: '아이디는 소문자와 숫자 조합으로만 만들 수 있습니다' });
    }

    // 비밀번호 길이 확인 (최소 6자 이상)
    if (password.length < 6) {
      return res.status(400).json({ error: '비밀번호는 최소 6자리 이상으로 설정해주세요' });
    }

    // 비밀번호와 비밀번호 확인이 일치하는지 확인
    if (password !== confirmPassword) {
      return res.status(400).json({ error: '비밀번호가 서로 일치하지 않습니다' });
    }

    // 비밀번호 암호화
    const hashedPassword = await bcrypt.hash(password, 10);

    // 새로운 사용자 생성
    const newUser = await prisma.account.create({
      data: {
        userId,
        password: hashedPassword,
      },
    });

    res.status(201).json({ message: '회원 가입에 성공하였습니다!', user: { id: newUser.id, userId: newUser.userId } });
  } catch (error) {
    next(error);
  }
};

export const login = async (req, res, next) => {
    const { userId, password } = req.body;
  
    try {
      // 아이디로 사용자 찾기
      const user = await prisma.account.findUnique({
        where: { userId },
      });
  
      if (!user) {
        return res.status(401).json({ error: '아이디가 존재하지 않거나 틀렸습니다' });
      }
  
      // 비밀번호 비교
      const isPasswordValid = await bcrypt.compare(password, user.password);
      if (!isPasswordValid) {
        return res.status(401).json({ error: '비밀번호가 틀렸습니다' });
      }
  
      // JWT 발급
      const token = jwt.sign({ id: user.accountId, userId: user.userId }, process.env.JWT_SECRET, { expiresIn: '1h' });
  
      res.json({ message: '로그인 성공!!', token });
    } catch (error) {
      next(error);
    }
  };

characterController.js

  • 캐릭터 생성 구현
import { prisma } from '../utils/prisma/index.js';

// 캐릭터 생성 API
export const createCharacter = async (req, res, next) => {
  const { name } = req.body;
  const userId = req.locals.user?.userId; // JWT에서 추출한 userId 확인

  // userId가 없으면 오류 반환
  if (!userId) {
    return res.status(400).json({ error: '사용자 정보가 존재하지 않습니다' });
  }

  try {
    // userId로 계정 조회
    const userAccount = await prisma.account.findUnique({
      where: { userId }, // userId가 Prisma 모델에서 고유해야 합니다.
    });

    if (!userAccount) {
      return res.status(404).json({ error: '계정이 존재하지 않습니다' });
    }

    // 이미 존재하는 캐릭터명인지 확인
    const existingCharacter = await prisma.character.findUnique({
      where: { name },
    });

    if (existingCharacter) {
      return res.status(400).json({ error: '캐릭터 이름이 이미 존재합니다' });
    }

    // 캐릭터 생성
    const newCharacter = await prisma.character.create({
      data: {
        name,
        accountId: userAccount.accountId, // 계정과 연결된 캐릭터 생성
      },
    });

    res.status(201).json({
      message: '캐릭터 생성 완료',
      characterId: newCharacter.characterId,
    });
  } catch (error) {
    next(error);
  }
};

middlewares

authenticateJWT.js

  • JWT 사용자 인증 미들웨어
import jwt from 'jsonwebtoken';

/**
 * JWT 인증 미들웨어
 */
export const authenticateJWT = (req, res, next) => {
  const authHeader = req.headers.authorization;

  if (!authHeader) {
    const error = new Error('Authorization header is missing');
    error.statusCode = 401;
    return next(error); // next()를 통해 에러 미들웨어로 전달
  }

  const tokenParts = authHeader.split(' ');

  if (tokenParts[0] !== 'Bearer' || tokenParts.length !== 2) {
    const error = new Error('Authorization header must be in the format: Bearer <token>');
    error.statusCode = 400;
    return next(error);
  }

  const token = tokenParts[1];

  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) {
      if (err.name === 'TokenExpiredError') {
        const error = new Error('JWT has expired');
        error.statusCode = 401;
        return next(error);
      } else {
        const error = new Error('Invalid JWT token');
        error.statusCode = 403;
        return next(error);
      }
    }

    req.locals = req.locals || {};
    req.locals.user = user;
    next();
  });
};

errorHandler.js

  • 에러 처리 미들웨어
export const errorHandler = (err, req, res, next) => {
    console.error(err); // 서버에서 에러 로깅
  
    const statusCode = err.statusCode || 500; // 기본값은 500 (서버 오류)
    const message = err.message || 'Internal Server Error';
  
    res.status(statusCode).json({
      error: message,
      ...(process.env.NODE_ENV === 'development' && { stack: err.stack }), // 개발 환경에서는 스택 트레이스를 함께 반환
    });
  };  

logger.js

  • 현재 실행 상태 로깅 시스템
import winston from 'winston';

// Winston 로그 설정
const loggerInstance = winston.createLogger({
  level: 'info', // 로그 레벨 설정 (info, error 등)
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.printf(({ timestamp, level, message }) => {
      return `[${timestamp}] ${level.toUpperCase()}: ${message}`;
    })
  ),
  transports: [
    new winston.transports.File({ filename: 'logs/combined.log' }), // 로그 파일에 저장
    new winston.transports.File({ filename: 'logs/error.log', level: 'error' }), // 에러 로그를 별도 파일에 저장
    new winston.transports.Console() // 콘솔에도 로그 출력
  ],
});

// 로그 처리 미들웨어
export const logger = (req, res, next) => {
  const start = Date.now();

  res.on('finish', () => {
    const duration = Date.now() - start;
    const logMessage = `${req.method} ${req.originalUrl} ${res.statusCode} - ${duration}ms`;

    // 요청 성공 로그는 info 레벨로 기록
    if (res.statusCode >= 400) {
      // 에러 상태코드인 경우는 error 레벨로 기록
      loggerInstance.error(logMessage);
    } else {
      loggerInstance.info(logMessage);
    }
  });

  next();
};

오늘 소스코드 구현은 여기까지입니다!

profile
개발자로 한걸음!

0개의 댓글