포스트

[Angular 마스터하기] Day 3 - 템플릿 문법, 화면에 데이터 표시하기

[Angular 마스터하기] Day 3 - 템플릿 문법, 화면에 데이터 표시하기

이제와서 시작하는 Angular 마스터하기 - Day 3 “데이터와 화면을 연결하는 마법을 배워봅시다! ✨”

오늘 배울 내용

  • 보간(Interpolation): {{ }} 문법 마스터하기
  • 속성 바인딩: [속성]="값" 완벽 이해
  • 클래스와 스타일 바인딩
  • 실습: 동적인 사용자 카드 만들기

1. 보간 (Interpolation) - {{ }}

기본 사용법

보간은 컴포넌트의 데이터를 화면에 표시하는 가장 기본적인 방법입니다.

1
2
3
4
5
6
// component.ts
export class UserComponent {
  username = '홍길동';
  age = 25;
  email = 'hong@example.com';
}
1
2
3
4
5
6
<!-- component.html -->
<div>
  <p>이름: {{ username }}</p>
  <p>나이: {{ age }}</p>
  <p>이메일: {{ email }}</p>
</div>

결과:

1
2
3
이름: 홍길동
나이: 25
이메일: hong@example.com

표현식 사용하기

{{ }} 안에는 TypeScript 표현식을 넣을 수 있습니다!

1
2
3
4
5
export class CalculatorComponent {
  price = 10000;
  quantity = 3;
  taxRate = 0.1;
}
1
2
3
4
5
6
7
<div class="invoice">
  <p>단가: {{ price }}원</p>
  <p>수량: {{ quantity }}개</p>
  <p>소계: {{ price * quantity }}원</p>
  <p>세금 (10%): {{ price * quantity * taxRate }}원</p>
  <p>총액: {{ price * quantity * (1 + taxRate) }}원</p>
</div>

결과:

1
2
3
4
5
단가: 10000원
수량: 3개
소계: 30000원
세금 (10%): 3000원
총액: 33000원

메서드 호출하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export class GreetingComponent {
  username = '김개발';

  getGreeting(): string {
    const hour = new Date().getHours();

    if (hour < 12) {
      return '좋은 아침입니다';
    } else if (hour < 18) {
      return '좋은 오후입니다';
    } else {
      return '좋은 저녁입니다';
    }
  }

  getFormattedDate(): string {
    return new Date().toLocaleDateString('ko-KR', {
      year: 'numeric',
      month: 'long',
      day: 'numeric'
    });
  }
}
1
2
3
4
<div class="greeting-box">
  <h2>{{ getGreeting() }}, {{ username }}님!</h2>
  <p>오늘은 {{ getFormattedDate() }}입니다.</p>
</div>

문자열 조작

1
2
3
4
export class TextComponent {
  message = 'angular is awesome';
  count = 42;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<div>
  <!-- 대문자 변환 -->
  <p>대문자: {{ message.toUpperCase() }}</p>

  <!-- 첫 글자 대문자 -->
  <p>첫 글자: {{ message.charAt(0).toUpperCase() + message.slice(1) }}</p>

  <!-- 문자열 길이 -->
  <p>길이: {{ message.length }}자</p>

  <!-- 숫자 포맷 -->
  <p>천 단위: {{ count.toLocaleString() }}</p>
</div>

결과:

1
2
3
4
대문자: ANGULAR IS AWESOME
첫 글자: Angular is awesome
길이: 18자
천 단위: 42

2. 속성 바인딩 (Property Binding)

[속성]=”값” 문법

속성 바인딩은 HTML 요소의 속성을 동적으로 설정합니다.

graph LR
    A[컴포넌트 데이터] -->|"[property]='value'"| B[HTML 속성]

    style A fill:#dd0031,stroke:#fff,color:#fff
    style B fill:#4CAF50,stroke:#fff,color:#fff

이미지 바인딩

1
2
3
4
5
export class ImageComponent {
  imageUrl = 'https://picsum.photos/400/300';
  imageDescription = '랜덤 이미지';
  imageWidth = 400;
}
1
2
3
4
5
6
7
8
9
10
<div class="image-container">
  <!-- ✅ 속성 바인딩 -->
  <img
    [src]="imageUrl"
    [alt]="imageDescription"
    [width]="imageWidth">

  <!-- ❌ 일반 HTML (동적 변경 안 됨) -->
  <img src="imageUrl" alt="imageDescription">
</div>

링크 바인딩

1
2
3
4
export class LinkComponent {
  blogUrl = 'https://blog.park-labs.com';
  isExternalLink = true;
}
1
2
3
4
5
6
7
<div>
  <a
    [href]="blogUrl"
    [target]="isExternalLink ? '_blank' : '_self'">
    블로그 방문하기
  </a>
</div>

버튼 비활성화

1
2
3
4
5
6
7
8
export class FormComponent {
  username = '';
  isSubmitting = false;

  canSubmit(): boolean {
    return this.username.length >= 3 && !this.isSubmitting;
  }
}
1
2
3
4
5
6
7
<div>
  <input type="text" [(ngModel)]="username" placeholder="사용자명 (3자 이상)">

  <button [disabled]="!canSubmit()">
    {{ isSubmitting ? '제출 중...' : '제출하기' }}
  </button>
</div>

3. 클래스 바인딩

단일 클래스 바인딩

1
2
3
4
5
export class StatusComponent {
  isActive = true;
  isError = false;
  isSuccess = true;
}
1
2
3
4
5
6
<div
  [class.active]="isActive"
  [class.error]="isError"
  [class.success]="isSuccess">
  상태 표시
</div>

렌더링 결과:

1
2
3
<div class="active success">
  상태 표시
</div>

CSS 스타일

1
2
3
4
5
6
7
8
9
10
11
12
13
.active {
  background-color: #4CAF50;
  color: white;
}

.error {
  background-color: #f44336;
  color: white;
}

.success {
  border: 3px solid #4CAF50;
}

여러 클래스 한번에 바인딩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export class CardComponent {
  cardType = 'premium';
  isLarge = true;

  getCardClasses() {
    return {
      'card': true,                    // 항상 적용
      'card-premium': this.cardType === 'premium',
      'card-basic': this.cardType === 'basic',
      'card-large': this.isLarge,
      'card-small': !this.isLarge
    };
  }
}
1
2
3
<div [ngClass]="getCardClasses()">
  프리미엄 카드
</div>

렌더링 결과:

1
2
3
<div class="card card-premium card-large">
  프리미엄 카드
</div>

4. 스타일 바인딩

단일 스타일 바인딩

1
2
3
4
5
export class BoxComponent {
  boxColor = '#dd0031';
  boxWidth = 200;
  fontSize = 16;
}
1
2
3
4
5
6
<div
  [style.background-color]="boxColor"
  [style.width.px]="boxWidth"
  [style.font-size.px]="fontSize">
  스타일 박스
</div>

렌더링 결과:

1
2
3
<div style="background-color: #dd0031; width: 200px; font-size: 16px;">
  스타일 박스
</div>

여러 스타일 한번에

1
2
3
4
5
6
7
8
9
10
11
export class StyledComponent {
  getBoxStyles() {
    return {
      'background-color': '#667eea',
      'color': 'white',
      'padding': '20px',
      'border-radius': '10px',
      'box-shadow': '0 4px 6px rgba(0,0,0,0.1)'
    };
  }
}
1
2
3
<div [ngStyle]="getBoxStyles()">
  멋진 스타일 박스
</div>

조건부 스타일

1
2
3
4
5
6
7
8
9
export class ProgressComponent {
  progress = 75;  // 퍼센트

  getProgressColor(): string {
    if (this.progress < 30) return '#f44336';      // 빨강
    if (this.progress < 70) return '#ff9800';      // 주황
    return '#4CAF50';                               // 초록
  }
}
1
2
3
4
5
6
7
8
<div class="progress-bar">
  <div
    class="progress-fill"
    [style.width.%]="progress"
    [style.background-color]="getProgressColor()">
    {{ progress }}%
  </div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.progress-bar {
  width: 100%;
  height: 30px;
  background-color: #e0e0e0;
  border-radius: 15px;
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-weight: bold;
  transition: all 0.3s;
}

5. 실전 예제: 동적 사용자 카드

모든 개념을 활용한 완전한 예제를 만들어봅시다!

user-card.component.ts

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
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-user-card',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './user-card.component.html',
  styleUrl: './user-card.component.css'
})
export class UserCardComponent {
  // 사용자 데이터
  name = '김개발';
  role = 'Senior Frontend Developer';
  avatar = 'https://i.pravatar.cc/150?img=33';
  email = 'kim.dev@example.com';
  location = '서울, 대한민국';

  // 상태
  isOnline = true;
  experience = 5;  // 년
  skills = ['Angular', 'TypeScript', 'RxJS'];
  profileComplete = 85;  // 퍼센트

  // 메서드
  getStatusText(): string {
    return this.isOnline ? '온라인' : '오프라인';
  }

  getExperienceLevel(): string {
    if (this.experience < 2) return '주니어';
    if (this.experience < 5) return '미들';
    return '시니어';
  }

  getProfileColor(): string {
    if (this.profileComplete < 50) return '#f44336';
    if (this.profileComplete < 80) return '#ff9800';
    return '#4CAF50';
  }

  getCardClass() {
    return {
      'user-card': true,
      'online': this.isOnline,
      'offline': !this.isOnline
    };
  }
}

user-card.component.html

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
<div [ngClass]="getCardClass()">
  <!-- 헤더 -->
  <div class="card-header">
    <div class="avatar-wrapper">
      <img
        [src]="avatar"
        [alt]="name"
        class="avatar">
      <span
        class="status-indicator"
        [class.online]="isOnline"
        [class.offline]="!isOnline"
        [title]="getStatusText()">
      </span>
    </div>
  </div>

  <!-- 기본 정보 -->
  <div class="card-body">
    <h2 class="name">{{ name }}</h2>
    <p class="role">{{ role }}</p>

    <div class="info-row">
      <span class="label">📧</span>
      <span class="value">{{ email }}</span>
    </div>

    <div class="info-row">
      <span class="label">📍</span>
      <span class="value">{{ location }}</span>
    </div>

    <div class="info-row">
      <span class="label"></span>
      <span class="value">{{ getExperienceLevel() }} ({{ experience }}년)</span>
    </div>

    <!-- 스킬 태그 -->
    <div class="skills">
      <span
        class="skill-tag"
        *ngFor="let skill of skills">
        {{ skill }}
      </span>
    </div>

    <!-- 프로필 완성도 -->
    <div class="profile-progress">
      <div class="progress-label">
        <span>프로필 완성도</span>
        <span>{{ profileComplete }}%</span>
      </div>
      <div class="progress-bar">
        <div
          class="progress-fill"
          [style.width.%]="profileComplete"
          [style.background-color]="getProfileColor()">
        </div>
      </div>
    </div>
  </div>

  <!-- 푸터 -->
  <div class="card-footer">
    <button class="btn btn-primary">프로필 보기</button>
    <button class="btn btn-secondary">메시지</button>
  </div>
</div>

user-card.component.css

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
164
165
166
167
168
169
170
171
.user-card {
  max-width: 400px;
  background: white;
  border-radius: 20px;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
  overflow: hidden;
  transition: transform 0.3s;
}

.user-card:hover {
  transform: translateY(-5px);
  box-shadow: 0 15px 50px rgba(0, 0, 0, 0.15);
}

.user-card.online {
  border: 2px solid #4CAF50;
}

.user-card.offline {
  border: 2px solid #9e9e9e;
}

/* 헤더 */
.card-header {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  padding: 30px;
  text-align: center;
}

.avatar-wrapper {
  position: relative;
  display: inline-block;
}

.avatar {
  width: 120px;
  height: 120px;
  border-radius: 50%;
  border: 5px solid white;
  object-fit: cover;
}

.status-indicator {
  position: absolute;
  bottom: 10px;
  right: 10px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 3px solid white;
}

.status-indicator.online {
  background-color: #4CAF50;
}

.status-indicator.offline {
  background-color: #9e9e9e;
}

/* 본문 */
.card-body {
  padding: 30px;
}

.name {
  margin: 0 0 5px 0;
  font-size: 1.8em;
  color: #333;
}

.role {
  margin: 0 0 20px 0;
  color: #666;
  font-size: 1.1em;
}

.info-row {
  display: flex;
  align-items: center;
  margin-bottom: 10px;
}

.info-row .label {
  margin-right: 10px;
  font-size: 1.2em;
}

.info-row .value {
  color: #555;
}

/* 스킬 */
.skills {
  margin: 20px 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.skill-tag {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  padding: 6px 12px;
  border-radius: 15px;
  font-size: 0.9em;
}

/* 프로그레스 바 */
.profile-progress {
  margin-top: 20px;
}

.progress-label {
  display: flex;
  justify-content: space-between;
  margin-bottom: 8px;
  font-size: 0.9em;
  color: #666;
}

.progress-bar {
  height: 10px;
  background-color: #e0e0e0;
  border-radius: 5px;
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  transition: all 0.3s;
}

/* 푸터 */
.card-footer {
  padding: 20px 30px;
  display: flex;
  gap: 10px;
  background-color: #f5f5f5;
}

.btn {
  flex: 1;
  padding: 12px;
  border: none;
  border-radius: 8px;
  font-size: 1em;
  font-weight: bold;
  cursor: pointer;
  transition: all 0.3s;
}

.btn-primary {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
}

.btn-primary:hover {
  transform: scale(1.05);
}

.btn-secondary {
  background: white;
  color: #667eea;
  border: 2px solid #667eea;
}

.btn-secondary:hover {
  background: #667eea;
  color: white;
}

🧪 직접 해보기

실습 1: 날씨 카드 만들기

미션: 날씨 정보를 표시하는 카드를 만드세요!

1
ng g c weather-card

요구사항:

  • 도시명
  • 온도 (색상: 30도 이상 빨강, 20-30도 주황, 20도 미만 파랑)
  • 날씨 아이콘 (맑음 ☀️, 흐림 ☁️, 비 🌧️)
  • 습도 프로그레스 바

힌트:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export class WeatherCardComponent {
  city = '서울';
  temperature = 28;
  weather = 'sunny';  // sunny, cloudy, rainy
  humidity = 65;

  getTemperatureColor(): string {
    if (this.temperature >= 30) return '#f44336';
    if (this.temperature >= 20) return '#ff9800';
    return '#2196F3';
  }

  getWeatherIcon(): string {
    const icons = {
      'sunny': '☀️',
      'cloudy': '☁️',
      'rainy': '🌧️'
    };
    return icons[this.weather] || '';
  }
}

실습 2: 상태 변경 버튼

미션: 버튼을 클릭하면 상태가 바뀌는 카드를 만드세요!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export class ToggleCardComponent {
  isDark = false;

  toggle() {
    this.isDark = !this.isDark;
  }

  getBackgroundColor(): string {
    return this.isDark ? '#333' : '#fff';
  }

  getTextColor(): string {
    return this.isDark ? '#fff' : '#333';
  }
}
1
2
3
4
5
6
7
8
9
<div
  [style.background-color]="getBackgroundColor()"
  [style.color]="getTextColor()"
  style="padding: 40px; transition: all 0.3s;">
  <h2>{{ isDark ? '다크 모드' : '라이트 모드' }}</h2>
  <button (click)="toggle()">
    {{ isDark ? '🌞' : '🌙' }} 모드 전환
  </button>
</div>

💡 자주 하는 실수

❌ 실수 1: {{ }} 와 [] 혼용

1
2
3
4
5
6
7
8
<!-- ❌ 틀린 예 -->
<img src="{{ imageUrl }}">

<!-- ✅ 올바른 예 -->
<img [src]="imageUrl">

<!-- 또는 -->
<p>{{ imageUrl }}</p>

❌ 실수 2: 따옴표 빼먹기

1
2
3
4
5
<!-- ❌ 틀린 예 -->
<div [class.active]=isActive>

<!-- ✅ 올바른 예 -->
<div [class.active]="isActive">

❌ 실수 3: 표현식에서 문자열 비교

1
2
3
4
5
<!-- ❌ 틀린 예 - 따옴표 없음 -->
<div *ngIf="status === active">

<!-- ✅ 올바른 예 -->
<div *ngIf="status === 'active'">

❌ 실수 4: ngClass에 객체 대신 문자열

1
2
3
4
5
<!-- ❌ 틀린 예 -->
<div [ngClass]="'card active'">

<!-- ✅ 올바른 예 -->
<div [ngClass]="{'card': true, 'active': isActive}">

📝 정리

바인딩 문법 비교

문법 용도 예시
{{ }} 텍스트 표시 {{ username }}
[property] 속성 바인딩 [src]="imageUrl"
[class.name] 단일 클래스 [class.active]="isActive"
[ngClass] 여러 클래스 [ngClass]="getClasses()"
[style.prop] 단일 스타일 [style.color]="textColor"
[ngStyle] 여러 스타일 [ngStyle]="getStyles()"

핵심 개념

mindmap
  root((템플릿 문법))
    보간
      텍스트 표시
      표현식 평가
      메서드 호출
    속성 바인딩
      HTML 속성
      이미지/링크
      버튼 상태
    클래스 바인딩
      단일 클래스
      여러 클래스
      동적 적용
    스타일 바인딩
      인라인 스타일
      동적 색상
      조건부 스타일

체크리스트

  • {{ }} 문법을 이해했나요?
  • [속성]="값" 바인딩을 할 수 있나요?
  • 클래스를 동적으로 추가할 수 있나요?
  • 스타일을 조건부로 적용할 수 있나요?
  • 사용자 카드를 만들어봤나요?

📚 다음 학습

다음 시간에는 이벤트 처리를 배웁니다!

지금까지는 데이터를 보여주기만 했는데요, 이제 사용자의 클릭, 입력에 반응하는 방법을 배웁니다:


💬 마무리하며

“데이터를 화면에 표시하는 것은 프론트엔드의 기본! 오늘 배운 내용으로 다양한 UI를 만들 수 있어요! 🎨”

오늘은 템플릿 문법의 핵심을 배웠습니다. 보간, 속성 바인딩, 클래스/스타일 바인딩 모두 실무에서 매일 사용하는 기능들이에요!

내일은 드디어 사용자와 상호작용하는 방법을 배웁니다! 기대하세요! 🚀

실습하면서 궁금한 점이 생기면 언제든 댓글로 질문해주세요! 🙋‍♀️


“한 걸음 한 걸음, 꾸준히 가면 목표에 도달합니다!” 💪

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.