TypeORM 사용을 시작하며 느낀 특징을 기록하고자 한다. 더불어 데이터베이스 모델링 도중 Enum 타입 컬럼을 도입하며 마주한 어려움들이 있었기에 작성하는 로그이기도 하다 🤓
진행 중인 프로젝트에 대해
우선 우리는 nodejs + express + typescript 로 개발 중이다. 전체적인 아키텍처는 아래와 같다. (Route53은 경우에 따라 사용하지 않을 수도 있다.)
사실 나는 리액트만 조금 해봤고 그마저도 리액트 개발자 분들이 보면 코웃음 칠 찍먹.. 노드 익스프레스 타입스크립트로 백엔드 개발이 처음이다. 동기 비동기도 더 많이 공부하고, 프로젝트로 실제 경험해보면서 블로그 작성을 해야겠다.
이제 DND 프로젝트 기간이 한달 남았고 그 안에 개발을 끝내야 하는 상황이라 부랴부랴 어제부터 모델 스키마 작성에 들어갔다.
우리는 ORM(객체-관계 매핑)으로 Sequelize 와 TypeORM 중 후자를 택했다. Sequelize와 비교했을 때의 장점을 찾아보니
그리고 개발 시작하면서 내가 실제로 느낀 최고의 장점은 migration을 따로 하지 않아도 디비 적용이 바로 된다는 점!!
TypeORM 써보기
우선 TypeORM 에는 두 가지 방식이 있다. 하나는 간단한 쿼리를 지향하고 작은 프로젝트에 어울리는 Active Record 방식이고 / 다른 하나는 일반적으로 스프링 프로젝트를 할 때와 비슷한 느낌의 Data Mapper 방식이다. 내가 스프링과 비슷하다는 느낌을 받은 것처럼 repository를 거쳐 의존성을 줄이는, 큰 프로젝트에 어울리는 방식이다.
Active Record와 Data Mapper 방식 비교
이제 직접 디비 연결을 해보자.
TypeORM 설치
npm install -g typeorm --save
npm install mysql --save
typeorm init --database mysql
이 명령어들로 설치를 해주고 간단한 설정 파일을 생성해준다. 마지막 커맨드로 ormconfig.js파일이 생성되고, tsconfig.json 파일이 수정되는데 ormconfig.js 파일의 경로는 루트 디렉토리 바로 밑이다 절대 변경하지 말 것!
ormconfig.js
const dotenv = require('dotenv');
dotenv.config();
module.exports = {
type: 'mysql',
host: process.env.DB_HOST,
port: process.env.DB_PORT,
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
synchronize: true,
logging: false,
dropSchema: true,
entities: ['./srcs/entity/*.ts'],
subscribers: ['srcs/migration/*.ts'],
migrations: ['srcs/migration/*.ts'],
};
env 파일에 디비 관련 정보들을 숨겨놓은 뒤 config 파일에서 불러오도록 했다. migration 설정과 cli 설정이 더 있는데, 우리 프로젝트에서는 당장 필요한 것 같지 않아서 필요하게 되면 작성할 것이다. migration 관련 내용은 여기 에 잘 정리되어 있는 것 같다.
export enum IsHouseOwner {
HOST = '세대주',
MEMBER = '세대구성원',
PRIVATE = '미공개',
}
내가 설정한 enum 들 중 하나를 예시로 가져왔다. 우리는 컬럼에서 enum을 사용할 부분들이 많았기 때문에 따로 파일을 분리해 관리하는게 좋을 것이라고 판단했다.
import {
Entity,
Column,
PrimaryGeneratedColumn,
BaseEntity,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { IsHouseOwner } from './common/Enums';
@Entity()
export class User extends BaseEntity { // Active Record 방식은 BaseEntity를 꼭 extend 해줘야 함
@PrimaryGeneratedColumn()
id!: number;
@Column('varchar', { name: 'nickname', unique: true, nullable: false, length: 50 })
nickname!: string;
@Column({ type: 'int', nullable: false })
age!: number;
@Column('varchar', { name: 'address', length: 50, nullable: false })
address!: string;
// 중략
@Column({ type: 'enum', name: 'is_house_owner', enum: IsHouseOwner })
isHouseOwner!: IsHouseOwner;
@CreateDateColumn({
type: 'timestamp',
name: 'created_at',
})
createdAt: Date | undefined;
@UpdateDateColumn({
type: 'timestamp',
name: 'updated_at',
})
updatedAt: Date | undefined;
}
export default User;
참고로 아직 프로젝트와 코드리뷰가 진행 중이기 때문에 형식은 바뀔 수 있다.
난 사실 typeorm이 처음이라 그런지 enum 타입 컬럼을 어떻게 정의해야될 지 많이 헤맸었는데,
Enum 타입 컬럼 대응하기
아래 단계부터는 실제 서버와 디비를 연결해주는 작업인데, 이 부분은 같이 하는 멋진 팀원 분께서 맡아주셨다.
import { getConnectionOptions, createConnection } from 'typeorm';
import { User } from '../entity/User';
const connectDb = async (): Promise<void> => {
const connectionOptions = await getConnectionOptions(process.env.NODE_ENV);
await createConnection({ ...connectionOptions })
.then((connection) => {
connection.getRepository(User);
console.log('DB connected');
})
.catch((err) => console.log(err));
};
export default connectDb;
export { default as connectDB } from './connectDB';
import app from './app';
import dotenv from 'dotenv';
import { connectDB } from './config/index';
dotenv.config();
const runServer = async () => {
await connectDB();
/* Run server */
console.log('Set application...');
app.listen(process.env.PORT, () => console.log(`server Run with port: ${process.env.PORT}`));
};
runServer();
npm start
했을 때 'DB connected' 와 'server Run with post: 포트번호' 메시지가 뜨고 localhost:포트번호 에 접속이 잘 되면 성공이다.
그리고 디비에 접속하면 자동으로 컬럼들이 sync 되어있는 것을 확인할 수 있다 - 난 이게 정말 큰 장점이자 단점이 될 수 있다고 생각한다. 아직 개발 초보이고 플젝 초기 단계라 장점으로 느껴지는게 더 크지만, 이런 작은 사이드 플젝이 아니라
실제 운영을 크게 해야 하는 규모 있는 서비스라면 migration을 직접 생성해주는 것이 관리에 좋을 것 같다.