🗄️ TypeORM6. Migration & Schema06 · CLI & datasource.ts 설정

06 · CLI & datasource.ts왜 마이그레이션을 시작도 못 하는가

질문: typeorm migration:generate세팅에서부터 안 돈다 — 왜? 한 줄 답: TypeORM CLI는 ts-node 위에서 datasource.ts를 평가하는 구조다. ESM, "type": "module", path alias, decorators experimental몇 개의 환경 변수맞지 않으면 즉시 깨진다. typeorm-ts-node-commonjs사실상 표준 우회로다.


Pyramid Top

TypeORM CLI는 컴파일된 JS도 받지만, 대부분의 사람TypeScript 소스 그대로 돌리길 원한다 — 그래야 entity·migration 폴더src/ 안에서 한 줄로 끝난다.

문제: TypeORM CLI 자체는 .ts를 모른다. 그래서 ts-node를 끼워야 한다. 그런데:

  • TypeScript의 decorator 메타데이터를 켜야 한다.
  • "type": "module"이면 ESM 동적 import 문제가 생긴다.
  • tsconfigpaths alias가 런타임에 해결되지 않는다.
  • NestJS의 webpack 빌드TypeORM의 직접 import가 부딪힌다.

이 문서는 그 함정들의 카탈로그각 함정의 정착된 우회로를 다룬다.


사고 흐름


Why — CLI왜 ts-node에 의존하나

TypeORM이 마이그레이션을 generate하려면:

  1. entity 클래스런타임에 불러와 메타데이터를 읽어야 한다.
  2. 데코레이터가 reflect-metadata로 박은 정보를 Node 프로세스 안에 가져야 한다.
  3. DataSource 설정(connection, entities 경로 등)을 알아야 한다.

이 셋은 JS 파일에서도 가능하지만, 소스가 TS인 환경에서는 컴파일 단계를 거치거나, ts-node로 즉석 평가하거나 둘 중 하나다. 후자가 DX훨씬 짧아서 사실상 표준이 됐다.


How — 정착된 데이터 소스 패턴

src/data-source.ts (CommonJS 기준)

import 'reflect-metadata';
import { DataSource } from 'typeorm';
import { User } from './entities/User';
import { Team } from './entities/Team';
 
export const AppDataSource = new DataSource({
  type: 'postgres',
  host: process.env.DB_HOST ?? 'localhost',
  port: Number(process.env.DB_PORT ?? 5432),
  username: process.env.DB_USER,
  password: process.env.DB_PASS,
  database: process.env.DB_NAME,
 
  synchronize: false,         // 절대 true 금지
  migrationsRun: false,       // CLI에서 직접 돌릴 거면 false
  logging: ['error', 'migration'],
 
  entities: [User, Team],     // *명시* 권장 (glob도 가능)
  migrations: ['src/migrations/*.ts'],
  migrationsTableName: 'migrations',
});

package.json 스크립트

{
  "scripts": {
    "typeorm": "typeorm-ts-node-commonjs",
    "migration:generate": "npm run typeorm -- migration:generate -d src/data-source.ts",
    "migration:create":   "npm run typeorm -- migration:create",
    "migration:run":      "npm run typeorm -- migration:run -d src/data-source.ts",
    "migration:revert":   "npm run typeorm -- migration:revert -d src/data-source.ts",
    "migration:show":     "npm run typeorm -- migration:show -d src/data-source.ts"
  }
}

tsconfig.json 필수 옵션

{
  "compilerOptions": {
    "target": "ES2021",
    "module": "commonjs",
    "experimentalDecorators": true,   // ← 데코레이터 활성화
    "emitDecoratorMetadata": true,    // ← @Column 타입을 메타로 박기
    "strict": true
  }
}

emitDecoratorMetadata데코레이터가 column type을 추론하는 핵심 토큰. 끄면 모든 @Column이 type unknown으로 박힌다.


What — 함정 카탈로그

함정 1 — "type": "module" (ESM)

// package.json
{ "type": "module" }

TypeORM CLI는 기본 CommonJS 가정이다. ESM 모드에서는 import 문이 동적이어야 하고, .ts 파일을 ts-node로 평가하는 방식도 완전히 다르다.

증상:

SyntaxError: Cannot use import statement outside a module

우회로:

  • "type": "module"지운다 (가장 단순).
  • 또는 typeorm-ts-node-esm을 쓴다 (CLI ESM 버전, 불안정).
  • 또는 컴파일된 JS로 CLI를 돌린다 (migrations: ['dist/migrations/*.js']).

함정 2 — tsconfig paths alias

// tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@/*": ["src/*"]
    }
  }
}
// data-source.ts
import { User } from '@/entities/User';  // ← ts-node가 못 찾음

ts-node 기본paths를 해결하지 않는다.

우회로:

// package.json
{
  "scripts": {
    "typeorm": "node -r tsconfig-paths/register -r ts-node/register ./node_modules/typeorm/cli.js"
  }
}

또는 상대 경로로 import (from '../entities/User').

함정 3 — reflect-metadata 누락

// data-source.ts 상단에 *반드시*
import 'reflect-metadata';

이 import가 없으면 — 모든 데코레이터가 작동하지만 메타가 박히지 않는다. 증상은 런타임 에러가 아니라 generate가 빈 SQL을 뱉는 것 → 디버깅이 어렵다.

reflect-metadata프로젝트 진입점마다 한 번씩 import해야 한다data-source.ts, app.ts, 테스트 파일의 setup 등.

함정 4 — NestJS의 webpack 빌드

NestJS는 기본적으로 webpack으로 컴파일한다. 마이그레이션 CLI는 src 그대로 돌리려 한다 — 두 빌드가 부딪힌다.

우회로: NestJS 프로젝트에서도 마이그레이션은 별도 entry로 돌린다. data-source.tsNestJS 모듈 시스템과 무관하게 만들고, CLI도 그 파일만 본다.

함정 5 — 환경변수 로딩 누락

// data-source.ts
import { config } from 'dotenv';
config(); // ← 반드시
 
import { DataSource } from 'typeorm';
// ...

CLI는 애플리케이션이 아니다.env 자동 로딩이 없다. 수동으로 dotenv.config()를 호출해야 한다. 빠지면 DB 호스트가 undefined로 잡혀 연결 실패.

함정 6 — entities glob vs 명시 배열

// glob 방식
entities: ['src/entities/*.ts']
// 명시 방식
entities: [User, Team, Project]

glob은 경로 환경에 따라 깨진다 — 컴파일된 dist에서는 .js, src에서는 .ts. 큰 프로젝트는 명시 배열더 안전하다. 작은 프로젝트는 glob도 무방.

함정 7 — migrations glob 확장자

migrations: ['src/migrations/*.ts'],  // CLI 직접 실행 시
// vs
migrations: ['dist/migrations/*.js'], // 컴파일된 production 부팅 시

개발용 CLIproduction 부팅다른 경로를 본다 — 이 둘을 환경 변수로 분기하거나, 두 개의 datasource 파일을 둔다.


What — typeorm-ts-node-commonjs 패키지가 하는 일

내부적으로 정확히 두 줄이다 (단순화):

#!/usr/bin/env node
require('ts-node/register');
require('typeorm/cli');

ts-node를 등록한 뒤 TypeORM CLI를 호출하는 얇은 wrapper다. 완전한 마법이 아니라 세 단계를 한 명령어로 묶은 것뿐 — 그래서 그 안의 함정들은 여전히 다 자기 자리에 있다.


What — 방언 드라이버 설치 누락

방언필요한 npm
PostgreSQLpg
MySQL/MariaDBmysql2
SQLitesqlite3 (또는 better-sqlite3)
MSSQLmssql
Oracleoracledb
MongoDBmongodb

TypeORM은 드라이버를 자동 설치하지 않는다. 빠지면:

DriverPackageNotInstalledError: Postgres package has not been found installed.

What-if — ESM 환경의 한계

ESM 모드에서 TypeORM CLI는 여전히 불안정하다. 2024년 기준:

시도결과
typeorm-ts-node-esm + "type": "module"일부 명령은 됨, generate는 간헐 실패
ESM + 컴파일된 JS만 사용안전하지만 매번 build 필요
Bun으로 실행부분 호환, 일부 entity 메타가 안 잡힘
tsx + CLI 직접 호출가장 호환성 좋음, 비공식

현실적 권장: 마이그레이션 도구만큼은 CommonJS로 유지. 애플리케이션은 ESM이어도, datasource.ts와 CLI는 CommonJS 모드로 분리한다. 두 모드를 한 프로젝트에서 함께 살리는 패턴은 tsconfig.cjs.json + package.json의 sub-export로 푼다.


What — 디버깅 명령

# DataSource가 초기화 가능한지 확인 (entity 자체 검증)
npx typeorm-ts-node-commonjs migration:show -d src/data-source.ts
 
# 로그를 최대치로
TYPEORM_LOGGING=true npx ... migration:generate ...
 
# 어떤 paths/tsconfig가 잡히는지
TS_NODE_PROJECT=tsconfig.cli.json npx ...

흥미로운 이야기

typeorm-ts-node-commonjs별도 패키지로 분리되었다 — 그 자체가 한 함정의 역사”

TypeORM 0.2.x 시대에는 typeorm 명령어 하나가 ts-node도 처리하려 했다. 그런데 프로젝트마다 tsconfig 모양이 너무 달라서CLI에 ts-node를 박아두는 것더 많은 문제를 만들었다. 0.3.x에서 별도 패키지(typeorm-ts-node-commonjs, typeorm-ts-node-esm)로 떼어냈다. 이게 해결인 동시에 고통의 추가였다 — 이제 어떤 패키지를 깔아야 하는지 처음 사용자는 모른다. TypeORM의 거의 모든 실패의 출처이 CLI 부트스트랩이고, 사실상 Prisma·Drizzle이 TypeORM을 위협하는 첫 번째 이유가 *“세팅이 한 줄이다”*다. CLI 함정의 무게는 프레임워크 선택을 바꿀 만큼 크다.


Insight — 기본값생태계 표준의 격차

TypeORM의 기본값은 2016년에 잡혔다 — ESM도 없고, NestJS도 없고, monorepo도 없던 시절. 그 기본값이 오늘의 환경맞지 않는 자리함정이 된다. 개별 함정의 우회로는 다 있고, 그 우회로의 모음이 사실상 표준이다 — 문서가 따라잡지 못한 자리.


한 단락 요약

TypeORM CLI는 ts-node 위에서 datasource.ts를 평가하는 구조다. typeorm-ts-node-commonjs 패키지가 ts-node 등록 + CLI 호출을 한 명령어로 묶은 사실상 표준. 함정은 카탈로그로 외운다"type": "module", paths alias, reflect-metadata 누락, NestJS webpack, .env 로딩, glob 경로, 방언 드라이버. ESM 환경은 2024년 기준 여전히 불안정 — 마이그레이션 도구는 CommonJS 분리가 가장 안전한 선택. CLI 부트스트랩 함정의 무게가 프레임워크 선택을 바꿀 만큼 크다 — Prisma·Drizzle이 세팅의 짧음을 무기로 시장을 가져가는 첫 이유. 챕터의 끝. 이 챕터 인덱스(index)로 돌아가 전 도메인 지도(TypeORM 도메인 홈)로 이어진다.