AngularJS의 @ < & = 바인딩 해부하기


원문
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">`
});

이 방식은 바인딩되는 값을 항상 문자열로 가정하기 때문에 약간 부족한 구현이지만, 말하고자 하는 바가 무엇인 지 전달될 것이다.

정리하기

필자는 지금까지의 여정이 교육적이었고, 이제 여러분이 @, <, =, 그리고 & 바인딩을 덜 무서워하길 바란다. 그리고 < 바인딩이 어떻게 나머지 표현들을 대체하는 지 알아챘기를 바란다. 이 표현식은 모든 것을 할 수 있다. = 바인딩도 물론 할 수 있지만, < 바인딩이 더 좋은 방식이다.

표현식 정리

두 개 모두 문자열을 읽는 용도로는 적당하지 않은 것 같지만(<은 문자열 리터럴이 필요하다 , 그리고 =는 프록시 변수가 필요하다), 이는 보통의 자바스크립트에서도 할 수 있는 쉬운 것이기 때문에 @를 너무 특별하다고 생각할 필요가 없다.

한편, &은 바로바로 결과값을 전달해준다.