안녕하세요, 코린이의 코딩 학습기 채니 입니다.
[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 공유!!!!!
NodeJS Express Typescript로 Sequelize환경구축
이번 프로젝트를 진행하는데 있어서 ORM으로 Sequelize를 쓰기로 했다. Sequelize는 사용해봤지만, 타입스크립트를 적용해 사용해 보는 것은 이번이 처음이다. 참고한 자료Coder Singh 유튜브철철 - node.js
velog.io
ts-node | NodeBird | 시퀄라이즈
※ 인프런 - Node.js에 TypeScript 적용하기(feat. NodeBire) by 조현영 강의를 기반으로 정리한 내용입니다. 미들웨어 세팅이 끝났으면 이제 시퀄라이즈 설치! npm i sequelize npm i sequelize-cli B -> A -> B -> ... 이
dapsu-startup.tistory.com
https://sequelize.org/docs/v6/core-concepts/model-basics/
Model Basics | Sequelize
In this tutorial you will learn what models are in Sequelize and how to use them.
sequelize.org
https://velog.io/@qhgus/Node-Express-TypeScript-%ED%99%98%EA%B2%BD-%EC%84%B8%ED%8C%85
TypeScript와 함께 Node.js + Express 환경 세팅(feat. npm, yarn)
Node.js + Express + Typescript 환경 세팅
velog.io
'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 |