기존 프로젝트에서는 synchronize: true 설정을 통해서 데이터베이스 스키마를 설정하였다.
문제는 위와 같은 설정을 한다면 애플리케이션이 시작될 때 TypeORM이 엔티티를 검사하고, 현재 데이터베이스 스키마와 비교한 후, 변경된 부분이 있다면 자동으로 수정한다는 것이다.
개발 환경에서는 즉각적인 피드백을 받을 수 있기 때문에, 유용하게 사용할 수 있지만 프로덕션 환경에서는 의도치 않은 데이터 손실이 발생할 수 있기 때문에, 위와 같은 설정은 지양해야만 한다.
프로젝트는 혼자서 작업하는게 아니다. 여러 개발자가 동시에 작업할 때, 마이그레이션을 사용한다면 데이터베이스 관리자를 통해 모든 변경 사항을 사전에 검토하고 승인할 수 있어 안정성을 높일 수 있다.
누가 어떤 변경을 했는지 쉽게 파악할 수 있고, 각 마이그레이션 파일에는 변경된 날짜와 시간이 포함되어 있기 때문에, 이를 통해 변경 사항이 언제 적용되었는지도 확인할 수 있다.
또한 변경 사항을 적용한 후 문제가 발생했을 때, 개발자가 쉽게 이전 상태로 되돌릴 수 있다.
// datasource.config.ts
import { DataSource, DataSourceOptions } from 'typeorm';
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
import * as dotenv from 'dotenv';
import * as path from 'path';
dotenv.config({
path: process.env.NODE_ENV === 'production' ? '.production.env' : '.development.env',
});
const entityPath = path.join(__dirname, '../entities/*/*.entity.{js,ts}');
const migrationPath = path.join(__dirname, '../migrations/*.{js,ts}');
export const dataSourceOptions: DataSourceOptions = {
type: 'mysql',
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
entities: [entityPath],
synchronize: false,
migrationsRun: false,
namingStrategy: new SnakeNamingStrategy(),
migrationsTableName: 'migrations',
migrations: [migrationPath],
logging: process.env.NODE_ENV === 'production' ? ['error'] : true,
};
export const MysqlDataSource = new DataSource(dataSourceOptions);
기존 설정에서는 서버가 실행될 때, 데이터베이스와 연동할 수 있었기 때문에, 동적으로 환경 변수를 설정하기 위해서 ConfigService를 사용하였다.
그러나, migration을 실행하기 위해서는 서버가 실행되기 전에 typeorm-cli를 통해 적용해야만 했다.
따라서, dotenv를 이용하여 NODE_ENV에 설정되어 있는 값에 따라 환경 변수를 가져와 사용할 수 있도록 하였다.
// typeorm.module.ts
@Module({
imports: [TypeOrmModule.forRoot(dataSourceOptions)]
...
})
export class TypeOrmModule {}
기존에 데이터베이스와 연동하기 위해 작성해놓은 코드가 존재했지만, 데이터베이스의 설정을 일관적으로 유지하기 위해 작성해둔 dataSourceOptions를 가져와 적용하였다.
"scripts": {
...
"typeorm": "node -r ts-node/register ./node_modules/typeorm/cli.js",
"typeorm:dev": "cross-env NODE_ENV=development node -r ts-node/register ./node_modules/typeorm/cli.js -d src/configs/datasource.config.ts",
"typeorm:prod": "cross-env NODE_ENV=production node -r ts-node/register ./node_modules/typeorm/cli.js -d src/configs/datasource.config.ts"
},
TypeScript로 작성된 파일은 Node.js 환경에서 직접 실행할 수 없다. 따라서, ts-node를 사용하여 TypeScript로 작성된 파일을 직접 실행할 수 있도록 하였다.
또한 TypeORM CLI를 이용해서 데이터베이스 연결 설정 파일을 통해 마이그레이션할 수 있도록 스크립트를 작성하였다.
// 명령어
npm run typeorm migration:create ./src/{migrations-dir}/{filename}
// 명령어 예시
npm run typeorm migration:create ./src/migrations/CreateUserTable
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateUserTable1718647700144 implements MigrationInterface {
name = 'CreateUserTable1718647700144';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS user (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'PK',
email VARCHAR(50) NOT NULL UNIQUE COMMENT '유저 이메일',
password VARCHAR(100) NULL COMMENT '유저 비밀번호',
provider VARCHAR(50) NULL COMMENT 'OAuth 제공자',
provider_id VARCHAR(100) NULL COMMENT 'OAuth 제공자 id',
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '생성 시간',
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)
ON UPDATE CURRENT_TIMESTAMP(6) COMMENT '수정 시간'
) ENGINE=InnoDB;
`);
await queryRunner.query(`ALTER TABLE user COMMENT = '유저의 중요 정보를 관리하는 테이블';`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS user;`);
}
}
생성된 마이그레이션 파일에 위처럼 SQL문을 작성해주면 된다.
up() 메서드를 통해 테이블과 필요한 주석을 생성하고, down() 메서드를 통해 생성된 테이블을 삭제한다.
// 실행 명령어
npm typeorm:prod migration:run
// 롤백 명령어
npm typeorm:prod migration:revert
마이그레이션 실행 명령어를 입력하면,
migrations 테이블이 생성되고,
TypeORM이 현재 데이터베이스 상태와 마이그레이션 파일을 비교하여, 아직 실행되지 않은 마이그레이션을 순서대로 실행한다. 이를 통해 데이터베이스 스키마를 최신 사앹로 업데이트한다.