Angular는 최신 버전에서 개발자 경험을 크게 개선하는 여러 기능을 도입했습니다. 그 중에서도 Standalone Component와 inject API는 Angular 애플리케이션의 구조와 의존성 관리 방식을 변화시키는 핵심 요소입니다. 이 글에서는 이러한 변화들이 Angular 개발에 미치는 영향을 살펴보고, 최신 의존성 주입 방식인 inject와 컴포넌트 생명주기 관리에 관한 새로운 패턴을 소개하겠습니다.
기존 Angular 프로젝트에서 모든 컴포넌트는 하나 이상의 NgModule에 종속되었습니다. 그러나 Angular 15부터는 Standalone Component 개념이 도입되어 모듈 종속성을 줄일 수 있게 되었습니다. 이를 통해 개발자는 더 간결하고 독립적인 컴포넌트를 만들 수 있습니다.
Standalone Component를 설정할 때 다양한 옵션을 사용할 수 있습니다:
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
standalone: true,
selector: 'app-example',
templateUrl: './example.component.html',
styleUrl: './example.component.css',
imports: [CommonModule],
providers: [HttpService]
})
export class ExampleComponent {}
기존 Angular에서 의존성 주입은 생성자 주입(Constructor Injection)을 통해 이루어졌습니다. 컴포넌트 또는 서비스의 생성자에 의존성을 주입함으로써 Angular의 DI(Dependency Injection) 시스템이 자동으로 객체를 관리했습니다. 예를 들어, 다음과 같은 코드가 일반적이었습니다:
import { Component } from '@angular/core';
import { SomeService } from './some.service';
@Component({
selector: 'app-example',
templateUrl: './example.component.html'
})
export class ExampleComponent {
constructor(private someService: SomeService) {}
}
이 패턴은 여전히 유용하지만, Angular 15부터는 더 유연한 방식으로 의존성을 주입할 수 있는 inject API가 도입되었습니다.
inject를 사용하면 의존성을 클래스 생성자가 아닌, 메서드나 함수 내부에서 주입할 수 있습니다. 이를 통해 더 유연한 코드 작성이 가능해졌으며, 특히 비동기 작업이나 동적 서비스 관리에 강력한 도구가 됩니다.
import { Component, inject } from '@angular/core';
import { SomeService } from './some.service';
@Component({
selector: 'app-example',
templateUrl: './example.component.html'
})
export class ExampleComponent {
private someService = inject(SomeService);
someMethod() {
this.someService.performAction();
}
}
Angular에서 컴포넌트가 파괴될 때 주로 ngOnDestroy 메서드를 사용하여 구독을 해제하거나 자원을 정리합니다. 하지만 Angular 15에서는 더 간편한 방식인 DestroyRef를 도입하여 생명주기 관리가 더욱 직관적으로 변화했습니다.
DestroyRef는 의존성 주입을 통해 제공되며, RxJS의 takeUntil 연산자와 함께 사용하면 컴포넌트 파괴 시 구독을 자동으로 해제할 수 있습니다.
import { Component, inject, OnInit } from '@angular/core';
import { DestroyRef } from '@angular/core';
import { interval } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'app-example',
template: `<h1>DestroyRef Example</h1>`
})
export class ExampleComponent implements OnInit {
private destroyRef = inject(DestroyRef);
private numbers$ = interval(1000).pipe(takeUntil(this.destroyRef.onDestroy()));
ngOnInit() {
this.numbers$.subscribe(num => console.log('Number:', num));
}
}
이처럼 DestroyRef는 컴포넌트가 파괴될 때 리소스를 자동으로 정리해주어, 메모리 누수를 방지하고 코드 복잡성을 줄입니다.
Angular는 렌더링 후에 발생하는 작업을 처리하기 위해 ngAfterViewInit과 ngAfterViewChecked 같은 생명주기 훅을 제공합니다. 그러나 최근에는 보다 세밀한 제어를 가능하게 하는 afterRender와 afterNextRender 메서드가 도입되었습니다.
afterRender는 뷰가 렌더링된 직후 호출되며, DOM 요소가 완전히 준비된 시점에서 스타일 적용, DOM 조작 등을 수행하는 데 유용합니다.
@Component({
standalone: true,
selector: 'my-chart-cmp',
template: `<div #chart>{{ ... }}</div>`,
})
export class MyChartCmp {
@ViewChild('chart') chartRef: ElementRef;
chart: MyChart|null;
constructor() {
afterNextRender(() => {
this.chart = new MyChart(this.chartRef.nativeElement);
}, {phase: AfterRenderPhase.Write});
}
}
afterNextRender는 다음 렌더링 주기가 완료된 후에 호출되며, 데이터 변경 후 UI 업데이트 작업을 효율적으로 처리할 수 있습니다.
@Component({
standalone: true,
selector: 'my-cmp',
template: `<span #content>{{ ... }}</span>`,
})
export class MyComponent {
resizeObserver: ResizeObserver|null = null;
@ViewChild('content') contentRef: ElementRef;
constructor() {
afterNextRender(() => {
this.resizeObserver = new ResizeObserver(() => {
console.log('Content was resized');
});
this.resizeObserver.observe(this.contentRef.nativeElement);
}, {phase: AfterRenderPhase.Write});
}
ngOnDestroy() {
this.resizeObserver?.disconnect();
this.resizeObserver = null;
}
}