안녕하세요, 코린이의 코딩 학습기 채니 입니다.
[Node.js 교과서]의 책을 참고하여 포스팅한 개인 공부 내용입니다.
시퀄라이즈 사용하기
- MySQL 작업을 쉽게 할 수 있도록 도와주는 라이브러리
- ORM으로 분류되며, ORM은 자바스크립트 객체와 데이터베이스의 릴레이션을 매핑해주는 도구
- 자바스크립트 구문을 알아서 SQL로 바꿔주기 때문에 SQL 언어를 쓰지 않고도 MySQL 조작 가능
프로젝트 생성 후 패키지 설치
$ yarn add express morgan nunjucks sequelize sequelize-cli mysql2
$ yarn add -D nodemon
- sequelize-cli
: 시퀄라이즈 명령어를 실행하기 위한 패키지 - mysql2
: MySQL과 시퀄라이즈를 이어주는 드라이버
설치 완료 후 sequelize init 명령어 호출 - 전역 설치 없이 명령어로 사용하려면 npx 붙임
$ npx sequelize init
Created "config/config.json"
Successfully created models folder at "/Users/parkchaeeun/project/playground-cepark/learn-sequelize/models".
Successfully created migrations folder at "/Users/parkchaeeun/project/playground-cepark/learn-sequelize/migrations".
Successfully created seeders folder at "/Users/parkchaeeun/project/playground-cepark/learn-sequelize/seeders".
config, models, migrations, seeders 폴더가 설치 되었고, models 폴더 하위에 index.js 파일이 생성되었는지 확인합니다.
해당 파일을 그대로 사용하면 오류 및 필요 없는 부분들이 많아서 아래와 같이 수정해줍니다.
models/index.ts
"use strict";
import { Sequelize } from "sequelize";
const config = {
development: {
username: "username",
password: "password",
database: "database",
host: "127.0.0.1",
dialect: "mysql",
},
};
export const sequelize = new Sequelize(
config.development.database,
config.development.username,
config.development.password,
{
host: config.development.host,
dialect: "mysql",
timezone: "+09:00",
}
);
Sequelize는 시퀄라이즈 패키지이자 생성자이며, config/config.json에서 데이터베이스 설정을 불러온 후 new Sequelize를 통해 MySQL 연결 객체를 생성하였습니다. 하고 싶었으나.. config/config.json을 타입스크립트에 어떻게 불러오는지 모르겠어서 위 처럼 해결했습니다.
해당 객체를 export 해주어 사용할 수 있도록 하였습니다.
MySQL 연결하기
app.ts
import express, { NextFunction, Request, Response } from "express";
import { sequelize } from "./models";
import morgan from "morgan";
import path from "path";
import nunjucks from "nunjucks";
import indexRouter from "./routes";
import usersRouter from "./routes/users";
const app = express();
app.set("port", process.env.PORT || 3001);
app.set("view engine", "html");
nunjucks.configure("views", {
express: app,
watch: true,
});
sequelize
.sync({ force: false })
.then(() => {
console.log("데이터베이스 연결 성공!");
})
.catch((error) => {
console.error(error);
});
app.use(morgan("dev"));
app.use(express.static(path.join(__dirname, "public")));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use("/", indexRouter);
app.use("/users", usersRouter);
interface Error {
status?: number;
message?: string;
}
app.use((req: Request, res: Response, next: NextFunction) => {
const error: Error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
error.status = 404; // property 'status' does not exist on type 'error'. 발생해서 Error interface 정의
next(error);
});
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
res.locals.message = err.message;
res.locals.error = process.env.NODE_ENV !== "production" ? err : {};
res.status(err.status || 500);
res.render("error");
});
app.listen(app.get("port"), () => {
console.log(app.get("port"), "번 포트에서 대기 중~");
});
export 했던 sequelize를 불러와 sync 메소드를 사용해 서버를 실행할 때 MySQL과 연동되도록 하였습니다.
force 옵션을 true로 설정하면 서버를 실행할 때마다 테이블을 재생성합니다. 테이블을 잘못 만든 경우 true로 설정하면 됩니다.
MySQL과 연동할 때는 config/config.json 정보가 사용되고, 아래와 같이 수정합니다.
config/config.json
{
"development": {
"username": "유저네임",
"password": "[비밀번호]"
"database": "데이터베이스 이름",
"host": "127.0.0.1",
"dialect": "mysql"
},
...
만일 operatorAliases 속성이 들어있다면 삭제해줍니다.
test는 테스트 용도, production은 배포 용도로 접속하기 위해 사용됩니다.
해당 설정은 process.env.NODE_ENV가 'development'일 때 적용되며, 추후 배포할 때는 'production'으로 설정하면 그에 맞는 데이터베이스가 연동됩니다.
서버 실행
$ yarn start
yarn run v1.22.19
$ nodemon app
[nodemon] 2.0.20
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node app.js`
3001 번 포트에서 대기 중!
Executing (default): SELECT 1+1 AS result
데이터베이스 연결 성공!
모델 정의하기
- MySQL 테이블은 시퀄라이즈의 모델과 대응
- 시퀄라이즈 모델과 MySQL의 테이블을 연결해주는 역할
- 시퀄라이즈는 기본적으로 모델 이름은 단수형, 테이블 이름은 복수형으로 사용
MySQL에서 users 테이블과 comments 테이블을 생성한 상태이고, 이에 대응되는 모델을 만들겠습니다.
models/user.ts
import { DataTypes, Model } from "sequelize";
import { sequelize } from ".";
// user 모델 구성요소 명시
interface UsersAttributes {
name: string;
age: number;
married: boolean;
comment?: Text; // nullable 할 때는 ?:로 타입 정의
created_at?: Date; // default 값이 있어서 ?:로 타입 정의
}
export class User extends Model<UsersAttributes> {
public readonly id!: number; // !를 붙이는 이유? 반드시 존재한다는 것을 시퀄라이즈에 확신시키는 것
public age!: number;
public married!: boolean;
public comment!: Text;
public created_at!: Date;
//public static associations: { };
}
// 모델 생성
User.init(
{
name: {
type: DataTypes.STRING(20),
allowNull: false,
unique: true,
},
age: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false,
},
married: {
type: DataTypes.BOOLEAN,
allowNull: false,
},
comment: {
type: DataTypes.TEXT,
allowNull: true,
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
},
},
{
sequelize,
timestamps: false,
underscored: false,
modelName: "User",
tableName: "users",
paranoid: false,
charset: "utf8",
collate: "utf8_general_ci",
}
);
관계 정의는 추후에 다뤄보도록 하겠습니다.
시퀄라이즈의 자료형은 MySQL 자료형과 상이하다는 것을 확인할 수 있습니다.
- VARCHAR → STRING
- INT → INTERGER
- TINYINT → BOOLEAN
- DATETIME → DATE
- INTERGER.UNSIGNED → UNSIGNED 옵션이 적용된 INT를 의미
COMMENT 모델은 추후 정의
쿼리 알아보기
- 쿼리는 프로미스를 반환하므로 then 혹은 async/await 문법과 함께 사용
- 시퀄라이즈의 자료형대로 기입
로우 생성 쿼리
// INSERT INTO test.users (name, age, married, comment) VALUES ('zero', 24, 0, '자기소개1');
import User from './user';
User.create({
name: 'zero',
age: 24,
married: false,
comment: '자기소개1'
});
로우 조회 쿼리
// SELECT * FROM test.users;
User.findAll({});
하나의 데이터 조회 쿼리
// SELECT * FROM test.users LIMIT 1;
User.findOne({});
원하는 컬럼 조회 쿼리
// SELECT name, married FROM test.users;
User.findAll({
attributes: ['name', 'married']
});
where 옵션 적용 쿼리
// SELECT name, age FROM test.users WHERE married = 1 AND age > 30;
import { Op } from "sequelize";
User.findAll({
attributes: ["name", "age"],
where: {
married: true,
age: { [Op.gt]: 30 },
},
});
MySQL에서는 undefined를 지원하지 않으므로 빈 값을 넣고자한다면 null을 기입해야 합니다.
또한, 시퀄라이즈는 자바스크립트 객체를 이용해 쿼리를 생성하기 때문에 Op.gt 같은 연산자를 사용합니다.
** 자주 쓰이는 연산자
- Op.gt (초과)
- Op.gte (이상)
- Op.lt (미만)
- Op.lte (이하)
- Op.ne (같지 않음)
- Op.or (또는)
- Op.in (배열 요소 중 하나)
- Op.notIn (배열 요소와 모두 다름)
or 적용 쿼리
// SELECT id, name FROM test.users WHERE married = 0 OR age > 30;
import { Op } from "sequelize";
User.findAll({
attributes: ["id", "name"],
where: {
[Op.or]: [{ married: 0 }, { age: { [Op.gt]: 30 } }],
},
});
정렬 적용 쿼리
// SELECT id, name FROM test.users ORDER BY age DESC;
import { Op } from "sequelize";
User.findAll({
attributes: ["id", "name"],
order: [["age", "DESC"]],
});
컬럼 하나가 아닌 두 개 이상으로도 정렬 가능하므로, 배열 안에 배열이 있는 것을 확인할 수 있습니다.
로우 개수 설정 쿼리
// SELECT id, name FROM test.users ORDER BY age DESC LIMIT 1;
import { Op } from "sequelize";
User.findAll({
attributes: ["id", "name"],
order: [["age", "DESC"]],
limit: 1,
});
limit 옵션으로 로우 개수를 설정해줄 수 있습니다. (limit가 1이라면, findOne메소드를 사용해도 됨)
OFFSET 적용 쿼리
// SELECT id, name FROM test.users ORDER BY age DESC LIMIT 1 OFFSET 1;
import { Op } from "sequelize";
User.findAll({
attributes: ["id", "name"],
order: [["age", "DESC"]],
limit: 1,
offset: 1,
});
로우 수정 쿼리
// UPDATE test.users SET comment = '바꿀 내용' WHERE id = 2;
import { Op } from "sequelize";
User.update(
{
comment: "바꿀 내용",
},
{
where: { id: 2 },
}
);
첫 번째 인수는 수정할 내용, 두 번째 인수는 어떤 로우를 수정할지에 대한 조건을 줍니다.
로우 삭제 쿼리
// DELETE FROM test.usrs WHERE id = 2;
import { Op } from "sequelize";
User.destory({
where: { id: 2 },
});
쿼리 수행하기
사용자 정보를 등록, 확인하고 사용자가 등록한 댓글을 확인하는 서버이지만, 댓글 관련은 추후 다루겠습니다.
모델에서 페이지를 받아 페이지를 렌더링하는 방식과 JSON 형식으로 데이터를 가져오는 방법을 알아보겠습니다.
views/sequelize.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>시퀄라이즈 서버</title>
<style>
table {
border: 1px solid black;
border-collapse: collapse;
}
table th,
table td {
border: 1px solid black;
}
</style>
</head>
<body>
<div>
<form id="user-form">
<fieldset>
<legend>사용자 등록</legend>
<div><input id="username" type="text" placeholder="이름" /></div>
<div><input id="age" type="number" placeholder="나이" /></div>
<div>
<input id="married" type="checkbox" /><label for="married"
>결혼 여부</label
>
</div>
<button type="submit">등록</button>
</fieldset>
</form>
</div>
<br />
<table id="user-list">
<thead>
<tr>
<th>아이디</th>
<th>이름</th>
<th>나이</th>
<th>결혼여부</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{user.id}}</td>
<td>{{user.name}}</td>
<td>{{user.age}}</td>
<td>{{ '기혼' if user.married else '미혼'}}</td>
</tr>
{% endfor %}
</tbody>
</table>
<br />
<div>
<form id="comment-form">
<fieldset>
<legend>댓글 등록</legend>
<div>
<input id="userid" type="text" placeholder="사용자 아이디" />
</div>
<div><input id="comment" type="text" placeholder="댓글" /></div>
<button type="submit">등록</button>
</fieldset>
</form>
</div>
<br />
<table id="comment-list">
<thead>
<tr>
<th>아이디</th>
<th>작성자</th>
<th>댓글</th>
<th>수정</th>
<th>삭제</th>
</tr>
</thead>
<tbody></tbody>
</table>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="/sequelize.js"></script>
</body>
</html>
public/sequelize.js
// 사용자 이름 눌렀을 때 댓글 로딩
document.querySelectorAll("#user-list tr").forEach((el) => {
el.addEventListener("click", function () {
const id = el.querySelector("td").textContent;
getComment(id);
});
});
// 사용자 로딩
async function getUser() {
try {
const res = await axios.get("/users");
const users = res.data;
console.log("sequelize : ", users);
const tbody = document.querySelector("#user-list tbody");
tbody.innerHTML = "";
users.map(function (user) {
const row = document.createElement("tr");
row.addEventListener("click", () => {
getComment(user.id);
});
// 로우 셀 추가
let td = document.createElement("td");
td.textContent = user.id;
row.appendChild(td);
td = document.createElement("td");
td.textContent = user.name;
row.appendChild(td);
td = document.createElement("td");
td.textContent = user.age;
row.appendChild(td);
td = document.createElement("td");
td.textContent = user.married ? "기혼" : "미혼";
row.appendChild(td);
tbody.appendChild(row);
});
} catch (err) {
console.error(err);
}
}
// 댓글 로딩
async function getComment(id) {
try {
const res = await axios.get(`/users/${id}/comments`);
const comments = res.data;
const tbody = document.querySelector("#comment-list tbody");
tbody.innerHTML = "";
comments.map(function (comment) {
// 로우 셀 추가
const row = document.createElement("tr");
let td = document.createElement("td");
td.textContent = comment.id;
row.appendChild(td);
td = document.createElement("td");
td.textContent = comment.User.name;
row.appendChild(td);
td = document.createElement("td");
td.textContent = comment.comment;
row.appendChild(td);
const edit = document.createElement("button");
edit.textContent = "수정";
edit.addEventListener("click", async () => {
// 수정 클릭 시
const newComment = prompt("바꿀 내용을 입력하세요");
if (!newComment) {
return alert("내용을 반드시 입력하셔야 합니다");
}
try {
await axios.patch(`/comments/${comment.id}`, { comment: newComment });
getComment(id);
} catch (err) {
console.error(err);
}
});
const remove = document.createElement("button");
remove.textContent = "삭제";
remove.addEventListener("click", async () => {
// 삭제 클릭 시
try {
await axios.delete(`/comments/${comment.id}`);
getComment(id);
} catch (err) {
console.error(err);
}
});
// 버튼 추가
td = document.createElement("td");
td.appendChild(edit);
row.appendChild(td);
td = document.createElement("td");
td.appendChild(remove);
row.appendChild(td);
tbody.appendChild(row);
});
} catch (err) {
console.error(err);
}
}
// 사용자 등록 시
document.getElementById("user-form").addEventListener("submit", async (e) => {
e.preventDefault();
const name = e.target.username.value;
const age = e.target.age.value;
const married = e.target.married.checked;
if (!name) {
return alert("이름을 입력하세요");
}
if (!age) {
return alert("나이를 입력하세요");
}
try {
await axios.post("/users", { name, age, married });
getUser();
} catch (err) {
console.error(err);
}
e.target.username.value = "";
e.target.age.value = "";
e.target.married.checked = false;
});
// 댓글 등록 시
document
.getElementById("comment-form")
.addEventListener("submit", async (e) => {
e.preventDefault();
const id = e.target.userid.value;
const comment = e.target.comment.value;
if (!id) {
return alert("아이디를 입력하세요");
}
if (!comment) {
return alert("댓글을 입력하세요");
}
try {
await axios.post("/comments", { id, comment });
getComment(id);
} catch (err) {
console.error(err);
}
e.target.userid.value = "";
e.target.comment.value = "";
});
라우터 생성하기 (sequelize.js에 나오는 GET, POST, PUT, DELETE 요청에 해당하는 라우터 생성)
routes/index.ts
import express, { NextFunction, Request, Response } from "express";
import { User } from "../models/user";
const router = express.Router();
router.get("/", async (req: Request, res: Response, next: NextFunction) => {
try {
const users = await User.findAll();
res.render("sequelize", { users }); // sequelize.html에 users 데이터 전송
} catch (error) {
console.error(error);
next(error);
}
});
export default router;
User.findAll 메소드로 모든 사용자를 찾은 후, sequelize.html을 렌더링할 때 반환된 users를 넣어줍니다.]
routes/users.ts
import express, { NextFunction, Request, Response } from "express";
import { User } from "../models/user";
const router = express.Router();
// GET /users 요청 -> User 목록 렌더링
// get, post 등의 메소드는 req, res, next에 타입 정의가 되어있어 따로 타입핑 하지 않아도 상관없음
router
.route("/")
.get(async (req: Request, res: Response, next: NextFunction) => {
try {
const users = await User.findAll();
console.log("route/users : ", users);
res.json(users); // json형식으로 넘겨줌
} catch (error) {
console.error(error);
next(error);
}
})
// Post /users 요청 -> User 등록
.post(async (req: Request, res: Response, next: NextFunction) => {
try {
const user = await User.create({
name: req.body.name,
age: req.body.age,
married: req.body.married,
});
console.log("POST /users : ", user);
res.status(201).json(user); // res.status(201) - 정상적으로 등록 완료
} catch (error) {
console.error(error);
next(error);
}
});
// 추후 댓글 관련 라우터 생성
export default router;
같은 메소드 경로는 router.route()로 묶어주었고, 이번에는 json 형식으로 데이터를 넘겨주었습니다.
C, R 까지 하였고, 댓글할 때 U, D 할 예정!!
☆ 책에선 javascript로 진행하였지만, typescript로 해보고 싶어서 진짜 왕왕 헤맸다....ㅠ
정말 많은 정보를 얻은 url 공유!!!!!
https://sequelize.org/docs/v6/core-concepts/model-basics/
https://velog.io/@qhgus/Node-Express-TypeScript-%ED%99%98%EA%B2%BD-%EC%84%B8%ED%8C%85
'JavaScript > Node.js' 카테고리의 다른 글
Node) 익스프레스로 SNS 서비스 만들기 - Passport 모듈로 로그인 구현하기 (0) | 2023.01.02 |
---|---|
Node) 익스프레스로 SNS 서비스 만들기 - 프로젝트 구조 갖추기, 모델 생성, 데이터베이스 연결 (0) | 2023.01.02 |
Node) MySQL - 워크벤치 설치하기 (0) | 2022.12.29 |
Node) 익스프레스로 웹 서버 만들기 (1) | 2022.12.29 |
Node) 패키지 매니저 (0) | 2022.12.28 |