티스토리 뷰

안녕하세요. 한장현입니다.

오늘은 Angular 커스텀 컴포넌트에 양방향 바인딩 구현하면서 삽질했던 경험을 공유해 봅니다.


목표 : 커스텀 컴포넌트에 [( )] 문법으로 양방향 바인딩 구현하기




1. Angular 데이터 바인딩

Angular에서 기본 데이터 바인딩은 단방향이죠. 간단하게 정리하면 다음과 같습니다.

  • 데이터에서 뷰로 향하는 바인딩 (프로퍼티 바인딩) : property="{{ expression }}", [property]="expression", bind-property="expression"
  • 뷰에서 데이터로 향하는 바인딩 (이벤트 바인딩) : (target)="statement", on-target="statement"
  • 양방향 바인딩 : [(target)]="expression", bindon-target="expression"


{{ 평가식 }} 으로 사용하는 방법과 [프로퍼티] 로 사용하는 방법이 일반적인데, 찾아보니 bind- 접두사를 붙이는 방법도 있었네요.

마찬가지로 (target) 을 사용하는 방법 말고 on- 접두사를 사용하는 방법도 있고, 양방향 바인딩도 bindon- 접두사를 사용하는 방법도 있습니다.


양방향 바인딩 문법이 [( )] 인지, ([ ]) 인지 헷갈릴 때는 '상자[ ]에 든 바나나( )'를 생각하면 됩니다. Angular 공식 문서에서도 이렇게 설명을 하고 있네요 ㅎㅎㅎ

사진을 찾아보니 적절한 비유 같습니다 ㅎ

banana in a box만 생각하면 문법이 헷갈릴 일은 없겠네요~




2. 컴포넌트 연결

컴포넌트를 외부와 연결할 때는 @angular/core에 있는 Input, Output 데코레이터를 사용합니다.

컴포넌트로 들어가는 데이터는 @Input() 데코레이터로 지정하는 프로퍼티에,

컴포넌트에서 나오는 이벤트는 @Output() 데코레이터로 지정하는 프로퍼티로 지정하죠.


외부에서 boolean 값을 받고 값(checked)이 바뀌는 이벤트(checkChanged)를 처리하는 CustomCheckComponent는 다음과 같이 구현합니다.


- app.component.html

<custom-check [checked]="checkValue" (checkedChange)="changed($event)"></custom-check>
<p>value : {{ checkValue  }}</p>


- custom-check.component.ts

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

@Component({
	selector : 'custom-check',
	template : `
		<button (click)="toggle()">toggle</button>
		<span>{{ checked }}</span>
	`
})
export class CustomCheckComponent {
	@Input() checked : boolean = false;
	@Output() checkedChange : EventEmitter = new EventEmitter();

	toggle () {
		this.checked = !this.checked;
		this.checkedChange.emit(this.checked);
	}
}


그런데 이 방식은 완벽한 양방향 바인딩이 아닙니다. 단방향 바인딩이 양쪽으로 각각 연결되어 있을 뿐이죠.

AppComponent에서 checkedChange 이벤트를 보고 있다가, 이벤트를 받으면 AppComponent에 있는 checkValue를 수동으로 바꿔줘야 합니다.

원하는 건 CustomCheckComponent에 바인딩 한 checked 값이 바뀔때 부모 컴포넌트의 값도 같이 바뀌는 거죠


<input> 태그에 [(ngModel)]를 사용하듯이 [(checked)] 같이 사용하고 싶은데... 방법이 없나 열심히 찾아봤습니다.

Angular 가이드 문서에는 딱히 이런 내용이 없는데, 구글링을 하다가 JavaScript 문법에 있는 getter와 setter를 사용하는 방법을 찾았습니다.



3. getter, setter

getter와 setter는 프로퍼티 값을 참조하거나 변경하는 시점을 가로채는 함수입니다. 이 때 getter와 setter로 지정하는 함수의 이름이 접근하는 프로퍼티 이름으로 사용되며, 이 프로퍼티 값을 저장하는 변수를 컴포넌트 안에 추가로 만들어야 합니다.

이 기능은 TypeScript 문법으로 다음과 같이 사용합니다.


- custom-check.component.ts

export class CustomCheckComponent {
	private value : boolean = false;

	@Input()
	get checked () : boolean {
		return this.value;
	}

	set checked (value) {
		this.value = value;
	}

	@Output() checkedChange : EventEmitter<boolean> = new EventEmitter();

	toggle () {
		this.checked = !this.checked;
		this.checkedChange.emit(this.checked);
	}
}


이렇게 구현하면 컴포넌트 외부에서 checked 프로퍼티에 접근할 때 getter와 setter로 지정한 함수를 실행하면서 내부 변수인 value 프로퍼티를 사용합니다.

toggle() 함수에서도 this.value 프로퍼티를 사용하지 않고, getter와 setter를 사용하기 위해 checked 프로퍼티를 사용합니다.


그런데 checked 프로퍼티 값이 바뀌는 것을 CustomCheckComponent 외부에서 알 수 있는 방법은 여전히 checkedChange 이벤트를 활용해야 합니다. 추가 작업이 발생하는 것은 마찬가지네요.

checked를 양방향으로 연결하기 위해 [( )] 문법을 사용하고 checked 프로퍼티에 @Input(), @Output()를 같이 적용하면 되나 싶은데, @Input() 데코레이터와 @Output() 데코레이터는 한 프로퍼티에 함께 적용할 수 없습니다. 에러 나요 ㅎ


이 때 AppComponent 템플릿에서는 [(checked)] 표현식을 사용하고,

(checkedChange) 이벤트를 받는 부분을 지우면서 setter 함수에서 checkedChange 프로퍼티로 이벤트를 보내줍니다.


그러면 checked 값(실제로는 value 값)이 바뀔 때 이벤트를 외부로 보내는데, 신기한 일이 발생합니다.

이벤트 이름이 프로퍼티명 + 'Change'인 경우에, 위 코드에서는 checked + Change === checkedChange인 이벤트가 있으면, 이 이벤트를 잡지 않아도 양방향 바인딩이 연결됩니다.

이 부분이 중요합니다!! 이벤트 프로퍼티 이름이 이 규칙을 벗어나면 양방향 바인딩이 연결되지 않습니다.

그리고 (checkedChange) 이벤트를 AppComponent에서 잡고 있어도 양방향 바인딩은 연결되지 않습니다.


최종 코드는 이렇게 됩니다.

- app.component.html

<custom-check [(checked)]="checkValue"></custom-check>
<p>checkValue = {{ checkValue }}</p>


- custom-check.component.ts

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

@Component({
	selector : 'custom-check',
	template : `
		<button (click)="toggle()">toggle</button>
		<span>{{ checked }}</span>
	`
})
export class CustomCheckComponent {
	private value : boolean = false;
	@Output() checkedChange : EventEmitter = new EventEmitter();

	@Input()
	get checked () {
		return this.value;
	}

	set checked (val) {
		this.value = val;
		this.checkedChange.emit(this.value);
	}

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


커스텀 컴포넌트도 양방향 바인딩을 사용할 수 있네요 ㅎㅎㅎㅎ

원하는 대로 깔끔하게 동작해서 좋습니다 ㅎㅎ


참고할만한 코드 샘플은 이 링크를 참고하세요~


고맙습니다.

댓글
댓글쓰기 폼
공지사항
Total
254,500
Today
225
Yesterday
256
링크
«   2018/04   »
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          
글 보관함