[코드에 대한 고민] 유저 정보 관련 코드

최주희·2024년 4월 4일
1

코드 리팩토링

목록 보기
1/3
post-thumbnail
post-custom-banner

✅ 구현한 기능

[유저관련 기능]
① 로그인
② 회원가입
③ 회원 개별 조회
④ 회원 개별 탈퇴

[채널관련 기능]

⑤ 채널 전체 조회
⑥ 채널 개별 생성
⑦ 채널 개별 조회
⑧ 채널 개별 수정
⑨ 채널 개별 삭제

1. 기본 셋팅

[mariadb.js]
DB 연결을 위해 Mysql2 모듈 사용

const mysql = require('mysql2');
const connection = mysql.createConnection({
  host: 'localhost',
  user: 'root',
  password: 'root',
  database: 'pt_Youtube',
  dateStrings: true,
});

module.exports = connection;

[users.js]

  • 라우터 설정 : express
  • db 연결 : mariadb
  • 유효성 검사 : express-validator
  • jwt 토큰 사용 : jsonwebtoken
  • .env 환경변수 사용 : dotenv
// 라우터 설정
const express = require('express');
const router = express.Router();

router.use(express.json());
// db 연결
const dbConnection = require('../mariadb');
// 유효성 검사
const { body, validationResult } = require('express-validator');
// jwt
const jwt = require('jsonwebtoken');
//dotenv
const dotenv = require('dotenv');

[channels.js]

  • jwt 토큰 사용을 제외하고 users.js와 동일

Q. 1.1. 공통된 모듈을 분리하여 사용할 수 있을까?

[common.js]

const express = require('express');
const router = express.Router();

router.use(express.json());

const dbConnection = require('./mariadb');

const { validationResult } = require('express-validator');
const validate = (req, res, next) => {
  const err = validationResult(req);

  if (err.isEmpty()) {
    return next();
  }
  return res.status(400).json(err.array());
};

module.exports = { router, validate, dbConnection };

[users.js] / [channels.js]

//users.js
const { router, validate, dbConnection } = require('../common');
const { body } = require('express-validator');

//channels.js
const { router, validate, dbConnection } = require('../common');
const { body, param } = require('express-validator');

🤔 고민: 공통되지 않은 거 같다.

지금은 수업용으로 간단한 코드를 예시로 들었지만, 실제로는 common 모듈에 포함될 기능(라우터설정, json형태변경, db연결, 유효성검사)들이 전혀 공통되지 않는 것 같다. 코드가 짧아지는 것이 항상 좋은 것은 아니다. 공통 모듈에 포함되는 기능들은 실제로 공통된 기능이어야 한다고 생각이 든다.

✅ A.1.1 공통된 기능으로 분리하자.

[common.js]에는 라우터 설정이나 데이터베이스 연결,
[validate.js]을 추가해, 유효성검사를 이렇게 기능에 따라 파일을 분리했다.

2. 로그인

router.post(
  '/signin',
  [
    body('email').notEmpty().isEmail().withMessage('email 유효성을 확인해주세요'),
    body('password').notEmpty().isString().withMessage('password 유효성을 확인해주세요'),
    validate,
  ],
  (req, res) => {
    const { email, password } = req.body;
    const sql = `SELECT  * FROM users WHERE email=? AND password=?`;
    const requiredFields = [email, password];
    dbConnection.query(sql, requiredFields, (err, results) => {
      if (err) {
        return res.status(400).end();
      }

      const userData = results[0];
      if (userData) {
        const token = jwt.sign({ email: userData.email, name: userData.name }, process.env.PRIVATE_KEY, {
          expiresIn: '5m',
          issuer: 'juhee',
        });
        res.cookie('token', token, { httpOnly: true });
        return res.status(200).json({ status: 200, msg: `${userData.email}님 로그인 성공` });
      }
      return res.status(403).json({ status: 403, msg: `아이디와 비번을 다시 확인해주세요` });
    });
  }
);

현재 모든 기능에서 아래의 코드 형식이 반복되고 있다.

Q. 2.1. 데이터베이스 쿼리를 분리해도 되는가?

일단 데이터베이스 쿼리의 콜백 매개변수가 어떻게 사용되는지 살펴봤다.

  • error로 실행 종료
  • results 값으로 다양한 형태의 응답 전달

1) 분리하는 함수에는 error 처리를 내포하고,
result값은 다양한 형태로 전달되기에 result 값만 반환시켜주는 방향으로 생각해봤다.

2) 데이터베이스 쿼리의 매개변수로 sql과 데이터를 전달해줘야하기에 분리하는 함수에 두가지의 매개변수를 담았다.

  • sql
  • data

[구현코드]

// dbQuery
const dbQuery = (sql, requiredFields) => {
  dbConnection.query(sql, requiredFields, (err, results) => {
    if (err) {
      return res.status(400).end();
    }
    return results;
  });
};

// 로그인
router.post(
  '/signin',
  [
    body('email').notEmpty().isEmail().withMessage('email 유효성을 확인해주세요'),
    body('password').notEmpty().isString().withMessage('password 유효성을 확인해주세요'),
    validate,
  ],
  (req, res) => {
    const { email, password } = req.body;
    const sql = `SELECT  * FROM users WHERE email=? AND password=?`;
    const requiredFields = [email, password];
    const results = dbQuery(sql, requiredFields);
    if (results.length > 0) {
      const userData = results[0];

      const token = generateToken(userData.email, userData.name);
      res.cookie('token', token, { httpOnly: true });
      return res.status(200).json({ status: 200, msg: `${userData.email}님 로그인 성공` });
    }
    return res.status(403).json({ status: 403, msg: `아이디와 비번을 다시 확인해주세요` });
  }
);

문제 발생 ❌

여기서 생긴 문제는 아래와 같다.

TypeError: Cannot read properties of undefined (reading '0')

아마도 아래의 코드에서 results는 매번 undefined.가 나올 것이다

 const results = dbQuery(sql, requiredFields);

왜냐면,dbQuery 함수의 비동기적인 특성때문이다. dbConnection.query 메서드는 비동기 함수이기 때문에 dbQuery 함수는 결과를 반환하기 전에 실행이 완료되고, 따라서 results 값은 아직 설정되지 않은 상태에서 반환되고 undefined가 된다.

✅ A. 2.1. 비동기로 처리

1) dbQuery 함수
콜백 대신 프로미스 또는 async/await를 사용하여 DB 쿼리를 처리한다.

const dbQuery = (sql, requiredFields) => {
  return new Promise((resolve, reject) => {
    dbConnection.query(sql, requiredFields, (err, results) => {
      err ? reject(err) : resolve(results);
    });
  });
};

2) try-catch 구문을 사용

// 로그인
router.post(
  '/signin',
  [
    body('email').notEmpty().isEmail().withMessage('email 유효성을 확인해주세요'),
    body('password').notEmpty().isString().withMessage('password 유효성을 확인해주세요'),
    validate,
  ],
  async (req, res) => {
    const { email, password } = req.body;
    const sql = `SELECT  * FROM users WHERE email=? AND password=?`;
    const requiredFields = [email, password];

    try {
      const results = await dbQuery(sql, requiredFields);
      const userData = results[0];

      const token = generateToken(userData.email, userData.name);
      res.cookie('token', token, { httpOnly: true });
      return res.status(200).json({ status: 200, msg: `${userData.email}님 로그인 성공` });
    } catch (error) {
      return res.status(403).json({ status: 403, msg: `아이디와 비번을 다시 확인해주세요` });
    }
  }
);

Q. 2.2. 유효성 검사 미들웨어 배열을 한번에 묶어서 사용 할 수 있을까?

 [
    body('email').notEmpty().isEmail().withMessage('email 유효성을 확인해주세요'),
    body('password').notEmpty().isString().withMessage('password 유효성을 확인해주세요'),
    validate,
  ],

✅ A. 2.2.checkSchema함수 사용

Express-validator 라이브러리에서 제공하는 checkSchema 함수를 사용하면 여러 필드의 유효성 검사를 한 번에 정의할 수 있다.

const validationSchema = {
  email: {
    in: ['body'],
    notEmpty: {
      errorMessage: '이메일을 입력하세요',
    },
    isEmail: {
      errorMessage: '올바른 이메일 형식이 아닙니다.',
    },
  },
  password: {
    in: ['body'],
    notEmpty: {
      errorMessage: '비밀번호를 입력하세요',
    },
    isString: {
      errorMessage: '비밀번호는 문자열이어야 합니다',
    },
  },
};

module.exports = validationSchema;

아래처럼 한번에 정의 한 유효성 검사를 pick 함수를 통해서 선택적으로 할 수 있다.

const validationSchema = require('../modules/validationSchema');

const { body, checkSchema } = require('express-validator');

router.post('/signin', [checkSchema(pick(validationSchema, ['email', 'password'])), validate], validate], async (req, res) => {

결과 코드

const { router, dbQuery } = require('../modules/common');
const { validate } = require('../modules/validate');
const validationSchema = require('../validationSchema');
const { pick } = require('lodash');
const { checkSchema } = require('express-validator');
// jwt
const jwt = require('jsonwebtoken');
//dotenv
const dotenv = require('dotenv');
dotenv.config();

// token 생성
const generateToken = (email, name) => {
  return jwt.sign({ email: email, name: name }, process.env.PRIVATE_KEY, {
    expiresIn: '5m',
    issuer: 'juhee',
  });
};

// 로그인
router.post(
  '/signin',
  [checkSchema(pick(validationSchema, ['email', 'password'])), validate],
  async (req, res) => {
    const { email, password } = req.body;
    const sql = `SELECT  * FROM users WHERE email=? AND password=?`;
    const requiredFields = [email, password];

    try {
      const results = await dbQuery(sql, requiredFields);
      const userData = results[0];

      const token = generateToken(userData.email, userData.name);
      res.cookie('token', token, { httpOnly: true });
      return res.status(200).json({ status: 200, msg: `${userData.email}님 로그인 성공` });
    } catch (error) {
      return res.status(403).json({ status: 403, msg: `아이디와 비번을 다시 확인해주세요` });
    }
  }
);

//  회원가입
router.post('/signup', [checkSchema(validationSchema), validate], async (req, res) => {
  const { email, name, password, tel } = req.body;
  const requiredFields = [email, name, password, tel];
  const sql = `INSERT INTO users (email, name, password, tel) VALUES (?, ?, ?, ?)`;
  try {
    await dbQuery(sql, requiredFields);
    res.status(200).json({ msg: '회원가입에 성공했습니다' });
  } catch (error) {
    res.status(500).json({ msg: '회원가입 중에 문제가 발생했습니다' });
  }
});

router
  .route('/users')
  //  회원 개별 조회
  .get([checkSchema(pick(validationSchema, ['email'])), validate], async (req, res) => {
    const { email } = req.body;
    const sql = `SELECT * FROM users WHERE email=?`;
    try {
      const results = await dbQuery(sql, email);
      if (results.length === 0) {
        return res.status(404).json({ status: 404, msg: '해당 유저가 존재하지 않습니다.' });
      }
      res.status(200).json(results);
    } catch (error) {
      res.status(500).json({ msg: '조회 중에 문제가 발생했습니다' });
    }
  })

  //  회원 개별 탈퇴
  .delete([checkSchema(pick(validationSchema, ['email'])), validate], async (req, res) => {
    const { email } = req.body;
    const sql = `DELETE FROM users WHERE email=?`;
    try {
      const results = await dbQuery(sql, email);
      if (results.affectedRows === 0) {
        return res.status(404).json({ status: 404, msg: '해당 유저가 존재하지 않습니다.' });
      }
      res.status(200).json({ status: 200, msg: '회원정보가 삭제되었습니다' });
    } catch (error) {
      res.status(500).json({ msg: '탈퇴 중에 문제가 발생했습니다' });
    }
  });

module.exports = router;


[+ 2024.04.09 추가 수정] createPool과 execute 사용

우선 왜 이 두가지를 사용했는지 설명하고자한다.

1. 리소스 낭비가 적은 createPool

전에 사용한 createConnection와 createPool의 차이점을 설명하고자한다.

1) createConnection

이 함수는 데이터 베이스와 단일 연결을 하는 방법으로 아래와 같은 순서로 동작한다.

  • 데이터베이스에 접근을 하기 위해서 connection 객체를 생성한다.
  • connection을 열고
  • query문을 실행 한 후
  • connection을 종료한다.

이 함수는 요청이 있을때마다 connection 객체를 생성하고 제거하는 로직이 반복됩니다. 리소스를 낭비 할 수 있다.

Q1.왜 createConnection을 사용하면 리소스 낭비가 생기는가?

  • 매번 새로운 연결을 생성하면 매번 네트워크 및 데이터베이스 서버에 대한 연결을 설정해야한다.
  • 연결이 제대로 종료되지 않으면 메모리 누수 등의 문제도 발생할 수 있다.
  • 사용자가 많을 경우에는 connection을 계속 생성하게 되어 서버에 과부하가 일어난다.

2) createPool

이 함수는 connection pool 방식을 사용한다.

connection pool 이란?
DB에 연결된 connection을 미리 여러개 만들어 pool에 보관한 후, 필요할 때마다 pool에서 connection을 가져와 사용하고 사용이 끝나면 다시 pool로 반환하는 방법

createConnection의 경우,
단일 연결 방식이기에 동시에 여러 쿼리문을 처리 못하지만,
pool은 여러개 만들어 놓은 connection을 사용하기 때문에 여러 쿼리문을 병렬적으로 처리 가능하다.

2. execute

이 방식을 통해서 직접 만든 dbQuery 함수 없앨 수 있었다.

처음에는 따로 함수를 분리하여 dbQuery 함수를 가지고 와서 사용했었다.
하지만 execute를 통해서 추가적인 작업 없이 그냥 async/await 방식을 바로 사용할 수 있어 불필요한 작업을 생략 할 수 있었다.

우선 execute 말고도 query라는 방식이 있다.
이 두개의 차이점을 설명하고자한다.

query vs execute

1) query => Callback 방식:

  • 오래된 방식
  • 콜백(callback)을 사용하여 쿼리를 실행하고 결과를 처리합니다.

아래는 콜백을 사용한 코드의 예시:

Copy code
pool.query(sql, requiredFields, (error, results) => {
  if (error) {
    return res.status(500).json({ status: 500, msg: '로그인 중에 오류가 발생했습니다' });
  }
  const userData = results[0];
  if (!userData) {
    return res.status(403).json({ status: 403, msg: '아이디와 비밀번호를 다시 확인해주세요' });
  }
  const token = generateToken(userData.email, userData.name);
  res.cookie('token', token, { httpOnly: true });
  return res.status(200).json({ status: 200, msg: `${userData.email}님 로그인 성공` });
});

이 방식은 비동기 코드를 작성할 때 콜백 지옥(callback hell)에 빠질 위험이 있고, 가독성이 좋지 않을 수 있다.

2) execute => Promise 방식:

  • 최근에 도입된 방식
  • mysql2 패키지에서 제공하는 execute() 함수를 사용하여 Promise를 반환
  • async/await 문법을 사용하여 비동기적으로 쿼리를 실행하고 결과를 처리
  • 코드에서는 await pool.execute()를 사용하며, try/catch 블록을 통해 에러를 처리

아래는 Promise를 사용한 코드의 예시:

Copy code
try {
  const [results] = await pool.execute(sql, requiredFields);
  const userData = results[0];
  if (!userData) {
    return res.status(403).json({ status: 403, msg: '아이디와 비밀번호를 다시 확인해주세요' });
  }
  const token = generateToken(userData.email, userData.name);
  res.cookie('token', token, { httpOnly: true });
  return res.status(200).json({ status: 200, msg: `${userData.email}님 로그인 성공` });
} catch (error) {
  return res.status(500).json({ status: 500, msg: '로그인 중에 오류가 발생했습니다' });
}

이 방식은 비동기 코드를 보다 구조화하고 가독성을 높일 수 있으며, 콜백 지옥을 피할 수 있다.

[createPool,execute를 사용한 수정 코드]

아래는 createPool과 execute를 사용해 수정한 코드이다.

[mariadb.js]

const mysql = require('mysql2/promise');
const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'root',
  database: 'pt_Youtube',
  dateStrings: true,
});

module.exports = pool;

[users.js]

const pool = require('../mariadb');

// createPool 사용
 router.post(
  '/signin',
  [checkSchema(pick(validationSchema, ['email', 'password'])), validate],
  async (req, res) => {
    const { email, password } = req.body;
    const sql = `SELECT  * FROM users WHERE email=? AND password=?`;
    const requiredFields = [email, password];

    try {
      const [results] = await pool.execute(sql, requiredFields);
      const userData = results[0];
      if (!userData) {
        return res.status(403).json({ status: 403, msg: `아이디와 비번을 다시 확인해주세요` });
      }
      const token = generateToken(userData.email, userData.name);
      res.cookie('token', token, { httpOnly: true });
      return res.status(200).json({ status: 200, msg: `${userData.email}님 로그인 성공` });
    } catch (error) {
      return res.status(500).json({ status: 500, msg: `로그인 중에 오류가 발생했습니다` });
    }
  }
);

[참고자료]

profile
큰 목표보단 꾸준한 습관 만들기
post-custom-banner

0개의 댓글