16.2 환경 설정 — Angular CLI, 프로젝트 구조, Standalone 컴포넌트
사전 준비
Angular 개발을 시작하기 전에 Node.js와 npm이 필요합니다.
# Node.js 버전 확인 (18.19+ 또는 20+ 권장)
node --version
# v20.11.0
# npm 버전 확인
npm --version
# 10.2.4
Angular 19는 Node.js 18.19.1 이상 또는 20.x / 22.x를 지원합니다.
Angular CLI 설치
Angular CLI는 프로젝트 생성, 컴포넌트/서비스 생성, 빌드, 테스트를 자동화하는 필수 도구입니다.
# 글로벌 설치
npm install -g @angular/cli
# 버전 확인
ng version
설치 후 ng version 출력 예시:
Angular CLI: 19.x.x
Node: 20.x.x
Package Manager: npm 10.x.x
OS: ...
Angular: 19.x.x
... animations, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, router
프로젝트 생성
# Standalone 컴포넌트로 새 프로젝트 생성 (Angular 17+ 기본값)
ng new my-angular-app
# 대화형 프롬프트
# ? Which stylesheet format would you like to use? CSS
# ? Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)? No
옵션을 미리 지정해서 생성할 수도 있습니다:
# 스타일: SCSS, SSR 없음, 라우팅 포함
ng new my-app --style=scss --no-ssr --routing
# 또는 인터랙티브 없이
ng new my-app \
--style=scss \
--routing=true \
--skip-tests=false \
--no-ssr
프로젝트 디렉토리 구조
my-angular-app/
├── src/
│ ├── app/
│ │ ├── app.component.ts ← 루트 컴포넌트 (클래스)
│ │ ├── app.component.html ← 루트 컴포넌트 (템플릿)
│ │ ├── app.component.scss ← 루트 컴포넌트 (스타일)
│ │ ├── app.component.spec.ts ← 루트 컴포넌트 (테스트)
│ │ └── app.config.ts ← 앱 설정 (providers, router)
│ │ └── app.routes.ts ← 라우트 정의
│ ├── assets/ ← 정적 파일 (이미지, 폰트)
│ ├── index.html ← 앱 진입점 HTML
│ ├── main.ts ← TypeScript 진입점
│ └── styles.scss ← 전역 스타일
├── public/ ← 빌드 시 그대로 복사되는 파일
├── angular.json ← Angular CLI 설정
├── tsconfig.json ← TypeScript 기본 설정
├── tsconfig.app.json ← 앱 빌드용 TypeScript 설정
├── tsconfig.spec.json ← 테스트용 TypeScript 설정
└── package.json ← 의존성 및 스크립트
핵심 파일 살펴보기
// src/main.ts — 앱 진입점
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));
// src/app/app.config.ts — 앱 전역 설정
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(), // HttpClient 전역 등록
]
};
// src/app/app.routes.ts — 라우트 정의
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
redirectTo: '/home',
pathMatch: 'full'
},
{
path: 'home',
loadComponent: () =>
import('./home/home.component').then(m => m.HomeComponent)
}
];
// src/app/app.component.ts — 루트 컴포넌트
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'my-angular-app';
}
angular.json 주요 설정
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"projects": {
"my-angular-app": {
"projectType": "application",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/my-angular-app",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"assets": [
{ "glob": "**/*", "input": "public" }
],
"styles": ["src/styles.scss"],
"scripts": []
},
"configurations": {
"production": {
"optimization": true, // 코드 최소화
"outputHashing": "all", // 캐시 버스팅
"sourceMap": false, // 소스맵 비활성화
"namedChunks": false,
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB", // 번들 크기 경고
"maximumError": "1MB" // 번들 크기 오류
}
]
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true // 개발 시 소스맵 활성화
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": { "buildTarget": "my-angular-app:build:production" },
"development": { "buildTarget": "my-angular-app:build:development" }
},
"defaultConfiguration": "development"
}
}
}
}
}
개발 서버 실행
# 기본 개발 서버 (http://localhost:4200)
ng serve
# 다른 포트 사용
ng serve --port 4201
# 외부 접속 허용
ng serve --host 0.0.0.0
# 프로덕션 설정으로 서빙 (최적화 적용)
ng serve --configuration production
코드를 수정하면 브라우저가 자동으로 새로고침됩니다 (Hot Module Replacement).
Angular CLI 주요 명령어
코드 생성 (generate)
# 컴포넌트 생성
ng generate component features/user-list
# 단축: ng g c features/user-list
# 서비스 생성
ng g service services/user
# 디렉티브 생성
ng g directive directives/highlight
# 파이프 생성
ng g pipe pipes/format-date
# 가드 생성
ng g guard guards/auth
# 인터페이스 생성
ng g interface models/user
# 열거형 생성
ng g enum models/user-role
빌드
# 개발 빌드
ng build
# 프로덕션 빌드 (최적화, 트리 쉐이킹)
ng build --configuration production
# 번들 분석 (별도 도구 필요)
ng build --stats-json
npx webpack-bundle-analyzer dist/my-app/browser/stats.json
테스트
# 단위 테스트 (Karma + Jasmine)
ng test
# 단위 테스트 한 번만 실행 (CI용)
ng test --no-watch --browsers ChromeHeadless
# e2e 테스트 (Playwright, Cypress 등 별도 설치)
ng e2e
린트 & 포맷
# ESLint 실행
ng lint
# 자동 수정
ng lint --fix
VS Code 추천 확장
| 확장 | 역할 |
|---|---|
| Angular Language Service | 템플릿 자동완성, 오류 표시 |
| ESLint | 코드 품질 검사 |
| Prettier | 코드 포맷팅 |
| Angular Snippets | 코드 스니펫 |
| Material Icon Theme | 파일 아이콘 |
# VS Code 확장 명령행 설치
code --install-extension angular.ng-template
code --install-extension esbenp.prettier-vscode
code --install-extension dbaeumer.vscode-eslint
Standalone 컴포넌트 vs NgModule
Angular 17 이전에는 모든 컴포넌트가 NgModule에 속해야 했습니다.
기존 NgModule 방식 (레거시)
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { UserListComponent } from './user-list/user-list.component';
@NgModule({
declarations: [
AppComponent,
UserListComponent, // 모든 컴포넌트를 여기서 선언해야 함
],
imports: [
BrowserModule,
AppRoutingModule,
],
bootstrap: [AppComponent]
})
export class AppModule { }
새 Standalone 방식 (Angular 17+ 권장)
// user-list.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-user-list',
standalone: true, // ← 이것만 추가하면 NgModule 불필요
imports: [CommonModule], // ← 필요한 것만 직접 import
template: `...`
})
export class UserListComponent { }
비교
| 항목 | NgModule | Standalone |
|---|---|---|
| 설정 복잡도 | 높음 | 낮음 |
| 코드 양 | 많음 | 적음 |
| 트리 쉐이킹 | 모듈 단위 | 컴포넌트 단위 (더 효율적) |
| Lazy Loading | 모듈 단위 | 컴포넌트 단위도 가능 |
| 권장 여부 | 레거시 | 현재 표준 |
실전 예제 — 기본 앱 컴포넌트 구조
// src/app/app.component.ts
import { Component, signal, computed } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
interface NavItem {
path: string;
label: string;
icon: string;
}
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, RouterLink, RouterLinkActive],
template: `
<div class="app-shell">
<!-- 헤더 네비게이션 -->
<header class="header">
<nav class="nav">
<a class="brand" routerLink="/">{{ appName() }}</a>
<ul class="nav-links">
@for (item of navItems; track item.path) {
<li>
<a
[routerLink]="item.path"
routerLinkActive="active"
[routerLinkActiveOptions]="{ exact: item.path === '/' }"
>
{{ item.icon }} {{ item.label }}
</a>
</li>
}
</ul>
<div class="user-info">
@if (isLoggedIn()) {
<span>{{ userName() }}</span>
<button (click)="logout()">로그아웃</button>
} @else {
<a routerLink="/login">로그인</a>
}
</div>
</nav>
</header>
<!-- 메인 콘텐츠 -->
<main class="main-content">
<router-outlet />
</main>
<!-- 푸터 -->
<footer class="footer">
<p>© {{ currentYear() }} {{ appName() }}. All rights reserved.</p>
</footer>
</div>
`,
styleUrl: './app.component.scss'
})
export class AppComponent {
// Signals로 반응형 상태 관리
appName = signal('My Angular App');
isLoggedIn = signal(false);
userName = signal('');
// computed — 파생 상태
currentYear = computed(() => new Date().getFullYear());
navItems: NavItem[] = [
{ path: '/', label: '홈', icon: '🏠' },
{ path: '/products', label: '상품', icon: '📦' },
{ path: '/about', label: '소개', icon: 'ℹ️' },
];
logout() {
this.isLoggedIn.set(false);
this.userName.set('');
}
}
tsconfig.json 주요 설정
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"strict": true, // 엄격한 타입 검사 (권장)
"noImplicitOverride": true, // override 키워드 강제
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true, // 모든 코드 경로에서 return 강제
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"sourceMap": true,
"declaration": false,
"moduleResolution": "bundler",
"experimentalDecorators": true, // @Component 등 데코레이터
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022", "dom"],
// 경로 별칭 (선택 사항)
"paths": {
"@app/*": ["src/app/*"],
"@env/*": ["src/environments/*"]
}
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true // 템플릿 타입 검사 활성화
}
}
고수 팁
팁 1: 경로 별칭 설정으로 import 간소화
// tsconfig.json에 paths 추가 후
// 변경 전
import { UserService } from '../../../services/user.service';
// 변경 후
import { UserService } from '@app/services/user.service';
팁 2: 환경별 설정 파일 활용
// src/environments/environment.ts (개발)
export const environment = {
production: false,
apiUrl: 'http://localhost:3000/api'
};
// src/environments/environment.production.ts (프로덕션)
export const environment = {
production: true,
apiUrl: 'https://api.myapp.com'
};
팁 3: Nx로 모노레포 관리
대규모 프로젝트에서는 Nx를 사용해 여러 Angular 앱과 라이브러리를 모노레포로 관리하면 빌드 캐싱과 의존성 분석이 강력해집니다.
npx create-nx-workspace@latest my-workspace
팁 4: Schematic 활용
ng g 명령어는 내부적으로 Schematic을 사용합니다. Angular Material 등 외부 라이브러리도 스키마틱을 제공합니다:
# Angular Material 설치 (자동 설정)
ng add @angular/material
# NgRx Store 설치
ng add @ngrx/store@latest
정리
| 명령어 | 역할 |
|---|---|
ng new | 프로젝트 생성 |
ng serve | 개발 서버 실행 |
ng build | 프로덕션 빌드 |
ng g c | 컴포넌트 생성 |
ng g s | 서비스 생성 |
ng test | 단위 테스트 실행 |
ng lint | 코드 품질 검사 |
ng update | 의존성 업데이트 |
다음 챕터에서는 Angular 컴포넌트와 템플릿 문법을 깊이 있게 학습합니다.