개발자 도구
라이브러리

Angular 패키지 형식

이 문서는 Angular 패키지 형식(Angular Package Format, APF)을 설명합니다. APF는 npm 패키지의 구조와 형식을 정의하는 Angular 전용 명세로, Angular에서 직접 제공하는 모든 패키지(@angular/core, @angular/material 등)와 대부분의 서드 파티 Angular 라이브러리에서 사용합니다.

APF를 사용하면 Angular를 사용하는 일반적인 상황에서 패키지가 원활하게 동작합니다. APF를 따르는 패키지는 Angular 팀이 제공하는 도구뿐 아니라 JavaScript 생태계의 다양한 도구와 호환됩니다. 서드 파티 라이브러리 개발자도 같은 npm 패키지 형식을 따르는 것이 좋습니다.

도움말: APF는 Angular와 함께 버전이 관리되며, 메이저 버전마다 패키지 형식이 개선됩니다. v13 이전 버전의 명세는 이 Google 문서에서 확인할 수 있습니다.

패키지 형식을 지정하는 이유

오늘날 JavaScript 개발자는 여러 도구 모음(webpack, Rollup, Rolldown, esbuild 등)으로 다양한 방식으로 패키지를 사용합니다. 도구마다 이해하거나 요구하는 입력이 다를 수 있습니다. 어떤 도구는 최신 ES 언어 버전을 처리할 수 있지만, 다른 도구는 이전 ES 버전의 코드를 직접 사용하는 편이 유리할 수 있습니다.

Angular 배포 형식은 널리 사용되는 개발 도구와 작업 흐름을 모두 지원하며, 애플리케이션 전송 크기를 줄이거나 개발 반복 주기(빌드 시간)를 단축하는 최적화에 중점을 둡니다.

개발자는 Angular CLI와 ng-packagr(Angular CLI가 사용하는 빌드 도구)를 사용해서 Angular 패키지 형식의 패키지를 만들 수 있습니다. 자세한 내용은 라이브러리 만들기 가이드를 참고하세요.

파일 구조

다음 예제는 @angular/core 패키지의 파일 구조를 간략하게 나타낸 것입니다. 이어서 패키지의 각 파일을 설명합니다.

node_modules/@angular/core
├── README.md
├── package.json
├── fesm2022
│ ├── core.mjs
│ ├── core.mjs.map
│ ├── testing.mjs
│ └── testing.mjs.map
└── types
│ ├── core.d.ts
│ ├── testing.d.ts

다음 표는 node_modules/@angular/core 아래의 파일 구조와 각 파일 및 디렉터리의 목적을 설명합니다:

파일 목적
README.md npmjs 웹 UI에서 사용하는 패키지 README입니다.
package.json 패키지 자체와 사용 가능한 모든 진입점 및 코드 형식을 설명하는 기본 package.json입니다. 런타임과 도구가 모듈을 찾을 때 사용하는 "exports" 매핑을 포함합니다.
fesm2022/
  ─ core.mjs
  ─ core.mjs.map
  ─ testing.mjs
  ─ testing.mjs.map
모든 진입점의 코드를 평탄화한(FESM) ES2022 형식으로 저장하며 소스 맵도 포함합니다.
types/
  ─ core.d.ts
  ─ testing.d.ts
모든 공개 진입점의 TypeScript 타입 정의를 번들로 묶은 파일입니다.

package.json

기본 package.json에는 다음과 같은 중요한 패키지 메타데이터가 포함됩니다:

  • 패키지가 EcmaScript 모듈(ESM) 형식임을 선언합니다.

  • 모든 진입점에서 사용할 수 있는 소스 코드 형식을 정의하는 "exports" 필드가 있습니다.

  • "exports"를 이해하지 못하는 도구를 위해 기본 @angular/core 진입점에서 사용할 수 있는 소스 코드 형식을 정의하는 키가 있습니다. 이 키는 사용 중단을 권고하며, 생태계 전반에서 "exports" 지원이 확대되면 제거될 수 있습니다.

  • 패키지에 부수 효과가 있는지 선언합니다.

ESM 선언

최상위 package.json에는 다음 키가 포함됩니다:

{
  "type": "module"
}

이 키는 패키지의 코드가 CommonJS 모듈이 아니라 EcmaScript 모듈을 사용한다는 것을 모듈 해석기에 알려줍니다.

"exports"

"exports" 필드의 구조는 다음과 같습니다:

"exports": {
  "./schematics/*": {
    "default": "./schematics/*.js"
  },
  "./package.json": {
    "default": "./package.json"
  },
  ".": {
    "types": "./types/core.d.ts",
    "default": "./fesm2022/core.mjs"
  },
  "./testing": {
    "types": "./types/testing.d.ts",
    "default": "./fesm2022/testing.mjs"
  }
}

주로 살펴볼 키는 "."와 "./testing"입니다. 각각 기본 진입점인 @angular/core와 보조 진입점인 @angular/core/testing에서 사용할 수 있는 코드 형식을 정의합니다. 각 진입점에서 사용할 수 있는 형식은 다음과 같습니다:

형식 설명
타입 선언(.d.ts 파일) TypeScript는 패키지에 의존할 때 .d.ts 파일을 사용합니다.
default 하나의 소스로 평탄화한 ES2022 코드입니다.

이 키를 이해하는 도구는 "exports"에서 원하는 코드 형식을 우선적으로 선택할 수 있습니다.

라이브러리에서는 Sass 믹스인이나 미리 컴파일한 CSS처럼 JavaScript 기반 진입점의 export에 포함되지 않는 정적 파일도 추가로 노출해야 할 수 있습니다.

자세한 내용은 라이브러리의 정적 리소스 관리하기를 참고하세요.

기존 모듈 탐색 키

최상위 package.json에는 "exports"뿐 아니라 이를 지원하지 않는 모듈 해석기를 위한 기존 모듈 탐색 키도 정의되어 있습니다. @angular/core에서는 다음 키를 사용합니다:

{
  "module": "./fesm2022/core.mjs",
  "typings": "./types/core.d.ts",
}

위 코드처럼 모듈 해석기는 이 키를 사용해서 특정 코드 형식을 로드할 수 있습니다.

부수 효과

package.json의 마지막 역할은 패키지에 부수 효과가 있는지 선언하는 것입니다.

{
  "sideEffects": false
}

대부분의 Angular 패키지는 최상위 수준의 부수 효과에 의존하지 않아야 하므로 이 선언을 포함해야 합니다.

진입점과 코드 분할

Angular 패키지 형식의 패키지에는 하나의 기본 진입점과 0개 이상의 보조 진입점(예: @angular/common/http)이 있습니다. 진입점에는 여러 역할이 있습니다.

  1. 사용자가 코드를 불러오는 모듈 지정자를 정의합니다(예: @angular/core, @angular/core/testing).

    사용자는 보통 이러한 진입점을 목적이나 기능이 서로 다른 심볼 그룹으로 인식합니다.

    일부 진입점은 테스트 같은 특별한 용도로만 사용합니다. 이런 API를 기본 진입점에서 분리하면 실수로 사용하거나 잘못 사용할 가능성을 줄일 수 있습니다.

  2. 코드를 지연 로딩할 수 있는 단위를 정의합니다.

    많은 최신 빌드 도구는 ES 모듈 수준에서만 "코드 분할"(즉, 지연 로딩)을 수행할 수 있습니다. Angular 패키지 형식은 주로 진입점 하나당 하나의 "평탄화된" ES 모듈을 사용합니다. 따라서 대부분의 빌드 도구는 단일 진입점의 코드를 여러 출력 청크로 나누지 못합니다.

APF 패키지에서는 논리적으로 연결된 코드를 가능한 한 작은 단위로 나누어 진입점으로 제공하는 것이 일반적인 원칙입니다. 예를 들어 Angular Material은 Button, Tabs 등 각각의 논리적 컴포넌트나 컴포넌트 묶음을 별도 진입점으로 배포합니다. 따라서 필요하면 각 Material 컴포넌트를 개별적으로 지연 로딩할 수 있습니다.

모든 라이브러리에 이렇게 세밀한 구분이 필요한 것은 아닙니다. 하나의 논리적 목적을 가진 대부분의 라이브러리는 단일 진입점으로 배포하는 것이 좋습니다. 예를 들어 Angular 런타임은 보통 하나의 단위로 사용하므로 @angular/core는 런타임에 단일 진입점을 사용합니다.

보조 진입점 찾기

보조 진입점은 패키지 package.json의 "exports" 필드로 찾을 수 있습니다.

라이브러리에서 보조 진입점을 설정하는 방법은 진입점을 참고하세요.

README.md

Markdown 형식의 README 파일은 npm과 GitHub에서 패키지 설명을 표시하는 데 사용합니다.

@angular/core 패키지의 README 내용은 다음과 같습니다:

Angular ======= The sources for this package are in
the main [Angular](https://github.com/angular/angular) repo.Please file issues and pull requests
against that repo. License: MIT

라이브러리 컴파일과 패키지 최적화

이 섹션에서는 라이브러리 컴파일 과정과 결과물을 레지스트리에 배포하기 전에 npm 패키지에 적용하는 최적화를 설명합니다.

부분 컴파일

Angular 패키지 형식의 라이브러리는 "부분 컴파일" 모드로 배포해야 합니다. 이 모드는 특정 Angular 런타임 버전에 종속되지 않는 Angular 코드를 생성하는 ngc 컴파일 모드입니다. Angular 컴파일러와 런타임 버전이 정확히 일치해야 하는 애플리케이션의 전체 컴파일과는 다릅니다.

Angular 코드를 부분 컴파일하려면 tsconfig.json의 angularCompilerOptions 프로퍼티에 compilationMode 플래그를 사용하세요:

{
  …
  "angularCompilerOptions": {
    "compilationMode": "partial",
  }
}

부분 컴파일된 라이브러리 코드는 애플리케이션 빌드 과정에서 Angular CLI가 전체 컴파일된 코드로 변환합니다.

빌드 파이프라인에서 Angular CLI를 사용하지 않는다면 Angular CLI 외부에서 부분 Ivy 코드 사용하기 가이드를 참고하세요.

ES 모듈 평탄화

Angular 패키지 형식은 코드를 "평탄화된" ES 모듈 형식으로 배포하도록 규정합니다. 이 방식은 Angular 애플리케이션의 빌드 시간뿐 아니라 최종 애플리케이션 번들의 다운로드 시간과 파싱 시간도 크게 줄여 줍니다. Nolan Lawson의 "작은 모듈의 비용" 글을 참고하세요.

Angular 컴파일러는 인덱스 ES 모듈 파일을 생성할 수 있습니다. Rollup / Rolldown 같은 도구는 이 파일을 사용해서 평탄화된 ES 모듈 (FESM) 파일 형식의 모듈을 생성합니다.

FESM은 진입점에서 접근할 수 있는 모든 ES 모듈을 하나의 ES 모듈로 평탄화한 파일 형식입니다. 패키지의 모든 import를 따라가면서 코드를 단일 파일에 복사하고, 공개 ES export는 유지하며 비공개 import는 제거해서 만듭니다. 다만 경우에 따라 FESM은 여러 진입점이 공유하는 청크에 의존할 수 있습니다.

FESM은 페솜 으로 읽으며, FESM2020처럼 뒤에 숫자를 붙일 수 있습니다. 숫자는 모듈 안에 있는 JavaScript의 언어 수준을 나타냅니다. 따라서 FESM2022 파일은 ESM+ES2022이며, import/export 구문과 ES2022 소스 코드를 포함합니다.

평탄화된 ES 모듈 인덱스 파일을 생성하려면 tsconfig.json에 다음 설정 옵션을 사용하세요:

{
  "compilerOptions": {
    …
    "module": "esnext",
    "target": "es2022",
    …
  },
  "angularCompilerOptions": {
    …
    "flatModuleOutFile": "my-ui-lib.js",
    "flatModuleId": "my-ui-lib"
  }
}

ngc가 인덱스 파일(예: my-ui-lib.js)을 생성하면 Rollup / Rolldown 같은 번들러와 최적화 도구로 평탄화된 ESM 파일을 만들 수 있습니다.

"sideEffects" 플래그

EcmaScript 모듈에는 기본적으로 부수 효과가 있다고 간주합니다. 모듈에서 무언가를 불러오면 해당 모듈의 최상위 코드가 실행되어야 하기 때문입니다. 하지만 일반적인 모듈에서 부수 효과가 있는 것처럼 보이는 코드 대부분은 실제로는 특정 심볼에만 영향을 주므로, 이런 동작이 바람직하지 않을 때가 많습니다. 해당 심볼을 불러와 사용하지 않는다면 트리 셰이킹이라는 최적화 과정에서 제거하는 것이 좋지만, 부수 효과가 있는 코드가 이를 방해할 수 있습니다.

webpack 같은 빌드 도구는 패키지가 모듈 최상위 수준의 부수 효과에 의존하지 않는다고 선언하는 플래그를 지원합니다. 그러면 도구가 패키지의 코드를 더 자유롭게 트리 셰이킹할 수 있습니다. 이러한 최적화를 통해 번들 크기를 줄이고 코드 분할 후 각 번들 청크에 코드를 더 적절하게 배치할 수 있습니다. 코드에 해당 범위 밖까지 영향을 미치는 부수 효과가 있으면 이 최적화로 코드가 깨질 수 있습니다. 하지만 Angular 애플리케이션에서 이런 경우는 흔하지 않으며 대개 설계가 좋지 않다는 신호입니다. 모든 패키지에서 sideEffects 프로퍼티를 false로 설정해서 부수 효과가 없다고 선언하고, 개발자는 범위 밖에 부수 효과를 만들지 않는 코드를 자연스럽게 작성할 수 있도록 Angular 스타일 가이드를 따르는 것이 좋습니다.

자세한 내용: webpack 부수 효과 문서

ES2022 언어 수준

ES2022는 현재 Angular CLI와 다른 도구가 사용하는 기본 언어 수준입니다. Angular CLI는 애플리케이션을 빌드할 때 모든 대상 브라우저가 지원하는 언어 수준으로 번들을 변환합니다.

타입 정의 평탄화(.d.ts 번들링)

APF v8부터는 TypeScript 타입 정의를 번들로 묶는 것이 좋습니다. 특히 라이브러리에 개별 .ts 소스 파일이 많다면 타입 정의 번들링으로 사용자의 컴파일 속도를 크게 높일 수 있습니다.

Angular는 rolldown-plugin-dts를 사용해서 .d.ts 파일을 평탄화합니다(FESM 파일을 만드는 방식과 비슷하게 rolldown을 사용합니다).

rolldown은 진입점 사이의 코드 분할을 지원하므로 .d.ts 번들링에 유용합니다. 예를 들어 여러 진입점이 같은 공통 타입에 의존한다면, 평탄화된 큰 .d.ts 파일들과 함께 공통 .d.ts 파일이 생성됩니다. 이렇게 하면 타입 중복을 피할 수 있습니다.

Tslib

APF v10부터는 기본 진입점의 직접 의존성으로 tslib를 추가하는 것이 좋습니다. tslib 버전이 라이브러리를 컴파일할 때 사용하는 TypeScript 버전에 연결되기 때문입니다.

예제

Angular 프레임워크의 npm 패키지는 APF 규격에 맞는 라이브러리 패키지를 만드는 좋은 참고 자료입니다. UNPKG에서 파일 구조와 npm 패키지를 살펴보세요.

용어 정의

이 문서에서는 다음 용어를 일관된 의미로 사용합니다. 의미를 명확하게 이해할 수 있도록 이 섹션에서 각 용어를 정의합니다.

패키지

@angular/core처럼 npm에 배포되고 함께 설치되는 가장 작은 파일 묶음입니다. 패키지에는 package.json이라는 매니페스트, 컴파일된 소스 코드, TypeScript 정의 파일, 소스 맵, 메타데이터 등이 포함됩니다. 패키지는 npm install @angular/core로 설치합니다.

심볼

모듈에 포함된 클래스, 함수, 상수, 변수이며, 필요한 경우 모듈 export를 통해 외부에 공개합니다.

모듈

ECMAScript 모듈을 줄여 부르는 말입니다. 심볼을 import하거나 export하는 구문이 포함된 파일입니다. ECMAScript 명세의 모듈 정의와 같습니다.

ESM

ECMAScript 모듈의 약어입니다(위 설명 참고).

FESM

Flattened ES Modules의 약어로, 진입점에서 접근할 수 있는 모든 ES 모듈을 하나의 ES 모듈로 평탄화해서 만든 파일 형식입니다. FESM은 보통 단일 파일이지만 다른 FESM과 공유하는 청크에 의존할 수 있습니다.

모듈 ID

import 구문에서 사용하는 모듈 식별자입니다(예: @angular/core). ID는 파일 시스템 경로에 직접 대응하는 경우가 많지만, 모듈 탐색 전략이 다양하므로 항상 그렇지는 않습니다.

모듈 지정자

모듈 식별자를 뜻합니다(위 설명 참고).

모듈 탐색 전략

모듈 ID를 파일 시스템 경로로 변환하는 알고리즘입니다. Node.js에는 명확하게 정의되어 널리 사용하는 전략이 있고, TypeScript는 여러 모듈 탐색 전략을 지원하며, Closure Compiler는 또 다른 전략을 사용합니다.

모듈 형식

최소한 파일에서 import와 export를 하는 문법을 포함하는 모듈 문법 명세입니다. 일반적인 모듈 형식에는 CommonJS(CJS, 주로 Node.js 애플리케이션에서 사용)와 ECMAScript 모듈(ESM)이 있습니다. 모듈 형식은 개별 모듈을 묶는 방식만 나타내며, 모듈 내용에 사용된 JavaScript 언어 기능은 나타내지 않습니다. 따라서 Angular 팀은 모듈 형식 뒤에 언어 수준을 붙여 표현할 때가 많습니다(예: ESM+ES2022는 ESM 형식의 모듈에 ES2022 코드가 포함되어 있다는 의미입니다).

번들

webpack이나 Rollup 같은 빌드 도구가 생성하는 단일 JS 파일 형태의 결과물이며, 하나 이상의 모듈에서 가져온 심볼을 포함합니다. 번들은 브라우저가 수백 개에서 많게는 수만 개의 파일을 다운로드할 때 발생하는 네트워크 부담을 줄이기 위한 브라우저용 해결책입니다. Node.js는 일반적으로 번들을 사용하지 않습니다. 일반적인 번들 형식에는 UMD와 System.register가 있습니다.

언어 수준

코드의 언어 버전(ES2022)입니다. 모듈 형식과는 독립적입니다.

진입점

사용자가 불러오도록 의도된 모듈입니다. 고유한 모듈 ID로 참조하며, 해당 모듈 ID가 가리키는 공개 API를 export합니다. 예를 들어 @angular/core와 @angular/core/testing이 있습니다. 두 진입점 모두 @angular/core 패키지 안에 있지만 서로 다른 심볼을 export합니다. 하나의 패키지에 여러 진입점이 있을 수 있습니다.

깊은 경로 가져오기

진입점이 아닌 모듈에서 심볼을 가져오는 과정입니다. 이러한 모듈 ID는 보통 비공개 API로 간주하며, 프로젝트 개발 중이나 패키지 번들 생성 과정에서 변경될 수 있습니다.

최상위 가져오기

진입점에서 가져오는 import입니다. 사용 가능한 최상위 import가 공개 API를 정의하며, @angular/core나 @angular/common 같은 "@angular/name" 모듈에서 노출됩니다.

트리 셰이킹

애플리케이션에서 사용하지 않는 코드를 찾아 제거하는 과정이며, 죽은 코드 제거라고도 합니다. Rollup, Rolldown, Closure Compiler, Terser 같은 도구를 사용해서 애플리케이션 수준에서 수행하는 전체 최적화입니다.

AOT 컴파일러

Angular의 사전 컴파일러(Ahead of Time Compiler)입니다.

평탄화된 타입 정의

API Extractor나 rollup-plugin-dts 같은 도구로 생성한 TypeScript 타입 정의 번들입니다.