이제 프로젝트의 회원가입과 로그인의 프론트엔드 파트가 어느정도 마무리 되었기 때문에 데이터베이스를 통해서 사용자의 정보를 저장할 필요가 있다. 따라서 이번에는 데이터베이스를 설계하는 방법에 대해서 알아보자
데이터베이스를 설계하는 과정은 크게 다음과 같다.
이러한 과정을 거치면 데이터베이스는 시스템의 요구사항을 충족하면서도 효율적인 성능을 보장할 수 있는 구조를 가지게 된다. 또한, 이러한 설계 과정을 통해 데이터베이스의 정합성을 보장하고, 미래의 확장성을 고려한 설계를 할 수 있다.
먼저, MySQL로 회원을 관리하기 위한 기본적인 데이터베이스 스키마를 설계해 보자
이를 위해 users
라는 테이블을 만들고 일반적으로 회원 정보에는 아이디, 비밀번호, 이메일, 닉네임 등이 포함되므로 이를 고려해서 설계해 보자.
이 테이블은 다음과 같은 내용을 포함한다.
id
: 각 사용자를 유일하게 식별하는 ID이다. 이 필드는 자동으로 증가하는 정수이다.nickname
: 사용자의 닉네임으로 이 필드는 20자 이내의 문자열이어야 한다.email
: 사용자의 이메일 주소이며, 이 필드는 50자 이내의 문자열이어야 한다. 또한, 이 필드는 테이블 내에서 유일해야 한다.password
: 사용자의 비밀번호이며, 이 필드는 해싱된 상태로 저장된다.profileImage
: 사용자의 프로필 이미지 URL이다. 이 필드는 필수 항목이 아니므로 NULL 값을 허용한다.createdAt
: 사용자가 만들어진 시각이며 기본값으로 생성 시각을 자동으로 저장한다.updatedAt
: 사용자 정보가 마지막으로 수정된 시각이며, 기본값으로 수정 시각을 자동으로 업데이트한다.userType
: 회원의 권한을 나타내는 필드로, 'admin'과 'user' 같은 문자열을 저장하거나, 권한의 복잡성에 따라 더 많은 옵션을 저장할 수 있다.userStatus
: 회원의 상태를 나타내는 필드로, 'active'와 'inactive', 'deleted'와 같은 상태를 저장하거나, 상황에 따라 더 많은 상태를 저장할 수 있다.따라서 위에서 설명한 추가적인 필드들을 반영하여 테이블 스키마를 업데이트하면 다음과 같다
// models/User.js
"use strict";
const { Model, DataTypes } = require("sequelize");
module.exports = (sequelize) => {
class User extends Model {
static associate(models) {
// Define associations here
}
}
User.init(
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
nickname: {
type: DataTypes.STRING(20),
allowNull: false,
},
email: {
type: DataTypes.STRING(50),
allowNull: false,
unique: true,
},
password: {
type: DataTypes.STRING(255),
allowNull: false,
},
profileImage: DataTypes.STRING(255),
userType: {
type: DataTypes.ENUM,
values: ["admin", "user"],
defaultValue: "user",
},
userStatus: {
type: DataTypes.ENUM,
values: ["active", "inactive", "deleted"],
defaultValue: "active",
},
},
{
sequelize,
modelName: "User",
tableName: "users",
timestamps: true,
createdAt: "createdAt",
updatedAt: "updatedAt",
}
);
return User;
};
다음으로 MySQL Workbench를 사용해서 ER(Entity-Relationship) 다이어그램을 생성해 보자. ER 다이어 그램 생성 과정은 다음과 같다.
users와 cafes테이블을 각각 만들고 이 테이블들을 연결하는 users_cafes 테이블을 다대다 관계로 연결하였다.
Sequelize는 Node.js를 위한 ORM(Object-Relational Mapping)으로 JavaScript 객체와 데이터베이스 간의 관계를 매핑해주는 도구이다. ORM을 사용하는 이유는 코드의 가독성을 높이고 데이터베이스 스키마를 쉽게 변경하거나 업데이트할 수 있기 때문이다.
sequelize-cli
를 통해 데이터베이스를 관리하는데 필요한 파일들에 대해서 알아보자
config.json
파일이 생성된다. 이 파일은 Sequelize에게 어떻게 데이터베이스에 연결할지에 대한 정보를 제공한다. 각 개발 환경(개발, 테스트, 배포)에 대한 데이터베이스 연결 설정을 별도로 할 수 있다.앞서 작성한 사용자의 스키마를 바탕으로 애플리케이션 코드를 작성해보자.
애플리케이션 코드는 애플리케이션의 비즈니스 로직을 구현하는 코드로 데이터베이스 모델을 사용하여 CRUD(Create, Read, Update, Delete) 연산을 수행하는 코드를 작성하는 것을 포함한다. controllers/userController.js파일에서 사용자의 CRUD 연산 코드를 작성해 보자.
// controllers/userController.js
const { User } = require("../models");
// 모든 사용자를 가져오는 함수
exports.getAllUsers = async (req, res) => {
try {
const users = await User.findAll();
res.status(200).json(users);
console.log(req.body);
} catch (error) {
console.error(error);
res.status(400).json({ message: "사용자를 불러오는 중 오류가 발생했습니다" });
}
};
// 특정 사용자를 가져오는 함수
exports.getUser = async (req, res) => {
try {
const user = await User.findByPk(req.params.id);
if (!user) return res.status(404).json({ message: "사용자를 찾을 수 없습니다" });
res.status(200).json(user);
console.log(req.body);
} catch (error) {
console.error(error);
res.status(400).json({ message: "사용자를 불러오는 중 오류가 발생했습니다" });
}
};
// 새로운 사용자를 생성하는 함수
exports.createUser = async (req, res) => {
try {
const newUser = await User.create(req.body);
res.status(201).json(newUser);
console.log(req.body);
} catch (error) {
console.error(error);
res.status(400).json({ message: "사용자를 생성하는 중 오류가 발생했습니다" });
}
};
// 사용자 정보를 수정하는 함수
exports.updateUser = async (req, res) => {
try {
const user = await User.update(req.body, {
where: { id: req.params.id },
});
if (!user) return res.status(404).json({ message: "사용자를 찾을 수 없습니다" });
res.status(200).json({ message: "사용자가 성공적으로 업데이트되었습니다" });
console.log(req.body);
} catch (error) {
res.status(400).json({ message: "사용자를 업데이트하는 중 오류가 발생했습니다" });
}
};
// 사용자를 삭제하는 함수
exports.deleteUser = async (req, res) => {
try {
const user = await User.destroy({
where: { id: req.params.id },
});
if (!user) return res.status(404).json({ message: "사용자를 찾을 수 없습니다" });
res.status(200).json({ message: "사용자가 성공적으로 삭제되었습니다" });
console.log(req.body);
} catch (error) {
console.error(error);
res.status(400).json({ message: "사용자를 삭제하는 중 오류가 발생했습니다" });
}
};
해당 코드는 사용자 데이터를 관리하기 위한 Controller의 일부를 나타낸다. 여기서는 User 모델에 대해 CRUD(Create, Read, Update, Delete) 연산을 수행하는 함수들이 정의되어 있다. 각 함수는 HTTP 요청을 받아 알맞은 데이터베이스 작업을 수행하고, 그 결과를 클라이언트에게 응답으로 보내준다.
이런 컨트롤러 함수를 정의한 후, 이 함수들을 Express 라우트와 연결하면 완성된다. 이제 라우팅 및 미들웨어 설정을 해보자
// routes/userRoutes.js
const express = require('express');
const userController = require('../controllers/userController');
const router = express.Router();
router
.route('/')
.get(userController.getAllUsers)
.post(userController.createUser);
router
.route('/:id')
.get(userController.getUser)
.put(userController.updateUser)
.delete(userController.deleteUser);
module.exports = router;
CRUD 연산을 위한 라우트 정의는 다음과 같다.
router.route('/:id')
는 URL에서 :id
부분에 동적인 값을 받을 수 있게 해준다. 예를 들어, http://localhost:3000/users/1
와 같이 요청하면, :id
부분에 1
이라는 값이 들어가게 되며, 이 값을 req.params.id
로 가져올 수 있다.get(userController.getUser)
는 특정 사용자를 가져오는 요청을 처리한다.put(userController.updateUser)
는 특정 사용자의 정보를 업데이트하는 요청을 처리한다.delete(userController.deleteUser)
는 특정 사용자를 삭제하는 요청을 처리한다.이렇게 각 컨트롤러는 해당하는 모델에 대한 CRUD 연산을 정의하고, 각 라우트는 해당 연산을 특정 URL 경로에 바인딩한다. 이렇게 하면 클라이언트가 특정 URL에 HTTP 요청을 보내면, 그 요청이 알맞은 함수에 의해 처리되어 적절한 응답을 보내게 된다.
이렇게 작성한 코드는 서버를 구동하는 app.js 파일에서 불러와 사용해야 한다. 이렇게 서버를 구동하면 사용자와 카페에 대한 API 엔드포인트가 생성되고, 이를 통해 데이터베이스와 상호작용할 수 있다.
// app.js
const express = require("express");
const app = express();
const userRoutes = require("./routes/userRoutes");
const cafeRoutes = require("./routes/cafeRoutes")
const PORT = process.env.PORT || 8000;
// JSON 데이터 파싱을 위한 미들웨어
app.use(express.json());
// 라우팅 설정
app.use("/", cafeRoutes);
app.use("/users", userRoutes);
app.listen(PORT, () => {
console.log(`서버가 ${PORT} 포트에서 실행 중입니다.`);
});
module.exports = app;
마지막으로 sequelize-cli를 이용하여 앞서 작성한 스키마를 기반으로 실제 테이블을 생성해보자
마이그레이션 파일을 만들기 위해서는 Sequelize CLI를 사용하고 다음 명령어를 실행하면 된다.
npx sequelize-cli migration:generate --name create_users
생성된 마이그레이션 파일에 미리 설계한 스키마를 바탕으로 다음과 같이 작성한다.
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('Users', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
nickname: {
type: Sequelize.STRING
},
email: {
type: Sequelize.STRING
},
password: {
type: Sequelize.STRING
},
profileImage: {
type: Sequelize.STRING
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
},
userType: {
type: Sequelize.ENUM('admin', 'user'),
defaultValue: 'user'
},
userStatus: {
type: Sequelize.ENUM('active', 'inactive', 'deleted'),
defaultValue: 'active'
}
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('Users');
}
};
마이그레이션 파일을 작성한 후, 다음 명령어를 실행하여 이 마이그레이션을 데이터베이스에 적용할 수 있다.
npx sequelize-cli db:migrate
이것으로 데이터베이스에 'Users' 테이블이 생성된다.
cafes
테이블과 users_cafes
테이블에 대해서도 동일하게 생성하면 MySQL 워크벤치에 다음과 같이 테이블이 생성된 모습을 볼 수 있다.
마지막으로 postman에서 내가 생성한 users 테이블이 정상적으로 HTTP 요청을 처리하고 있는지 테스트 해보자.
URL에http://localhost8000/users
로 데이터를 입력하고 POST 요청을 해보자.
MySQL 워크벤치에서 해당 데이터가 추가된 것을 확인할 수 있다.