1. SQL 대신 Sequelize ORM 적용기 (초기설정,회원관련)

최주희·2024년 5월 24일
0

코드 리팩토링

목록 보기
2/3
post-thumbnail

첫 프로젝트 "도서 정보 사이트 구축 프로젝트"에서 SQL을 사용했다.
하지만, 조금 더 효율적이고 유지보수하기 쉬운 코드를 작성하기 위해 Sequelize ORM를 사용해보기로 했다.

Sequelize ORM를 사용한 이유는 아래와 같다.

  • DB 작업을 자동화 해주기 때문에 반복적인 SQL 쿼리 작성을 피할 수 있다.
  • 객체 간의 관계를 정의하고 관리할 수 있어 관계를 쉽게 표현하고 사용할 수 있다.
  • DB의 테이블을 모델로 정의할 수 있어 이 모델로 데이터에 접근이 가능하다.

    근데 직접 테이블을 모델로 정의하는 과정이 오히려 나는 조금 더 복잡하고 익숙하지 않아 어려웠다.
    이 부분은 Sequelize CLI을 통해서 모델과 마이그레이션 파일을 자동으로 생성하면 작업이 간소화되어 좋을 것 같다.

  • 스키마 변경을 자동화할 수 있는 마이그레이션 도구를 제공하기 때문에 개발자 간의 협업이 원활하다.

    작은 변경 사항이라도 마이그레이션을 해야하는 점에서 더 번거로운 게 아닌 가 하는 생각이 들었지만, 협업에 있어서는 좋은 도구라 생각했다.

  • 자주 사용되는 모델 속성 정의나 관계 설정을 유틸리티 함수로 작성해서 재사용이 가능하다.

⚠️ 타입스크립트 함께 사용


타입스크립트를 사용하려했으나, 모델 형성하는데 시퀄라이즈가 가지고 와지지 않는 오류가 발생하여 이유를 찾던 중 시퀄라이즈는 타입스크립트를 잘 지원해주지 않는다는 답을 얻을 수 있었다.

Q. 그럼 불가능한가?

  • 지원은 한다. 하지만, 완벽한 지원을 위해서 많은 수동 타입 선언이 필요하다. 특히 모델 정의와 관계 설정에서 타입을 명시하는 과정이 번거롭다.
  • 일반적으로 TypeORM이나, MikroORM을 많이 사용하는 것 같다.

로직 작성

Sequelize 설정 및 환경 구성

config/config

require('dotenv').config();

const env = process.env;

module.exports = {
  development: {
    username: env.DB_USER,
    password: env.DB_PASSWORD,
    database: env.DB_NAME,
    host: env.DB_HOST,
    dialect: 'mysql',
    timezone: 'Asia/Seoul',
  },
  test: {
    username: env.DB_USER,
    password: null,
    database: 'database_test',
    host: env.DB_HOST,
    dialect: 'mysql',
    timezone: 'Asia/Seoul',
  },
  production: {
    username: env.DB_USER,
    password: null,
    database: 'database_production',
    host: env.DB_HOST,
    dialect: 'mysql',
    timezone: 'Asia/Seoul',
  },
};

models/index

1. [전체 코드]

'use strict';

const { Sequelize, DataTypes } = require('sequelize');
const env = process.env.NODE_ENV || 'development';

const config = require('../config/config')[env];

const db = {};

const sequelize = new Sequelize(config.database, config.username, config.password, config);

db.User = require('./User.js')(sequelize, DataTypes);


db.sequelize = sequelize;

module.exports = db;

2. 환경 설정

'use strict';

const { Sequelize, DataTypes } = require('sequelize');
const env = process.env.NODE_ENV || 'development';

const config = require('../config/config')[env];
  • NODE_ENV 환경변수를 사용해서 현재 환경을 설정
  • 환경에 맞는 DB 설정을 config/config 파일에서 불러오기

3. db & sequelize 인스턴스 초기화

const db = {};
const sequelize = new Sequelize(config.database, config.username, config.password, config);
  • db객체는 나중에 모든 모델을 포함할 객체
  • sequelize 인스턴스를 초기화

Q. sequelize 인스턴스를 초기화 왜 하는 걸까?

: DB와의 연결을 설정하기 위함

4. 모델 정의 & db 추가

db.User = require('./User.js')(sequelize, DataTypes);
  • 모델을 정의하고 db 객체에 추가
  • 모델 파일(User.js)에서 모델을 정의하는 함수가 반환, sequelize 인스턴스와 DataTypes를 인자로 받음

모델 정의(models/user)

'use strict';

module.exports = (sequelize, DataTypes) => {
  const User = sequelize.define(
    'User',
    {
      user_id: {
        type: DataTypes.INTEGER(11),
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
      },
      username: { 
        type: DataTypes.STRING(35), 
        allowNull: false 
      },
      email: { 
        type: DataTypes.STRING(50), 
        allowNull: false 
      },
      password: {
        type: DataTypes.STRING(20),
        allowNull: false,
      },
      address: DataTypes.STRING(50),
      contact: DataTypes.STRING(50),
      salt: {
        type: DataTypes.STRING,
      },
    },
    {
      timestamps: false,
    }
  );

  return User;
};

기본 구조

[User 모델 정의]

sequelize.define('User', {...},{...})
  • 첫번째 인자는 모델 이름
  • 두번째 인자는 모델의 속성 정의 객체
  • 추가 옵션 객체

1. 모델 속성

  • user_id: 기본 키(primaryKey)이자 자동 증가(autoIncrement) 속성
  • username, email, password: 필수(allowNull: false) 속성
  • address, contact, salt: 선택적 속성

2. 모델 옵션

  • timestamps: false: 생성 및 수정 시간 스탬프를 자동으로 추가하지 않도록 설정

Singup/Singin

routers/UserRouter

const {
  signup,
  signin,
  reqResetPassword,
  resetPassword,
  getUserInfo,
  updateUserInfo,
  deleteUserAccount,
} = require('../controllers/UserController.js');
const router = require('express').Router();

router.post('/signup', signup);
router.post('/signin', signin);
router.post('/reset-password/request', reqResetPassword);
router.post('/reset-password', resetPassword);
router.get('/profile', getUserInfo).put('/profile', updateUserInfo);

router.delete('/account', deleteUserAccount);

module.exports = router;

services/userSerive

const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const db = require('../models');
const { validateToken } = require('../utils/auth');
const User = db.User;

const userService = {
  createUser: async (user) => {
    const salt = crypto.randomBytes(10).toString('base64');
    const hashPwd = userService.hashPwd(user.password, salt);
    return await User.create({ ...user, password: hashPwd, salt: salt });
  },

  getDecodedUser: async (req, res) => {
    const decodedPayload = await validateToken(req);
    if (decodedPayload instanceof jwt.TokenExpiredError) {
      return res.status(StatusCodes.UNAUTHORIZED).json({ msg: '토큰이 만료됐습니다.' });
    } else if (decodedPayload instanceof jwt.JsonWebTokenError) {
      return res.status(StatusCodes.UNAUTHORIZED).json({ msg: '토큰이 잘못됐습니다' });
    }
    return decodedPayload;
  },

  updateUser: async (updatedUser, email) => {
    const [numOfAffectedRows, affectedRows] = await User.update(updatedUser, { where: { email } });
    return affectedRows;
  },

  deleteUser: async (email) => {
    return await User.destroy({ where: { email } });
  },

  findUserByEmail: async (email) => {
    return await User.findOne({ where: { email } });
  },

  generateToken: (id, email, name) => {
    return jwt.sign({ id, email, name }, process.env.PRIVATE_KEY, {
      expiresIn: '1h',
      issuer: 'juhee',
    });
  },

  hashPwd: (password, salt) => {
    return crypto.pbkdf2Sync(password, salt, 10000, 10, 'sha512').toString('base64');
  },

  resetPassword: async (email, password, salt) => {
    const hashPwd = userService.hashPwd(password, salt);
    return await User.update({ password: hashPwd }, { where: { email } });
  },
};

module.exports = userService;

controllers/UserController

const userService = require('../services/userService');
const { StatusCodes } = require('http-status-codes');

const signup = async (req, res) => {
  try {
    const user = req.body;
    await userService.createUser(user);
    res.status(StatusCodes.CREATED).json({ status: 201 });
  } catch (err) {
    return res.status(StatusCodes.BAD_REQUEST).json({ message: '회원가입 중에 오류가 발생했습니다.' });
  }
};

const signin = async (req, res) => {
  try {
    const { email, password } = req.body;
    const user = await userService.findUserByEmail(email);

    if (!user) {
      return res.status(StatusCodes.UNAUTHORIZED).json({ status: 401 });
    }
    const hashPwd = userService.hashPwd(password, user.salt);

    if (user.password !== hashPwd) {
      return res.status(StatusCodes.UNAUTHORIZED).json({ status: 401 });
    }

    const token = userService.generateToken(user.id, user.email, user.password);
    res.cookie('token', token, { httpOnly: true });
    res.status(StatusCodes.OK).json({ status: 200, user: user });
  } catch (err) {
    res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ message: '로그인 중에 오류가 발생했습니다.' });
  }
};

const reqResetPassword = async (req, res) => {
  try {
    const { email } = req.body;
    const user = await userService.findUserByEmail(email);
    if (user) {
      return res
        .status(StatusCodes.OK)
        .json({ email: user.email, msg: '비밀번호 초기화가 요청됐습니다.' });
    }
    res.status(StatusCodes.UNAUTHORIZED).json({ msg: '비밀번호 초기화 요청에 실패 했습니다.' });
  } catch (err) {
    res
      .status(StatusCodes.INTERNAL_SERVER_ERROR)
      .json({ msg: '비밀번호 초기화 요청 중에 문제가 발생했습니다.' });
  }
};

const resetPassword = async (req, res) => {
  try {
    const { email, password } = req.body;
    const user = await userService.findUserByEmail(email);
    if (!user) {
      return res.status(StatusCodes.NOT_FOUND).json({ status: 404, msg: '회원이 존재하지 않습니다.' });
    }
    await userService.resetPassword(email, password, user.salt);
    res.status(StatusCodes.OK).json({ msg: '비밀번호가 초기화됐습니다.' });
  } catch (err) {
    res
      .status(StatusCodes.INTERNAL_SERVER_ERROR)
      .json({ msg: '비밀번호 초기화 중에 문제가 발생했습니다.' });
  }
};

const getUserInfo = async (req, res) => {
  try {
    const decodedPayload = await userService.getDecodedUser(req, res);
    const user = userService.findUserByEmail(decodedPayload.email);
    if (!user) {
      return res.status(StatusCodes.NOT_FOUND).json({ status: 404, msg: '회원이 존재하지 않습니다.' });
    }
    res.status(StatusCodes.OK).json({ status: 200, user: user });
  } catch (err) {
    res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ msg: '회원 조회 중에 문제가 발생했습니다.' });
  }
};

const updateUserInfo = async (req, res) => {
  try {
    const updatedUser = req.body;
    const decodedPayload = await userService.getDecodedUser(req, res);
    const user = userService.findUserByEmail(decodedPayload.email);

    if (!user) {
      return res.status(StatusCodes.NOT_FOUND).json({ status: 404, msg: '회원이 존재하지 않습니다.' });
    }

    await userService.updateUser(updatedUser, decodedPayload.email);
    res.status(StatusCodes.OK).json({ status: 200, msg: '회원정보가 수정되었습니다' });
  } catch (err) {
    res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ msg: '회원 수정 중에 문제가 발생했습니다.' });
  }
};

const deleteUserAccount = async (req, res) => {
  try {
    const decodedPayload = await userService.getDecodedUser(req, res);
    const user = userService.findUserByEmail(decodedPayload.email);
    if (!user) {
      return res.status(StatusCodes.NOT_FOUND).json({ status: 404, msg: '회원이 존재하지 않습니다.' });
    }

    await userService.deleteUser(decodedPayload.email);
    res.status(StatusCodes.OK).json({ status: 200, msg: '회원이 삭제되었습니다.' });
  } catch (err) {
    res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ msg: '회원 삭제 중에 문제가 발생했습니다.' });
  }
};

module.exports = {
  signup,
  signin,
  reqResetPassword,
  resetPassword,
  getUserInfo,
  updateUserInfo,
  deleteUserAccount,
};

Sequelize ORM 사용 후,

sql을 작성하지 않아도 되는 부분과, 미리 정의된 관계를 활용할 수 있는 것은 Sequelize ORM의 가장 큰 장점이고 편의하다는 생각이 들었다.
아직까진 sql이 필요한 부분도 있어서 익숙해지는 데는 시간이 필요하지만, 오로지 sql만으로 사용하는 부분에 있어 Sequelize ORM은 그런 단점들을 보완해주는 데 정말 좋은 이점이라고 생각한다.
여전히 모델을 정의와 관계를 매핑하는 데 있어 어려움이 있지만 잘 사용할 수 있다면 여러 작업을 하는데 훨씬 빠르고 간편하게 사용할 수 있을 것 같다.

profile
큰 목표보단 꾸준한 습관 만들기

0개의 댓글