[TypeORM] NestJS + TypeORM + Postgres에서 DB 마이그레이션 작업 사용하기 (+ 에러 정리)

루나·2022년 10월 27일
0

NestJS에서 TypeORM으로 PostgreSQL을 사용할 때 DB 스키마 변경, 등에서 중요한 마이그레이션 기능을 사용하기 위한 절차이다.

setup

yarn add typeorm@0.2.45 @nestjs/typeorm pg

의존성 패키지들을 설치한 뒤 package.json을 설정해준다.

// package.json
  "scripts": {
    // 이 부분의 --config 중요!!
    "typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js --config src/ormconfig.ts",
    "make:migrations": "yarn typeorm migration:generate -n",
    "make:migrate": "yarn typeorm migration:run",
    "make:rollback": "yarn typeorm migration:revert"
  }

TypeOrm 모듈에 설정 파일을 전달하기 위한 방법으로 ormconfig.ts를 넣어주는 방법을 사용한다.

# .env
DB_HOST=
DB_PORT=
DB_USER=
DB_PASSWORD=
DB_NAME=
// src/ormconfig.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm'
import * as dotenv from 'dotenv'

dotenv.config()

const ormconfig: TypeOrmModuleOptions = {
  type: 'postgres',
  host: process.env.DB_HOST,
  port: process.env.DB_PORT,
  username: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  logging: true,
  entities: [__dirname + '/**/*.entity{.ts,.js}'],

  // TypeOrm 자동 동기화, migration 작업을 위해 중지
  synchronize: false,

  // dist/migrations에 있는 파일들을 실행
  migrations: [__dirname + '/migrations/**/*{.ts,.js}'],
  // migrate run 자동 실행
  // migrationsRun: true,
  cli: {
    // entitiesDir: 'src/entities',
    // src/migrations에 있는 파일들을 dist/migrations에 생성
    migrationsDir: 'src/migrations',
  },
}

export default ormconfig
// src/app.module.ts
import ormconfig from './ormconfig'

@Module({
  imports: {
    ...
    TypeOrmModule.forRoot(ormconfig),
    ...
  ],
  ...

run

yarn make:migrations (이름)
yarn make:migrate
yarn make:rollback

yarn make:migrations {이름} 실행 시
1. DB 상태와 현재 Entity파일의 차이를 찾아낸다.
2. src/migrations 폴더에 ....-{이름}.ts를 생성한다.
3. dist/migrations 폴더에 ....-{이름}.js로 컨버팅된다.

yarn make:migrate / rollback은 각각 up, down (마이그레이션 실행, 되돌리기) 역할을 수행한다.


known error

'TypeOrmModuleOptions' 형식에 할당할 수 없습니다.

'{ type: "postgres"; host: string; port: number; username: string; password: string; database: string; logging: true; entities: string[]; synchronize: true; migrations: string[]; cli: { migrationsDir: string; }; }' 형식은 'TypeOrmModuleOptions' 형식에 할당할 수 없습니다.
개체 리터럴은 알려진 속성만 지정할 수 있으며 '{ retryAttempts?: number | undefined; retryDelay?: number | undefined; toRetry?: ((err: any) => boolean) | undefined; autoLoadEntities?: boolean | undefined; keepConnectionAlive?: boolean | undefined; verboseRetryLog?: boolean | undefined; } & Partial<...>' 형식에 'cli'이(가) 없습니다.ts(2322)

이 에러가 뜨는 이유는 TypeORM 신버전(0.3.x)에서 TypeOrmModuleOptions에 type으로 PostgreSQL을 설정했을때 cli 속성이 사라졌기 때문이다. 레퍼런스를 찾아본 결과 해당 옵션 없이도 해결할 수 있는 방법이 있긴 한 거 같지만 굳이 최신버전이어야 될 이유도 없고 굉장히 번거로웠기 때문에 다운그레이드로 해결했다.

// package.json
  "dependencies": {
    ...
    "typeorm": "^0.2.45",
    ...
  }

MissingDriverError: Wrong driver: "undefined" given. Supported drivers are: "aurora-data-api", "...".

[Nest] 23260 - 2022. 10. 27. 오전 11:15:58 ERROR [ExceptionHandler] Wrong driver: "undefined" given. Supported drivers are: "aurora-data-api", "aurora-data-api-pg", "better-sqlite3", "capacitor", "cockroachdb", "cordova", "expo", "mariadb", "mongodb", "mssql", "mysql", "nativescript", "oracle", "postgres", "react-native", "sap", "sqlite", "sqljs".
MissingDriverError: Wrong driver: "undefined" given. Supported drivers are: "aurora-data-api", "aurora-data-api-pg", "better-sqlite3", "capacitor", "cockroachdb", "cordova", "expo", "mariadb", "mongodb", "mssql", "mysql", "nativescript", "oracle", "postgres", "react-native", "sap", "sqlite", "sqljs".

nestjs에서 typeorm은 TypeOrmModule.forRoot()에 아무 인수도 제공하지 않고도 ormconfig.json을 자동으로 인식할 수 있지만 타입 관련해서 제대로 잡지 못해서 나타나는 에러였다.
원인은 아마 실행되는 순서일 것이라 생각되어 ormconfig을 ts파일로 만들어 타입을 부여해주는 것으로 해결했다.

// src/ormconfig.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm'

const ormconfig: TypeOrmModuleOptions = {
  type: 'postgres',
  ...
}
  
export default ormconfig

TypeORMError: No connection options were found in any orm configuration files.

이 에러는 typeorm 명령어를 사용할 때 config로 아무 파일도 전해지지 않았기 때문이다.
위의 에러와 마찬가지로 typeorm은 ormconfig.json은 자동으로 인식하지만 위의 과정으로 인해 ormconfig.ts로 변경했기 때문에 찾지 못해 발생한 에러였다.
명령어의 뒤에 --config 옵션으로 ormconfig.ts파일을 지정해준다.

// package.json
  "scripts": {
    ...
    "typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js --config src/ormconfig.ts",
    ...
  }

No migrations are pending

query: SELECT FROM current_schema()
query: SHOW server_version;
query: SELECT
FROM "information_schema"."tables" WHERE "table_schema" = 'public' AND "table_name" = 'migrations'
query: SELECT FROM "information_schema"."tables" WHERE "table_schema" = 'public' AND "table_name" = 'typeorm_metadata'
query: SELECT
FROM "migrations" "migrations" ORDER BY "id" DESC
No migrations are pending
✨ Done in 1.67s.

typeorm migration:generate 명령도 제대로 동작했고 migrations폴더에 마이그레이션에 관한 코드도 제대로 생성됐는데 migrations 파일을 찾을 수 없다는 에러가 발생했다.
ts파일을 js로 컨버팅하고 dist로 옮겨졌을 때 위치를 제대로 잡아주지 못해서 나타나는 에러였다.
ormconfig에서 migrations랑 migrationsDir에 적절한 위치를 잡아주면 해결된다.

// src/ormconfig.ts
const ormconfig: TypeOrmModuleOptions = {
  ...
  // migration:run 명령어에서 사용하는 위치
  // 컨버팅 이후의 __dirname: dist, dist/migrations에 있는 파일들을 실행
  migrations: [__dirname + '/migrations/**/*{.ts,.js}'],
  cli: {
    // migration:generate 명령어에서 사용하는 위치
    // 실제 프로젝트에서 migrations 폴더를 어디에 만들 것인지 지정한다.
    // src/migrations에 생성된 파일들은 tsc를 거쳐 dist/migrations에 생성된다.
    migrationsDir: 'src/migrations',
  },

migration 파일들은 src/migrations에 생성할 것이기 때문에 migrationsDir은 이렇게 지정한다.
이후 tsc를 거쳐 컨버팅된 파일들은 dist/migrations에 생성된다. (src -> dist)
ormconfig 파일도 마찬가지로 컨버팅되어 dist로 전달되고 dist에서 실행되며 이 시점의 경로를 migrations에 잡아줘야 해결된다.
'src/migrations'나 '/migrations/'이면 에러가 발생하며 __dirname을 사용해 제대로 dist 위치를 잡아주는 것으로 해결했다.


Error: EACCES: permission denied, scandir '/Library/Application Support/Apple/AssetCache/Data'

[Nest] 24076 - 2022. 10. 27. 오전 11:47:53 ERROR [TypeOrmModule] Unable to connect to the database. Retrying (1)...
Error: EACCES: permission denied, scandir '/Library/Application Support/Apple/AssetCache/Data'

이 에러 메시지 자체는 아마도 Mac에서만 발생할 것으로 예상되지만 발생 조건으론 위와 같이 경로를 잘못 지정했을 때 발생한다.
해당 에러가 발생했을 때의 상황은 ormconfig 파일을 따로 service로 만들어 주입받게해 TypeOrmModule로 집어넣는 방식을 선택했을 때였다.

// src/config/database/postgres-config.service./ts
@Injectable()
export class PostgresConfigService implements TypeOrmOptionsFactory {
  constructor(private configService: ConfigService) {}

  createTypeOrmOptions(): TypeOrmModuleOptions {
    return {
      entities: ['/**/*.entity{.ts,.js}'],

에러 메시지로는 권한이 없다고 하는데 에러의 원인은 entities필드의 경로가 잘못되었을 때 발생했으며 경로를 제대로 잡아주면 해결된다.

entities: [__dirname + '../../../**/*.entity{.ts,.js}'],

하지만 어차피 이 방법으론 typeorm 명령어에서 config를 넘겨주지 못했기 때문에 다른 방법을 선택하기로 했다.


이 과정중 굉장히 많은 에러가 발생했고 각 에러 하나하나를 해결하는데 꽤 많은 시간을 소모했으며 여기저기 검색해봐도 프로젝트마다 다른 ormconfig, package.json의 설정을 사용해 해결하기 어려웠다.
결국 해결한 결과도 ormconfig.ts가 typeorm에서 읽히게 하기 위해서 nest에 의존하지 않아야 했기 때문에 configService를 사용하지 못했고 dotenv.config()를 사용했다는 점이 찜찜하긴 했지만 ormconfig에 대한 내용을 중복해서 작성하기도 싫어 이쯤에서 타협하기로 했다.
이후에도 새 프로젝트를 처음 설정할 때 발생할 문제였기 때문에 기록으로 남긴다.

profile
백엔드 개발자

0개의 댓글