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 문제가 생긴다.tsconfig의pathsalias가 런타임에 해결되지 않는다.- NestJS의 webpack 빌드와 TypeORM의 직접 import가 부딪힌다.
이 문서는 그 함정들의 카탈로그와 각 함정의 정착된 우회로를 다룬다.
사고 흐름
Why — CLI가 왜 ts-node에 의존하나
TypeORM이 마이그레이션을 generate하려면:
- entity 클래스를 런타임에 불러와 메타데이터를 읽어야 한다.
- 데코레이터가 reflect-metadata로 박은 정보를 Node 프로세스 안에 가져야 한다.
- 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.ts는 NestJS 모듈 시스템과 무관하게 만들고, 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 부팅 시개발용 CLI와 production 부팅이 다른 경로를 본다 — 이 둘을 환경 변수로 분기하거나, 두 개의 datasource 파일을 둔다.
What — typeorm-ts-node-commonjs 패키지가 하는 일
내부적으로 정확히 두 줄이다 (단순화):
#!/usr/bin/env node
require('ts-node/register');
require('typeorm/cli');즉 ts-node를 등록한 뒤 TypeORM CLI를 호출하는 얇은 wrapper다. 완전한 마법이 아니라 세 단계를 한 명령어로 묶은 것뿐 — 그래서 그 안의 함정들은 여전히 다 자기 자리에 있다.
What — 방언 드라이버 설치 누락
| 방언 | 필요한 npm |
|---|---|
| PostgreSQL | pg |
| MySQL/MariaDB | mysql2 |
| SQLite | sqlite3 (또는 better-sqlite3) |
| MSSQL | mssql |
| Oracle | oracledb |
| MongoDB | mongodb |
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",pathsalias,reflect-metadata누락, NestJS webpack,.env로딩, glob 경로, 방언 드라이버. ESM 환경은 2024년 기준 여전히 불안정 — 마이그레이션 도구는 CommonJS 분리가 가장 안전한 선택. CLI 부트스트랩 함정의 무게가 프레임워크 선택을 바꿀 만큼 크다 — Prisma·Drizzle이 세팅의 짧음을 무기로 시장을 가져가는 첫 이유. 챕터의 끝. 이 챕터 인덱스(index)로 돌아가 전 도메인 지도(TypeORM 도메인 홈)로 이어진다.