diff --git a/JinoBae/2023-11-17-angular-standalone-component.md b/JinoBae/2023-11-17-angular-standalone-component.md new file mode 100644 index 00000000..bfcee15a --- /dev/null +++ b/JinoBae/2023-11-17-angular-standalone-component.md @@ -0,0 +1,316 @@ +// https://blex.me/@baealex/angular-standalone-component + +## NgModule + +--- + +오늘날의 프론트엔드 프레임워크는 컴포넌트 간의 결합으로 어플리케이션을 개발하는데 앵귤러는 그 위에 모듈(NgModule)이라는 것이 하나 더 있다. 이는 대규모 어플리케이션을 개발할 때, 코드의 재사용성을 높혀주고 어플리케이션의 구조를 명확하게 정의하는데 도움되지만... + +특정 사례에서는 코드의 복잡성과 오버헤드 높히는 요소가 되기도 한다. 모듈은 재사용되기 좋은 단위이지만 모듈의 각 요소들은 재사용이 어렵다. 모듈 내에서 강하게 의존하고 있기 때문이다. + +
+ +#### 모듈의 흔한 문제 사례 + +1. 복잡성을 높힌 사례 : 리펙토링이 필요하다. `A` 모듈의 `x` 컴포넌트를 `B` 모듈로 옮기자. +2. 오버헤드를 높힌 사례 : 이 컴포넌트는... 어디보자... `SharedModule`에 넣자! + +먼저 1번 사례를 살펴보면, 리액트에서는 컴포넌트 파일만 봐도 의존성을 대체로 파악할 수 있는데 반해 모듈 기반으로 만들어진 앵귤러 어플리케이션은 컴포넌트 파일만 보고는 컴포넌트의 의존성을 정확히 파악하기 어렵다. 컴포넌트가 속한 모듈의 `declarations`을 비롯해 모듈이 `imports`하고 있는 다른 모듈의 `exports` 구문도 모두 살펴봐야 의존 관계를 파악할 수 있다. + +따라서 1번 사례인 `A` 모듈의 `x` 컴포넌트를 `B` 모듈로 옮기는 작업은 의존 관계를 모두 파악하여 B 모듈에도 의존하는 요소를 동일하게 포함시켜야 하므로 리팩토링을 어렵게 만들고 결과적으로 컴포넌트를 비롯해 각 요소들의 재사용성을 떨어트린다. + +2번 사례는 충분히 독립 가능한 컴포넌트가 거대한 모듈에 포함되어 (앵귤러는 모듈간에 결합이 이뤄지므로) 하나의 컴포넌트만 호출이 필요한 상황에서 묶여있는 모든 컴포넌트가 같이 호출되어 불필요한 오버헤드가 발생한다. + +
+ +#### SCAM! + +이것에 대한 해결책으로 제안된 것이 \*SCAM 패턴이다. + +> \*SCAM = Single Component Angular Module + +하나의 컴포넌트가 하나의 모듈에 속하는 것이다. 컴포넌트, 파이프, 디렉티브를 작은 단위의 모듈로 만든다. 그러면 보다 쉽게 의존 관계를 파악할 수 있고 재사용 가능하며, 앵귤러에서는 모듈 단위로 지연 로딩을 설정할 수 있으므로 불필요한 오버헤드를 발생시키지 않을 수 있다. + +앵귤러의 독립형 컴포넌트는 이 패턴의 연장선이다. 앵귤러 14 부터 독립형 컴포넌트라는 개념을 실험적으로 도입하여 컴포넌트, 파이프, 디렉티브를 모듈없이 독립형으로 선언할 수 있도록 하였고, 이는 컴포넌트 간의 결합으로 어플리케이션을 개발할 수 있게 만들었다. + +독립형 컴포넌트는 모듈에 대한 이해 없이도 앵귤러 어플리케이션을 개발할 수 있어 진입 장벽을 낮춰준다고 하는데... 최근 앵귤러를 접한 입문자로써 모듈과 독립형 컴포넌트가 섞여있는 현재 구조에서 더 많은 어려움과 혼란을 겪은 듯 하다. (나중에는 정말 쉬워질지도?) + +여하지간 모듈에 대해서 어느정도 이해하니 이제야 독립형 컴포넌트가 눈에 들어오기 시작했다. 실제로 독립형 컴포넌트 만으로 작은 어플리케이션을 만들어보니 별다른 문제가 없었으며 모듈을 작성하지 않으니 오히려 간결하게 느껴지기도 했다. + + + ![](https://static.blex.me/images/content/2023/11/17/2023111720_aO72TlqIQVTTP6LSiEKm.png) + 출처 : reddit - r/angular + + +커뮤니티 반응을 살펴보니 아키텍처를 위해서 모듈을 유지하겠다는 의견도 있고, 변화를 긍정적으로 생각하는 사용자도 많다. 독립형 컴포넌트는 앵귤러 16 부터는 실험 기능이 아닌 정식 기능으로 도입되었다. + +
+ +## 독립형 컴포넌트 + +--- + +> 여기서는 포괄적인 용어로 독립형 컴포넌트라는 단어를 사용하지만 파이프나 디렉티브도 독립형으로 선언할 수 있으니 포함된다고 생각해도 좋다. + +독립형 컴포넌트를 어떻게 사용하고, 기존 모듈과 공존시키는지, 기존 앱 모듈을 걷어내고 시작부터 독립형 컴포넌트로 전환할 수 있을지, 라우터에서 간단하게 레이지 로딩하는 방안에 대해서 살펴본다. + +
+ +#### standalone + +독립형 컴포넌트는 컴포넌트를 생성할 때 `@Component` 데코레이터 안에 `standalone` 옵션을 설정해주면 된다. 그럼 컴포넌트 내부에서도 `imports`를 사용할 수 있다. + +```ts +@Component({ + standalone: true, // + + selector: 'app-my-component', + imports: [ + // ... + ], + template: ` +

My Component

+ `, + styles: [ + ] +}) +``` + +
+ +#### 모듈과 공존하기 + +훗날 앵귤러에서 모듈이 사라진다면 말은 달라지겠지만, 쉽게 이해하기 위해 현재로써는 독립형 컴포넌트는 컴포넌트라는 이름으로 둔갑한 모듈(SCAM)이라고 생각하는게 편하다. + +###### 독립형 컴포넌트 <- 모듈 + +`imports` 구문에는 모듈만 선언할 수 있다. 독립형 컴포넌트도 작은 단위의 모듈이라고 생각하자. 따라서 일반 컴포넌트의 경우에는 `imports` 구문에 직접 명시할 수 없으므로 기존과 동일하게 모듈을 통해 불러오거나 또는 `standalone`을 명시하여 독립형 컴포넌트로 전환하여 불러올 수 있다. + +**잘못된 유형** + +```ts +// general.component.ts +@Component({ + selector: 'app-general', + template: ` + ... + `, + styles: [ + ] +}) +export class GeneralComponent { + +} + +// standalone.componet.ts +@Component({ + standalone: true, + selector: 'app-standalone', + imports: [ + CommonModule, + GeneralComponent // error + ], + template: ` + + `, + styles: [ + ] +}) +export class StandaloneComponent { + +} +``` + +**수정 방안 1 : Module을 통한 결합** + +```ts +// general.module.ts +@NgModule({ + declarations: [ + GeneralComponent, + ], + imports: [ + CommonModule + ], + exports: [ + GeneralComponent, + ] +}) +export class GeneralModule { } + +// standalone.componet.ts +@Component({ + standalone: true, + selector: 'app-standalone', + imports: [ + CommonModule, + GeneralModule + ], + template: ` + + `, + styles: [ + ] +}) +export class StandaloneComponent { + +} +``` + +**수정 방안 2 : 독립형 컴포넌트로 전환** + +```ts +// general.component.ts +@Component({ + standalone: true, + selector: 'app-general', + imports: [ + CommonModule, + ], + template: ` + ... + `, + styles: [ + ] +}) +export class GeneralComponent { + +} + +// standalone.componet.ts +@Component({ + standalone: true, + selector: 'app-standalone', + imports: [ + CommonModule, + GeneralComponent + ], + template: ` + + `, + styles: [ + ] +}) +export class StandaloneComponent { + +} +``` + +
+ +###### 모듈 <- 독립형 컴포넌트 + +모듈에서 일반 컴포넌트는 `declarations`에 선언됨에 반해 독립형 컴포넌트는 `imports` 구문에 선언되어야 함을 유의해야 한다. + + ```ts +// app.module.ts +@NgModule({ + declarations: [ + AppComponent, + // StandaloneComponent (x) + ], + imports: [ + BrowserModule, + AppRoutingModule, + StandaloneComponent // (o) + ], + providers: [], + bootstrap: [AppComponent] +}) +export class AppModule { } + +// app.component.ts +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + template: ` + + `, + styles: [] +}) +export class AppComponent { + title = 'app'; +} +``` + +
+ +#### 부트스트랩 + +독립형 컴포넌트는 앵귤러의 진입점으로 지정하여 모듈을 전혀 사용하지 않고도 어플리케이션을 개발할 수 있다. 차이점을 살펴보기 위해서 모듈이 진입점으로 지정되는 기존의 형태를 살펴보자. + +```ts +// main.ts +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); + +// app.module.ts +@NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + AppRoutingModule, + ], + providers: [ + // ... + ], + bootstrap: [AppComponent] +}) +export class AppModule { } +``` + +기존 모듈의 경우 라우팅 모듈을 앱 모듈 내에서 임포트하여 부트스트랩하는 구조이지만, 독립형 컴포넌트는 라우팅 모듈이라는 개념 없이도 `provideRouter`를 활용해 라우터를 적용시킬 수 있다. + +```ts +// main.ts +bootstrapApplication(AppComponent, { + providers: [ + provideRouter(routes), + // ... + ] +}).catch((err) => console.error(err)); + +// app.component.ts +@Component({ + selector: 'app-root', + standalone: true, + imports: [CommonModule, RouterOutlet], + template: ` + ... + `, + styles: [], +}) +export class AppComponent { + title = 'app'; +} +``` + +
+ +#### 레이지 로딩 + +독립형 컴포넌트를 사용하면 라우터에서 손쉽게 레이지 로딩을 처리할 수 있다. + +```ts +const routes: Routes = [{ + path: '', + loadComponent: () => import('./list/list.component').then(m => m.ListComponent), +}] +``` + +독립형 컴포넌트를 활용하면 앵귤러로 작은 규모의 앱을 만들때 비교적 유용할 듯 하다. + +
+ +## 참고자료 + +--- + +- Your Angular Module is a SCAM! · Medium @Younes +[#](https://medium.com/marmicode/your-angular-module-is-a-scam-b4136ca3917b) +- Getting started with standalone components · Angular +[#](https://angular.io/guide/standalone-components) +- Ngmodules vs Standalone Components or Angular2 vs Angular3 · DEV @layzee +[#](https://blog.stackademic.com/ngmodules-vs-standalone-components-or-angular2-vs-angular3-ba54fab04ce3) diff --git a/JinoBae/2023-11-25-angular-pipe.md b/JinoBae/2023-11-25-angular-pipe.md new file mode 100644 index 00000000..dfbaba76 --- /dev/null +++ b/JinoBae/2023-11-25-angular-pipe.md @@ -0,0 +1,222 @@ +// https://blex.me/@baealex/angular-pipe + +앵귤러는 템플릿에 표기되는 데이터를 변환하는 기능을 파이프라고 부른다. 아래와 같이 사용할 수 있다. + +```ts +{{ someData | somePipe }} +``` + +
+ +#### 기본 파이프 + +CommonModule에 포함된 기본 파이프 + +https://angular.io/api/common/CommonModule#pipes + +- [AsyncPipe](https://angular.io/api/common/AsyncPipe) + +```ts +promise = new Promise((resolve) => setTimeout(() => resolve('Umm... Hello!'), 1000)) + +{{ promise | async }} +// (1 second later) Umm... Hello! +``` + +- [DatePipe](https://angular.io/api/common/DatePipe) + +```ts +date = new Date() + +{{ date | date:'short' }} +// 2023-11-23 +``` + +- [CurrencyPipe](https://angular.io/api/common/CurrencyPipe) + +```ts +price = 100 + +{{ price | currency:'USD' }} +// $100.00 +``` + +- [PercentPipe](https://angular.io/api/common/PercentPipe) + +```ts +percentage = 0.15 + +{{ percentage | percent }} +// 15% +``` + +- [UpperCasePipe](https://angular.io/api/common/UpperCasePipe) + +```ts +name = 'Jino Bae' + +{{ name | uppercase }} +// JINO BAE +``` + +- [LowerCasePipe](https://angular.io/api/common/LowerCasePipe) + +```ts +{{ name | lowercase }} +// jino bae +``` + +- [TitleCasePipe](https://angular.io/api/common/TitleCasePipe) + +```ts +{{ name | titlecase }} +// Jino Bae +``` + +- [SlicePipe](https://angular.io/api/common/SlicePipe) + +```ts +longText = "Hello, everyone. My name is Jino." + +{{ longText | slice:0:10 }} +// Hello, eve +``` + +- [JsonPipe](https://angular.io/api/common/JsonPipe) + +```ts +object = { + name: 'Jino Bae' +} + +{{ object | json }} +// { "name": "Jino Bae" } +``` + +
+ +#### 커스텀 파이프 + +파이프를 만드려면 `@Pipe` 테코레이터로 감싼 클래스를 `PipeTransform` 추상 클래스를 상속받아 `transform` 메서드를 구현해주면 된다. 파라미터는 `(value, ...args)`, 형태이고 위 기본 파이프를 보면 알겠지만 데이터가 `value`이고 뒤에 붙는 값이 각각의 `arg`가 된다. + +```ts +{{ someData | somePipe : 'arg1' : 'arg2' : 'arg3' }} +``` + +파이프의 옵션은 다음과 같다. + +###### name: `string` + +파이프를 호출할 이름이다. 카멜 케이스로 작명할 것을 권장하고 있다. + +```ts +import { Pipe } from '@angular/core'; + +@Pipe({ + name: 'myPipe', +}) +export class MyPipePipe implements PipeTransform { + transform(value: string, format: string): unknown { + return null + } +} +``` + +###### pure: `boolean` + +옵셔널한 속성으로 기본값은 `true`이다. 순수 함수, 비순수 함수를 말하는 것은 알겠는데, 설명만 보고는 어떻게 동작하는지 명확히 이해하기가 어려웠다. 동작하는 형태로만 보면 pure pipe는 선언한 데이터의 참조 값에 변화가 있는 경우에만 실행되고 impure 파이프는 매 순간에 실행된다. + +예를들어 아래와 같은 impure pipe가 있을 때 + +```ts +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'myPipe', + pure: false, +}) +export class MyPipePipe implements PipeTransform { + transform(value: string, format: string): unknown { + console.log('pipe.transform()') + return null + } +} +``` + +아래와 같은 템플릿을 생성한 상황인 경우 + +```ts +

{{ state1 }}

+

{{ state2 | myPipe }}

+``` + +파이프를 바인딩하지 않은 `state1`의 값이 변화되는 시점에도 콘솔에는 `pipe.transform()`이 찍히게 된다. pure pipe의 경우에는 `state2`의 변화에만 해당 로그가 찍힌다. + +정확히 말하면 pure pipe의 경우에는 컴포넌트의 렌더링을 새로하는 상황이라도 입력 데이터의 참조 값이 변하지 않았다면 해당 파이프의 transform 메서드를 실행하지 않는다. 입력에 대하여 캐싱된 결과를 출력하기 때문이다. pure pipe로 왠만하면 다 처리할 수 있을 것 같은데.. impure pipe는 어떨 때 사용하면 좋을까? + +- 순수 파이프 : 같은 입력에 같은 결과가 나와도 상관없을 때 (`uppercase`, `format`) +- 비순수 파이프 : 매번 새로운 결과가 필요할 때 또는 값의 변화를 감지해야 할 때 (`sort`, `async`) + +비순수 파이프가 필요한 상황을 생각하다가 기본으로 제공되는 `async` 파이프가 떠올랐다. 이 파이프를 간단하게 직접 구현해보자. + +```ts +@Pipe({ + name: 'myAsync', + pure: false +}) +export class MyAsyncPipe implements PipeTransform { + latestPromise: Promise | null = null; + value = null; + + transform(promise: Promise, ...args: unknown[]) { + if (this.latestPromise !== promise) { + this.latestPromise = promise; + this.latestPromise.then((value: any) => { + this.value = value; + }); + } + return this.value; + } +} +``` + +이 코드는 참고용으로 만들었다. 이 파이프를 pure pipe로 바꾸면 화면에는 영원히 resolve 되는 값을 출력할 수 없다. impure pipe는 참조 값 뿐 아니라 값의 변화에도 동작하므로 지속적으로 참조 비교 및 resolve 된 값을 출력할 수 있음에 반해 pure pipe 입장에서는 참조 값이 변한 것은 아니므로 캐싱된 결과인 null을 출력하기 때문이다. + +###### standalone: `boolean` + +옵셔널한 속성으로 기본값은 `false`이다. 이 옵션을 사용하면 모듈에 포함하지 않고도 파이프 자체가 모듈로서 동작한다. 자세한 것은 여기를 참고하는게 좋을 듯 하다. + +- Angular :: Standalone Component · BLEX @baealex +[#](https://blex.me/@baealex/angular-standalone-component) + +텍스트의 포멧팅을 지정해주는 파이프를 생성해 보았다. + +```ts +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'myPipe', + standalone: true, +}) +export class MyPipePipe implements PipeTransform { + transform(value: string, format: string, key = 'x'): string { + let result = ''; + let j = 0; + + for (let i = 0; i < format.length; i++) { + if (format[i] === key) { + result += value.toString()[j++]; + } else { + result += format[i]; + } + } + + return result; + } +} +``` + +```ts +

{{ '01012345678' | myPipe: 'xxx-xxxx-xxxx'}}

+// 010-1234-5678 +```