Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Angular.js는 왜 좋은가?

내가 원페이지 앱도 별로 안좋아하고 또 HTML에 동작을 집어넣는 접근을 안좋아하기 때문에 그동안 안보고 있다가 최근 주변의 설득에 넘어가서 최근 Angular.js를 열심히 만져보고 있다. 보통 이러한 새로운 걸 만져보면 간단한 Hello World성 글을 올리는 편이지만 Angular.js는 초기 러닝커브가 좀 있는 편이라 이해를 하는데 시간이 꽤 걸렸고 개념을 제대로 이해하지 못한가는데 튜토리얼성 글을 어설프레 올리는게 위험해 보여서 굳이 올리고 있지 않았다.(그러다가 갑자기 Angular.js 팁같은 걸 올리는것도 좀 생뚱맞고...)

그러다가 Angular Tips 블로그에서 Why Does Angular.js Rock?라는 글을 보고 Angular.js를 소개하는 글로 괜찮고 예제도 잘 구성이 되어 있어서 굳이 내가 설명하는 것보다 낫겠다고 생각했다. 해당 블로그를 운영하는 Jesus Rodriguez한테 번역해도 된다는 허락도 받았기에 해당 글을 번역해서 올린다. Angular Tips 블로그는 Octopress로 되어 있어서 글마다 Angular.js 예제를 글 가운데 삽입하기가 괜찮았지만 내 블로그는 그런 구조가 아니므로 JSfiddle을 사용했다. 해보고 나니 예제마다 전체 소스를 볼 수 있어서 처음 Angular를 보는 사람에게는 소스를 보기 좀더 괜찮을 꺼라고 생각한다.


Angular.js는 왜 좋은가?(Why Does Angular.js Rock?)

Angular.js는 구글이 만든 MV*(Model - View - Whatever) 자바스크립트 프레임워크로 원페이지 어플리케이션에서 뛰어나면서 전통적인 웹 어플리케이션에 약간의 "마법"을 부린 것처럼 동작하는 것도 가능하다.

새로운 프로젝트에 Angular.js를 사용해야 하는 이유에 대해서 하루 종일 글을 쓸수도 있지만 직접 동작하는 것을 보는 것이 더 좋겠다고 생각했다.

데이터바인딩과 스코프(scope)

Angular.js에서 가장 처음 궁금해 하는 부분은 "데이터 바인딩의 지원여부"이다.

Angular.js의 데이터 바인딩 예제를 보자.

<!-- index.html -->
<body ng-app>
  <span>Insert your name:</span>
  <input type="text" ng-model="user.name" />
  <h3>Echo: {{user.name}}</h3>
</body>

이 예제코드에서 설명해야 할 부분이 좀 있기는 하지만 설명하기 전에 먼저 Angular.js 코드에 좀 익숙해 져야 한다.


여기서는 `ng-app`을 몰라도 괜찮다.


이 예제에서 보듯이 input에 입력한 내용이 바로 출력된다. 동작방식을 설명하자면 ng-model 디렉티브로 input이 양방향 바인딩(two-way binding) 된 것이다.(디렉티브에 대해서는 뒤에서 설명한다.)

그러면 user.name는 어디에 저장되는지가 궁금할텐데 user.name$scope에 저장되어 있다. input에 입력할 때마다 스코프의 user.name 객체가 갱신되고 Angular.js의 {{ ... }} 인터폴레이션으로 모델을 출력할 수 있다. 그래서 HTML에서 user.name의 값을 볼 수 있는 것이다. input에 입력할 때 user.name이 스코프에 저장되고 인터폴레이션으로 HTML에서 그 값을 보게 된다.

어려운 내용은 아니지만 여기서 언급한 $scope는 무엇인가가 궁금할 것이다. $scope는 기본적으로 컨트롤러와 템플릿을 연결하고 모델을 보광해서 양방향 바인딩을 할 수 있게 하는 객체다. 이 개념은 다음과 같이 표현할 수 있다.

사용자 삽입 이미지

즉, 템플릿에서 $scopeuser.name을 설정하면 컨트롤러에서도 user.name에 접근할 수 있다. 약간 더 복잡한 예제를 보자.

// app.js
var app = angular.module('app', []);

app.controller('MainCtrl', function($scope) {
  $scope.message = 'World';
});
<!-- index.html -->
<body ng-app="app" ng-controller="MainCtrl">
  Hello, {{ message }}
</body>

가장 먼저 해야할 작업은 Angular 어플리케이션을 정의하는 것이다. Angular 어플리케이션을 정의하려면 이름과 의존성에 대한 배열을 받는(1번 줄) Angular 모듈을 생성하면 된다. 이어서 app에 컨트롤러를 생성해야 하는데 app module에서 controller 메서드를 호출해서 생성할 수 있는데 이 메서드에 이름과 함수를 전달한다. controller 메서드에 전달하는 함수는 양방향 데이터 바인딩에 사용할 $scope(자세한 내용은 뒤에서 설명한다)를 파라미터로 받고 이 함수내에서 $scopemessage 문자열을 추가한다.

뷰 파일에서 body태그에 무언가 추가된 것을 볼 수 있는데 이를 directive라고 한다. 디렉티브는 HTML에 새로운 기능을 알려 주는 역할을 하는데 이 예제에서는 다음의 두 가지 디렉티브를 사용했다.

  • ng-app은 Angular에게 body 요소가 Anuglar 어플리케이션에 포함되어 있다고 알려준다. 즉, body 요소내의 모든 것을 Angular 어플리케이션이 관리하도록 한다. 일반적으로는 html태그에 사용하고 파라미터는 어플리케이션의 이름으로 모듈에서 사용한 이름과 일치해야 한다.
  • ng-controller: 이 디렉티브를 사용하면 해당 요소의 스코프를 컨트롤러에 할당할다. 이 예제에서는 MainCtrl이다.

그리고 템플릿에서 message를 사용했다. 이를 그림으로 표현하면 다음과 같다.

사용자 삽입 이미지

아마 이때쯤 $scope에 함수를 바인딩할 수 있는지 궁금할 수 있는데 당연히 가능하다.

// app.js
var app = angular.module('app', []);

app.controller('MainCtrl', function($scope) {
  $scope.greet = function() {
    $scope.message = "Hello, " + $scope.user.name;
  }
});
<!-- index.html -->
<body ng-app="app" ng-controller="MainCtrl">
  What's your name?:
  <input type="text" ng-model="user.name" />
  <button ng-click="greet()">Click here!</button>
  <h3>{{ message }}</h3>
</body>

컨트롤러를 보면 $scope에 함수를 연결하는 방법을 알 수 있다. $scope에 연결한 함수에서 인풋에 입력된 값인 $scope.user.name의 문자열을 이어붙혀서 $scopemessage로 추가한다.

그리고 HTML에서 버튼을 생성하고 ng-click 디렉티브를 사용했다. 간단히 얘기하자면 ng-click 디렉티브는 해당 요소를 클릭할 수 있게 만들어서 클릴할 때마다 할당한 함수를 실행하는데 이 예제에서는 greet()를 실행한다.

`input`에서 엔터를 눌러도 동작하지 않는데 여기서는 정상적인 동작이다. 이 예제는 `ng-click`의 동작방식을 보여주기 위한 예제이다.


사용자 삽입 이미지


디렉티브(Directives)

디렉티브는 무엇인가? 디렉티브는 HTML에게 새로운 동작을 알려주는 방법이다. HTML은 아주 강력하기는 하지만 때로는 좀 더 강력했으면 할 때가 있다. 정말 그런지 직접 보자.

<!-- jquery_index.html -->
<body>
  <div id="chart"></div>
</body>

이 코드는 어떤 동작을 할까? 아무런 힌트도 없고 id가 있기는 하지만 이 id가 어떤 역할을 할 지 누가 알겠는가? 어떤 동작을 하는지 알려면 HTML 파일에 나오는 30개의 자바스크립트 파일중에 하나를 봐야 한다.

// charts.js
$('#chart').pieChart({ ... });

이 부분을 찾아내면 위의 HTML이 파이차트를 담는 곳이라는 것을 알 수 있다. 여기서 문제점은 페이지에 사용한 모든 자바스크립트 파일을 보지 않으면 페이지가 무엇을 하는지 정확히 알 수 없다는 것이다.

이제 Angular 어플리케이션의 코드를 보자.

<!-- angular_index.html-->
<body>
  <pie-chart width="400" height="400" data="data"></pie-chart>
</body>

훨씬 더 명확하지 않은가? 파이차트를 추가한다는 간다한 사실뿐만 아니라 크기가 어느정도 이고 어떤 데이터가 할당되었는지를 알 수 있다. 이 예제에 대해서 궁금하다면 재미삼아 만들어 본 pie-chart 예제에서 확인해 볼 수 있다.

내장 디렉티브

이미 ng-app, ng-controller, ng-click, ng-model를 봤지만(Angular는 ng 접두사를 사용한다.) Angular에는 다수의 내장 디렉티브가 존재한다. 이 내장 디렉티브를 살펴보자.

페이지에 어떤 프로퍼티가 true일 때만 보여줄 부분이 있다고 해보자.

<button ng-click="show = !show">Show</button>
  <div ng-show="show">
    I am only visible when show is true.
  </div>

ng-show는 표현식이 true일때만(예제에서는 바인딩한 값) 해당 요소를 보여준다. 이 예제에서 ng-click을 사용한 방법을 주의깊게 보길 바란다. 이 예제에서는 컨트롤러에 함수를 생성할 필요가 없으므로(컨트롤러 자체도 필요없다!) 디렉티브의 인자를 표현식으로 작성해서 show의 값을 토글했다. showundefined로 시작되고 첫 클릭시 true가 된다. ng-show의 반대인 ng-hide도 있다.

좀 더 재밌는 예제를 보자. 객체에 배열이 있고 이 배열의 목록을 출력하고자 하면 어떻게 하겠는가?

// app.js
var app = angular.module('app', []);

app.controller('MainCtrl', function($scope) {
  $scope.developers = [
      {
        name: "Jesus", country: "Spain"
      },
      {
        name: "Dave", country: "Canada"
      },
      {
        name: "Wesley", country: "USA"
      },
      {
        name: "Krzysztof", country: "Poland"
      }
    ];
});
<!-- index.html -->
<body ng-app="app" ng-controller="MainCtrl">
 <ul>
   <li ng-repeat="person in developers">
     {{person.name}} from {{person.country}}
   </li>
 </ul>
</body>

컨트롤러에 객체의 리스트를 정의한 것외에는 별로 특별한 내용은 없다. 이어서 HTML에서 ng-repeat 디렉티브를 사용했다. ng-repeat는 컬렉션의 아이템마다 새로운 템플릿을 생성할 것이다. 예제에서는 4개의 아이템이 있으므로 ng-repeat가 이 코드 부분을 4번 생성할 것이다.

<li ng-repeat="person in developers">
  {{person.name}} from {{person.country}}
</li>

ng-repeat에서 각 복사본은 자신만의 스코프를 가진다. 그래서 템플릿에서는 컨트롤러가 스코프의 역할을 하지 않고 이 예제에서는 person이 스코프가 된다. 다시 말하자면 여기서는 부모 컨트롤러에 대한 접근을 하지 않는다.(실제로는 부모 컨트롤러에 접근하는 방법이 있기는 하다.) 이를 그림으로 표현하면 다음과 같다.

사용자 삽입 이미지


자신만의 디렉티브를 만들 수 있는가?

당연히 가능하다! 거의 모든 것을 만들 수 있는데 모달 다이얼로그, 어코디언, 페이지네이터, 차트, 검색폼 등에 대한 디렉티브를 만들 수 있다. 예시로 든 것처럼 항상 보여지는 것이어야 하는 것은 아니고 보이지 않는 부분에 대한 디렉티브를 만들 수도 있다. 앞에서 사용했던 greet 예제로 돌아가 보자.

<!-- form.html -->
<body ng-app="app" ng-controller="MainCtrl">
  What's your name?:
  <input type="text" ng-model="user.name" />
  <button ng-click="greet()">Click here!</button>
  <h3>{{ message }}</h3>
</body>

이 코드는 잘 동작하기는 하지만 페이지를 로딩했을 때 인풋에 포커싱을 주고 싶다면 어떻게 해야 하는가? jQuery를 사용해서 인풋에 focus()메서드를 호출해야 하는가? 당연히 아니다. 우리는 디렉티브에서 HTML이 최대한 자기서술적이 되기를 원하기 때문에 focus 디렉티브를 생성할 것이다.

// focus.js
app.directive('focus', function() {
  return {
    link: function(scope, element, attrs) {
      element[0].focus();
    }
  };
});

그래서 app 객체의 directive 함수를 호출한다. directive는 컨트롤러처럼 디렉티브의 이름과 함수를 받는다. 디렉티브는 Angular.js에서 가장 복잡하지만 (이 글을 쇼케이스같은 글이므로) 간단히만 설명할 것이다.(추후에 이 주제로 글을 따로 올리겠다.) 디렉티브는 객체를 반환해야 하고 이 반환 객체에 몇가지 속성을 정의할 수 있지만 이 예제에서는 속성을 사용하지 않았다. 디렉티브는 link 함수를 반환할 수 있는데 이 link함수 안에 템플릿 로직의 대부분을 작성하고 여기서 DOM 리스너를 등록하거나 DOM을 갱신하는 등의 작업을 할 수 있다.

link함수는 3개의 파라미터를 받는다.(실제로는 4개지만 이는 약간 고급에 해당한다.) 3개의 파라미터는 scope와 디렉티브를 사용한 elementelement의 속성인 attr이다. 여기서 HTML 요소에 click 이벤트나 mouseenter 이벤트를 바인드할 수 있다.

이 예제에서는 첫번째 요소(인풋)을 가져와서 focus함수를 호출했다. element가 어떻게 동작하는지 궁금하면 공식문서 Element API를 참고해라.

이제 남은 작업을 이 디렉티브를 사용하는 것 뿐이다. 포커스를 주고자 하는 요소에 이 디렉티브 이름을 추가하면 된다.

<!-- form.html -->
<body ng-app="app" ng-controller="MainCtrl">
  What's your name?:
  <input type="text" focus ng-model="user.name" />
  <button ng-click="greet()">Click here!</button>
  <h3>{{ message }}</h3>
</body>

의도한 기능이 잘 동작하지만 이 디렉티브는 정말 간단하다.(이 예제는 블로그 포스트내에서는 제대로 포커스가 동작하지 않으니 JSFiddle에서 직접 확인하기 바란다. - 글쓰고 확인할때는 포커싱이 잘 안되어 이렇게 써놨었는데 브라우저에 따라서는 아예 포커스가 옮겨가기도 하는것 같네요. 일단 실행결과가 바로 뜨지 않도록 수정했습니다.)

그러면 HTML을 렌더링하는 디렉티브는 어떻게 작성해야 할까? 다음 예제를 보자.

// hello.js
app.directive('hello', function() {
  return {
    restrict: "E",
    replace: true,
    template: "<div>Hello readers, thank you for coming</div>"
  }
});

이 예제는 (앞에서 얘기했듯이) 몇 가지 속성이 설정된 객체를 반환한다.

  • restrict: 디렉티브는 여러 위치에 사용할 수 있다.

    • Attribute, 예시: <div foo></div>
    • Element, 예시: <foo></foo>
    • Class, 예시: <div class="foo"></div>
    • CoMment, 예시: <!-- directive: foo -->
  • replace: 이 값을 true로 설정하면 해당 요소는 새로운 템플릿으로 대체될 것이다.
  • template: HTML 요소에 추가할(또는 대체할) 템플릿을 여기에 둔다.

요소에 사용할 디렉티브를 제한하고(기본값은 속성으로 제한된다.) HTML 요소를 대체할 템플릿을 지정했다. 디렉티브에서 사용할 수 있는 많은 옵션이 더 있기는 하지만 추가적인 작업이 좀 더 필요하다. 이 디렉티브에서는 로직이 필요없으므로 link 함수는 사용하지 않았다. 사용방법은 다음 예제를 보자.

<hello></hello>

코드를 보면 기대한 대로 <hello></hello><div>Hello readers, thank you for coming</div>로 대체된 것을 알 수 있다.

필터

쇼핑 장바구니를 보여주는 화면이 있다고 해보자.

<span>There are 13 phones in the basket. Total: {{ 1232.12 }}</span>

이 예제로 인터폴레이션에서 기본적인 표현식을 사용하는 방법을 알 수 있다. 예제에서는 숫자를 출력했는데 당연히 읽을 수 있고 이 숫자가 $1,232.12를 의미한다는 것을 알 수 있지만 숫자를 금액으로 변환해서 보여준다면 훨씬 좋을 것이다. 이는 필터를 사용하면 쉬운데 다음 예제는 currency 필터를 사용하는 예제다.

<span>There are 13 phones in the basket. Total: {{ 1232.12 | currency }}</span>

훨씬 나아졌다. 이 예제에서 볼 수 있듯이 |로 필터를 사용할 수 있는데 이는 유닉스 환경에서 파이프를 사용하는 것과 비슷하다. 표현식에서는 다수의 필터를 파이프로 연결할 수 있는데 예를 들어 ng-repeat에서 정렬을 할 수도 있다. 앞에서 보았던 개발자 목록을 출력하는 예제를 다시 보자.

<ul>
  <li ng-repeat="person in developers | orderBy:'name'">
    {{ person.name }} from {{ person.country }}
  </li>
</ul>

이 예제에는 흥미로운 부분이 있는데 필터에 파라미터를 전달했다. orderBy 필터는 정렬에 사용할 값(predicate)를 받는다. 예제에서는 name을 전달했으므로 name으로 목록을 정렬할 것이고 이 파라미터에 -name를 전달하면 역순으로 정렬할 것이다.

이정도로도 유용하다고 생각할 것이지만 좀 더 괜찮은 예제를 보자. 4명의 개발자가 아니라 300명의 개발자가 있고 이를 (name, country등으로) 필터링하기를 원한다고 해보자. 이를 위해서 컬렉터를 필터링하는 방법을 정하고 필터링되지 않은 컬렉션을 필터링한 컬렉션으로 교체하려고 할텐데 더 간단한 방법이 있다.(개발자 목록 예제에서 사용한 것과 같은 컨트롤러를 다시 사용한다.)

<body ng-app="app" ng-controller="MainCtrl">
  Search: <input ng-model="search" type="text" />
  <ul>
    <li ng-repeat="person in developers | filter:search">
      {{ person.name }} from {{ person.country }}
    </li>
  </ul>
</body>

놀랍지 않은가? 이 예제에서는 필터를 사용한 것이 전부인데 filter필터에 담길 파라미터를 전달했다. 예제에서 스코프에 바인딩되어 있으면서 인풋에 할당된 search를 전달했다. 필요하다면 다음과 같이 좀 더 정밀한 필터를 만들 수도 있다.

<body ng-app="app" ng-controller="MainCtrl">
  Search: <input ng-model="search.name" type="text" />
   <ul>
     <li ng-repeat="person in developers | filter:search">
       {{ person.name }} from {{ person.country }}
     </li>
   </ul>
</body>

이 예제에서는(인풋에서 search.name를 바인딩하는 방법을 봐라.) 단순히 이름으로 필터링하고 있고 필터 파라미터는 바꾸지 않았다. 필터 파라미터는 search객체에 바인딩되어 있고 입력의 이름을 찾아서 이름으로 필터링한다.

이번에는 자신만의 필터를 직접 생성해 보자. 첫글자를 대문자를 필터를 어떻게 작성하는지 보자.

app.filter('capitalize', function() {
  return function(input, param) {
    return input.substring(0,1).toUpperCase()+input.substring(1);
  }
});

필터는 인풋(인터폴레이션의 결과값이다.)과 필터 파라미터를 받는 함수를 반환하고 반환된 함수는 새로운 인풋을 반환한다. 이 예제에서는 인풋의 첫글자를 대문자로 바꿨다. 이제 이 필터를 사용해 보자.

<span>{{ "this is some text" | capitalize }}</span>

문자열을 문자열 리터럴로 만들기 위해 대문자로 감싼 뒤 capitalize 필터를 "파이프"로 연결하면 동작한다.

서비스

이 글의 마지막 부분인 서비스다. 서비스는 무엇일까? 서비스는 어플리케이션의 어떤 기능을 제공하는 싱글톤 클래스다. 어플리케이션 로직을 컨트롤러에 분산시키는 대신 다른 서비스에 로직을 둘 수 있다.

Angular에는 HTTP 요청을 관리하는 $http나 프로미스(Promise)에 대한 $q와 같은 많은 내장 서비스가 있다. 하지만 내장 서비스는 설명하기가 더 어렵기 때문에 다른 글에서 설명하기로 하고 여기서는 내장 서비스는 따로 설명하지 않을 것이다. 대신 간단한 서비스를 생성해 보자.

서비스를 사용하는 가장 일반적인 경우는 컨트롤러간에 정보를 공유해야하는 경우이다. 모든 컨트롤러는 자신만의 스코프를 가지므로 다른 컨트롤러의 스코프를 바인딩할 수 없다. 이 문제를 해결하기 위해 서비스를 사용하면 한 곳에서 데이터를 가지고 있고 필요한 어디서나 이 데이터를 사용하도록 할 수 있다. 우선 문제의 상황을 보기 위해 서비스가 없는 다음 예제를 살펴보자.

<!-- index.html -->
<div ng-controller="MainCtrl">
  MainCtrl:
  <input type="text" ng-model="user.name">
</div>
<div ng-controller="SecondCtrl">
  SecondCtrl:
  <input type="text" ng-model="user.name">
</div>
// controllers.js
app.controller('MainCtrl', function($scope) {

});

app.controller('SecondCtrl', function($scope) {

});

인풋을 같은 모델에 바인딩했으므로 인풋에 글을 쓰면 다른 쪽에도 업데이트 되기를 기대할 것이다. 즉, 다음과 같은 형태를 기대한 것이다.

사용자 삽입 이미지

하지만 이런 형태가 되는 것이 아니라 다음과 같이 된다.

사용자 삽입 이미지

이 문제를 해결하기 위해 두 컨트롤러에서 사용할 수 있도록 사용자 이름을 가진 서비스를 생성할 것이다.

// user_information.js
app.factory('UserInformation', function() {
  var user = {
    name: "Angular.js"
  };

  return user;
});

서비스를 생성하기 위해 app 모듈의 factory 함수를 사용했다. 서비스를 생성하는 좀더 고급적인 방법도 있다.(serviceprovider 함수를 사용하지만 이는 다른 글에서 설명하겠다.) 서비스를 생성하는 여러가지 방법이 있지만 이 예제에서는 미리 정의해 놓은 이름으로 private 사용자 객체를 생성해서 반환했다. 이 서비스는 컨트롤러에서 다음과 같이 사용한다.

// controllers.js
app.controller('MainCtrl', function($scope, UserInformation) {
  $scope.user = UserInformation;
});

app.controller('SecondCtrl', function($scope, UserInformation) {
  $scope.user = UserInformation;
});

이 예제는 다음과 같이 된다.

사용자 삽입 이미지

이 예제는 의도대로 잘 동작한다. MainCtrlSecondCtrl 양쪽의 $scope.userUserInformation를 사용하고 서비스가 싱글톤이기 때문에 한 컨트롤러에서 UserInformation의 값을 바꾸면 다른 쪽에서도 바뀐다.

여기서 UserInformation 파라미터가 어디서 왔는지 궁금할 것이다. Angular는 서비스를 필요로 하는 곳에 서비스를 주입하는 의존성 주입을 사용한다. 의존성 주입이 동작하는 방식을 설명하는 것은 이 글의 주제를 벗어나지만 간단히 말하자면 서비스를 생성하면 어느 컨트롤러나 디렉티브, 다른 서비스에도 이 서비스를 주입할 수 있다. 주입하는 방법은 그냥 파라미터에 서비스의 이름을 전달하면 된다. 아마 이 의존성 주입이 $scope 파라미터를 사용한 것과 같은 것인지 궁금할텐데 $scope는 다른 컨트롤러에 주입되기는 하지만 실제로 서비스는 아닌 예외사항 중 하나이다.

결론

이 글이 Angular Tips 블로그의 첫번째 글이다.(마지막은 아니다. ^^) Angular.js는 뛰어난 프레임워크이고 아마도 이미 그 매력을 느꼈을 것이다. 이 글이 도움이 되었기를 바란다.

덧) 참고삼아 얘기하면 예제는 Angular.js 앱으로 동작하기 위해서 body태그에 ng-app속성과 컨트롤러를 위한 속성등이 들어가 있다. 이런 부분까지 보려면 JSFiddle에 예제에서 소스보기를 해서 봐야할 것이다.

2013/08/27 01:57 2013/08/27 01:57