Angular 버전 15 가 출시되면서 소개된 여러 기능들과 Standalone Component 에 대해 알아본다. {: .prompt-tip }
중요한 특징들이 대거 반영된 메이저 버전이라 블로그의 내용을 옮겨본다.
- 참고 : 버전 15의
ng new <my-app>
생성 패키지
{
"dependencies": {
"@angular/animations": "^15.0.0",
"@angular/common": "^15.0.0",
"@angular/compiler": "^15.0.0",
"@angular/core": "^15.0.0",
"@angular/forms": "^15.0.0",
"@angular/platform-browser": "^15.0.0",
"@angular/platform-browser-dynamic": "^15.0.0",
"@angular/router": "^15.0.0",
"rxjs": "~7.5.0",
"tslib": "^2.3.0",
"zone.js": "~0.12.0"
},
"devDependencies": {
// ...
"typescript": "~4.8.2"
}
}
NgModules를 사용하지 않고 애플리케이션을 구축할 수 있는 새로운 독립형 API (하단에서 예제로 자세히 설명)
- v14 에서 실험적 기능으로 포함되어 있었는데, 이번에 해제됨
- NgModule 에 포함되는 기본 모듈의 크기가 크고 제약이 있었음
Standalone 은 NgModule 과 분리될 수 있기 때문에, 라우팅에서도 하위 경로를 동적으로 호출할 수 있다.
- 사용하지 않는 라우팅은 번들링에서 제외된다. (코드 크기 감소)
- bootstrapApplication 으로 독립형 컴포넌트부터 시작할 수 있음
- 애플리케이션을 임의의 지점부터 로딩하고 호출한다는 뜻인듯
- 기존에는
app-root
부터 강하게 연결되어 있는 구조였음
- 기존에는
- 애플리케이션을 임의의 지점부터 로딩하고 호출한다는 뜻인듯
Directive 개발시 host 속성에 직접 파라미터 설정을 할 수 있음
이미지 태그 기능 향상
srcset
으로 다양한 크기의 이미지들을 등록하고 최적의 이미지 크기가 로딩되도록 할 수 있음 (로딩시간 절약)- 이미지가 부모 컨테이너 크기에 맞춰 채워지도록 함
- 따로 크기를 지정하지 않아도 넘치거나 모자라지 않게됨
라우팅의 guard (사용가능 여부) 설정을 함수형으로 설정할 수 있음
- 가독성 향상
- 함수형으로 신뢰성 향상
참고 : Advancements in the Angular Router
const route = {
path: ‘edit’,
component: EditCmp,
canDeactivate: [
(component: EditCmp) => !component.hasUnsavedChanges
]
};
const route = {
path: ‘admin’,
canActivate: [() => inject(LoginService).isLoggedIn()]
};
지연로딩의 loadComponent 사용 문법이 기본값 설정으로 단순화됨
오류에 대해 더 정확하고 가독성을 높인 형태로 스택을 출력
MDC 기반의 Angular Material 리팩토링 버전을 출시 (재구현)
-
Angular Components v15.0.0 : DOM/CSS 가 대부분 변경됨
- 매터리얼 디자인 MDC(Material Design Components for Web)
- 웹컴포넌트 Material Web Components v0.27.0
-
Material 디자인을 원본에 가깝게, 온전히 사용할 수 있음
- 기존에는 Angular Material 을 통해서 사용
- CDN 을 통해서 일반 HTML 에서도 사용 가능
-
기존 Material 컴포넌트들은
legacy-
prefix 가 붙게됨- ex) 매터리얼 버튼 모듈
MatLegacyButtonModule
- ex) 매터리얼 버튼 모듈
매터리얼 컴포넌트의 인터페이스 개선 - 사용자 추천에 의한 개선
- CDK(Component Dev Kit) 리스트 박스의 프로그래밍 인터페이스 개선
- 스타일이 빠진 공백 상태의 인터페이스용 컴포넌트를 말함
- input/output 속성, 메소드 등이 포함됨
- 스타일이 빠진 공백 상태의 인터페이스용 컴포넌트를 말함
더 빠른 성능의 실험적 빌더를 포함했다.
- 실험적 빌더
@angular-devkit/build-angular:browser-esbuild
선언되지 않은 항목을 자동으로 import 문 추가하는 편의 기능 추가
참고 : Getting started with standalone components
-
예제 : 깃허브/walk-my-dog
- 리스트
/
: AppComponent 에 연결된 화면 - 상세
/details/{ 번호 }
: 완전 분리된 화면- 컴파일도 따로 되고, 로딩도 호출시점에 따로 올라옴
- 리스트
-
별 다른 설정 없이 이미지가 article 안에 맞춰서 꽉 차게 출력됨
- 원래부터 이랬는지는 잘 모르겠음
- 지연 로딩된 상세 페이지
$ ng new walk-my-dog --inline-template --inline-style --skip-tests
? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? CSS
$ ng g service Dogs --flat
$ ng g component DogsList --flat --standalone
$ ng g component DogView --flat --standalone
$ ng g component DogsListCard --flat --standalone
- src
- index.html
- main.ts
- app
- app-routing.module.ts
- app.component.ts
- app.module.ts : AppComponent 만 포함
- (독립형) dog-view.component.ts
- (독립형) dogs-list-card.component.ts
- (독립형) dogs-list.component.ts : DogsListCardComponent 참조
- dogs.service.ts
- assets
- 이미지들...
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, AppRoutingModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
- 독립형이라 해도 component 를 직접 연결하면 메인 번들에 포함된다
- 번들 분리와 지연 로딩을 노리려면 loadComponent 사용
- 따로 import 할 필요 없음. 소스에서 직접 로딩
import { RouterModule, Routes } from '@angular/router';
import { DogsListComponent } from './dogs-list.component';
export const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: 'list' },
{ path: 'list', component: DogsListComponent },
// { path: 'details/:index', component: DogViewComponent },
{
path: 'details/:index',
// lazy load the DogViewComponent
loadComponent: () =>
import('./dog-view.component').then((m) => m.DogViewComponent),
},
];
- standalone 속성과 함께 imports 모듈을 직접 등록
- CommonModule 에는 NgIf, NgFor 등 기본 지시자가 있다
- 이마저도 필요한 것만 골라서 등록할 수도 있음
- CommonModule 에는 NgIf, NgFor 등 기본 지시자가 있다
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Dog } from './dogs.service';
// routerLink 등의 라우팅 관련 디렉티브를 사용하기 위해
import { RouterModule } from '@angular/router';
@Component({
selector: 'app-dogs-list-card',
standalone: true,
imports: [CommonModule, RouterModule],
template: `...
<a routerLink="/details/{ { index } }">Learn More</a>
...`,
styles: [`...`]
})
export class DogsListCardComponent {
@Input() dog!: Dog; // Dog 인터페이스
@Input() index!: Number; // 숫자 Interface
}
지연로딩 되는 dog-view-comp...
뷰가 따로 번들링 되었다.
$ ng build
✔ Browser application bundle generation complete.
✔ Copying assets complete.
✔ Index html generation complete.
Initial Chunk Files | Names | Raw Size | Transfer Size
main.194e6f9.js | main | 216.23 kB | 59.41 kB
polyfills.0df5128.js | polyfills | 33.08 kB | 10.66 kB
runtime.4f5e7b5.js | runtime | 2.63 kB | 1.24 kB
styles.ef4e999.css | styles | 0 bytes | -
| Initial Total | 251.94 kB | 71.31 kB
Lazy Chunk Files | Names | Raw Size | Transfer Size
566.caca22c.js | dog-view-comp | 1.56 kB | 785 bytes
Build at: 2022-11-22T06:43:15.285Z - Time: 2018ms
- 메인 번들은 최초 호출시 모두 로딩됨
src_app_dog-view_component_ts.js
파일이 따로 로딩됨- 함께 참조되는 assets 들도 지연 로딩
- src
- index.html : app-root 포함
- main.ts : Application 시작 지점과 routing 설정 제공
- app
- (독립형) app.component.ts => 라우팅 포함
- (독립형) foo.component.ts => 지연 로딩
app.component.ts 과 foo.component.ts 은 서로 완전 독립이다.
import { Component, OnInit } from '@angular/core';
import { CommonModule, Location } from '@angular/common';
import { RouterModule, Router } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterModule],
template: `...
<a *ngIf="route == 'Home'" routerLink="/foo">Foo</a>
<a *ngIf="route != 'Home'" href="javascript:void(0)"
(click)="goBack()">🔙 Back</a>
<router-outlet></router-outlet>
`,
styles: [],
})
export class AppComponent implements OnInit {
title = 'bootstrap-lonely';
route: string = '';
constructor(private location: Location, private router: Router) {
router.events.subscribe((val) => {
if (location.path() != '') {
this.route = location.path();
} else {
this.route = 'Home';
}
});
}
ngOnInit() {}
goBack() {
// window.history.back();
this.router.navigate(['/']);
}
}
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-foo',
standalone: true,
imports: [CommonModule],
template: ` <h3>foo works!</h3> `,
styles: [],
})
export class FooComponent {}
- bootstrap 설정 : AppComponent 으로 애플리케이션 시작
- providers 로 라우팅 설정을 제공 (RouterModule 에 포함됨)
import { importProvidersFrom } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app/app.component';
const routes = [
{ path: '', children: [] }, // empty path
{ path: 'foo',
loadComponent: () =>
import('./app/foo.component').then((m) => m.FooComponent),
},
];
bootstrapApplication(AppComponent, {
providers: [importProvidersFrom(RouterModule.forRoot(routes))],
}).catch((err) => console.error(err));
빌드하면 app-foo-component
뷰가 분리되어 번들링 됨
Welcome to bootstrap-lonely!
bootstrap-lonely app is running!
current: Home
[Foo]
######################################
## Foo 링크 클릭 후
######################################
Welcome to bootstrap-lonely!
bootstrap-lonely app is running!
current: /foo
🔙 Back
foo works!
1) purgecss : 사용하지 않는 css 제거
-
scripts 의 post-build 단계로 등록해서 사용
- 설치
npm i purgecss -D
- 설치
-
기존 css 에서 사용되는 부분만 남겨서 css 업데이트
- bootstrap 의 경우 268kb 가 25kb 로 1/10 축소됨
-
참고 Remove unused CSS styles from Bootstrap using PurgeCSS
- React, Angular 등에서도 사용
- TailwindCSS 에서 스폰서하고 있음
purgecss -css dist/*.css --content dist/index.html dist/*.js -o dist
# ...
Run PurgeCSS...
PurgeCSS done
┌─────┬───────────────────────┬──────────────┬───────────┐
│ idx │ file │ originalSize │ newSize │
├─────┼───────────────────────┼──────────────┼───────────┤
│ 0 │ 'styles.7302e820.css' │ '268.25kb' │ '25.44kb' │
└─────┴───────────────────────┴──────────────┴───────────┘
Typescript 오류 메세지
Element implicitly has an 'any' type because index expression is not of type 'number'.
-
window 가 any 타입을 key 로 사용하고 있기 때문에 지적한 오류
tsconfig.json
의 ts 컴파일 옵션에서 무시해야 한다
-
interface 의 key 대신에 다른 타입이 사용된 경우
as keyof
캐스팅 처리로 오류 수정
// ts(7015) 오류!
if (window['ngRef']) {
window['ngRef'].destroy();
}
window['ngRef'] = ref;
/////////////////////////////
interface IMyObj { title: string; content: string; }
const myObj: IMyObj = { title: 'Hi', content: 'Hope all is well' };
const myKey: string = 'content';
// ts(7015) 오류!
myObj[myKey] = 'All is great now!'; // title, content 만 가능
// `as keyof` 캐스팅 처리로 오류 수정
// myObj[myKey as keyof IMyObj] = 'All is great now!';
- ts 컴파일 옵션 수정 (항목 추가)
// tsconfig.json
{
"compilerOptions": {
"suppressImplicitAnyIndexErrors": true, // <-- 추가
"strictNullChecks":false,
"strictPropertyInitialization": false,
}
}
- 갈수록 좋아지는 앵귤러인데, 국내에서는 왜 찬밥인지..
- 앵귤러가 다이어트 하려고 노력중이다. (트리 쉐이킹, 코드 분리)
- Tree Shaking 이란? (코딩 용어)
- 나무를 흔들어 잔가지를 털어내듯 불필요한 코드를 제거하는 최적화
끝! 읽어주셔서 감사합니다. {: .prompt-info }