[Angular 마스터하기] Day 9 - 라우팅 기초, 여러 페이지 만들기
[Angular 마스터하기] Day 9 - 라우팅 기초, 여러 페이지 만들기
이제와서 시작하는 Angular 마스터하기 - Day 9 “여러 페이지를 자유롭게 이동하는 앱을 만들어봅시다! 🗺️”
오늘 배울 내용
- SPA에서의 라우팅 개념
- 라우트 설정하기
- RouterLink, RouterOutlet 사용
- 라우트 파라미터 전달
- 실습: 다중 페이지 앱 만들기
1. SPA와 라우팅
Single Page Application (SPA)
graph LR
A[전통적인 웹] -->|페이지 이동| B[서버에서<br/>새 HTML 받음]
B -->|새로고침| C[화면 깜빡임]
D[SPA] -->|페이지 이동| E[JavaScript로<br/>화면만 교체]
E -->|부드러운 전환| F[빠른 사용자 경험]
style A fill:#f44336,color:#fff
style D fill:#4CAF50,color:#fff
Angular는 SPA 프레임워크:
- 실제로는 하나의 HTML 파일
- JavaScript로 화면을 동적으로 변경
- 빠르고 부드러운 사용자 경험
2. 라우트 설정하기
기본 설정
1
2
3
4
5
6
7
8
9
10
11
// app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './pages/home.component';
import { AboutComponent } from './pages/about.component';
import { ContactComponent } from './pages/contact.component';
export const routes: Routes = [
{ path: '', component: HomeComponent }, // localhost:4200/
{ path: 'about', component: AboutComponent }, // localhost:4200/about
{ path: 'contact', component: ContactComponent }, // localhost:4200/contact
];
1
2
3
4
5
6
7
8
9
10
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes)
]
};
페이지 컴포넌트 만들기
1
2
3
ng g c pages/home
ng g c pages/about
ng g c pages/contact
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// home.component.ts
@Component({
selector: 'app-home',
standalone: true,
template: `
<div class="page">
<h1>🏠 홈</h1>
<p>Angular 마스터하기에 오신 것을 환영합니다!</p>
</div>
`,
styles: [`
.page {
padding: 40px;
text-align: center;
}
`]
})
export class HomeComponent {}
3. RouterOutlet과 RouterLink
RouterOutlet - 페이지가 표시되는 곳
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, RouterLink],
template: `
<div class="app">
<!-- 네비게이션 -->
<nav>
<a routerLink="/">홈</a>
<a routerLink="/about">소개</a>
<a routerLink="/contact">연락처</a>
</nav>
<!-- 라우트된 컴포넌트가 여기 표시됨 -->
<router-outlet></router-outlet>
</div>
`,
styles: [`
nav {
display: flex;
gap: 20px;
padding: 20px;
background: #667eea;
}
nav a {
color: white;
text-decoration: none;
padding: 10px 20px;
border-radius: 8px;
}
nav a:hover {
background: rgba(255,255,255,0.2);
}
`]
})
export class AppComponent {}
routerLinkActive - 활성 링크 스타일
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component({
template: `
<nav>
<a routerLink="/"
routerLinkActive="active"
[routerLinkActiveOptions]="{exact: true}">
홈
</a>
<a routerLink="/about" routerLinkActive="active">소개</a>
<a routerLink="/contact" routerLinkActive="active">연락처</a>
</nav>
`,
styles: [`
a.active {
background: white;
color: #667eea;
font-weight: bold;
}
`]
})
4. 라우트 파라미터
URL 파라미터 받기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// app.routes.ts
export const routes: Routes = [
{ path: 'product/:id', component: ProductDetailComponent }
];
// product-detail.component.ts
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-product-detail',
standalone: true,
template: `
<div class="detail-page">
<h1>상품 상세</h1>
<p>상품 ID: {{ productId }}</p>
</div>
`
})
export class ProductDetailComponent {
private route = inject(ActivatedRoute);
productId = '';
ngOnInit() {
// 파라미터 읽기
this.productId = this.route.snapshot.paramMap.get('id') || '';
}
}
프로그래밍 방식 네비게이션
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Router } from '@angular/router';
@Component({
template: `
<button (click)="goToProduct(123)">상품 보기</button>
`
})
export class ProductListComponent {
private router = inject(Router);
goToProduct(id: number) {
this.router.navigate(['/product', id]);
}
}
5. 실전 예제: 블로그 앱
라우트 설정
1
2
3
4
5
6
7
8
// app.routes.ts
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'posts', component: PostListComponent },
{ path: 'posts/:id', component: PostDetailComponent },
{ path: 'about', component: AboutComponent },
{ path: '**', component: NotFoundComponent } // 404 페이지
];
포스트 목록
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// post-list.component.ts
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { CommonModule } from '@angular/common';
interface Post {
id: number;
title: string;
excerpt: string;
author: string;
date: Date;
}
@Component({
selector: 'app-post-list',
standalone: true,
imports: [CommonModule, RouterLink],
template: `
<div class="post-list">
<h1>📚 블로그 포스트</h1>
<div class="posts">
@for (post of posts; track post.id) {
<div class="post-card">
<h2>
<a [routerLink]="['/posts', post.id]">
{{ post.title }}
</a>
</h2>
<p class="excerpt">{{ post.excerpt }}</p>
<div class="meta">
<span>{{ post.author }}</span>
<span>{{ post.date | date }}</span>
</div>
<button [routerLink]="['/posts', post.id]">
자세히 보기 →
</button>
</div>
}
</div>
</div>
`,
styles: [`
.post-list {
max-width: 800px;
margin: 0 auto;
padding: 40px 20px;
}
.posts {
display: grid;
gap: 20px;
}
.post-card {
padding: 30px;
background: white;
border-radius: 15px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.post-card h2 a {
color: #333;
text-decoration: none;
}
.post-card h2 a:hover {
color: #667eea;
}
.excerpt {
color: #666;
margin: 15px 0;
}
.meta {
display: flex;
gap: 20px;
color: #999;
font-size: 0.9em;
margin: 15px 0;
}
button {
background: #667eea;
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
}
`]
})
export class PostListComponent {
posts: Post[] = [
{
id: 1,
title: 'Angular 시작하기',
excerpt: 'Angular의 기초를 배워봅시다.',
author: '홍길동',
date: new Date('2025-01-01')
},
{
id: 2,
title: 'TypeScript 완벽 가이드',
excerpt: 'TypeScript로 타입 안전한 코드를 작성하세요.',
author: '김철수',
date: new Date('2025-01-05')
},
{
id: 3,
title: 'Signal 마스터하기',
excerpt: 'Signal로 반응형 프로그래밍을 배워봅시다.',
author: '이영희',
date: new Date('2025-01-10')
}
];
}
포스트 상세
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
// post-detail.component.ts
import { Component, inject, signal } from '@angular/core';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-post-detail',
standalone: true,
imports: [CommonModule, RouterLink],
template: `
<div class="post-detail">
@if (post(); as p) {
<article>
<header>
<button routerLink="/posts" class="back-btn">← 목록으로</button>
<h1>{{ p.title }}</h1>
<div class="meta">
<span>✍️ {{ p.author }}</span>
<span>📅 {{ p.date | date:'yyyy-MM-dd' }}</span>
</div>
</header>
<div class="content">
{{ p.content }}
</div>
<footer>
<div class="nav-buttons">
@if (p.id > 1) {
<button (click)="navigate(p.id - 1)">
← 이전 글
</button>
}
@if (p.id < 3) {
<button (click)="navigate(p.id + 1)">
다음 글 →
</button>
}
</div>
</footer>
</article>
} @else {
<div class="not-found">
<h2>포스트를 찾을 수 없습니다</h2>
<button routerLink="/posts">목록으로 돌아가기</button>
</div>
}
</div>
`,
styles: [`
.post-detail {
max-width: 800px;
margin: 0 auto;
padding: 40px 20px;
}
article {
background: white;
padding: 40px;
border-radius: 15px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.back-btn {
background: #f5f5f5;
color: #333;
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
margin-bottom: 20px;
}
h1 {
margin: 20px 0;
color: #333;
}
.meta {
display: flex;
gap: 20px;
color: #666;
padding-bottom: 20px;
border-bottom: 2px solid #eee;
}
.content {
margin: 30px 0;
line-height: 1.8;
color: #444;
}
.nav-buttons {
display: flex;
justify-content: space-between;
margin-top: 40px;
padding-top: 20px;
border-top: 2px solid #eee;
}
.nav-buttons button {
background: #667eea;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
cursor: pointer;
}
.not-found {
text-align: center;
padding: 60px 20px;
}
`]
})
export class PostDetailComponent {
private route = inject(ActivatedRoute);
private router = inject(Router);
post = signal<any>(null);
// 가짜 데이터베이스
private posts = [
{
id: 1,
title: 'Angular 시작하기',
content: 'Angular는 구글이 만든 프론트엔드 프레임워크입니다...',
author: '홍길동',
date: new Date('2025-01-01')
},
{
id: 2,
title: 'TypeScript 완벽 가이드',
content: 'TypeScript는 JavaScript의 슈퍼셋입니다...',
author: '김철수',
date: new Date('2025-01-05')
},
{
id: 3,
title: 'Signal 마스터하기',
content: 'Signal은 Angular의 새로운 반응형 시스템입니다...',
author: '이영희',
date: new Date('2025-01-10')
}
];
ngOnInit() {
this.loadPost();
}
private loadPost() {
const id = Number(this.route.snapshot.paramMap.get('id'));
const foundPost = this.posts.find(p => p.id === id);
this.post.set(foundPost || null);
}
navigate(id: number) {
this.router.navigate(['/posts', id]);
// 라우트가 변경되면 다시 로드
setTimeout(() => this.loadPost(), 0);
}
}
🧪 직접 해보기
실습 1: 제품 목록/상세 페이지
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// routes
export const routes: Routes = [
{ path: 'products', component: ProductListComponent },
{ path: 'products/:id', component: ProductDetailComponent }
];
// product-list.component.ts
@Component({
template: `
@for (product of products; track product.id) {
<div class="product">
<h3>{{ product.name }}</h3>
<p>{{ product.price }}원</p>
<a [routerLink]="['/products', product.id]">보기</a>
</div>
}
`
})
export class ProductListComponent {
products = [
{ id: 1, name: '노트북', price: 1500000 },
{ id: 2, name: '마우스', price: 50000 }
];
}
💡 자주 하는 실수
❌ 실수 1: RouterOutlet 빼먹기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ❌ 틀린 예
@Component({
template: `
<nav>...</nav>
<!-- RouterOutlet이 없음! -->
`
})
// ✅ 올바른 예
@Component({
template: `
<nav>...</nav>
<router-outlet></router-outlet>
`
})
❌ 실수 2: routerLink에 / 빼먹기
1
2
3
4
5
<!-- ❌ 상대 경로 -->
<a routerLink="about">소개</a>
<!-- ✅ 절대 경로 -->
<a routerLink="/about">소개</a>
📝 정리
핵심 개념
| 요소 | 역할 |
|---|---|
Routes | 라우트 설정 배열 |
RouterOutlet | 컴포넌트가 표시될 위치 |
RouterLink | 링크 생성 |
ActivatedRoute | 현재 라우트 정보 |
Router | 프로그래밍 방식 네비게이션 |
체크리스트
- 라우트를 설정할 수 있나요?
- RouterLink로 페이지 이동을 할 수 있나요?
- URL 파라미터를 받을 수 있나요?
- 블로그 앱을 만들어봤나요?
📚 다음 학습
다음 시간에는 HTTP 통신을 배웁니다!
실제 API와 통신하여 데이터를 주고받는 방법을 배웁니다:
- HttpClient 사용법
- GET/POST/PUT/DELETE 요청
- Signal과 함께 사용하기
-
실습: REST API 연동
- 이전: Day 8: 서비스와 의존성 주입
- 다음: Day 10: HTTP 통신 - 실제 데이터 가져오기
- 전체 커리큘럼: Angular 마스터하기 시리즈
“좋은 라우팅은 좋은 사용자 경험의 시작입니다!” 🗺️
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.
