본문 바로가기
JavaScript/Node.js

Node) sequelize 사용하기 - typescript, express, 쿼리 알아보기

by 박채니 2022. 12. 30.
안녕하세요, 코린이의 코딩 학습기 채니 입니다.
[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://velog.io/@dlawogus/NodeJS-Express-Typescript%EB%A1%9C-Sequelize%ED%99%98%EA%B2%BD%EA%B5%AC%EC%B6%95

 

NodeJS Express Typescript로 Sequelize환경구축

이번 프로젝트를 진행하는데 있어서 ORM으로 Sequelize를 쓰기로 했다. Sequelize는 사용해봤지만, 타입스크립트를 적용해 사용해 보는 것은 이번이 처음이다. 참고한 자료Coder Singh 유튜브철철 - node.js

velog.io

https://dapsu-startup.tistory.com/entry/ts-node-NodeBird-%EC%8B%9C%ED%80%84%EB%9D%BC%EC%9D%B4%EC%A6%88

 

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