HJW's IT Blog

조각집 프로젝트 기록 - 01 본문

카테고리 없음

조각집 프로젝트 기록 - 01

kiki1875 2024. 9. 23. 15:29

주어진 요구사항에 따라 ERD 설계를 시작하였다.

만들다 보니 GROUP은 SQL 예약어이기 때문에 다른 이름으로 해야겠다는 생각이 들었다.

 

GROUP 과 BADGE 의 관계는 N:N 관계이기 때문에, 이 두 테이블의 PK를 Composite Key 로 사용하는 GROUP_BADGE 테이블을 생성하였다.

TAG 와 POST 의 관계 또한 N:N 관계이기에 Composite Key 를 생성하였다.

 

다음은 주요 table sql 이다.

 

CREATE TABLE `Groups` (
    `GID` INTEGER PRIMARY KEY AUTOINCREMENT,
    `GName` VARCHAR(100) NOT NULL,
    `GImage` VARCHAR(255),
    `GIntro` TEXT,
    `IsPublic` BOOLEAN NOT NULL,
    `GPassword` VARCHAR(255) NOT NULL,
    `CreatedDate` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `GLikes` INTEGER DEFAULT 0,
    `GBadgeCount` INTEGER DEFAULT 0,
    `PostCount` INTEGER DEFAULT 0
);

CREATE TABLE `Posts` (
    `PostID` INTEGER PRIMARY KEY AUTOINCREMENT,
    `GID` INTEGER NOT NULL,
    `Nickname` VARCHAR(50) NOT NULL,
    `Title` VARCHAR(100) NOT NULL,
    `Image` VARCHAR(255),
    `Content` TEXT NOT NULL,
    `Location` VARCHAR(100),
    `MemoryMoment` DATETIME,
    `IsPublic` BOOLEAN NOT NULL,
    `PPassword` VARCHAR(255) NOT NULL,
    `CreatedDate` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `LikeCount` INTEGER DEFAULT 0,
    `CommentCount` INTEGER DEFAULT 0,
    FOREIGN KEY (`GID`) REFERENCES `Groups`(`GID`) ON DELETE CASCADE
);

CREATE TABLE `Comments` (
    `CommentID` INTEGER PRIMARY KEY AUTOINCREMENT,
    `PostID` INTEGER NOT NULL,
    `Nickname` VARCHAR(50) NOT NULL,
    `Content` TEXT NOT NULL,
    `Password` VARCHAR(255) NOT NULL,
    `CreatedDate` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (`PostID`) REFERENCES `Posts`(`PostID`) ON DELETE CASCADE
);

CREATE TABLE `Badges` (
    `BadgeID` INTEGER PRIMARY KEY AUTOINCREMENT,
    `Name` VARCHAR(100) NOT NULL,
    `Description` TEXT NOT NULL,
    `Condition` TEXT NOT NULL
);

CREATE TABLE `Group_Badge` (
    `GID` INTEGER NOT NULL,
    `BadgeID` INTEGER NOT NULL,
    `ObtainedDate` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`GID`, `BadgeID`),
    FOREIGN KEY (`GID`) REFERENCES `Groups`(`GID`) ON DELETE CASCADE,
    FOREIGN KEY (`BadgeID`) REFERENCES `Badges`(`BadgeID`) ON DELETE CASCADE
);

CREATE TABLE `Tags` (
    `TagID` INTEGER PRIMARY KEY AUTOINCREMENT,
    `Name` VARCHAR(50) NOT NULL
);

CREATE TABLE `Post_Tag` (
    `PostID` INTEGER NOT NULL,
    `TagID` INTEGER NOT NULL,
    PRIMARY KEY (`PostID`, `TagID`),
    FOREIGN KEY (`PostID`) REFERENCES `Posts`(`PostID`) ON DELETE CASCADE,
    FOREIGN KEY (`TagID`) REFERENCES `Tags`(`TagID`) ON DELETE CASCADE
);

현재 INDEX 를 어느 컬럼에 대해 생성 해야 할 지 고민중인데, 후보군은 다음과 같다

  1. Groups 의 Gname : 사용자들이 그룹명으로 검색할 수 있다
  2. Posts 의 Title : 사용자들은 게시글 검색을 할 수 있다
  3. Post 의 CreateDate : 개시글을 최신순으로 정렬할 때
  4. Post의 LikeCount : 개시글을 공감 순으로 정렬할 때

위 인덱스중 정말 필수적이라 생각되는 것은 idx_posts_createddate 인데, 기본적으로 게시글을 최신순으로 보여주는것이 일반적이기 때문이다.

 

Node.js 프로젝트 세팅

이제 프로젝트 기본 세팅을 시작하겠다.

node -v
npm -v
  • node.js 와 npm 이 설치되어 있는지 확인 후,
npm init -y
  • 초기화를 해주었다.
  • 필자는 해당 프로젝트를 typescript로 진행할 것이기에, 필요한 개발 의존성을 설치해야 한다
npm install --save-dev typescript ts-node @types/node @types/express @types/sequelize
  • typescript: TypeScript 컴파일러
  • ts-node: TypeScript 코드를 직접 실행하기 위한 도구
  • @types/*: TypeScript용 타입 정의 파일
  • typescript는 개발환경에서만 사용되기 때문에 save-dev 옵션을 꼭 사용해야 한다
npx tsc --init
  • tsconfig.json 생성
  • tsc → typescript 컴파일러

 

//package.json 내에 작성
"scripts": {
  "build": "tsc",
  "start": "node dist/app.js",
  "dev": "ts-node src/app.ts",
  "format": "prettier --write ."
},

npm install express
npm install --save-dev @types/express

npm install sequelize mysql2
npm install --save-dev @types/sequelize

npm install sequelize-typescript

 

`Sequelize` 란?

  • DB 작업을 도와주는 ORM(Object Relational Mapping) 라이브러리 이다
  • 자바스크립트 객체와 관계형 DB를 서로 연결해 주는 도구이다.

 

Model 생성

 

/src/config/model 내부에 각 테이블의 모델을 생성하였다.

 

모델의 정의

  • 테이블의 추상화 : 모델은 테이블을 코드 상에서 표현한 것으로 각 테이블의 구조를 클래스나 객체로 나타낸다
  • 객체 지향적 접근을 가능하게 만들어, SQL 쿼리를 직접 작성하지 않고도 객체 지향적 방식으로 DB와 상호작용 할 수 있다
  • 모델은 DB와의 CRUD 작업이 필요한 모든 곳에서 사용된다.
  • 다음은 모델의 예시이다

sequelize.ts

이 코드는 sequelize-typescript 라이브러리 사용시, ORM 인스턴스를 설정하는 코드이다. DB 연결및 관련 설정을 처기화하고, 모델을 DB 테이블과 연결한다.

 

import { Sequelize } from 'sequelize-typescript';
import path from 'path';

export const sequelize = new Sequelize({
  database: 'ZOGAKZIP',
  dialect: 'mysql',
  username: 'admin',
  password: '1234',
  storage: ':memory:',
  models: [path.resolve(__dirname, '../models')] // 모델 폴더 위치
});
import { Table, Column, Model, DataType, PrimaryKey, AutoIncrement, Default, CreatedAt, UpdatedAt, AllowNull } from 'sequelize-typescript';

@Table({
  tableName: 'Group',
  timestamps: false
})
export default class Group extends Model<Group>{
  
  @Column({
    type: DataType.INTEGER,
    primaryKey: true,
    autoIncrement: true
  })
  GID!:number;

  @Column({
    type: DataType.STRING(100),
    allowNull: false,
  })
  GName!:string;

  @Column({
    type: DataType.STRING(255)
  })
  GImage!:string;

  @Column({
    type: DataType.TEXT
  })
  GIntro!:string;

  @Column({
    type: DataType.BOOLEAN,
    allowNull: false
  })
  IsPublic!: boolean;

  @Column({
    type: DataType.STRING(255),
    allowNull: false
  })
  GPassword!: string;
  
  @CreatedAt
  @Column({
    type: DataType.DATE,
    defaultValue: DataType.NOW
  })
  CreatedDate!: Date;

  @Default(0)
  @Column({
    type: DataType.INTEGER
  })
  GLikes!:number;

  @Default(0)
  @Column({
    type: DataType.INTEGER
  })
  GBadgeCount!: number;

  @Default(0)
  @Column({
    type: DataType.INTEGER
  })
  PostCount!: number;

}

 

프로젝트 구조

  • src/config : DB 설정과 같은 구성 파일
  • src/controllers : 라우트의 로직을 처리하는 컨트롤러
  • models : DB 모델을 Sequelize를 통해 정의
  • routes : 어플리케이션의 라우트 정의

 

MySQL 서버 설정

DB 생성

CREATE DATABASE ZOGAKZIP
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_unicode_ci;
  • DEFAULT CHARACTER SET utf8mb4
    • 기본 문자 집합을 utf8mb4 로 설정하여, 유니코드 문자를 4바이트 까지 지원하는 문자 집합으로 설정하였다. 이를 이용해, 이모지와 같은 다양한 문자를 저장할 수 있다.
  • DEFAULT COLLATE utf8mb4_unicode_ci
    • 기본 정렬 규칙을 정의하는 것이다.
    • 대소문자를 구분하지 않고 case-insective 문자를 정렬 하고 비교하는 방식

사용자 생성 및 권한 부여

CREATE USER 'admin'@'localhost' IDENTIFIED BY '1234';
GRANT ALL PRIVILEGES ON ZOGAKZIP.* TO 'admin'@'localhost';
FLUSH PRIVILEGES;
  • root user 를 사용하는것은 보안상의 이유로 안전하지 못하므로 admin 을 생성해 주었다.

환경변수 설정

root dirctory 에 .env 파일 생성

DB_USERNAME=admin
DB_PASSWORD=1234
DB_NAME=ZOGAKZIP
DB_HOST=localhost
DB_DIALECT=mysql

마이그레이션

config/config.js

//require('dotenv').config(); 

module.exports = {
  development: {
    username: 'admin',
    password: '1234',
    database: 'ZOGAKZIP',
    host: '127.0.0.1',
    dialect: 'mysql',
  },

};

npm install --save-dev sequelize-cli-typescript
// 마이그레이션 파일 작성
npx sequelize-cli db:migrate
'use strict';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable('Group', {
      GID: {
        type: Sequelize.INTEGER,
        primaryKey: true,
        autoIncrement: true,
      },
      GName: {
        type: Sequelize.STRING(100),
        allowNull: false,
      },
      GImage: {
        type: Sequelize.STRING(255),
        allowNull: true,
      },
      GIntro: {
        type: Sequelize.TEXT,
        allowNull: true,
      },
      IsPublic: {
        type: Sequelize.BOOLEAN,
        allowNull: false,
      },
      GPassword: {
        type: Sequelize.STRING(255),
        allowNull: false,
      },
      CreatedDate: {
        type: Sequelize.DATE,
        allowNull: false,
        defaultValue: Sequelize.fn('NOW'),
      },
      GLikes: {
        type: Sequelize.INTEGER,
        allowNull: false,
        defaultValue: 0,
      },
      GBadgeCount: {
        type: Sequelize.INTEGER,
        allowNull: false,
        defaultValue: 0,
      },
      PostCount: {
        type: Sequelize.INTEGER,
        allowNull: false,
        defaultValue: 0,
      },
    });
  },
  down: async (queryInterface, Sequelize) => {
    await queryInterface.dropTable('Group');
  },
};

Sequelize → Prisma 전환

npm install prisma --save-dev
npm install @prisma/client

// schema.prisma 작성

npx prisma migrate dev --name init
npx generate prisma

prisma 설정

// .env
DATABASE_URL="mysql://admin:1234@localhost:3306/zogakzip"
  • schema.prisma
  • // This is your Prisma schema file,
    // learn more about it in the docs: <https://pris.ly/d/prisma-schema>
    
    // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
    // Try Prisma Accelerate: <https://pris.ly/cli/accelerate-init>
    
    generator client {
      provider = "prisma-client-js"
    }
    
    datasource db {
      provider = "mysql"
      url      = env("DATABASE_URL")
    }
    
    model Badge {
      BadgeID     Int     @id @default(autoincrement()) @map("BadgeID")
      Name        String  @db.VarChar(100)
      Description String  @db.Text
      Condition   String  @db.Text
    
      GroupBadges GroupBadge[]
    
      @@map("Badges")
    }
    
    model Comment {
      CommentID   Int     @id @default(autoincrement()) @map("CommentID")
      PostID      Int
      Nickname    String  @db.VarChar(50)
      Content     String  @db.Text
      Password    String  @db.VarChar(255)
      CreatedDate DateTime @default(now()) @map("CreatedDate")
    
      post Post @relation(fields: [PostID], references: [PostID])
    
      @@map("Comments")
    }
    
    model Group {
      GID          Int     @id @default(autoincrement()) @map("GID")
      GName        String  @db.VarChar(100)
      GImage       String? @db.VarChar(255)
      GIntro       String? @db.Text
      IsPublic     Boolean
      GPassword    String  @db.VarChar(255)
      CreatedDate  DateTime @default(now()) @map("CreatedDate")
      GLikes       Int     @default(0)
      GBadgeCount  Int     @default(0)
      PostCount    Int     @default(0)
    
      posts        Post[]
      groupBadges  GroupBadge[]
    
      @@map("Group")
    }
    
    model GroupBadge {
      GID          Int
      BadgeID      Int
      ObtainedDate DateTime @default(now()) @map("ObtainedDate")
    
      group Group @relation(fields: [GID], references: [GID])
      badge Badge @relation(fields: [BadgeID], references: [BadgeID])
    
      @@id([GID, BadgeID])
      @@map("Group_Badge")
    }
    
    model Post {
      PostID       Int      @id @default(autoincrement()) @map("PostID")
      GID          Int
      Nickname     String   @db.VarChar(51)
      Title        String   @db.VarChar(100)
      Image        String?  @db.VarChar(255)
      Content      String   @db.Text
      Location     String?  @db.VarChar(100)
      MemoryMoment DateTime @map("MemoryMoment")
      IsPublic     Boolean
      PPassword    String   @db.VarChar(255)
      CreatedDate  DateTime @default(now()) @map("CreatedDate")
      LikeCount    Int      @default(0)
      CommentCount Int      @default(0)
    
      group    Group   @relation(fields: [GID], references: [GID])
      comments Comment[]
      postTags PostTag[]
    
      @@map("Posts")
    }
    
    model PostTag {
      PostID Int
      TagID  Int
    
      post Post @relation(fields: [PostID], references: [PostID])
      tag  Tag  @relation(fields: [TagID], references: [TagID])
    
      @@id([PostID, TagID])
      @@map("Post_Tag")
    }
    
    model Tag {
      TagID Int     @id @default(autoincrement()) @map("TagID")
      Name  String  @db.VarChar(50)
    
      postTags PostTag[]
    
      @@map("Tags")
    }
    
    

Prisma로 변경한 이유

  1. Type Safety → Prisma 는 정적 타입을 보장하며, DB 스키마에 변경이 생기면 자동으로 업데이트 된다
  2. 자동 생성 스키마 → prisma 는 스키마 우선 접근 방식을 제공한다. DB 스키마 변경시, 마이그레이션과 타입이 자동으로 업데이트되어 관리가 편하다
  3. 마이그레이션 시스템 자동화