[유저관련 기능]
① 로그인
② 회원가입
③ 회원 개별 조회
④ 회원 개별 탈퇴
[채널관련 기능]
⑤ 채널 전체 조회
⑥ 채널 개별 생성
⑦ 채널 개별 조회
⑧ 채널 개별 수정
⑨ 채널 개별 삭제
[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]
// 라우터 설정
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]
[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연결, 유효성검사)들이 전혀 공통되지 않는 것 같다. 코드가 짧아지는 것이 항상 좋은 것은 아니다. 공통 모듈에 포함되는 기능들은 실제로 공통된 기능이어야 한다고 생각이 든다.
[common.js]에는 라우터 설정이나 데이터베이스 연결,
[validate.js]을 추가해, 유효성검사를 이렇게 기능에 따라 파일을 분리했다.
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: `아이디와 비번을 다시 확인해주세요` });
});
}
);
현재 모든 기능에서 아래의 코드 형식이 반복되고 있다.
일단 데이터베이스 쿼리의 콜백 매개변수가 어떻게 사용되는지 살펴봤다.
1) 분리하는 함수에는 error 처리를 내포하고,
result값은 다양한 형태로 전달되기에 result 값만 반환시켜주는 방향으로 생각해봤다.
2) 데이터베이스 쿼리의 매개변수로 sql과 데이터를 전달해줘야하기에 분리하는 함수에 두가지의 매개변수를 담았다.
[구현코드]
// 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가 된다.
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: `아이디와 비번을 다시 확인해주세요` });
}
}
);
[
body('email').notEmpty().isEmail().withMessage('email 유효성을 확인해주세요'),
body('password').notEmpty().isString().withMessage('password 유효성을 확인해주세요'),
validate,
],
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;
우선 왜 이 두가지를 사용했는지 설명하고자한다.
전에 사용한 createConnection와 createPool의 차이점을 설명하고자한다.
이 함수는 데이터 베이스와 단일 연결을 하는 방법으로 아래와 같은 순서로 동작한다.
이 함수는 요청이 있을때마다 connection 객체를 생성하고 제거하는 로직이 반복됩니다. 리소스를 낭비 할 수 있다.
이 함수는 connection pool 방식을 사용한다.
connection pool 이란?
DB에 연결된 connection을 미리 여러개 만들어 pool에 보관한 후, 필요할 때마다 pool에서 connection을 가져와 사용하고 사용이 끝나면 다시 pool로 반환하는 방법
createConnection의 경우,
단일 연결 방식이기에 동시에 여러 쿼리문을 처리 못하지만,
pool은 여러개 만들어 놓은 connection을 사용하기 때문에 여러 쿼리문을 병렬적으로 처리 가능하다.
이 방식을 통해서 직접 만든 dbQuery 함수 없앨 수 있었다.
처음에는 따로 함수를 분리하여 dbQuery 함수를 가지고 와서 사용했었다.
하지만 execute를 통해서 추가적인 작업 없이 그냥 async/await 방식을 바로 사용할 수 있어 불필요한 작업을 생략 할 수 있었다.
우선 execute 말고도 query라는 방식이 있다.
이 두개의 차이점을 설명하고자한다.
query vs execute
아래는 콜백을 사용한 코드의 예시:
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)에 빠질 위험이 있고, 가독성이 좋지 않을 수 있다.
아래는 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를 사용해 수정한 코드이다.
[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: `로그인 중에 오류가 발생했습니다` });
}
}
);
[참고자료]