본문 바로가기
개발자 도전기/[STUDY] JS || TS

ts-node | NodeBird | 시퀄라이즈

by 답수 2022. 4. 20.
728x90
SMALL

 

 

※ 인프런 - Node.js에 TypeScript 적용하기(feat. NodeBire) by 조현영 강의를 기반으로 정리한 내용입니다.

 

 

미들웨어 세팅이 끝났으면 이제 시퀄라이즈 설치!

npm i sequelize
npm i sequelize-cli   <-- 시퀄라이즈 명령어를 실행하기 위한 패키지 라이브러리

 

이번 프로젝트의 DB는 MySQL을 사용할 것이고, 타입스크립트로 DB를 사용할 때는 보통 TypeORM 아니면 시퀄라이즈를 사용한다고 한다. 하지만 시퀄라이즈가 가장 많이 쓰이기도 하기 때문에 강사님께서는 시퀄라이즈를 사용하는 것을 보여준다고 한다.

 

Sequelize

시퀄라이즈는 DB 작업을 보다 더 쉽게 사용할 수 있도록 편의를 제공하는 ORM(Object-Relational Mapping) 라이브러리다. 시퀄라이즈는 시퀄라이즈만의 특유의 문법이 있다. 만약 이게 싫다면 sequelize.query라고 쿼리문을 날리는 것이 있기 때문에 시퀄라이즈에 크게 거부감을 가질 필요는 없다.

 

※ 웬만하면 ORM 하나는 제대로 배우길 추천! 생 쿼리문을 날리면 나중에 엄청 후회할 것이다. 웬만하면 프로젝트 초기에 ORM을 도입하는 것을 추천한다. ORM으로 표현하기 너무 어려운 부분만 raw query 사용하는 것이 좋다.

 

시퀄라이즈 모듈이 설치됐으면, 시퀄라이즈 초기화를 먼저 해야 한다.

npx sequelize init

 초기화를 하면

config/config.json

migrations/

models/index.js

seeders/

파일들이 생성된다.

 

그 후 다음 명령어를 적는다.

npx sequelize db:create

 

다음으로 아래 명령어를 입력...

npx sequelize db:create

하면 ERROR: Access denied for user 'root'@'localhost' (using password: NO) 이런 에러가 뜬다. 이를 해결하기 위해 config.json파일을 수정해야 한다고 한다.

 

우선 config.json을 js파일로 바꾼다. Why? DB의 패스워드를 코드로 적어야 하는데 그대로 적으면 보안상 문제가 되기 때문에 dotenv를 사용해야 한다. 또한 DB이름도 재설정하면 다음과 같다.

// 초기 세팅은 js로 하고 넘어가보자.

const dotenv = require('dotenv');
dotenv.config();

module.exports = {
  // 이번 프로젝트에서는 사실상 development 부분만 사용할 예정
  "development": {
    "username": "root",
    "password": process.env.DB_PASSWORD,
    "database": "ts-nodeBird",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "test": {
    "username": "root",
    "password": process.env.DB_PASSWORD,
    "database": "ts-nodeBird",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "production": {
    "username": "root",
    "password": process.env.DB_PASSWORD,
    "database": "ts-nodeBird",
    "host": "127.0.0.1",
    "dialect": "mysql"
  }
}

 

자 그럼 다시 npx sequelize db:create 명령어를 통해 DB를 생성해.... 보려고 했는데 또 에러.

ERROR: Access denied for user 'root'@'localhost' (using password: YES)

 

이 에러는 mysql에 접근하려고 하는데 비밀번호가 틀렸을 경우 발생하는 문제다. 예전에 다른 프로젝트 진행하면서 같은 문제 겪어봤던 기억이 났다. 여하튼 dotenv에서 내가 적은 db의 비밀번호하고 내가 평소에 사용하던 mysql 계정의 비밀번호하고 일치하지 않기 때문에 발생하는 것! 비밀번호 수정 후 다시 명령어 입력. 그럼 다음과 같은 결과가 출력된다.

 

Sequelize CLI [Node: 16.13.0, CLI: 6.4.1, ORM: 6.19.0]

Loaded configuration file "config\config.js".
Using environment "development".
Database ts-nodeBird created.

 

DB 생성 완료!

 

이제 js로 만들었던 config파일을 ts파일로 바꿀 것이다. 시퀄라이즈CLI는 js밖에 인식을 못 하기 때문에 편의상 js로 환경설정을 해서 DB를 만들었고, 만든 후에는 ts로 바꾸는 것이 좋다.

import * as dotenv from 'dotenv';
dotenv.config();

export default {
  // 이번 프로젝트에서는 사실상 development 부분만 사용할 예정
  "development": {
    "username": "root",
    "password": process.env.DB_PASSWORD,
    "database": "ts-nodeBird",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "test": {
    "username": "root",
    "password": process.env.DB_PASSWORD,
    "database": "ts-nodeBird",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "production": {
    "username": "root",
    "password": process.env.DB_PASSWORD,
    "database": "ts-nodeBird",
    "host": "127.0.0.1",
    "dialect": "mysql"
  }
}

 

 

또한 models 디렉토리의 index.js 파일 역시 타입스크립트로 바꾸도록 하는데, 강사님은 시퀄라이즈를 자기만의 방식으로 한다고 한다. 우선 나도 강사님의 방법을 따라가자.

 

먼저 index.ts 파일 내 모든 코드를 지운다. 그리고 같은 디렉토리 내에 sequelize.ts 파일을 생성한다.

import { Sequelize } from 'sequelize';
import config from '../config/config';

const env = process.env.NODE_ENV as ('production' | 'test' | 'development') || 'development';
const { database, username, password } = config[env];
const sequelize = new Sequelize(database, username, password, config[env]);

export { sequelize };
export default sequelize;

 

그런데 sequelize 인스턴스를 생성하는데, 클래스 옵션에 들어가는 config[env] 부분이 에러가 뜬다. 에러 문구를 보니 타입 정의에 문제가 있다는 것. config에 타입 정의를 새로 작성해야 한다.

import * as dotenv from 'dotenv';
dotenv.config();

// 객체 config에 대한 타입 정의
type Config = {
  username: string,
  password: string,
  database: string,
  host: string,
  [key: string]: string,
}
interface IConfigGroup {  // 인터페이스 네임 앞에 I 붙이는 방식은 타입스크립트에서 자주 사용된다고 함
  development: Config;
  test: Config;
  production: Config;
}

const config: IConfigGroup = {
  // 이번 프로젝트에서는 사실상 development 부분만 사용할 예정
  "development": {
    "username": "root",
    "password": process.env.DB_PASSWORD!,
    "database": "ts-nodeBird",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "test": {
    "username": "root",
    "password": process.env.DB_PASSWORD!,
    "database": "ts-nodeBird",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "production": {
    "username": "root",
    "password": process.env.DB_PASSWORD!,
    "database": "ts-nodeBird",
    "host": "127.0.0.1",
    "dialect": "mysql"
  }
}

export default config;

 

그리고 models에 있던 index.ts 파일에 시퀄라이즈를 import함과 동시에 export하도록 한다.

export * from './sequelize';

 

 

되게 번거로워 보이는데 왜 이런 방식으로 시퀄라이즈를 적용한 것일까? 이제 models 디렉토리에 실제 유저 정보를 담는 user.ts 파일을 다룰 것인데 이 user파일과 index, sequelize 파일 사이에 관계가 형성된다. 세 파일 간에 순환 참조가 발생하게 되는데, 이를 해결하기 위해서 이렇게 나눴다고 한다. 물론 sequelize.ts 없이 작업을 진행하는 방법이 있겠지만, 강사님은 실무에서 써본 결과 지금과 같은 방식이 가장 적합했다고 한다.

 

순환 참조(Circular Dependancy)

그렇다면, 강사님이 얘기하는 순환 참조(Circular Dependancy)라는 것은 무엇일까? 간단하게 설명하자면 모듈들이 서로를 참조하여 발생하는 문제이다. 가령 A라는 모듈과 B라는 모듈이 있는데 서로를 참조하게 된다면 어떻게 될까?

A -> B -> A -> B -> ...

이렇게 서로를 계속 참조하게 되어 꼬리에 꼬리를 무는 무한 루프가 발생하게 될 것이다. 자바스크립트는 해당 문제를 방지하기 위해 순환 참조되는 상황이 오면 두 모듈 중 하나는 빈 객체({ })로 반환이 되고, 이는 무한루프는 막게 되지만 결국 모듈은 정상적으로 사용할 수 없는 상황이 생기는 것이다.

 

우리 프로젝트로 다시 이 상황을 보자면, models 폴더 내에 있는 파일들을 읽고 이를 모델로 정의하는 파일인 index.ts가 있고, 타입스크립트의 객체와 DB를 연결해주는 시퀄라이즈, 유저 데이터를 다루는 모델인 user가 있다. 유저는 시퀄라이즈의 모듈을 사용해야 하고, 인덱스는 유저의 모듈을 가져와야 한다. 

sequelize -> user -> index

그런데 만약 여기서 시퀄라이즈 파일을 따로 만들지 않고 인덱스 내부에서 시퀄라이즈 작업을 진행하게 된다면 유저와 인덱스는 서로를 참조하는 순환 참조가 발생하는 것이다.

index -> user -> index -> user -> ...

 

강사님은 순환 참조를 피하기 위해 sequelize.ts 파일을 따로 생성했다고 한다. 순환 참조가 항상 문제를 발생시키는 것은 아니라고 한다. 그러나 타입스크립트에서 타입 정의 부분에서는 문제 없이 넘어가지만, 런탐임 시 실행되는 코드에서 문제가 발생할 수 있다고 하고, 애초에 이런 상황은 방지하는 것이 좋다고 한다.

 

models/user.ts

유저의 데이터 모델 파일인 user.ts를 만든다.

import { DataTypes, Model } from 'sequelize';
import { dbType } from '.';
import { sequelize } from './sequelize';

class User extends Model {
    public readonly id!: number;   // !를 붙이는 이유? 반드시 존재한다는 것을 시퀄라이즈에 확신시키는 것
    public nickname!: string;
    public userId!: string;
    public password!: string;
    public readonly createAt!: Date;    // 시퀄라이즈 내에서 자체적으로 수정하기 때문에 readonly로
    public readonly updateAt!: Date;
}

User.init({
    nickname: {
        type: DataTypes.STRING(20),
    },
    userId: {
        type: DataTypes.STRING(20),
        allowNull: false,
        unique: true
    },
    password: {
        type: DataTypes.STRING(100),
        allowNull: false
    }
}, {    // 시퀄라이즈로 모델과 연동
    sequelize,
    modelName: 'User',
    tableName: 'user',
    charset: 'utf8',    // 한글 인식 가능하도록
    collate: 'utf8_general_ci'
});

export const associate = (db: dbType) => {

}

export default User;

 

User의 init()메소드는 sequelize의 Model 모듈에 내장된 메소드다. model.d.ts 파일의 코멘트 설명은 이렇게 되어 있다.

Initialize a model, representing a table in the DB, with attributes and options.
The table columns are define by the hash that is given as the second argument. Each attribute of the hash represents a column. 

 

데이터베이스의 테이블을 나타내는 모델을 초기화하는 기능을 가진 메소드이고, 첫 번째 인수로는 각 데이터의 타입들을 설정하고, 두 번째 인수에는 컬럼들을 설정한다. sql로 CREATE TABLE User( ... ); 하는 것 대신 사용하는 시퀄라이즈 문법이다. 되게 간편하고 좋은데...??

 

 

다음으로 index.ts에 user의 모듈을 참조한다. 이때 dbType은 user와 index간 순환 참조를 하게 되는데, 타입 정의하는 부분은 런타임할 때 없어지는 코드이기 때문에 문제가 발생하지 않는다.

import User, { associate as associateUser } from './user';
export * from './sequelize';

const db = {
    User, 
};
export type dbType = typeof db;

associateUser(db);

 

models/post.ts

게시판에서 다루는 데이터베이스 모델

import { DataTypes, Model } from "sequelize";
import { dbType } from ".";
import { sequelize } from "./sequelize";

class Post extends Model {
    public readonly id!: number;
    public content!: string;
    public readonly createAt!: Date;
    public readonly updateAt!: Date;
}

Post.init({
    content: {
        type: DataTypes.TEXT,
        allowNull: false,
    }
}, {
    sequelize,
    modelName: 'Post',
    tableName: 'post',
    charset: 'utf8mb4', // 이모티콘 등의 문자들도 많이 사용하기 때문에
    collate: 'utf8mb4_general_ci'
});

// 모델간 관계 형성
export const associate = (db: dbType) => {

};

export default Post;

 

models/comment.ts

코멘트 다루는 데이터베이스 모델

import { DataTypes, Model } from "sequelize/types";
import { dbType } from ".";
import { sequelize } from "./sequelize";

class Comment extends Model {
    public readonly id!: number;
    public content!: string;
    public readonly createAt!: Date;
    public readonly updateAt!: Date;
}

Comment.init({
    content: {
        type: DataTypes.TEXT,
        allowNull: false
    }
}, {
    sequelize,
    modelName: 'Comment',
    tableName: 'comment',
    charset: 'utf8mb4',
    collate: 'utf8mb4_general_ci'
});

export const associate = (db: dbType) => {

};

export default Comment;

 

models/image.ts

이미지 다루는 데이터베이스 모델

import { DataTypes, Model } from "sequelize";
import { dbType } from ".";
import { sequelize } from "./sequelize";

class Image extends Model {
    public readonly id!: number;
    public src!: number;
    public readonly createAt!: Date;
    public readonly updateAt!: Date;
}

Image.init({
    src: {
        type: DataTypes.STRING(200),
        allowNull: false
    }
}, {
    sequelize,
    modelName: 'Image',
    tableName: 'image',
    charset: 'utf8',
    collate: 'utf8_general_ci'
});

export const associate = (db: dbType) => {

};

export default Image;

 

models/hashtag.ts

해시태그 다루는 데이터베이스 모델

import { DataTypes, Model } from "sequelize";
import { dbType } from ".";
import { sequelize } from "./sequelize";

class Hashtag extends Model {
    public readonly id!: number;
    public name!: string;
    public readonly createAt!: Date;
    public readonly updateAt!: Date;
}

Hashtag.init({
    name: {
        type: DataTypes.STRING(20),
        allowNull: false
    }
}, {
    sequelize,
    modelName: 'Hashtag',
    tableName: 'hashtag',
    charset: 'utf8mb4',
    collate: 'utf8mb4_general_ci'
});

export const associate = (db: dbType) => {

};

export default Hashtag;

 

index.ts 모델들 관계 설정

필요한 모델들은 다 작성했고, 이제 index에 코드들을 입력하면 된다.

import User, { associate as associateUser } from './user';
import Post, { associate as associatePost } from './post';
import Comment, { associate as associateComment } from './comment';
import Image, { associate as associateImage} from './image';
import Hashtag, { associate as associateHashTag} from './hashtag';
export * from './sequelize';

// db객체에 전부 삽입
const db = {
    User,
    Post,
    Comment,
    Image,
    Hashtag
};
export type dbType = typeof db;

// 모델들 관계 설정
associateUser(db);
associatePost(db);
associateComment(db);
associateImage(db);
associateHashTag(db);

 

728x90
LIST

댓글