여러 Angular CLI 명령어는 애플리케이션 빌드, 테스트, 실행과 같이 코드에 복잡한 작업을 수행합니다. 이 명령어는 Architect라는 내부 도구를 사용해서 CLI 빌더 를 실행하며, 빌더는 다른 도구(번들러, 테스트 실행기, 서버)를 호출해서 원하는 작업을 수행합니다. 커스텀 빌더로 완전히 새로운 작업을 수행하거나 기존 명령어가 사용하는 서드 파티 도구를 변경할 수 있습니다.
이 문서에서는 CLI 빌더가 워크스페이스 설정 파일과 어떻게 통합되는지 설명하고 직접 빌더를 만드는 방법을 알아봅니다.
도움말: 여기에서 사용하는 예제 코드는 GitHub 저장소에서 확인할 수 있습니다.
CLI 빌더
내부 도구인 Architect는 빌더 라고 부르는 핸들러 함수에 작업을 위임합니다. 빌더 핸들러 함수는 두 인자를 받습니다:
| 인자 | 타입 |
|---|---|
options |
JSONObject |
context |
BuilderContext |
여기서 관심사를 분리하는 방식은 ng generate처럼 코드를 변경하는 다른 CLI 명령어에서 사용하는 스키매틱과 같습니다.
options객체는 CLI 사용자가 지정한 옵션과 설정으로 구성되며context객체는 CLI Builder API가 자동으로 제공합니다.context객체는 실행 맥락에 대한 정보 외에도 스케줄링 메서드인context.scheduleTarget()을 제공합니다. 스케줄러는 지정한 대상 설정으로 빌더 핸들러 함수를 실행합니다.
빌더 핸들러 함수는 동기적으로 값을 반환하거나 비동기적으로 Promise를 반환할 수 있으며, 변경을 감지하면서 여러 값을 반환하도록 Observable을 반환할 수도 있습니다.
반환값은 항상 BuilderOutput 타입이어야 합니다.
이 객체에는 불리언 success 필드와 오류 메시지를 담을 수 있는 선택적인 error 필드가 있습니다.
Angular는 CLI가 ng build, ng test 등의 명령어에서 사용하는 빌더를 제공합니다.
이 빌더와 다른 기본 CLI 빌더의 기본 대상 설정은 워크스페이스 설정 파일인 angular.json의 "architect" 항목에서 확인하고 변경할 수 있습니다.
직접 빌더를 만들어 Angular를 확장하고 원하는 방식으로 구성할 수도 있으며, 이 빌더는 ng run CLI 명령어로 바로 실행할 수 있습니다.
빌더 프로젝트 구조
빌더는 Angular 워크스페이스와 비슷한 구조의 "프로젝트" 폴더에 위치합니다.
최상위에는 전역 설정 파일이 있고, 소스 폴더에는 구체적인 설정과 동작을 정의하는 코드 파일이 있습니다.
예를 들어 myBuilder 폴더는 다음 파일을 포함할 수 있습니다.
| 파일 | 용도 |
|---|---|
src/my-builder.ts |
빌더를 정의하는 주요 소스 파일입니다. |
src/my-builder.spec.ts |
테스트 소스 파일입니다. |
src/schema.json |
빌더 입력 옵션을 정의합니다. |
builders.json |
빌더를 정의합니다. |
package.json |
의존성입니다. https://docs.npmjs.com/files/package.json을 참고하세요. |
tsconfig.json |
TypeScript 설정입니다. |
빌더는 npm에 배포할 수 있습니다.
라이브러리 배포하기를 참고하세요.
빌더 만들기
예제로 파일을 새 위치에 복사하는 빌더를 만들어 봅시다.
빌더는 CLI Builder의 createBuilder() 함수로 생성하고 Promise<BuilderOutput> 객체를 반환하도록 작성합니다.
src/my-builder.ts (빌더 기본 구조)
import {BuilderContext, BuilderOutput, createBuilder} from '@angular-devkit/architect';
import {JsonObject} from '@angular-devkit/core';
interface Options extends JsonObject {
source: string;
destination: string;
}
export default createBuilder(copyFileBuilder);
async function copyFileBuilder(options: Options, context: BuilderContext): Promise<BuilderOutput> {
}
이제 로직을 추가해 봅시다.
다음 코드는 사용자 옵션에서 원본 및 대상 파일 경로를 가져온 뒤 파일을 원본에서 대상으로 복사합니다(Node.js 내장 copyFile() 함수의 Promise 버전을 사용합니다).
복사에 실패하면 문제의 원인을 설명하는 메시지와 함께 오류를 반환합니다.
src/my-builder.ts (빌더)
import {BuilderContext, BuilderOutput, createBuilder} from '@angular-devkit/architect';
import {JsonObject} from '@angular-devkit/core';
import {promises as fs} from 'fs';
interface Options extends JsonObject {
source: string;
destination: string;
}
export default createBuilder(copyFileBuilder);
async function copyFileBuilder(options: Options, context: BuilderContext): Promise<BuilderOutput> {
try {
await fs.copyFile(options.source, options.destination);
} catch (err) {
return {
success: false,
error: (err as Error).message,
};
}
return {success: true};
}
출력 처리하기
copyFile()은 기본적으로 프로세스의 표준 출력이나 표준 오류에 아무것도 출력하지 않습니다.
오류가 발생하면 당시 빌더가 정확히 어떤 작업을 하려 했는지 파악하기 어려울 수 있습니다.
Logger API로 추가 정보를 기록해서 실행 맥락을 더 제공하세요.
이렇게 하면 표준 출력과 표준 오류를 비활성화한 상태에서도 빌더 자체를 별도 프로세스로 실행할 수 있습니다.
컨텍스트에서 Logger 인스턴스를 가져올 수 있습니다.
src/my-builder.ts (출력 처리)
try {
await fs.copyFile(options.source, options.destination);
} catch (err) {
context.logger.error('Failed to copy file.');
return {
success: false,
error: (err as Error).message,
};
}
진행률과 상태 보고하기
CLI Builder API는 특정 기능과 인터페이스에 정보를 제공할 수 있는 진행률 및 상태 보고 도구를 포함합니다.
진행률을 보고하려면 현재 값, 선택적인 전체 값, 상태 문자열을 인자로 받는 context.reportProgress() 메서드를 사용합니다.
전체 값은 어떤 숫자든 사용할 수 있습니다.
예를 들어 처리해야 할 파일 수를 알고 있다면 전체 값은 파일 수가 되고 현재 값은 지금까지 처리한 파일 수가 됩니다.
상태 문자열은 새 문자열 값을 전달하지 않는 한 변경되지 않습니다.
이 예제의 복사 작업은 완료되었거나 실행 중인 상태만 있으므로 진행률을 보고할 필요는 없습니다.
하지만 이 빌더를 호출한 상위 빌더가 현재 상황을 알 수 있도록 상태를 보고할 수 있습니다.
context.reportStatus() 메서드로 길이에 제한 없이 상태 문자열을 지정하세요.
도움말: 긴 문자열이 항상 모두 표시되는 것은 아닙니다. 표시하는 UI에 맞춰 잘릴 수 있습니다.
상태를 제거하려면 빈 문자열을 전달하세요.
src/my-builder.ts (진행률 보고)
context.reportStatus(`Copying ${options.source} to ${options.destination}.`);
try {
await fs.copyFile(options.source, options.destination);
} catch (err) {
context.logger.error('Failed to copy file.');
return {
success: false,
error: (err as Error).message,
};
}
context.reportStatus('Done.');
return {success: true};
빌더 입력
빌더는 ng build 같은 CLI 명령어를 통해 간접적으로 호출하거나 Angular CLI의 ng run 명령어로 직접 호출할 수 있습니다.
어느 경우든 필수 입력은 제공해야 합니다.
나머지 입력에는 특정 대상 에 미리 지정된 기본값, 설정에 지정된 값, 명령줄에서 지정한 값을 사용할 수 있습니다.
입력 유효성 검사
빌더 입력은 해당 빌더에 연결된 JSON 스키마에 정의합니다.
스키매틱과 마찬가지로 Architect는 최종 입력값을 options 객체에 모으고, 빌더 함수에 전달하기 전에 스키마를 기준으로 타입을 검증합니다.
이 예제 빌더의 options는 source와 destination이라는 두 키를 가진 JsonObject여야 하며, 두 값은 모두 문자열입니다.
다음 스키마로 이 값들의 타입을 검증할 수 있습니다.
schema.json
{
"$schema": "https://json-schema.org/schema",
"type": "object",
"properties": {
"source": {
"type": "string"
},
"destination": {
"type": "string"
}
}
}
도움말: 간단한 예제이지만 스키마를 활용하면 강력한 유효성 검사를 수행할 수 있습니다. 자세한 내용은 JSON 스키마 웹사이트를 참고하세요.
빌더 구현을 스키마 및 이름과 연결하려면 빌더 정의 파일을 생성하고 package.json에서 해당 파일을 가리키도록 해야 합니다.
다음과 같이 builders.json 파일을 생성하세요:
builders.json
{
"builders": {
"copy": {
"implementation": "./dist/my-builder.js",
"schema": "./src/schema.json",
"description": "Copies a file."
}
}
}
package.json에 builders 키를 추가해서 Architect에 빌더 정의 파일의 위치를 알려주세요.
package.json
{
"name": "@example/copy-file",
"version": "1.0.0",
"description": "Builder for copying files",
"builders": "builders.json",
"dependencies": {
"@angular/build": "^21.2.0"
}
}
이제 빌더의 공식 이름은 @example/copy-file:copy입니다.
앞부분은 패키지 이름이고 뒷부분은 builders.json 파일에 지정된 빌더 이름입니다.
입력값은 options.source와 options.destination으로 접근합니다.
src/my-builder.ts (상태 보고)
await fs.copyFile(options.source, options.destination);
대상 설정
빌더에는 특정 입력 설정 및 프로젝트와 연결하는 대상이 정의되어 있어야 합니다.
대상은 angular.json CLI 설정 파일에 정의합니다.
대상에는 사용할 빌더, 기본 옵션 설정, 이름이 있는 대체 설정을 지정합니다.
Angular CLI의 Architect는 대상 정의를 사용해서 실행에 필요한 입력 옵션을 결정합니다.
angular.json 파일에는 프로젝트별 항목이 있으며, 각 프로젝트의 "architect" 항목은 'build', 'test', 'serve' 등의 CLI 명령어에서 사용하는 빌더의 대상을 설정합니다.
예를 들어 ng build 명령어는 기본적으로 @angular/build:application 빌더로 빌드 작업을 수행하고, angular.json의 build 대상에 지정된 기본 옵션 값을 전달합니다.
angular.json
{
"myApp": {
"...": "...",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"outputPath": "dist/myApp",
"index": "src/index.html",
"...": "..."
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"...": "..."
}
}
},
"...": "..."
}
}
}
명령어는 "options" 항목에 지정된 기본 옵션을 빌더에 전달합니다.
--configuration=production 플래그를 전달하면 production 설정에 지정된 값으로 덮어씁니다.
명령줄에서 개별 옵션을 지정하면 추가로 덮어쓸 수 있습니다.
대상 문자열
범용 CLI 명령어인 ng run은 첫 번째 인자로 다음 형식의 대상 문자열을 받습니다.
project:target[:configuration]
| 항목 | 설명 |
|---|---|
| project | 대상이 연결된 Angular CLI 프로젝트의 이름입니다. |
| target | angular.json 파일의 architect 항목에 정의된 이름 있는 빌더 설정입니다. |
| configuration | 선택 사항입니다. angular.json 파일에서 해당 대상에 정의한 특정 대체 설정의 이름입니다. |
빌더에서 다른 빌더를 호출할 때 전달받은 대상 문자열을 읽어야 할 수 있습니다.
@angular-devkit/architect의 targetFromTargetString() 유틸리티 함수로 이 문자열을 객체로 파싱하세요.
예약하고 실행하기
Architect는 빌더를 비동기로 실행합니다. 빌더를 호출하려면 모든 설정의 해석이 끝났을 때 실행할 작업을 예약합니다.
빌더 함수는 스케줄러가 BuilderRun 제어 객체를 반환한 이후에 실행됩니다.
CLI는 일반적으로 context.scheduleTarget() 함수로 작업을 예약한 뒤 angular.json 파일의 대상 정의를 사용해서 입력 옵션을 결정합니다.
Architect는 주어진 대상의 기본 옵션 객체를 가져온 뒤 설정 값으로 덮어쓰고, 다시 context.scheduleTarget()에 전달한 재정의 객체의 값으로 덮어써서 입력 옵션을 결정합니다.
Angular CLI에서는 명령줄 인자로 재정의 객체를 구성합니다.
Architect는 최종 옵션 값을 빌더의 스키마에 따라 검증합니다. 입력이 유효하면 컨텍스트를 생성하고 빌더를 실행합니다.
자세한 내용은 워크스페이스 설정을 참고하세요.
도움말: 다른 빌더나 테스트에서 context.scheduleBuilder()를 호출해서 빌더를 직접 실행할 수도 있습니다.
이 메서드에는 options 객체를 직접 전달하며, 추가 조정 없이 해당 옵션 값을 빌더의 스키마에 따라 검증합니다.
angular.json 파일을 통해 설정과 재정의 값을 해석하는 것은 context.scheduleTarget() 메서드뿐입니다.
기본 Architect 설정
대상 설정을 구체적으로 살펴볼 수 있도록 간단한 angular.json 파일을 만들어 봅시다.
빌더를 npm에 배포한 뒤(라이브러리 배포하기 참고) 다음 명령어로 설치할 수 있습니다:
npm install @example/copy-file
ng new builder-test로 새 프로젝트를 만들면 기본 빌더 설정만 포함한 다음과 같은 angular.json 파일이 생성됩니다.
angular.json
{
"projects": {
"builder-test": {
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"outputPath": "dist/builder-test",
"index": "src/index.html",
"browser": "src/main.ts",
"tsConfig": "tsconfig.app.json"
},
"configurations": {
"production": {
"optimization": true,
"aot": true
}
}
}
}
}
}
}
대상 추가하기
빌더를 실행해서 파일을 복사하는 새 대상을 추가하세요.
이 대상은 빌더에 package.json 파일을 복사하도록 지시합니다.
- 프로젝트의
architect객체에 새 대상 항목을 추가합니다. copy-package라는 대상은@example/copy-file로 배포한 빌더를 사용합니다.- 옵션 객체는 앞에서 정의한 두 입력의 기본값을 제공합니다.
source- 복사할 기존 파일입니다.destination- 파일을 복사할 대상 경로입니다.
angular.json
{
"projects": {
"builder-test": {
"architect": {
"copy-package": {
"builder": "@example/copy-file:copy",
"options": {
"source": "package.json",
"destination": "package-copy.json"
}
}
// 기존 대상...
}
}
}
}
빌더 실행하기
새 대상의 기본 설정으로 빌더를 실행하려면 다음 CLI 명령어를 사용하세요.
ng run builder-test:copy-package
이 명령어는 package.json 파일을 package-copy.json으로 복사합니다.
설정된 기본값은 명령줄 인자로 덮어쓸 수 있습니다.
예를 들어 다른 destination 값으로 실행하려면 다음 CLI 명령어를 사용하세요.
ng run builder-test:copy-package --destination=package-other.json
이 명령어는 파일을 package-copy.json 대신 package-other.json으로 복사합니다.
source 옵션은 덮어쓰지 않았으므로 원본 파일은 기본값인 package.json을 그대로 사용합니다.
빌더 테스트하기
빌더에는 통합 테스트를 사용하세요.
그러면 이 예제처럼 Architect 스케줄러로 컨텍스트를 생성할 수 있습니다.
빌더의 소스 디렉터리에 my-builder.spec.ts 테스트 파일을 만드세요.
테스트에서는 스키마 검증용 JsonSchemaRegistry, 메모리에서 동작하는 ArchitectHost 구현인 TestingArchitectHost, 그리고 Architect의 새 인스턴스를 생성합니다.
다음은 파일 복사 빌더를 실행하는 테스트 예제입니다.
이 테스트는 빌더로 package.json 파일을 복사한 뒤 복사된 파일의 내용이 원본과 같은지 검증합니다.
src/my-builder.spec.ts
import {Architect} from '@angular-devkit/architect';
import {TestingArchitectHost} from '@angular-devkit/architect/testing';
import {schema} from '@angular-devkit/core';
import {promises as fs} from 'fs';
import {join} from 'path';
describe('Copy File Builder', () => {
let architect: Architect;
let architectHost: TestingArchitectHost;
beforeEach(async () => {
const registry = new schema.CoreSchemaRegistry();
registry.addPostTransform(schema.transforms.addUndefinedDefaults);
// TestingArchitectHost() takes workspace and current directories.
// Since we don't use those, both are the same in this case.
architectHost = new TestingArchitectHost(__dirname, __dirname);
architect = new Architect(architectHost, registry);
// This will either take a Node package name, or a path to the directory
// for the package.json file.
await architectHost.addBuilderFromPackage(join(__dirname, '..'));
});
it('can copy files', async () => {
// A "run" can have multiple outputs, and contains progress information.
const run = await architect.scheduleBuilder('@example/copy-file:copy', {
source: 'package.json',
destination: 'package-copy.json',
});
// The "result" member (of type BuilderOutput) is the next output.
const output = await run.result;
// Stop the builder from running. This stops Architect from keeping
// the builder-associated states in memory, since builders keep waiting
// to be scheduled.
await run.stop();
// Expect that the copied file is the same as its source.
const sourceContent = await fs.readFile('package.json', 'utf8');
const destinationContent = await fs.readFile('package-copy.json', 'utf8');
expect(destinationContent).toBe(sourceContent);
});
});
도움말: 저장소에서 이 테스트를 실행하려면 ts-node 패키지가 필요합니다.
my-builder.spec.ts의 이름을 my-builder.spec.js로 변경하면 이 패키지를 사용하지 않아도 됩니다.
파일 감시 모드
대부분의 빌더는 한 번 실행한 뒤 반환합니다. 하지만 개발 서버처럼 변경 사항을 감지하는 빌더에는 이러한 동작 방식이 완전히 맞지는 않습니다. Architect는 파일 감시 모드를 지원하지만 몇 가지 주의할 점이 있습니다.
파일 감시 모드에서 사용하려면 빌더 핸들러 함수가
Observable을 반환해야 합니다. Architect는Observable이 완료될 때까지 구독하며, 같은 인자로 빌더가 다시 예약되면 이를 재사용할 수 있습니다.빌더는 실행할 때마다 항상
BuilderOutput객체를 방출해야 합니다. 실행 후에는 외부 이벤트로 다시 실행할 수 있도록 파일 감시 모드로 들어갈 수 있습니다. 이벤트 때문에 다시 시작할 때는context.reportRunning()함수를 호출해서 Architect에 다시 실행 중임을 알려야 합니다. 그러면 다른 실행이 예약되어도 Architect가 빌더를 중지하지 않습니다.
파일 감시 모드를 종료하려고 BuilderRun.stop()을 호출하면 Architect는 빌더의 Observable 구독을 해제하고 빌더의 정리 로직을 호출합니다.
이 동작을 통해 오래 실행되는 빌드도 중지하고 정리할 수 있습니다.
일반적으로 외부 이벤트를 감지하는 빌더의 실행은 세 단계로 나누는 것이 좋습니다.
| 단계 | 설명 |
|---|---|
| 실행 | 컴파일러 호출 등의 작업을 수행합니다. 컴파일러가 작업을 마치고 빌더가 BuilderOutput 객체를 방출하면 끝납니다. |
| 감지 | 두 실행 사이에 외부 이벤트 스트림을 감지합니다. 예를 들어 파일 시스템의 변경을 감지합니다. 컴파일러가 다시 시작하고 context.reportRunning()이 호출되면 끝납니다. |
| 완료 | 여러 번 실행해야 하는 컴파일러 등의 작업이 완전히 끝났거나 BuilderRun.stop()으로 빌더 실행이 중지된 상태입니다. Architect는 정리 로직을 실행하고 빌더의 Observable 구독을 해제합니다. |
정리
CLI Builder API는 빌더에서 커스텀 로직을 실행해서 Angular CLI의 동작을 변경하는 방법을 제공합니다.
- 빌더는 동기 또는 비동기로 동작할 수 있고, 한 번 실행하거나 외부 이벤트를 감지할 수 있으며, 다른 빌더나 대상을 예약할 수 있습니다.
- 빌더 옵션의 기본값은
angular.json설정 파일에 지정합니다. 대상의 대체 설정으로 덮어쓸 수 있으며 명령줄 플래그로 다시 덮어쓸 수 있습니다. - Angular 팀은 Architect 빌더를 테스트할 때 통합 테스트를 권장합니다. 빌더가 실행하는 로직은 단위 테스트로 검증하세요.
- 빌더가
Observable을 반환한다면 해당Observable의 정리 로직에서 빌더를 정리해야 합니다.