들어가며
백엔드 개발을 하다보면 데이터베이스 스키마가 변경되어 마이그레이션을 수행하거나, 데이터를 수동으로 조작해야 하는 경우가 종종 발생합니다. 이러한 작업을 수행할 때 데이터베이스에 직접 SQL 쿼리(Raw Query)를 실행할 수도 있고, 프로젝트의 코드베이스를 활용해 스크립트를 작성하고 실행하는 방법도 있습니다.
Raw Query 방식은 실수가 발생할 위험이 있고, 반복적으로 수행해야하는 작업인 경우 DB 스키마나 비즈니스 로직이 변경될 때마다 별도로 반영해야하는 불편함이 있습니다. 그래서 저는 ORM과 기존의 비즈니스 로직을 활용할 수 있는 스크립트 방식을 더 선호합니다.
NestJS는 의존성 주입(Dependency Injection, DI) 방식으로 동작하기 때문에, Service나 Repository 같은 프로바이더의 메서드를 활용하려면 객체를 직접 인스턴스화하는 것이 번거로울 수 있습니다. 이때 nestjs-command 라이브러리를 활용하면 NestJS의 DI를 그대로 사용할 수 있어 편리하게 스크립트를 작성하고 실행할 수 있습니다. 이번 글에서는 nestjs-command 를 활용하여 NestJS에서 효율적으로 스크립트 작업을 하는 방법을 정리해보겠습니다!
라이브러리 소개
nestjs-command는 NestJS에서 CLI(Command Line Interface) 명령어를 쉽게 정의하고 실행할 수 있도록 도와주는 라이브러리입니다. DI를 지원해 리포지토리의 코드를 쉽게 활용할 수 있고, yargs 기반으로 만들어져 CLI 명령어에 인자나 옵션도 추가할 수 있습니다.
기본 설정하기
1. npm 패키지 설치
$ npm install --save-dev nestjs-command yargs
$ npm install --save-dev @types/yargs
저는 로컬에서만 사용하는 것이 목적이어서 dev dependency로 설치(`--save-dev`) 했고, TypeScript를 사용하고있어서 `@types/yargs`도 설치했습니다.
2. CLI 명령어 구현을 위한 독립적인 모듈 추가
기존 프로젝트가 모노레포(monorepo)로 구성되어있는 상태이고, 커맨드 스크립트를 기존 로직과 분리하기 위해서 새로운 모듈로 추가하였습니다. (`admin`과 `api`는 기존 모노레포 모듈)
`nest g app task` 명령어로 추가할 수도 있고, 수동으로 파일을 추가할 수도 있습니다. 저는 `task` 모듈에 대해 `nest start`나 `nest build`와 같은 명령을 사용하지 않을 것이기 때문에, `nest-cli.json`에 포함되지 않아도 되어서 수동으로 추가했습니다!
3. 베이스 모듈 설정
앞에서 추가한 `task`모듈에서 nestjs-command 라이브러리를 활용해 스크립트를 실행하는 CLI 명령어를 구현하고 실행하기 위해 `task` 모듈의 베이스 모듈에서 `CommandModule`을 import 합니다. (`apps/task/src/task.module.ts`)
import { Module } from '@nestjs/common';
import { CommandModule } from 'nestjs-command';
@Module({
imports: [CommandModule],
})
export class TaskModule {}
4. CLI entrypoint 설정
커맨드 실행을 위한 entrypoint를 아래와같이 설정합니다. (`apps/task/src/cli.ts`)
import { NestFactory } from '@nestjs/core';
import { CommandModule, CommandService } from 'nestjs-command';
import { TaskModule } from './task.module';
async function bootstrap() {
const app = await NestFactory.createApplicationContext(TaskModule, {
logger: ['error'],
});
try {
await app.select(CommandModule).get(CommandService).exec();
await app.close();
} catch (error) {
console.error(error);
await app.close();
process.exit(1);
}
}
bootstrap();
4-1. NestJS 애플리케이션 컨텍스트 생성
const app = await NestFactory.createApplicationContext(TaskModule, {
logger: ['error'],
});
앞에서 설정한 `TaskModule`을 기반으로 NestJS 애플리케이션 컨텍스트를 생성합니다. 즉, HTTP 서버 없이 컨텍스트만 생성해 DI 컨테이너만 활성화하는 것입니다. DI 컨테이너를 이용해서 `TaskModule`과 그 내부의 의존성(서비스, 리포지토리, 프로바이더 등)을 사용할 수 있게됩니다.
라이브러리 README에서는 비즈니스로직이 있는 메인 `AppModule`(제 프로젝트의 경우, `ApiModule`)을 기반으로 애플리케이션 컨텍스트를 생성하지만, 저는 의존성을 최대한 분리하기 위해 `TaskModule`을 기반으로 생성하였습니다.
4-2. CLI 명령어 실행
await app.select(CommandModule).get(CommandService).exec();
NestJS DI 컨테이너에서 `CommandModule`의 `CommandService` 인스턴스를 가져와 CLI 명령어를 실행(`exec`)하는 부분입니다.
스크립트 작성하고 실행하기
예제 스크립트와 CLI 명령어를 구현하고 실행해보겠습니다.
1. 커맨드 스크립트 작성
`apps/task/src/product/product.task.ts`
import { Command, Option, Positional } from 'nestjs-command';
@Injectable()
export class ProductTask {
constructor(
private readonly prisma: PrismaTransaction,
private readonly productService: ProductService,
) {}
/** pnpm run:command update:product id (--price=10000) */
@Command({
command: 'update:product <id>',
describe: 'update product',
})
async getProducts(
@Positional({
name: 'id',
describe: 'id',
type: 'string',
})
id: string,
@Option({
name: 'price',
describe: 'price',
type: 'number',
})
price: number,
) {
try {
const product = await this.productService.getProductById(id);
if (!product) {
throw new Error('Product not found');
}
await this.prisma.tx.product.update({
where: { id },
data: { price: price || product.price }, updatedAt: new Date() },
});
} catch (error) {
console.log(error);
}
}
}
- `Command` 데코레이터의 `command` 프로퍼티로 명령어를 지정합니다.
- `Positional` 데코레이터로 명령어의 인자(argument)를 추가할 수 있습니다.
- `Positional` 인자의 경우, `command` 프로퍼티에 지정하는 명령어에 `<id>`와 같이 추가해주어야합니다.
- `Option` 데코레이터로 명령어의 옵션을 추가할 수 있습니다.
`apps/task/src/task.module.ts`
@Module({
imports: [NestCommandModule, DatabaseModule, ProductModule],
providers: [ProductTask],
})
export class TaskModule {}
`TaskModule`도 `ProductTask`를 사용할 수 있도록 수정합니다.
2. 커맨드 실행
디폴트 entrypoint 경로가 `./src/cli.ts` 인데, 저는 `./apps/task/src/cli.ts` 에 entrypoint를 두었기때문에 커맨드를 실행할 때 환경변수(`CLI_PATH`) 설정을 추가로 해주어야 합니다.
`CLI_PATH=./apps/command/src/cli.ts npx nestjs-command update:product 101 --price=5000`
id가 101번인 상품의 가격을 5000원으로 업데이트하는 커맨드를 이와같이 실행할 수 있습니다.
`package.json`에 스크립트를 추가하면 더 간단하게 명령어를 사용할 수 있습니다.
{
...
"scripts": {
...
"run:command": "CLI_PATH=./apps/command/src/cli.ts npx nestjs-command",
}
}
=> `npm run:command update:product 101 --price=5000`
마무리
이번 글에서는 nestjs-command 라이브러리를 활용하여 NestJS에서 스크립트를 작성하고 CLI 명령어로 실행하는 방법을 정리해 보았습니다. 이를 통해 기존 비즈니스 로직을 손쉽게 재사용 하고 API 서버와 분리된 환경에서 안전하게 데이터 조작 작업을 수행할 수 있게되었습니다! 앞으로 프로젝트에서 수동 데이터 조작 작업이 필요할 때 SQL을 직접 수행하기보다는 nestjs-command를 적용해보는 것을 추천합니다.
참고 자료
https://www.npmjs.com/package/nestjs-command
nestjs-command
nest.js command tool. Latest version: 3.1.4, last published: 2 years ago. Start using nestjs-command in your project by running `npm i nestjs-command`. There are 19 other projects in the npm registry using nestjs-command.
www.npmjs.com
'개발 > Node.js, TypeScript' 카테고리의 다른 글
[NestJS] Prisma Transaction (0) | 2025.03.02 |
---|---|
[NestJS] 마이크로서비스와 하이브리드 애플리케이션 구성하기 (1) | 2025.02.02 |
[Node.js] 인앱결제 서버 개발기2 (구글 플레이스토어) (0) | 2023.07.23 |
[Node.js] 인앱결제 서버 개발기1 (앱스토어) (1) | 2023.07.19 |