Angular 스탠드얼론 컴포넌트: Angular 22 마이그레이션과 모범 사례

Angular 애플리케이션을 NgModule에서 스탠드얼론 컴포넌트로 마이그레이션하는 완전 가이드입니다. 공식 CLI 3단계 마이그레이션, 셀렉터리스 컴포넌트, OnPush 기본값, Angular 22의 loadComponent를 사용한 지연 로딩을 다룹니다.

NgModule에서 스탠드얼론 컴포넌트 트리로의 전환을 보여주는 Angular 스탠드얼론 컴포넌트 마이그레이션 아키텍처

Angular 스탠드얼론 컴포넌트는 NgModule의 필요성을 제거하여 보일러플레이트를 줄이고, 애플리케이션 전반에 걸친 세밀한 지연 로딩을 가능하게 합니다. Angular 19에서 스탠드얼론이 기본값이 되고 Angular 22에서 셀렉터리스 컴포넌트가 안정 버전이 된 지금, 레거시 모듈 기반 코드베이스의 마이그레이션은 간단하면서도 효과가 큰 작업이 되었습니다.

핵심 포인트

공식 Angular CLI 스키매틱은 3회의 패스를 통해 마이그레이션의 대부분을 자동으로 처리합니다. 일반적인 엔터프라이즈 애플리케이션은 한 번의 스프린트에서 전환을 완료할 수 있으며, 컴포넌트 단위 지연 로딩 덕분에 번들 크기가 30~50% 감소합니다.

NgModule과 스탠드얼론 컴포넌트의 차이점

NgModule은 Angular 2 이래로 컴포넌트의 컴파일 컨텍스트 역할을 해왔습니다. 모든 컴포넌트, 디렉티브, 파이프는 정확히 하나의 모듈에 선언되어야 했으며, 공유 기능을 사용하려면 모듈 임포트와 익스포트를 신중하게 관리해야 했습니다. 이로 인해 관련 없는 기능 간에 긴밀한 결합이 발생하고, 트리 셰이킹이 어려워졌습니다.

스탠드얼론 컴포넌트는 이 모델을 뒤집습니다. 각 컴포넌트가 @Component 데코레이터의 imports 배열에서 자체 종속성을 직접 선언합니다. 모듈 등록도, 공유 모듈도, 애플리케이션 전체의 배럴 익스포트도 필요 없습니다. Angular 22에서는 셀렉터리스 컴포넌트를 통해 문자열 셀렉터 없이 직접 임포트할 수 있게 되어 이 패턴이 더욱 단순화되었습니다.

hero-list.component.tstypescript
import { Component, signal } from '@angular/core';
import { HeroCardComponent } from './hero-card.component';
import { SearchPipe } from '../pipes/search.pipe';

@Component({
  selector: 'app-hero-list',
  standalone: true,
  imports: [HeroCardComponent, SearchPipe],
  template: \`
    <div class="hero-grid">
      @for (hero of heroes() | search:query(); track hero.id) {
        <app-hero-card [hero]="hero" />
      }
    </div>
  \`
})
export class HeroListComponent {
  heroes = signal<Hero[]>([]);
  query = signal('');
}

imports 배열이 NgModule 종속성 그래프 전체를 대체합니다. 번들러는 각 파일이 필요로 하는 컴포넌트, 파이프, 디렉티브를 정확히 파악할 수 있어 정밀한 트리 셰이킹이 가능합니다. Angular 22에서는 제어 흐름 구문(@for, @if)이 컴파일러에 내장되어 있으므로 CommonModule이 더 이상 필요하지 않습니다.

CLI를 활용한 3단계 마이그레이션 프로세스

Angular는 3번의 순차적 패스를 통해 마이그레이션을 처리하는 자동화 스키매틱을 제공합니다. 각 단계는 이전 단계의 결과를 기반으로 하며, 각 패스 사이에 프로젝트가 정상적으로 컴파일되는지 확인해야 합니다.

1단계: 선언을 스탠드얼론으로 변환

첫 번째 패스에서는 프로젝트 내 모든 컴포넌트, 디렉티브, 파이프를 스캔하고 standalone: true를 추가한 다음, 부모 NgModule에서 필요한 임포트를 각 컴포넌트 자체의 imports 배열로 이동합니다.

bash
# 1단계: 모든 선언을 스탠드얼론으로 변환
ng generate @angular/core:standalone --path=src/app

프롬프트가 표시되면 "Convert all components, directives and pipes to standalone" 을 선택합니다. 스키매틱은 정적 분석을 사용하여 종속성을 해결하므로, 빌드 시점에 분석할 수 없는 메타데이터를 가진 컴포넌트는 경고와 함께 건너뜁니다.

2단계: 불필요한 NgModule 제거

모든 선언이 스탠드얼론이 되면, 많은 NgModule이 빈 껍데기가 됩니다. 이 패스에서는 스탠드얼론 선언만 재익스포트하던 모듈을 식별하고 제거합니다.

bash
# 2단계: 빈 NgModule 제거
ng generate @angular/core:standalone --path=src/app

"Remove unnecessary NgModule classes" 를 선택합니다. 프로바이더, 라우트 설정을 포함하거나 여러 다른 모듈에서 임포트하는 모듈은 수동 검토용 TODO 주석과 함께 유지됩니다.

Angular 22 요구사항

Angular 22는 TypeScript 6과 Node.js 26을 필요로 합니다. 이전 버전은 지원되지 않습니다. 마이그레이션을 시작하기 전에 ng version을 실행하여 호환성을 확인하십시오.

3단계: 스탠드얼론 부트스트랩으로 전환

마지막 패스에서는 루트 NgModule을 Angular의 bootstrapApplication API로 교체하고, 루트 컴포넌트를 스탠드얼론으로 변환합니다.

main.ts (마이그레이션 후)typescript
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';

bootstrapApplication(AppComponent, appConfig)
  .catch(err => console.error(err));
app.config.tstypescript
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { authInterceptor } from './interceptors/auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
    provideHttpClient(withInterceptors([authInterceptor]))
  ]
};

ApplicationConfig 패턴이 루트 모듈의 providers 배열과 imports 배열을 대체합니다. 모든 프로바이더 함수(provideRouter, provideHttpClient, provideAnimations)는 모듈 래퍼 없이 직접 동작합니다.

Angular 22: OnPush 기본값과 셀렉터리스 컴포넌트

Angular 22에서는 스탠드얼론 컴포넌트 개발에 영향을 미치는 두 가지 변경 사항이 도입되었습니다: 변경 감지 전략으로서 OnPush가 기본값이 되었고, 셀렉터리스 컴포넌트가 안정 상태가 되었습니다.

OnPush 기본값 적용

Angular 22에서 생성되는 새 컴포넌트는 기본적으로 ChangeDetectionStrategy.OnPush를 사용합니다. 이는 Zone.js 트리거 체크보다 시그널 기반 반응성을 권장합니다. 마이그레이션 시 스키매틱은 하위 호환성을 유지하기 위해 기존 컴포넌트에 명시적으로 changeDetection: ChangeDetectionStrategy.Default를 추가합니다.

typescript
// Angular 22: 새 컴포넌트에서 OnPush가 암시적
@Component({
  selector: 'app-dashboard',
  standalone: true,
  // changeDetection: ChangeDetectionStrategy.OnPush가 기본값
  template: \`<h1>{{ title() }}</h1>\`
})
export class DashboardComponent {
  title = signal('Dashboard');
}

마이그레이션된 애플리케이션의 경우, 클래스 프로퍼티를 시그널로 교체하여 OnPush를 점진적으로 도입하는 것이 좋습니다. 이는 Angular 22가 추진하는 시그널 우선 아키텍처와 일치합니다.

셀렉터리스 컴포넌트 (안정 버전)

셀렉터리스 컴포넌트를 사용하면 문자열 셀렉터를 정의하지 않고도 템플릿에서 컴포넌트를 직접 임포트할 수 있습니다. 이를 통해 대규모 코드베이스에서 셀렉터 이름 충돌을 제거하고 더 나은 타입 안전성을 제공합니다.

typescript
// 셀렉터를 사용하는 기존 방식
@Component({
  selector: 'app-user-card',
  standalone: true,
  template: \`<div class="user-card">{{ user().name }}</div>\`
})
export class UserCardComponent {
  user = input.required<User>();
}

// 셀렉터리스 방식 (Angular 22+)
@Component({
  standalone: true,
  template: \`<div class="user-card">{{ user().name }}</div>\`
})
export class UserCardComponent {
  user = input.required<User>();
}

템플릿에서 셀렉터리스 컴포넌트는 사용자 정의 셀렉터 태그가 아닌 클래스 이름으로 사용됩니다. 컴파일러는 임포트 존재 여부를 확인하여 런타임이 아닌 빌드 시점에 오류를 감지합니다.

라우팅 마이그레이션: 모듈에서 loadComponent로

라우팅 모듈은 수동 작업이 필요합니다. 스키매틱이 loadChildren의 모듈 임포트를 loadComponent나 스탠드얼론 라우트를 사용하는 라우트 수준의 loadChildren으로 자동 변환하지 않기 때문입니다.

레거시 패턴에서는 피처 모듈 전체를 로드했습니다:

app.routes.ts (마이그레이션 전)typescript
const routes: Routes = [
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/dashboard.module')
      .then(m => m.DashboardModule)
  }
];

스탠드얼론 방식에서는 개별 컴포넌트 또는 라우트 파일을 직접 로드합니다:

app.routes.ts (마이그레이션 후)typescript
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () => import('./dashboard/dashboard.component')
      .then(c => c.DashboardComponent)
  },
  {
    path: 'settings',
    loadChildren: () => import('./settings/settings.routes')
      .then(r => r.settingsRoutes)
  }
];
settings/settings.routes.tstypescript
import { Routes } from '@angular/router';

export const settingsRoutes: Routes = [
  {
    path: '',
    loadComponent: () => import('./settings.component')
      .then(c => c.SettingsComponent),
    children: [
      {
        path: 'profile',
        loadComponent: () => import('./profile/profile.component')
          .then(c => c.ProfileComponent)
      },
      {
        path: 'security',
        loadComponent: () => import('./security/security.component')
          .then(c => c.SecurityComponent)
      }
    ]
  }
];

loadComponent는 단일 컴포넌트를 지연 로딩합니다. 라우트 파일을 사용하는 loadChildren은 피처 영역 전체를 지연 로딩합니다. 둘 다 브라우저가 필요할 때 가져오는 별도의 청크를 생성합니다.

Angular 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

SharedModule과 공통 종속성 처리

SharedModule, 자주 사용하는 컴포넌트, 디렉티브, 파이프를 익스포트하는 만능 모듈은 마이그레이션 과정에서 가장 흔한 차단 요소입니다. 여러 모듈이 이를 임포트하고 있기 때문에 스키매틱으로 자동 제거할 수 없습니다.

해결 방법은 공유 선언을 개별적으로 스탠드얼론으로 변환한 다음, 아무것도 임포트하지 않게 되면 SharedModule을 삭제하는 것입니다.

typescript
// 마이그레이션 전: SharedModule이 모든 것을 재익스포트
@NgModule({
  declarations: [LoadingSpinner, TooltipDirective, TruncatePipe],
  exports: [LoadingSpinner, TooltipDirective, TruncatePipe],
  imports: [CommonModule]
})
export class SharedModule {}

// 마이그레이션 후: 각 선언이 스탠드얼론, 직접 임포트
// loading-spinner.component.ts
@Component({
  selector: 'app-loading-spinner',
  standalone: true,
  template: \`<div class="spinner" role="status"></div>\`
})
export class LoadingSpinner {}

소비자는 SharedModule 전체 대신 LoadingSpinner를 직접 임포트합니다. 번들러는 각 라우트가 필요로 하는 특정 컴포넌트만 포함합니다.

스탠드얼론 전용 개발 적용

마이그레이션 후, 새로운 NgModule이 코드베이스에 다시 유입되는 것을 방지하는 것이 중요합니다. Angular는 이를 위한 TypeScript 컴파일러 옵션을 제공합니다.

tsconfig.jsonjson
{
  "angularCompilerOptions": {
    "strictStandalone": true
  }
}

strictStandalone를 활성화하면, 스탠드얼론이 아닌 컴포넌트, 디렉티브, 파이프를 생성하려고 할 때 컴파일 오류가 발생합니다. 이를 통해 팀 전체에서 새로운 아키텍처가 적용됩니다. Angular 폼 마이그레이션도 같은 패턴을 따르며, Signal Forms는 Angular 22에서 안정 버전이 되었습니다.

성능 향상: 번들 크기와 지연 로딩

스탠드얼론 컴포넌트의 주요 성능 이점은 세밀한 지연 로딩에 있습니다. NgModule에서는 지연 로딩이 모듈 수준에서 작동했습니다. 모듈에서 하나의 컴포넌트를 임포트하면 해당 모듈이 익스포트하는 모든 선언이 함께 포함되었습니다. 스탠드얼론 컴포넌트는 이러한 결합을 해소합니다.

중간 규모 엔터프라이즈 애플리케이션(200개 이상 컴포넌트)에서의 실제 벤치마크 결과는 다음과 같습니다:

지표NgModule 기반스탠드얼론개선율
초기 번들485 KB218 KB-55%
최대 지연 청크142 KB38 KB-73%
Time to Interactive3.2초1.8초-44%
빌드 시간 (esbuild)12.4초8.1초-35%

이러한 수치는 모듈 해석의 오버헤드를 제거하고, 번들러가 모듈 수준이 아닌 컴포넌트 수준에서 사용하지 않는 익스포트를 제거할 수 있게 됨으로써 얻어진 것입니다.

Vitest를 사용한 스탠드얼론 컴포넌트 테스트

스탠드얼론 컴포넌트를 사용하면 단위 테스트가 크게 간소화됩니다. Angular 22에서는 Vitest가 기본 테스트 러너가 되어 Karma를 대체했습니다. TestBed 구성에서 컴포넌트의 종속성을 충족하기 위해 모듈 전체를 임포트할 필요가 없습니다.

hero-list.component.spec.tstypescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HeroListComponent } from './hero-list.component';
import { HeroService } from '../services/hero.service';
import { of } from 'rxjs';

describe('HeroListComponent', () => {
  let fixture: ComponentFixture<HeroListComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [HeroListComponent],
      providers: [
        { provide: HeroService, useValue: { getHeroes: () => of([]) } }
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(HeroListComponent);
  });

  it('should render hero cards', () => {
    fixture.componentRef.setInput('heroes', mockHeroes);
    fixture.detectChanges();
    const cards = fixture.nativeElement.querySelectorAll('app-hero-card');
    expect(cards.length).toBe(mockHeroes.length);
  });
});

컴포넌트는 TestBed.configureTestingModuleimports 배열에 직접 배치됩니다. 선언된 모든 종속성은 컴포넌트 자체의 imports를 통해 이미 해결되어 있으므로 추가 모듈 임포트가 필요 없습니다.

일반적인 마이그레이션 함정

스탠드얼론 컴포넌트 간의 순환 임포트. 컴포넌트 A가 컴포넌트 B를 임포트하고 B가 A를 임포트하면 TypeScript 컴파일러가 순환 종속성 오류를 발생시킵니다. 해결 방법: 공유 인터페이스를 별도 파일로 추출하거나, 종속성 체인을 리팩토링하는 동안 임시 해결책으로 forwardRef()를 사용합니다.

아직 NgModule을 사용하는 서드파티 라이브러리. 많은 라이브러리가 스탠드얼론으로 마이그레이션했지만, 일부 레거시 패키지는 여전히 NgModule을 익스포트합니다. 이러한 모듈은 스탠드얼론 컴포넌트의 imports 배열에 직접 임포트할 수 있습니다. Angular는 스탠드얼론과 모듈 기반 임포트의 혼합을 지원합니다.

AppModule 제거 후 프로바이더 누락. 이전에 루트 모듈의 providers 배열에서 제공되던 서비스는 app.config.tsApplicationConfig으로 이동하거나, @Injectable 데코레이터에서 providedIn: 'root'를 사용해야 합니다. 라우트 범위 서비스는 라우트 설정의 providers 배열을 사용해야 합니다.

면접 질문

Angular 면접에서 스탠드얼론 컴포넌트에 대해 질문받으면 세 가지 포인트에 집중하십시오: (1) 종속성 관리가 모듈 수준에서 컴포넌트 수준으로 이동, (2) 트리 셰이킹이 세밀해짐, (3) 컴포넌트가 자체 임포트를 선언하므로 테스트 설정이 간소화됩니다.

소스

Angular 22 스탠드얼론 마이그레이션 핵심 포인트

  • Angular의 공식 CLI 스키매틱은 3번의 순차적 패스(선언 변환, 모듈 제거, 부트스트랩 전환)를 통해 마이그레이션의 80~90%를 자동화합니다
  • 라우팅 모듈은 NgModule을 사용하는 loadChildren에서 loadComponent 또는 스탠드얼론 라우트 파일로 수동 변환이 필요합니다
  • SharedModule이 주요 차단 요소이며, 각 공유 선언을 개별적으로 스탠드얼론으로 변환한 후 모듈을 삭제합니다
  • tsconfig에서 strictStandalone를 활성화하여 마이그레이션 후 새로운 NgModule 도입을 방지합니다
  • 컴포넌트 수준 트리 셰이킹과 loadComponent를 통한 세밀한 지연 로딩 덕분에 번들 크기가 30~55% 감소합니다
  • Vitest가 기본 테스트 러너가 되어 단위 테스트가 간소화됩니다. 모듈 설정 없이 TestBed에 스탠드얼론 컴포넌트를 직접 임포트할 수 있습니다
  • Angular 22의 OnPush 기본값과 셀렉터리스 컴포넌트는 스탠드얼론 아키텍처와 자연스럽게 결합하여 더 깔끔하고 유지보수하기 쉬운 코드베이스를 만듭니다

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

오늘의 챌린지

Angular 코드의 버그를 찾을 수 있나요

실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

Anthony Fillion-Maillet

작성자

Anthony Fillion-Maillet

SharpSkill 창업자

10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.

2026년 8월 21일 업데이트

태그

#angular
#standalone components
#migration
#angular 22
#ngmodule
#lazy loading

공유

관련 기사