원문
David Waller, http://blog.krawaller.se/posts/dissecting-bindings-in-angularjs/
AngularJS의 디렉티브/컴포넌트에서 @ < & = 가 실제로 어떻게 동작하는 지 알아보고, < 기호가 어떻게 나머지 기호들을 대체할 수 있는 지 알아보자.
AngularJS에서는 컴포넌트(또는 디렉티브)를 정의할 때, 엘리먼트의 속성들로 내부 스코프의 변수를 생성할 수 있다. 이렇게하기 위한 API는 다소 복잡하다.
bindings: {
attr1: '@',
attr2: '<',
attr3: '=',
attr4: '&'
}
나는 이 API를 사용할 때마다 골머리를 앓는 것에 지쳤다. 그래서 이번 포스팅에서 이 4가지 표현이 갖는 차이점에 대해 완전히 해부해볼 것이다.
구체적으로 우리는...
@
)에 대해 알게될 것이다.<
)에 대해 알게될 것이다.&
)에 대해 알게될 것이다.=
)에 대해 알게될 것이다.<
이 다른 세 표현식을 대체할 수 있는(kicks the ass of the other three) 이유에 대해 알게될 것이다.@
바인딩부터 시작하자. 이 방식은 단순히 속성 값을 문자열로 읽기 때문에 4가지 중 가장 와닿는 기호이다 . 다른 말로하면, 컴포넌트에 문자열을 전달하는 것이다.
이런 컴포넌트를 만들었다고 하자:
app.component('readingstring', {
bindings: { text: '@' },
template: '<p>text: <strong>{{$ctrl.text}}</strong></p>'
});
그리고 우리는 이 컴포넌트를 이렇게 렌더링한다:
<readingstring text="hello"></readingstring>
그러면, 화면에는 이렇게 보인다:
@
바인딩을 사용하면, 주어진 속성의 문자열 값으로 채워진 내부 변수가 만들어진다. 이 방식은 컴포넌트의 초기 설정을 위한 문자열 전달을 위해 사용되기도 한다.
더 흥미로운 점은 속성을 표현식으로 실행하고, 이 표현식에 변경이 있을 때마다 다시 실행되는 경우가 있을 수 있다는 점이다. 동적인 입력말이다!
우리가 바라는 것은 렌더링 시에 동적인 표현식을 속성으로 지정해주면,
<dynamicinput in="outervariable"></dynamicinput>
표현식의 실행결과가 컴포넌트에 전달되는 것이다.
AngularJS 1.5 이전에는 =
바인딩이 표현식을 동적으로 전달하기 위한 유일한 문법이었다.
app.component("dynamicinput", {
bindings: { in: '=' },
template: '<p>dynamic input: <strong>{{$ctrl.in}}</strong></p>'
});
=
바인딩의 안 좋은 점은 이 방식이 단방향 바인딩만이 필요한 경우임에도 불구하고 양방향 데이터 바인딩을 형성한다는 것이다. 이는 또한 우리가 전달하는 표현식이 반드시 변수여야 한다는 것을 의미한다.
그러나 AngularJS 1.5에서부터 단방향 데이터 바인딩을 할 수 있는 <
바인딩이 나타났다. 이 덕분에 함수표현식을 포함하는 어떤 표현식이던 지 사용할 수 있게 되었다:
<dynamicinput in="calculateSomething()"></dynamicinput>
=
이 <
으로 바뀐다는 점만 빼면, 컴포넌트 구현 방식은 완전히 같다.
상황을 바꿔볼 때가 되었다 - 컴포넌트 안의 값을 전달받으려면 어떻게 해야할까? 아래의 작은 애플리케이션을 보자 - 버튼들은 자식 컴포넌트 안에서 렌더링되고 있는 데, 이것을 클릭했을 때 컴포넌트 밖의 값이 갱신되었으면 좋겠다.
이 경우, &
바인딩을 사용하면 된다. 이는 어트리뷰트의 값을 하나의 구문(statement)으로 보고 이를 하나의 함수로 감싼다. 컴포넌트는 원하는 대로 해당 함수를 호출하고, 명령문 속의 변수의 값을 가져온다. 부모 컴포넌트로 값을 보낼 수 있다!
이렇게 렌더링하고,
Outer value: {{count}}
<output out="count = count + amount"></output>
&
바인딩을 사용하는 output
컴포넌트를 생성한다.
app.component("output", {
bindings: { out: '&' },
template: `
<button ng-click="$ctrl.out({amount: 1})">buy one</button>
<button ng-click="$ctrl.out({amount: 5})">buy many</button>
`
});
이때, 필요한 변수를 객체를 통해 전달하는 방식에 주목해보자. 복잡한 문법을 통해 컴포넌트에서 출력값이 필요한 경우에는 두 가지를 알아야 한다는 것을 알 수 있다.
&
바인딩 패턴은 복잡한 편이다. 그래서 대부분은 컴포넌트 안의 값을 받아오기 위해 =
바인딩을 사용한다.
이 경우, 전달받고자 하는 변수를 컴포넌트의 속성으로 지정하기면 하면
Outer value: {{count}}
<output out="count"></output>
간단하게 컴포넌트 안의 변수를 변경하고, 변경된 값을 외부에서 전달받을 수 있다.
app.component("output", {
bindings: { out: '=' },
template: `<div>
<button ng-click="$ctrl.out = $ctrl.out + 1;">buy one</button>
<button ng-click="$ctrl.out = $ctrl.out + 5;">buy many</button>
</div>`
});
그러나 이는 정말이지 아름다운 방식이 아니다:
위의 모든 방식보다 나은 해결방법은 <
을 사용하여, 콜백함수로부터 출력값을 생성하는 것이다!
외부 컨트롤러에 콜백 함수를 만들고
$scope.callback = function(amout) {
$scope.count += amout;
}
컴포넌트 안으로 전달한다.
<output out="callback"></output>
컴포넌트는 이제 콜백함수를 적당한 때에 호출한다:
app.component("output", {
bindings: { out: '<' },
template: `
<button ng-click="$ctrl.out(1)">buy one</button>
<button ng-click="$ctrl.out(5)">buy many</button>
`
});
&
바인딩과 비슷하지만, 복잡하지 않다!
이런 점을 제쳐두더라도, 콜백 함수를 통해 컴포넌트 내부의 값을 전달받는 패턴이 React에서 방식과 정확히 일치한다는 점이 인상적이다.
=
바인딩은 AngularJS를 홍보할 때 주로 강조하는 부분이다. 다음 애플리케이션을 보자.
위의 애플리케이션은 아래와 같이 렌더링하고
Outer: <input ng-model="value">
<twoway connection="value"></twoway>
=
바인딩을 사용해서 twoway
컴포넌트를 구현해서 만들었다.
app.component("twoway", {
bindings: { connection: '=' },
template: `inner: <input ng-model="$ctrl.connection">`
});
정말 쉽다, 그러나 양방향 데이터 바인딩은 거의 필요하지 않다는 것에 주목하자. 때때로 정말 필요한 것은 입력과 출력뿐일 수 있다.
따라서 <
만을 사용해서 양방향 데이터 바인딩을 구현할 수 있다.
외부 컨트롤러에 콜백함수를 생성하고
$scope.callback = function(newval) {
$scope.value = newval;
};
입력값과 콜백함수를 전달해주면 된다.
<twoway value="value" callback="callback"></twoway>
그리고 컴포넌트를 다음과 같이 만들자:
app.component("twowayin", {
bindings: {
value: '<',
callback: '<'
},
template: `
<input ng-model="$ctrl.value" ng-change="$ctrl.callback($ctrl.value)">
`
});
양방향 데이터 바인딩이 되고 있다. 하지만 여전히 단반향 데이터 흐름을 고수하고 있다. 더 나은 방법이다!
사실, 4가지 기호들은 그저 축약 표현일 뿐이다. 이 모든 것들을 기호 없이 할 수 있다.
# 문자열을 전달받는 애플리케이션
이 디렉티브가 위에 처럼 렌더링되려면, 아래의 예제코드처럼 뷰를 작성하고
<readingstring text="hello"></readingstring>
컴포넌트에서 $element
서비스에 직접 접근하도록 구현하면 된다.
app.component("readingstring", {
controller: function($element) {
this.text = $element.attr("text");
},
template: '<p>text: <strong>{{$ctrl.text}}</strong></p>'
});
또는 디렉티브인 경우, link
함수에 attrs
를 전달하면 된다.
app.directive("readingstring", function() {
return {
restrict: 'E',
scope: {},
link: function(scope, elem, attrs) {
scope.text = attrs.text;
},
template: '<p>text: <strong>{{text}}</strong></p>'
}
});
# 동적인 표현식을 입력받는 애플리케이션
아래 코드처럼 렌더링하고
<dynamicinput in="outervariable"></dynamicinput>
다음과 같이 부모 스코프에서 $watch
를 호출하면 된다.
app.component("dynamicinput", {
controller: ($scope, $element) => {
let expression = $element.attr("in");
$scope.$parent.$watch(expression, newVal => $scope.in = newVal);
},
template: '<p>dynamic input: <strong>{{in}}</strong></p>'
});
# 컴포넌트 밖에서 컴포넌트의 데이터를 전달받는 애플리케이션
이렇게 렌더링하고
<output out="count = count + amount"></output>
부모 스코프에서 $scope.$apply
를 호출하면 된다.
app.component("output", {
controller: ($scope, $element, $timeout) => {
let statement = $element.attr("out");
$scope.increaseBy = by => {
$timeout(function() {
$scope.$parent.$apply(`amount = ${by}; ${statement}`);
});
}
},
template: `
<button ng-click="increaseBy(1)">buy one</button>
<button ng-click="increaseBy(5)">buy many</button>
`
});
사실, 이 방식은 &
와 완전히 같은 방식은 아니다. 왜냐하면 amount
변수가 부모 스코프에 전달되어 부모 스코프를 오염시키기 때문이다. 하지만, 값을 외부로 전달한다는 개념을 충분히 잘 표현하고 있다.
# 양방향 바인딩 애플리케이션
다음과 같이 렌더링하고,
<twoway connection="value"></twoway>
부모와 자식 스코프 모두에 $watch
를 설정하면 된다.
app.component("twoway", {
controller: ($scope, $element, $timeout) => {
let variable = $element.attr("connection");
$scope.$parent.$watch(variable, newVal => $scope.inner = newVal;
$scope.$watch('inner', (newVal='') => $timeout( () => {
$scope.$parent.$apply(`${variable} = "${newVal}";`);
}));
},
template: `inner: <input ng-model="inner">`
});
이 방식은 바인딩되는 값을 항상 문자열로 가정하기 때문에 약간 부족한 구현이지만, 말하고자 하는 바가 무엇인 지 전달될 것이다.
필자는 지금까지의 여정이 교육적이었고, 이제 여러분이 @
, <
, =
, 그리고 &
바인딩을 덜 무서워하길 바란다.
그리고 <
바인딩이 어떻게 나머지 표현들을 대체하는 지 알아챘기를 바란다. 이 표현식은 모든 것을 할 수 있다. =
바인딩도 물론 할 수 있지만, <
바인딩이 더 좋은 방식이다.
두 개 모두 문자열을 읽는 용도로는 적당하지 않은 것 같지만(<
은 문자열 리터럴이 필요하다 , 그리고 =
는 프록시 변수가 필요하다), 이는 보통의 자바스크립트에서도 할 수 있는 쉬운 것이기 때문에 @
를 너무 특별하다고 생각할 필요가 없다.
한편, &
은 바로바로 결과값을 전달해준다.