이 페이지에서는 Angular 기능을 확장하는 새 라이브러리를 만들고 배포하는 방법을 개념적으로 살펴봅니다.
여러 애플리케이션에서 같은 문제를 해결해야 하거나 해결 방법을 다른 개발자와 공유하고 싶다면 라이브러리로 만들 수 있습니다. 간단한 예로 회사에서 만드는 모든 애플리케이션에 포함할 회사 웹사이트 이동 버튼을 생각해 볼 수 있습니다.
시작하기
다음 명령으로 Angular CLI를 사용해서 새 워크스페이스에 라이브러리의 기본 구조를 생성하세요.
ng new my-workspace --no-create-application
cd my-workspace
ng generate library my-lib
라이브러리 이름 정하기
나중에 npm 같은 공개 패키지 레지스트리에 배포하려면 라이브러리 이름을 신중하게 정해야 합니다. 라이브러리 배포하기를 참고하세요.
ng-library처럼 ng-로 시작하는 이름은 피하세요.
ng- 접두사는 Angular 프레임워크와 라이브러리에서 사용하는 예약어입니다.
Angular에서 사용하는 라이브러리임을 나타내는 관례에 따라 ngx- 접두사를 사용하는 것이 좋습니다.
레지스트리 사용자도 이를 통해 다른 JavaScript 프레임워크용 라이브러리와 쉽게 구별할 수 있습니다.
ng generate 명령은 워크스페이스에 컴포넌트 하나가 들어 있는 projects/my-lib 폴더를 생성합니다.
도움말: 라이브러리 프로젝트 구조에 대한 자세한 내용은 프로젝트 파일 구조 가이드의 라이브러리 프로젝트 파일 섹션을 참고하세요.
같은 워크스페이스에서 여러 프로젝트를 사용하려면 모노레포 모델을 사용하세요. 여러 프로젝트를 위한 워크스페이스 설정하기를 참고하세요.
새 라이브러리를 생성하면 워크스페이스 설정 파일인 angular.json에 library 유형의 프로젝트가 추가됩니다.
"projects": {
…
"my-lib": {
"root": "projects/my-lib",
"sourceRoot": "projects/my-lib/src",
"projectType": "library",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular/build:ng-packagr",
…
CLI 명령으로 프로젝트를 빌드하고 테스트와 린트를 실행하세요:
ng build my-lib --configuration development
ng test my-lib
ng lint my-lib
이 프로젝트에 설정된 빌더는 애플리케이션 프로젝트의 기본 빌더와 다릅니다. 이 빌더는 라이브러리가 항상 AOT 컴파일러로 빌드되도록 보장하는 등의 역할을 합니다.
라이브러리 코드를 재사용하려면 공개 API를 정의해야 합니다. 이 "사용자 계층"은 라이브러리 사용자가 접근할 수 있는 기능을 정의합니다. 라이브러리 사용자는 서비스 프로바이더나 공통 유틸리티 함수 같은 공개 기능에 하나의 import 경로로 접근할 수 있어야 합니다.
라이브러리의 공개 API는 라이브러리 폴더의 public-api.ts 파일에서 관리합니다.
이 파일에서 export한 항목은 라이브러리를 애플리케이션으로 불러올 때 공개됩니다.
라이브러리에는 설치와 유지보수를 위한 문서(일반적으로 README 파일)를 제공해야 합니다.
진입점
진입점 은 라이브러리 사용자가 임포트할 때 지정하는 모듈 지정자와 해당 지정자가 노출하는 공개 API를 의미합니다. 모든 라이브러리는 하나의 기본 진입점 을 가지며, 원하는 만큼 보조 진입점 을 추가할 수 있습니다.
기본 진입점은 패키지 자체입니다.
라이브러리 루트의 ng-package.json 파일에서 설정하며 lib.entryFile로 공개 API를 정의하는 파일을 지정합니다.
projects/my-lib/ng-package.json
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/my-lib",
"lib": {
"entryFile": "src/public-api.ts"
}
}
사용자는 패키지 이름으로 기본 진입점을 임포트합니다.
import {ThemeService} from 'my-lib';
보조 진입점은 자체 ng-package.json 파일이 있는 라이브러리 내부 디렉터리입니다.
라이브러리 루트를 기준으로 한 디렉터리의 상대 경로가 임포트 하위 경로가 됩니다.
예를 들어 button 디렉터리가 있으면 사용자가 my-lib/button을 사용할 수 있습니다.
import {ButtonComponent} from 'my-lib/button';
Angular 패키지도 같은 구조를 사용합니다.
예를 들어 @angular/core는 기본 진입점이고 @angular/core/testing은 같은 패키지의 보조 진입점입니다.
도움말: Angular 패키지 형식에서 진입점으로 코드 분할을 지원하고 청크 경계를 정의하는 구조는 진입점과 코드 분할을 참고하세요.
보조 진입점 추가하기
라이브러리 내부에 자체 ng-package.json과 공개 API 파일이 있는 디렉터리를 만드세요.
projects/my-lib/
├── ng-package.json (기본 진입점: my-lib)
├── package.json
├── src/
│ ├── public-api.ts
│ └── lib/ …
└── button/
├── ng-package.json (보조 진입점: my-lib/button)
└── src/
├── public-api.ts
└── button.ts
보조 진입점의 ng-package.json은 해당 진입점만 설정합니다.
projects/my-lib/button/ng-package.json
{
"$schema": "../../../node_modules/ng-packagr/ng-entrypoint.schema.json",
"lib": {
"entryFile": "src/public-api.ts"
}
}
IMPORTANT: 보조 진입점의 ng-package.json에는 lib 옵션만 지정할 수 있습니다. dest, assets 같은 패키지 전체 옵션은 라이브러리 루트의 ng-package.json에 지정하며 모든 진입점에 적용됩니다.
보조 진입점을 다른 곳에 등록할 필요는 없습니다.
빌드 중 ng-packagr가 라이브러리 루트 아래의 모든 ng-package.json을 자동으로 발견하고 디렉터리 경로에서 임포트 하위 경로를 도출합니다. 예를 들어 button/은 my-lib/button, testing/harness/는 my-lib/testing/harness가 됩니다.
같은 워크스페이스의 애플리케이션에서 보조 진입점을 임포트하려면 워크스페이스의 tsconfig.json에 와일드카드 경로 매핑을 추가하세요.
기본적으로 ng generate library는 패키지 루트만 매핑하므로 하위 경로는 포함되지 않습니다.
tsconfig.json
{
"compilerOptions": {
"paths": {
"my-lib": ["./dist/my-lib"],
"my-lib/*": ["./dist/my-lib/*"]
}
}
}
IMPORTANT: 같은 라이브러리의 다른 진입점에서 코드를 임포트할 때는 상대 경로 대신 항상 패키지 임포트 경로(예: import {ThemeService} from 'my-lib')를 사용하세요. ng-packagr는 의존성 순서대로 각 진입점을 별도로 빌드하므로 진입점 사이의 상대 임포트나 순환 의존성이 있으면 빌드가 실패합니다.
애플리케이션 일부를 라이브러리로 리팩토링하기
해결책을 재사용하려면 특정 애플리케이션에만 해당하는 코드에 의존하지 않도록 조정해야 합니다. 애플리케이션 기능을 라이브러리로 옮길 때는 다음을 고려하세요.
컴포넌트와 파이프 같은 선언은 외부 변수에 의존하거나 외부 변수를 변경하지 않도록 상태 없이 설계해야 합니다. 상태에 의존한다면 각 경우를 검토해서 애플리케이션 상태인지 라이브러리가 관리해야 하는 상태인지 판단해야 합니다.
컴포넌트 내부에서 구독하는 옵저버블은 컴포넌트의 라이프사이클에 맞춰 정리하고 해제해야 합니다.
컴포넌트는 문맥을 전달받는 입력 프로퍼티와 다른 컴포넌트에 이벤트를 알리는 출력 프로퍼티로 상호작용을 공개해야 합니다.
모든 내부 의존성을 확인하세요.
- 컴포넌트나 서비스에서 사용하는 커스텀 클래스와 인터페이스가 함께 옮겨야 할 다른 클래스나 인터페이스에 의존하는지 확인하세요.
- 라이브러리 코드가 서비스에 의존한다면 해당 서비스도 옮겨야 합니다.
- 라이브러리 코드나 템플릿이 다른 라이브러리(예: Angular Material)에 의존한다면 그 의존성을 라이브러리에 설정해야 합니다.
라이브러리를 사용하는 애플리케이션에 서비스를 제공하는 방법을 고려하세요.
NgModule이나 컴포넌트에 프로바이더를 선언하는 대신 서비스에서 자체 프로바이더를 선언해야 합니다. 프로바이더를 이렇게 선언하면 서비스가 트리 셰이킹 대상 이 됩니다. 라이브러리를 불러온 애플리케이션에 해당 서비스가 주입되지 않는다면 컴파일러가 번들에서 서비스를 제외할 수 있습니다. 자세한 내용은 트리 셰이킹 가능한 프로바이더를 참고하세요.
전역 서비스 프로바이더를 등록한다면
provideXYZ()프로바이더 함수를 공개하세요.모든 애플리케이션에서 사용하지는 않는 선택적 서비스를 라이브러리가 제공한다면 경량 토큰 설계 패턴으로 적절한 트리 셰이킹을 지원하세요.
코드 생성 스키매틱으로 CLI와 통합하기
라이브러리에는 보통 프로젝트로 불러와 사용하는 컴포넌트, 서비스, 파이프, 디렉티브 등의 Angular 구성 요소를 정의한 재사용 가능한 코드 가 포함됩니다.
라이브러리는 배포와 공유를 위해 npm 패키지로 묶습니다.
이 패키지에는 CLI의 ng generate component가 기본 컴포넌트를 만드는 것처럼 프로젝트에서 코드를 직접 생성하거나 변환하는 방법을 정의한 스키매틱을 포함할 수도 있습니다.
예를 들어 라이브러리와 함께 제공하는 스키매틱은 라이브러리의 특정 기능이나 기능 묶음을 설정해서 사용하는 컴포넌트를 생성하는 데 필요한 정보를 Angular CLI에 제공할 수 있습니다.
Angular Material의 내비게이션 스키매틱이 한 예입니다.
이 스키매틱은 CDK의 BreakpointObserver를 설정하고 Material의 MatSideNav, MatToolbar 컴포넌트와 함께 사용합니다.
다음 종류의 스키매틱을 만들어 포함하세요:
ng add로 프로젝트에 라이브러리를 추가할 수 있도록 설치 스키매틱을 포함하세요.ng generate로 정의한 구성 요소(컴포넌트, 서비스, 테스트)의 기본 코드를 프로젝트에 생성할 수 있도록 생성 스키매틱을 포함하세요.ng update로 라이브러리 의존성을 업데이트하고 새 릴리즈의 호환성이 깨지는 변경에 맞춰 마이그레이션할 수 있도록 업데이트 스키매틱을 포함하세요.
라이브러리에 무엇을 포함할지는 작업에 따라 다릅니다.
예를 들어 준비된 데이터를 미리 넣은 드롭다운을 생성하는 스키매틱을 정의해서 애플리케이션에 추가하는 방법을 보여줄 수 있습니다.
매번 전달받은 서로 다른 값을 담는 드롭다운이 필요하다면 주어진 설정으로 드롭다운을 생성하는 스키매틱을 정의할 수 있습니다.
개발자는 ng generate로 자신의 애플리케이션에 맞는 인스턴스를 구성할 수 있습니다.
설정 파일을 읽은 다음 해당 설정을 기반으로 폼을 생성한다고 해 봅시다. 라이브러리 사용자가 폼을 추가로 수정해야 한다면 스키매틱이 가장 적합할 수 있습니다. 반면 폼이 항상 같고 개발자가 크게 수정할 필요가 없다면 설정을 받아 폼을 생성하는 동적 컴포넌트를 만들 수 있습니다. 일반적으로 커스터마이징이 복잡할수록 스키매틱 방식이 더 유용합니다.
자세한 내용은 스키매틱 개요와 라이브러리용 스키매틱을 참고하세요.
라이브러리 배포하기
Angular CLI와 npm 패키지 관리자를 사용해서 라이브러리를 빌드하고 npm 패키지로 배포하세요.
Angular CLI는 ng-packagr라는 도구로 컴파일된 코드를 npm에 배포할 수 있는 패키지로 만듭니다.
ng-packagr가 지원하는 배포 형식과 라이브러리에 맞는 형식을 선택하는 방법은 Ivy로 라이브러리 빌드하기를 참고하세요.
배포할 라이브러리는 항상 production 설정으로 빌드해야 합니다.
그러면 생성된 결과물에 적절한 최적화와 올바른 npm 패키지 형식이 적용됩니다.
ng build my-lib
cd dist/my-lib
npm publish
라이브러리의 정적 리소스 관리하기
Angular 라이브러리의 배포 결과물에는 테마 파일, Sass 믹스인, 변경 이력 같은 문서 등의 추가 자원을 포함할 수 있습니다. 자세한 내용은 빌드 중 라이브러리에 자원 복사하기와 컴포넌트 스타일에 자원 포함하기를 참고하세요.
중요: Sass 믹스인이나 미리 컴파일한 CSS 같은 추가 자원을 포함할 때는 기본 진입점의 package.json에서 조건부 "exports"에 직접 추가해야 합니다.
ng-packagr는 직접 작성한 "exports"를 자동 생성한 항목과 병합합니다.
따라서 라이브러리 작성자는 추가 export 하위 경로나 커스텀 조건을 설정할 수 있습니다.
"exports": {
".": {
"sass": "./_index.scss",
},
"./theming": {
"sass": "./_theming.scss"
},
"./prebuilt-themes/indigo-pink.css": {
"style": "./prebuilt-themes/indigo-pink.css"
}
}
위 예제는 @angular/material 배포 결과물에서 발췌한 것입니다.
피어 의존성
Angular 라이브러리는 의존하는 모든 @angular/* 패키지를 피어 의존성으로 나열해야 합니다.
그러면 각 모듈이 Angular를 요청할 때 정확히 같은 모듈을 받습니다.
라이브러리가 @angular/core를 peerDependencies 대신 dependencies에 나열하면 다른 Angular 모듈을 가져올 수 있으며, 이로 인해 애플리케이션이 정상 동작하지 않을 수 있습니다.
애플리케이션에서 직접 만든 라이브러리 사용하기
같은 워크스페이스에서 라이브러리를 사용하려면 npm에 배포할 필요는 없지만 먼저 빌드해야 합니다.
애플리케이션에서 직접 만든 라이브러리를 사용하려면 다음과 같이 진행하세요:
- 라이브러리를 빌드하세요. 빌드하기 전에는 라이브러리를 사용할 수 없습니다.
ng build my-lib
- 애플리케이션에서 라이브러리 이름으로 불러오세요:
import {myExport} from 'my-lib';
라이브러리 빌드와 다시 빌드하기
라이브러리를 npm 패키지로 배포한 뒤 npm에서 애플리케이션으로 다시 설치한 경우가 아니라면 빌드 단계가 중요합니다.
예를 들어 Git 저장소를 복제하고 npm install을 실행했더라도 라이브러리를 아직 빌드하지 않았다면 편집기는 my-lib import를 찾을 수 없다고 표시합니다.
도움말: Angular 애플리케이션에서 라이브러리의 항목을 불러올 때 Angular는 라이브러리 이름과 디스크 위치 사이의 매핑을 찾습니다.
라이브러리 패키지를 설치한 경우에는 node_modules 폴더에서 매핑을 찾습니다.
직접 만든 라이브러리를 빌드한 경우에는 tsconfig의 paths에서 매핑을 찾아야 합니다.
Angular CLI로 라이브러리를 생성하면 경로가 tsconfig 파일에 자동으로 추가됩니다.
Angular CLI는 tsconfig의 paths로 빌드 시스템에 라이브러리 위치를 알려줍니다.
자세한 내용은 경로 매핑 개요를 참고하세요.
라이브러리를 변경할 때마다 다시 빌드할 수 있지만 이 추가 단계에는 시간이 걸립니다. 증분 빌드 기능을 사용하면 라이브러리 개발이 더 편리해집니다. 파일이 변경될 때마다 부분 빌드를 수행해서 수정된 파일을 출력합니다.
개발 환경에서 증분 빌드를 백그라운드 프로세스로 실행할 수 있습니다.
이 기능을 사용하려면 빌드 명령에 --watch 플래그를 추가하세요:
ng build my-lib --watch
중요: CLI의 build 명령은 라이브러리와 애플리케이션에 서로 다른 빌더와 빌드 도구를 사용합니다.
- 애플리케이션 빌드 시스템인
@angular/build는esbuild를 기반으로 하며, 모든 새 Angular CLI 프로젝트에 포함됩니다. - 라이브러리 빌드 시스템은
ng-packagr를 기반으로 합니다.ng generate library my-lib로 라이브러리를 추가할 때만 의존성에 추가됩니다.
두 빌드 시스템이 지원하는 기능은 서로 다르며, 같은 기능을 지원하더라도 처리 방식이 다릅니다. 따라서 같은 TypeScript 소스라도 라이브러리로 빌드했을 때와 애플리케이션으로 빌드했을 때 생성되는 JavaScript 코드가 다를 수 있습니다.
이 때문에 라이브러리에 의존하는 애플리케이션은 빌드된 라이브러리 를 가리키는 TypeScript 경로 매핑만 사용해야 합니다.
TypeScript 경로 매핑이 라이브러리의 원본 .ts 파일을 가리켜서는 안 됩니다.
로컬 개발을 위해 라이브러리 연결하기
이 섹션에서는 패키지 관리자의 로컬 연결 기능(npm link나 pnpm link 등)을 사용해서 독립적인 Angular 라이브러리를 외부 애플리케이션과 함께 로컬에서 테스트하는 방법을 설명합니다.
모노레포 워크스페이스 구조에 의존하거나 npm 레지스트리에 배포할 필요가 없습니다.
참고: 라이브러리와 애플리케이션이 같은 Angular 워크스페이스에 있다면(모노레포 구성) 일반적인 모노레포 작업 흐름에서 연결을 자동으로 처리하며 대체로 더 효율적입니다. 로컬 연결 방식은 다음 경우에 가장 적합합니다:
- 독립적인 라이브러리를 개발하면서 이를 사용하는 외부 애플리케이션으로 변경 사항을 테스트해야 하는 경우
- 모노레포 워크스페이스 밖의 애플리케이션에서 라이브러리 변경 사항을 테스트하는 경우
라이브러리를 사용하는 애플리케이션 설정하기
연결한 라이브러리를 사용하려면 애플리케이션의 angular.json에 다음 설정을 추가해야 합니다:
{
"projects": {
"your-app": {
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"preserveSymlinks": true
},
"configurations": {
"development": {
"sourceMap": {
"scripts": true,
"styles": true,
"vendor": true
}
}
}
},
"serve": {
"builder": "@angular/build:dev-server",
"options": {
"prebundle": {
"exclude": ["my-lib"]
}
}
}
}
}
}
}
설정 옵션 설명:
preserveSymlinks: true: 빌드 시스템이 심볼릭 링크의 원래 위치를 해석하는 대신 패키지 관리자의 연결 명령으로 생성한 심볼릭 링크를 따라가도록 합니다. 의존하는 Node 패키지의 사본이 여러 개 생기지 않도록 하는 데 필수적입니다.sourceMap.vendor: vendor 소스 맵을 활성화하면(특히vendor: true) 연결한 라이브러리 코드를 더 쉽게 디버깅할 수 있습니다.prebundle.exclude: Angular CLI는 기본적으로 모든 Node 의존성을 미리 번들링할 수 있습니다. 라이브러리를 제외하면 연결된 소스 코드의 변경을 올바르게 감지하고 다시 빌드할 수 있습니다.
라이브러리의 배포 형식
라이브러리를 배포할 때 사용할 수 있는 형식은 두 가지입니다:
| 배포 형식 | 설명 |
|---|---|
| Partial-Ivy (권장) | v12 이상의 어떤 Angular 버전으로 빌드한 Ivy 애플리케이션에서도 사용할 수 있는 이식 가능한 코드를 포함합니다. |
| Full-Ivy | Angular의 비공개 Ivy 명령어를 포함하므로 서로 다른 Angular 버전 사이의 동작을 보장하지 않습니다. 라이브러리와 애플리케이션을 정확히 같은 Angular 버전으로 빌드해야 합니다. 모든 라이브러리와 애플리케이션 코드를 소스에서 직접 빌드하는 환경에 유용합니다. |
npm에 배포할 때는 Angular 패치 버전 사이에서도 안정적으로 유지되는 partial-Ivy 형식을 사용하세요.
생성된 Ivy 명령어는 Angular의 공개 API가 아니어서 패치 버전 사이에서도 변경될 수 있으므로, npm에 배포할 라이브러리는 full-Ivy 코드로 컴파일하지 마세요.
라이브러리 버전 호환성 보장하기
애플리케이션을 빌드하는 Angular 버전은 의존하는 모든 라이브러리를 빌드한 Angular 버전과 같거나 더 높아야 합니다. 예를 들어 Angular 버전 13을 사용하는 라이브러리가 있다면 이를 사용하는 애플리케이션은 Angular 버전 13 이상이어야 합니다. 애플리케이션에서 더 낮은 버전을 사용하는 것은 지원하지 않습니다.
npm에 라이브러리를 배포하려면 tsconfig.prod.json에 "compilationMode": "partial"을 설정해서 partial-Ivy 코드로 컴파일하세요.
이 부분 컴파일 형식은 서로 다른 Angular 버전 사이에서도 안정적으로 유지되므로 npm에 안전하게 배포할 수 있습니다.
이 형식의 코드는 애플리케이션 빌드 중에 같은 Angular 컴파일러 버전으로 처리되므로, 애플리케이션과 모든 라이브러리가 하나의 Angular 버전을 사용하게 됩니다.
생성된 Ivy 명령어는 Angular의 공개 API가 아니어서 패치 버전 사이에서도 변경될 수 있으므로, npm에 배포할 라이브러리는 full-Ivy 코드로 컴파일하지 마세요.
npm에 패키지를 배포한 적이 없다면 사용자 계정을 만들어야 합니다. 자세한 내용은 npm 패키지 배포하기를 참고하세요.
Angular CLI 외부에서 partial-Ivy 코드 사용하기
애플리케이션은 npm의 여러 Angular 라이브러리를 node_modules 디렉터리에 설치합니다.
하지만 이 라이브러리의 코드는 전체 컴파일된 상태가 아니므로 빌드된 애플리케이션과 바로 함께 번들링할 수 없습니다.
컴파일을 마무리하려면 Angular 링커를 사용하세요.
Angular CLI를 사용하지 않는 애플리케이션을 위해 링커를 Babel 플러그인으로 제공합니다.
플러그인은 @angular/compiler-cli/linker/babel에서 불러오면 됩니다.
Angular 링커 Babel 플러그인은 빌드 캐시를 지원합니다. 따라서 다른 npm 작업과 관계없이 라이브러리는 링커에서 한 번만 처리하면 됩니다.
다음은 babel-loader를 사용해서 링커를 Babel 플러그인으로 등록하고, 커스텀 webpack 빌드에 통합하는 예제입니다.
webpack.config.mjs
import linkerPlugin from '@angular/compiler-cli/linker/babel';
export default {
// ...
module: {
rules: [
{
test: /\.m?js$/,
use: {
loader: 'babel-loader',
options: {
plugins: [linkerPlugin],
compact: false,
cacheDirectory: true,
},
},
},
],
},
// ...
};
도움말: Angular CLI에는 링커 플러그인이 자동 통합됩니다. 따라서 라이브러리 사용자가 CLI를 사용한다면 추가 설정 없이 npm에서 Ivy 네이티브 라이브러리를 설치할 수 있습니다.