2024-03-23 작성

앵귤러 컴포넌트 데이터 전달 방법 및 Todo 리스트 예제 응용하기

컴포넌트 커뮤니케이션

1) 부모-> 자식 컴포넌트

부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달할 때는 @Input 데코레이터를 이용하면 부모->자식 컴포넌트로 속성값을 세팅할 수 있다.
 자식 컴포넌트의 속성을 ES6 setter를 이용해서 데이터를 가공한 후에 비공개 속성값을 세팅할 수도 있다. get을 통해 비공개 속성값을 공개적으로 가져오게 된다.
✅ @ViewChild 데코레이터를 이용하여 자식 컴포넌트 클래스에 인스턴스 레퍼런스를 부모 컴포넌트에서 직접 전달받을 수도 있다.

2) 자식->부모 컴포넌트

자식 컴포넌트에서 부모 컴포넌트로 이벤트를 발송할 때는 @Output 데코레이터 EventEmitter를 이용하여 특정 이벤트와 데이터를 발생시킬 수 있다.
 이벤트 바인딩을 통해 데이터를 전달 받거나, 
 자식 컴포넌트가 부모 컴포넌트를 생성자를 통해 직접 주입받을 수 있다. (단, 강력한 의존관계가 성립되므로 꼭 필요한 경우에만 사용하는 것이 좋음)

TODO 리스트 예제 응용하기

Todos 부모 컴포넌트와 Todos, AddTodo 라는 2개의 자식 컴포넌트 간 데이터를 주고받는지를 알아본다.

1. todo 컴포넌트 생성

이전 포스팅에서 반복문으로 표현했던 Todo 목록을 별도의 컴포넌트로 분리할 것이다. todos 폴더 우클릭 후 [New Component]를 선택하여 입력창에 todo를 입력한다. 그러면 오른쪽 사진과 같이 todos/todo 컴포넌트가 생성된다.

2. 모델 클래스 만들기

arc/app/todo 폴더에서 share 폴더 생성 후 todo.model.ts 파일을 생성한다. 하나의 모델 클래스를 만들어놓고, 여러 컴포넌트에서 참조해서 쓸 수 있도록 할 것이다.

src/app/todo/share/todo.model.ts 파일

// Todo 클래스는 2개의 속성을 정의한다.
export class Todo {
  done: boolean = false;
  text: string = '';
}

src/app/todo/todos/todos.component.ts 파일

import { Todo } from '../share/todo.model'; // 모델 추가

export class TodosComponent {

  // todos: { // 변경 전 : 모델로 분리하기 전
  //   done: boolean,
  //   text: string
  // }[]

  todos: Todo[]; // 변경 후 : 모델로 분리한 후 (모델 클래스 Todo[]로 대체)

}

3. @Input 사용하기

src/app/todo/todos/todo/todo.component.ts 파일

import { Todo } from '../../share/todo.model';

export class TodoComponent {
  @Input() todo: Todo = new Todo; // 하나의 객체를 의미하는 Todo 클래스로 대체
                                  // @Input으로 부모->자식 컴포넌트로 데이터 전달

src/app/todo/todos/todos.component.html 파일

<div *ngFor="let todo of todos" (click)="toggleTodo(todo)">
    <!-- app-todo 셀렉터로 todo 속성을 설정 -->
    <app-todo [todo]="todo"></app-todo>
</div>

src/app/todo/todos/todo/todo.component.html 파일

<!-- 체크박스 로직을 분리 -->
<input type="checkbox" [checked]="todo.done"><label>{{ todo.text }}</label>

여기까지 진행하면 흐름이 이렇게 된다. 

4. add-todo 컴포넌트 생성

이제 @Input과 반대의 케이스인 @Output 케이스를 보기 위해 별도의 컴포넌트로 분리할 것이다. todos 폴더 우클릭 후 [New Component]를 선택하여 입력창에 add-todo를 입력한다. 그러면 오른쪽 사진과 같이 todos/add-todo 컴포넌트가 생성된다.

src/app/todo/todos/add-todo/add-todo.component.html 파일

<!-- 할 일 추가 로직을 분리 -->
<input type="text" placeholder="할 일 추가" [(ngModel)]="newText">
<button (click)="addTodo(newText)">추가</button>

5. @Output 사용하기

src/app/todo/todos/add-todo/add-todo.component.ts 파일

import { Component, Output, EventEmitter } from '@angular/core';

// 생략...
export class AddTodoComponent {

  // @Input 및 EventEmitter를 이용하여 자식->부모 컴포넌트로 이벤트 및 데이터를 전달
  @Output() onTodoAdded = new EventEmitter();
  newText: string = '';

  addTodo(newText: string) {
    this.onTodoAdded.emit(newText);
    this.newText = '';
  }
}

src/app/todo/todos/todos.component.html 파일

<div>
  <!--  할 일 추가 부분에 onTodoAdded 내용 추가 -->
  <app-add-todo (onTodoAdded)="addTodo($event)"></app-add-todo>
</div>

여기까지 진행하면 흐름이 이렇게 된다.

여기까지의 최종 소스

src/app/todo/todos/add-todo/add-todo.component.html

<button (click)="addTodo(newText)">+</button>
<input type="text" placeholder="할 일 추가" [(ngModel)]="newText">

src/app/todo/todos/add-todo/add-todo.component.ts

import { Component, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-add-todo',
  templateUrl: './add-todo.component.html',
  styleUrl: './add-todo.component.css'
})
export class AddTodoComponent {

  @Output() onTodoAdded = new EventEmitter();
  newText: string = '';

  addTodo(newText: string) {
    this.onTodoAdded.emit(newText);
    this.newText = '';
  }
}

src/app/todo/todos/todo/todo.component.html

<input type="checkbox" [checked]="todo.done"><label>{{ todo.text }}</label>

src/app/todo/todos/todo/todo.component.ts

import { Component, Input } from '@angular/core';
import { Todo } from '../../share/todo.model';

@Component({
  selector: 'app-todo',
  templateUrl: './todo.component.html',
  styleUrl: './todo.component.css'
})
export class TodoComponent {
  @Input() todo: Todo = new Todo;
}

src/app/todo/todos/todos.component.html

<div class="title">
  <h1>나의 일정</h1>
  <h2>{{ today | date : 'M월 d일'}}</h2>
</div>
<div>
  <div *ngFor="let todo of todos" (click)="toggleTodo(todo)">
    <app-todo [todo]="todo"></app-todo>
  </div>
</div>
<div>
  <app-add-todo (onTodoAdded)="addTodo($event)"></app-add-todo>
</div>

src/app/todo/todos/todos.component.ts

import { Component } from '@angular/core';
import { Todo } from '../share/todo.model';

@Component({
  selector: 'app-todos',
  templateUrl: './todos.component.html',
  styleUrl: './todos.component.css'
})
export class TodosComponent {

  newText: string = '';

  todos: Todo[];

  constructor() {
    this.todos = [
      { done: false, text: '운동하기' },
      { done: true, text: '공부하기' },
    ]
  }

  toggleTodo(todo: any) {
    todo.done = !todo.done
  }

  addTodo(newText: string) {
    this.todos.push({
      done : false,
      text : newText
    })
  }
}

컴포넌트 css 처리 추가

src/app/todo/todos/todos.component.css

.title {
  background-color: blueviolet;
  padding: 46px 26px 26px 16px;
  color: white;
  font-weight: normal;
}

h1, h2 {
  margin: 0;
  font-weight: normal;
}
h1 {
  margin-bottom: 16px;
}

app-todo, app-add-todo {
  border-bottom: 1px solid #cccccc;
}

src/app/todo/todos/todo/todo.component.css

:host {
  display: block;
  padding: 16px;
  color: darkgray;
  background-color: white;
}

input {
  position: relative;
}
input:before {
  content: "";
  display: inline-block;
  width: 20px;
  height: 20px;
  background-color: white;
  border-radius: 20px;
  position: absolute;
  top: -6px;
  left: -8px;
  border: 1px solid dimgray;
}
input:checked:after{
  content: '\2713';
  display: inline-block;
  font-size: 18px;
  width: 20px;
  height: 20px;
  border-radius: 20px;
  position: absolute;
  top: -6px;
  left: -8px;
  border: 1px solid dimgray;
  background-color: dimgray;
  text-align: center;
  color: white;
}

input:checked + label {
  text-decoration: line-through;
}

src/app/todo/todos/add-todo/add-todo.component.css

:host {
  display: block;
  padding: 16px 16px 16px 10px;
  background-color: white;
}

input {
  display: inline-block;
  font-size: 18px;
  border: none;
}

input:focus, button:focus {
  outline: none;
}

button {
  width: 24px;
  height: 24px;
  border-radius: 24px;
  color: white;
  font-size: 16px;
  line-height: 17px;
  border: 1px solid dimgray;
  background-color: darkblue;
}

src/styles.css

/* You can add global styles to this file, and also import other style files */
body {
  margin: 0;
  background-color: #dddddd;
}

css까지 입힌 모습이다.

관련 내용 읽어보기