포스트

[Angular 마스터하기] Day 4 - 이벤트 처리, 클릭하면 반응하는 앱

[Angular 마스터하기] Day 4 - 이벤트 처리, 클릭하면 반응하는 앱

이제와서 시작하는 Angular 마스터하기 - Day 4 “사용자와 대화하는 앱을 만들어봅시다! 클릭, 입력, 키보드… 모든 것에 반응해요! 🖱️”

오늘 배울 내용

  • 이벤트 바인딩: (event)="handler()"
  • 다양한 이벤트 처리 (click, input, keyup 등)
  • $event 객체 활용하기
  • 실습: 카운터 앱, 좋아요 버튼, 입력 폼

1. 이벤트 바인딩 기초

(event)=”handler()” 문법

이벤트 바인딩은 사용자 동작에 반응하는 방법입니다.

graph LR
    A[사용자 액션] -->|클릭, 입력 등| B["(event)='handler()'"]
    B --> C[컴포넌트 메서드 실행]
    C --> D[데이터 변경]
    D --> E[화면 자동 업데이트]

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

가장 기본: 클릭 이벤트

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

@Component({
  selector: 'app-clicker',
  standalone: true,
  template: `
    <div class="container">
      <h2>클릭 횟수: 9</h2>
      <button (click)="increment()">클릭!</button>
    </div>
  `,
  styles: [`
    .container {
      text-align: center;
      padding: 50px;
    }
    button {
      font-size: 1.5em;
      padding: 15px 40px;
      cursor: pointer;
    }
  `]
})
export class ClickerComponent {
  count = 0;

  increment() {
    this.count++;
    console.log('현재 카운트:', this.count);
  }
}

작동 방식:

  1. 사용자가 버튼 클릭
  2. (click) 이벤트 발생
  3. increment() 메서드 실행
  4. count 값 증가
  5. 화면 자동 업데이트! ✨

2. 다양한 이벤트

클릭 이벤트 변형

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
@Component({
  selector: 'app-mouse-events',
  standalone: true,
  template: `
    <div class="event-box"
      (click)="onClick()"
      (dblclick)="onDoubleClick()"
      (mouseenter)="onMouseEnter()"
      (mouseleave)="onMouseLeave()">

      <p></p>
      <p class="hint">마우스를 올려보거나 클릭해보세요!</p>
    </div>
  `,
  styles: [`
    .event-box {
      padding: 50px;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      border-radius: 15px;
      text-align: center;
      cursor: pointer;
      transition: transform 0.3s;
    }
    .event-box:hover {
      transform: scale(1.05);
    }
    .hint {
      font-size: 0.9em;
      opacity: 0.8;
    }
  `]
})
export class MouseEventsComponent {
  message = '이벤트를 기다리는 중...';

  onClick() {
    this.message = '✅ 클릭됨!';
  }

  onDoubleClick() {
    this.message = '✅✅ 더블 클릭됨!';
  }

  onMouseEnter() {
    this.message = '👋 마우스가 들어왔어요!';
  }

  onMouseLeave() {
    this.message = '👋 마우스가 나갔어요!';
  }
}

입력 이벤트

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
@Component({
  selector: 'app-input-demo',
  standalone: true,
  template: `
    <div class="input-container">
      <h3>실시간 입력 감지</h3>

      <input
        type="text"
        placeholder="여기에 입력하세요..."
        (input)="onInput($event)"
        (focus)="onFocus()"
        (blur)="onBlur()">

      <p>입력한 내용: <strong></strong></p>
      <p>글자 수: </p>
      <p class="status"></p>
    </div>
  `,
  styles: [`
    .input-container {
      padding: 30px;
      max-width: 500px;
      margin: 0 auto;
    }
    input {
      width: 100%;
      padding: 15px;
      font-size: 1.1em;
      border: 2px solid #ddd;
      border-radius: 8px;
      margin-bottom: 15px;
    }
    input:focus {
      outline: none;
      border-color: #667eea;
    }
    .status {
      color: #666;
      font-style: italic;
    }
  `]
})
export class InputDemoComponent {
  inputValue = '';
  status = '입력 대기 중...';

  onInput(event: Event) {
    const target = event.target as HTMLInputElement;
    this.inputValue = target.value;
  }

  onFocus() {
    this.status = '⌨️ 입력 중...';
  }

  onBlur() {
    this.status = '✅ 입력 완료!';
  }
}

키보드 이벤트

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
@Component({
  selector: 'app-keyboard-demo',
  standalone: true,
  template: `
    <div class="keyboard-container">
      <h3>키보드 이벤트</h3>

      <input
        type="text"
        placeholder="Enter를 눌러보세요..."
        (keyup.enter)="onEnter()"
        (keyup.escape)="onEscape()"
        (keyup)="onKeyUp($event)">

      <p></p>
      <p class="key-info">마지막 키: </p>

      <!-- 특수 키 조합 -->
      <input
        type="text"
        placeholder="Ctrl+S로 저장해보세요"
        (keydown.control.s)="onSave($event)">
    </div>
  `,
  styles: [`
    .keyboard-container {
      padding: 30px;
    }
    input {
      display: block;
      width: 100%;
      padding: 15px;
      font-size: 1.1em;
      border: 2px solid #ddd;
      border-radius: 8px;
      margin-bottom: 15px;
    }
    .key-info {
      background: #f5f5f5;
      padding: 10px;
      border-radius: 5px;
      font-family: monospace;
    }
  `]
})
export class KeyboardDemoComponent {
  message = '';
  lastKey = '';

  onEnter() {
    this.message = '✅ Enter 키가 눌렸습니다!';
  }

  onEscape() {
    this.message = '❌ Escape 키가 눌렸습니다!';
  }

  onKeyUp(event: KeyboardEvent) {
    this.lastKey = event.key;
  }

  onSave(event: KeyboardEvent) {
    event.preventDefault();  // 브라우저 기본 동작 방지
    this.message = '💾 저장되었습니다!';
  }
}

3. $event 객체 활용하기

$event란?

$event이벤트에 대한 상세 정보를 담고 있는 객체입니다.

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
@Component({
  selector: 'app-event-object',
  standalone: true,
  template: `
    <div class="demo">
      <!-- 마우스 위치 추적 -->
      <div
        class="track-area"
        (mousemove)="onMouseMove($event)">
        <p>마우스 위치: X=, Y=</p>
      </div>

      <!-- 체크박스 -->
      <label>
        <input
          type="checkbox"
          (change)="onCheckboxChange($event)">
        
      </label>

      <!-- 드롭다운 -->
      <select (change)="onSelectChange($event)">
        <option value="">선택하세요</option>
        <option value="apple">사과</option>
        <option value="banana">바나나</option>
        <option value="orange">오렌지</option>
      </select>
      <p>선택한 과일: </p>
    </div>
  `,
  styles: [`
    .track-area {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      height: 200px;
      display: flex;
      align-items: center;
      justify-content: center;
      border-radius: 10px;
      margin-bottom: 20px;
      font-size: 1.2em;
    }
    select {
      width: 100%;
      padding: 10px;
      font-size: 1em;
      border-radius: 5px;
      margin: 10px 0;
    }
  `]
})
export class EventObjectComponent {
  mouseX = 0;
  mouseY = 0;
  checkboxText = '동의하지 않음';
  selectedFruit = '';

  onMouseMove(event: MouseEvent) {
    this.mouseX = event.clientX;
    this.mouseY = event.clientY;
  }

  onCheckboxChange(event: Event) {
    const target = event.target as HTMLInputElement;
    this.checkboxText = target.checked ? '동의함 ✅' : '동의하지 않음';
  }

  onSelectChange(event: Event) {
    const target = event.target as HTMLSelectElement;
    this.selectedFruit = target.value;
  }
}

이벤트 전파 제어

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
@Component({
  selector: 'app-event-propagation',
  standalone: true,
  template: `
    <div
      class="outer"
      (click)="onOuterClick()">
      <p>외부 영역</p>

      <div
        class="inner"
        (click)="onInnerClick($event)">
        <p>내부 영역 (전파 중단)</p>
      </div>
    </div>

    <p></p>
  `,
  styles: [`
    .outer {
      padding: 50px;
      background: #f0f0f0;
      border: 3px solid #ddd;
      cursor: pointer;
    }
    .inner {
      padding: 30px;
      background: #667eea;
      color: white;
      border: 3px solid #764ba2;
    }
  `]
})
export class EventPropagationComponent {
  message = '';

  onOuterClick() {
    this.message = '외부 영역 클릭됨';
  }

  onInnerClick(event: Event) {
    event.stopPropagation();  // 이벤트 전파 중단!
    this.message = '내부 영역 클릭됨 (외부 이벤트 실행 안 됨)';
  }
}

4. 실전 예제

예제 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
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
172
173
174
175
176
177
import { Component } from '@angular/core';

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <div class="counter-app">
      <h1>카운터 앱</h1>

      <div class="display">
        <span class="count" [class.negative]="count < 0" [class.positive]="count > 0">
          9
        </span>
      </div>

      <div class="controls">
        <button (click)="decrement()" class="btn btn-danger">-1</button>
        <button (click)="reset()" class="btn btn-secondary">리셋</button>
        <button (click)="increment()" class="btn btn-success">+1</button>
      </div>

      <div class="advanced-controls">
        <input
          type="number"
          [(ngModel)]="step"
          min="1"
          max="100"
          placeholder="증감 단위">

        <button (click)="incrementBy(step)" class="btn">+</button>
        <button (click)="decrementBy(step)" class="btn">-</button>
      </div>

      <div class="history">
        <h3>히스토리</h3>
        <div class="history-items">
          <div *ngFor="let item of history" class="history-item">
            
          </div>
        </div>
      </div>
    </div>
  `,
  styles: [`
    .counter-app {
      max-width: 500px;
      margin: 50px auto;
      padding: 40px;
      background: white;
      border-radius: 20px;
      box-shadow: 0 10px 40px rgba(0,0,0,0.1);
      text-align: center;
    }

    .display {
      margin: 30px 0;
    }

    .count {
      font-size: 5em;
      font-weight: bold;
      color: #333;
      transition: color 0.3s;
    }

    .count.positive {
      color: #4CAF50;
    }

    .count.negative {
      color: #f44336;
    }

    .controls, .advanced-controls {
      display: flex;
      gap: 10px;
      justify-content: center;
      margin-bottom: 20px;
    }

    .btn {
      padding: 15px 30px;
      font-size: 1.1em;
      font-weight: bold;
      border: none;
      border-radius: 10px;
      cursor: pointer;
      transition: transform 0.2s;
    }

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

    .btn-success {
      background: #4CAF50;
      color: white;
    }

    .btn-danger {
      background: #f44336;
      color: white;
    }

    .btn-secondary {
      background: #9e9e9e;
      color: white;
    }

    input[type="number"] {
      padding: 15px;
      font-size: 1em;
      border: 2px solid #ddd;
      border-radius: 10px;
      width: 100px;
    }

    .history {
      margin-top: 30px;
      padding-top: 20px;
      border-top: 2px solid #eee;
    }

    .history-items {
      max-height: 200px;
      overflow-y: auto;
    }

    .history-item {
      padding: 8px;
      background: #f5f5f5;
      margin: 5px 0;
      border-radius: 5px;
      font-size: 0.9em;
    }
  `]
})
export class CounterComponent {
  count = 0;
  step = 10;
  history: string[] = [];

  increment() {
    this.count++;
    this.addHistory(`+1 → ${this.count}`);
  }

  decrement() {
    this.count--;
    this.addHistory(`-1 → ${this.count}`);
  }

  incrementBy(value: number) {
    this.count += value;
    this.addHistory(`+${value}${this.count}`);
  }

  decrementBy(value: number) {
    this.count -= value;
    this.addHistory(`-${value}${this.count}`);
  }

  reset() {
    this.count = 0;
    this.addHistory('리셋 → 0');
  }

  private addHistory(action: string) {
    const timestamp = new Date().toLocaleTimeString('ko-KR');
    this.history.unshift(`[${timestamp}] ${action}`);

    // 최대 10개까지만 보관
    if (this.history.length > 10) {
      this.history = this.history.slice(0, 10);
    }
  }
}

예제 2: 좋아요 버튼

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
@Component({
  selector: 'app-like-button',
  standalone: true,
  template: `
    <button
      class="like-button"
      [class.liked]="isLiked"
      [class.animating]="isAnimating"
      (click)="toggleLike()">

      <span class="icon">{{ isLiked ? '❤️' : '🤍' }}</span>
      <span class="count">{{ likes }}</span>
    </button>
  `,
  styles: [`
    .like-button {
      background: white;
      border: 3px solid #e0e0e0;
      border-radius: 50px;
      padding: 15px 30px;
      font-size: 1.5em;
      cursor: pointer;
      display: inline-flex;
      align-items: center;
      gap: 10px;
      transition: all 0.3s;
    }

    .like-button:hover {
      transform: scale(1.1);
    }

    .like-button.liked {
      background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
      border-color: #f5576c;
      color: white;
    }

    .like-button.animating .icon {
      animation: heartbeat 0.6s;
    }

    @keyframes heartbeat {
      0%, 100% { transform: scale(1); }
      25% { transform: scale(1.3); }
      50% { transform: scale(1.1); }
      75% { transform: scale(1.2); }
    }

    .count {
      font-weight: bold;
      min-width: 30px;
      text-align: left;
    }
  `]
})
export class LikeButtonComponent {
  likes = 0;
  isLiked = false;
  isAnimating = false;

  toggleLike() {
    if (this.isLiked) {
      this.likes--;
      this.isLiked = false;
    } else {
      this.likes++;
      this.isLiked = true;
      this.animate();
    }
  }

  private animate() {
    this.isAnimating = true;
    setTimeout(() => {
      this.isAnimating = false;
    }, 600);
  }
}

🧪 직접 해보기

실습 1: 간단한 계산기

미션: +, -, ×, ÷ 계산기를 만드세요!

1
ng g c calculator

힌트:

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
export class CalculatorComponent {
  num1 = 0;
  num2 = 0;
  result = 0;

  add() {
    this.result = this.num1 + this.num2;
  }

  subtract() {
    this.result = this.num1 - this.num2;
  }

  multiply() {
    this.result = this.num1 * this.num2;
  }

  divide() {
    if (this.num2 !== 0) {
      this.result = this.num1 / this.num2;
    } else {
      alert('0으로 나눌 수 없습니다!');
    }
  }
}

실습 2: 할 일 추가 폼

미션: Enter를 누르면 할 일이 추가되는 폼을 만드세요!

1
2
3
4
5
6
7
8
9
10
11
export class TodoFormComponent {
  newTodo = '';
  todos: string[] = [];

  addTodo() {
    if (this.newTodo.trim()) {
      this.todos.push(this.newTodo);
      this.newTodo = '';  // 입력 초기화
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="todo-form">
  <input
    type="text"
    [(ngModel)]="newTodo"
    (keyup.enter)="addTodo()"
    placeholder="할 일을 입력하고 Enter를 누르세요">

  <button (click)="addTodo()">추가</button>

  <ul>
    <li *ngFor="let todo of todos"></li>
  </ul>
</div>

💡 자주 하는 실수

❌ 실수 1: 함수 호출 시 괄호 빼먹기

1
2
3
4
5
<!-- ❌ 틀린 예 -->
<button (click)="increment">클릭</button>

<!-- ✅ 올바른 예 -->
<button (click)="increment()">클릭</button>

❌ 실수 2: 이벤트명 오타

1
2
3
4
5
<!-- ❌ 틀린 예 -->
<button (onclick)="doSomething()">클릭</button>

<!-- ✅ 올바른 예 -->
<button (click)="doSomething()">클릭</button>

❌ 실수 3: $event 타입 미지정

1
2
3
4
5
6
7
8
9
10
// ❌ 타입 없음
onInput(event) {
  console.log(event.target.value);  // 에러 가능성
}

// ✅ 타입 지정
onInput(event: Event) {
  const target = event.target as HTMLInputElement;
  console.log(target.value);
}

❌ 실수 4: preventDefault 빠뜨리기

1
2
3
4
5
// Form submit 시 페이지 새로고침 방지
onSubmit(event: Event) {
  event.preventDefault();  // ← 필수!
  // 폼 처리 로직
}

📝 정리

주요 이벤트 목록

이벤트 발생 시점 예시
(click) 클릭 시 (click)="onClick()"
(dblclick) 더블 클릭 (dblclick)="onDblClick()"
(input) 입력값 변경 (input)="onInput($event)"
(change) 값 변경 완료 (change)="onChange($event)"
(keyup) 키 놓을 때 (keyup.enter)="onEnter()"
(keydown) 키 누를 때 (keydown)="onKeyDown($event)"
(mouseenter) 마우스 진입 (mouseenter)="onEnter()"
(mouseleave) 마우스 떠남 (mouseleave)="onLeave()"
(focus) 포커스 획득 (focus)="onFocus()"
(blur) 포커스 잃음 (blur)="onBlur()"

핵심 개념

mindmap
  root((이벤트 처리))
    기본 문법
      click 이벤트
      input 이벤트
      keyboard 이벤트
    $event 객체
      이벤트 정보
      타입 캐스팅
      preventDefault
      stopPropagation
    실전 활용
      카운터
      좋아요 버튼
      폼 처리

체크리스트

  • (click) 이벤트를 사용할 수 있나요?
  • 키보드 이벤트를 처리할 수 있나요?
  • $event 객체를 활용할 수 있나요?
  • 카운터 앱을 만들어봤나요?

📚 다음 학습

다음 시간에는 조건과 반복을 배웁니다!

지금까지는 고정된 화면이었다면, 이제 조건에 따라 보이고 안 보이고, 리스트를 반복해서 표시하는 방법을 배웁니다:


💬 마무리하며

“이벤트는 사용자와의 대화입니다. 클릭, 입력, 키보드… 모든 동작에 귀 기울이세요! 👂”

오늘은 이벤트 처리의 기초를 배웠습니다. 이제 사용자와 상호작용하는 앱을 만들 수 있어요!

내일은 조건과 반복으로 더욱 동적인 화면을 만들어봅니다! 🚀

실습하다가 궁금한 점이 있으면 언제든 댓글로 질문해주세요! 💬


“작은 클릭 하나가 큰 변화를 만듭니다!” ✨

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