회원가입 로직 분석

henry·2025년 1월 19일

타입 스크립트로 작성된 회원가입 로직의 흐름을 상세하게 이해하기 위해 작성한 글

흐름

  1. 서버 설정 (server.ts)
        ⬇️
  2. MongoDB 연결 (db.ts)
        ⬇️
  3. 라우터 설정 (userRoutes.ts)
        ⬇️
  4. 유효성 검사 (userValidator.ts)
        ⬇️
  5. 회원가입 처리 (userController.ts)
        ⬇️
  6. 회원가입 로직 (userService.ts)
        ⬇️
  7. 데이터베이스 작업 (userRepository.ts)


📌 1. 서버 설정 (server.ts)


import express, { Request, Response } from 'express';
import dotenv from 'dotenv';
import connectDB from './config/db';
import userRoutes from './routes/userRoutes';

dotenv.config();

const app = express();
const PORT = process.env.PORT || 5000;

// Middleware
app.use(express.json());

// Connect to MongoDB
const initializeServer = async () => {
  try {
    await connectDB();
    console.log('Database connected. Starting server');

    app.get('/', (req: Request, res: Response) => {
      res.send('Hello, TypeScript with Express!');
    });

    app.listen(PORT, () => {
      console.log(`Server is running on http://localhost:${PORT}`);
    });
  } catch (error) {
    console.error('Failed to start server : ', error);
  }
};

initializeServer();

app.use('/api/users', userRoutes);

코드상세 설명


1. dotenv.config()

  • .env 파일에서 환경 변수를 불러옵니다.
  • 민감한 정보(예: 데이터베이스 연결 주소)를 코드에 직접 노출하지 않고 환경 변수로 관리합니다.

2. express.json()

  • 클라이언트가 보낸 요청 데이터를 JSON 형식으로 파싱할 수 있게 설정합니다.
  • 예를 들어, 클라이언트가 { "username": "test", "password": "123456" } 데이터를 보냈을 때, 서버에서 이를 읽어올 수 있게 합니다.
    • express.json()을 사용하는 경우
      • 클라이언트가 전송한 JSON 데이터를 Express가 자동으로 파싱하여 req.body로 제공합니다.
    • express.json()을 사용하지 않는 경우
      • 데이터 전송은 가능하지만, 서버가 데이터를 자동으로 파싱하지 못합니다.
      • req.body는 undefined가 됩니다.

3. initializeServer()

  • 비동기로 데이터베이스 연결을 시도합니다.

    • 성공 시 서버를 실행하고, 실패 시 에러를 출력합니다.
  • 타입: async 함수는 항상 Promise를 반환합니다.

    • 성공 시: Promise (별도의 값을 반환하지 않음).

    • 실패 시: 에러를 던집니다.

      • async 함수는 항상 Promise 객체를 반환합니다.
        • 반환값이 있으면 그 값을 Promiseresolve 값으로 감싸서 반환합니다.
        • 반환값이 없으면 Promise를 반환합니다.
        • 에러가 발생하면 Promisereject 상태가 됩니다.
  • 내부 함수:

    • app.listen(PORT, ...)
      • 서버를 지정된 포트에서 실행합니다.
      • PORT는 숫자(5000) 또는 문자열('5000')이 가능합니다.
    • app.get('/', ...)
      • 브라우저가 / 경로로 요청하면, 간단한 "Hello" 메시지를 응답으로 보냅니다.

4. app.use('/api/users', userRoutes)

  • /api/users 경로로 들어오는 요청은 userRoutes에 정의된 라우터에서 처리합니다.
  • 회원가입, 로그인 등 사용자 관련 요청을 관리하는 창구를 설정한 것입니다.


📌 2. MongoDB 연결 (db.ts)

const mongoose = require('mongoose');

const connectDB = async () => {
  try {
    await mongoose.connect(process.env.MONGO_URI || '', {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    console.log('MongoDB connected successfully');
  } catch (error) {
    console.error('MongoDB connection failed:', error);
    process.exit(1);
  }
};

export default connectDB;

코드 상세 설명


mongoose.connect()

  • MongoDB 데이터베이스와 연결합니다.

  • MONGO_URI는 데이터베이스의 주소를 환경 변수로 지정한 값입니다.

  • useNewUrlParser: 새로운 URL 구문 해석 방식을 사용합니다.

    • MongoDB 드라이버 3.6 이전에는 연결 문자열의 해석 로직이 제한적이었습니다

    • 특수 문자 문제:

      • 연결 문자열에 특수 문자(예: @, : 등)가 포함된 경우, 올바르게 해석하지 못하는 경우가 있었습니다.
      • 비밀번호에 :가 포함된 경우 mongodb://user:pass:word@localhost:27017를 제대로 인식하지 못함.
    • useNewUrlParser:true

      • MongoDB 드라이버가 더 유연하고 표준에 맞는 방식으로 연결 문자열을 파싱합니다.
      • 특수 문자(예: @, :, /)를 올바르게 인식합니다.
  • useUnifiedTopology: 더 안정적이고 최신 연결 관리 방식을 사용합니다.

에러 처리

  • 데이터베이스 연결이 실패하면 에러를 출력하고, 프로세스를 종료(process.exit(1))하여 서버 실행을 중단합니다.


📌 3. 라우터 설정 (userRoutes.ts)

import express from 'express';
import { register } from '../controllers/userController';
import { userValidationRules, validate } from '../validators/userValidator';

const router = express.Router();

router.post('/register', [...userValidationRules, validate], register);

export default router;

코드 상세 설명


express.Router()

  • 라우터 객체를 생성합니다.
    • 라우터 객체는 Express에서 특정 경로와 HTTP 요청(예: GET, POST 등)을 처리하기 위해 사용하는 미니 애플리케이션입니다.
      • 라우터 객체는 서버의 각 부분을 독립적으로 분리하여 관리할 수 있게 해줍니다.
      • 여러 요청 경로와 그에 대한 처리를 한 곳에 모아 관리할 수 있습니다.
  • 특정 경로(/register)로 들어오는 요청을 처리하는 역할을 합니다.

라우트 정의

  • router.post('/register', ...):
    • 클라이언트가 /api/users/register로 POST 요청을 보낼 때 실행됩니다.
  • 미들웨어:
    • userValidationRules
      • 요청 데이터의 유효성을 검사합니다.
    • validate
      • 검증 결과를 확인하고, 에러가 있으면 클라이언트에게 에러 메시지를 응답합니다.

컨트롤러 연결

  • register
    • 유효성 검사를 통과한 요청 데이터를 처리하는 컨트롤러 함수입니다.


📌 4. 유효성 검사 (userValidator.ts)

import { body, validationResult } from 'express-validator';
import { Request, Response, NextFunction } from 'express';

export const userValidationRules = [
  body('username').notEmpty().withMessage('사용자 이름은 필수입니다.'),
  body('email').isEmail().withMessage('유효한 이메일을 입력해주세요.'),
  body('password').isLength({ min: 6 }).withMessage('비밀번호는 최소 6자 이상이어야 합니다.'),
];

export const validate = (req: Request, res: Response, next: NextFunction): void => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    res.status(400).json({ errors: errors.array() });
  } else {
    next();
  }
};

코드 상세 설명


userValidationRules

  • 클라이언트가 보낸 데이터가 조건을 만족하는지 검증하는 규칙입니다.
    • username: 비어 있으면 안 됩니다.
    • email: 유효한 이메일 형식이어야 합니다.
    • password: 최소 6자 이상이어야 합니다.

validate

  • 검증 결과를 확인하고, 에러가 있으면 클라이언트에게 에러 메시지를 응답으로 보냅니다.
  • 검증을 통과하면 다음 단계(회원가입 처리)로 진행합니다.



📌 5. 회원가입 처리 (userController.ts)

import { Request, Response, NextFunction } from 'express';
import { registerUser } from '../services/userService';

export const register = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
  try {
    const { username, email, password } = req.body;
    await registerUser(username, email, password);
    res.status(201).json({ message: '회원가입이 완료되었습니다.' });
  } catch (error) {
    next(error);
  }
};

코드 상세 설명


register 함수

  • 클라이언트가 보낸 데이터(username, email, password)를 registerUser 함수에 전달하여 회원가입 로직을 실행합니다.
  • 회원가입이 성공하면 상태 코드 201과 함께 성공 메시지를 응답으로 보냅니다.

에러 처리

  • 에러가 발생하면 next(error)를 호출하여 에러를 전달합니다.



📌 6. 회원가입 로직 (userService.ts)

import bcrypt from 'bcrypt';
import { createUser, findUserByEmail } from '../repositories/userRepository';
import { BadRequestError } from '../errors/httpError';

export const registerUser = async (username: string, email: string, password: string): Promise<void> => {
  const existingUser = await findUserByEmail(email);
  if (existingUser) {
    throw new BadRequestError('이미 사용 중인 이메일입니다.');
  }

  const hashedPassword = await bcrypt.hash(password, 10);
  await createUser({ username, email, password: hashedPassword });
};

코드 상세 설명


중복 이메일 확인

  • findUserByEmail 함수로 데이터베이스에서 이메일을 검색합니다.
  • 동일한 이메일이 이미 존재하면 에러를 발생시킵니다.

비밀번호 암호화

  • bcrypt.hash로 비밀번호를 암호화합니다.
  • 암호화된 비밀번호는 데이터베이스에 저장됩니다.
  • 누군가 데이터베이스를 탈취해도 비밀번호를 바로 알 수 없도록 안전 장치를 둡니다.

사용자 생성

  • 암호화된 비밀번호를 포함한 사용자 정보를 데이터베이스에 저장합니다.



📌 7. 데이터베이스 작업 (userRepository.ts)

import { User, IUser } from '../models/user';

export const createUser = async (userData: Partial<IUser>): Promise<IUser> => {
  const user = new User(userData);
  return await user.save();
};

export const findUserByEmail = async (email: string): Promise<IUser | null> => {
  return await User.findOne({ email });
};

코드 상세 설명


  • createUser

    • 사용자 데이터를 User 모델을 사용해 새 객체로 만들고 데이터베이스에 저장합니다.
  • findUserByEmail

    • 이메일을 기준으로 데이터베이스에서 사용자 정보를 검색합니다.
    • 검색 결과가 없다면 null을 반환합니다.

0개의 댓글