AngularJS PhoneCat 튜토리얼 앱 – 정적/동적 템플릿

angularjs-large

정적 템플릿

Angular가 표준 HTML을 어떻게 개선시키는지 설명하기 위해, 순수한 정적 HTML 페이지를 만들고 Angular가 데이터 셋의 결과를 이용하여 이 정적 HTML과 동일한 결과물을 동적으로 만들어내는 템플릿으로 바꿔보는 과정을 진행해 보겠습니다. 여기서는 HTML 페이지에 두개의 휴대폰 기본 정보를 추가해보겠습니다.

angular-phonecat 프로젝트의 저장소를 과정1으로 초기화 하겠습니다.

$ git checkout -f step-1

정적 템플릿을 사용한 app/index.html 의 내용은 다음과 같습니다.

<ul>
  <li>
    <span>Nexus S</span>
    <p>
      Fast just got faster with Nexus S.
    </p>
  </li>
  <li>
    <span>Motorola XOOM™ with Wi-Fi</span>
    <p>
      The Next, Next Generation tablet.
    </p>
  </li>
</ul>

실험

index.html 파일에 다음과 같은 정적 HTML 요소를 추가해 봅시다.

<p>Total number of phones: 2</p>

동적 템플릿

이제, 웹 페이지를 AngularJS를 이용하여 동적으로 변경해볼 시간입니다. 우리는 또한 우리가 추가하려는 컨트롤러의 코드를 검증하는 테스트도 추가할 것입니다.

어플리케이션 코드에 대한 구조적 방법론은 여러가지가 있습니다만 Angular 어플리케이션에서는 Model-View-Controller(MVC) 디자인 패턴을 사용하여 코드를 분리시키고 신경써야 할 부분을 분리할 것을 권장합니다. 이를 염두에 두고 우리의 어플리케이션에 모델, 뷰, 컨트롤러를 약간의 Angular 및 자바스크립트를 사용하여 구현해보도록 하겠습니다. 여기서는 3개의 휴대폰 정보를 데이터를 이용하여 동적으로 생성해 보겠습니다.

angular-phonecat 프로젝트의 저장소를 과정 2로 초기화 하겠습니다.

$ git checkout -f step-2

뷰와 템플릿

Angular에서는 뷰는 HTML 템플릿을 통해 모델을 투영한 결과물입니다. 이 말 뜻은 언제든지 모델이 변경되면 Angular는 적절한 바인딩 지점을 새로 고침 하면서 결과적으로 뷰를 업데이트 하게된다는 뜻입니다. 뷰는 Angular로 하여금 이 템플릿을 통해 만들어집니다.

<html ng-app="phonecatApp">
<head>
  ...
  <script src="bower_components/angular/angular.js"></script>
  <script src="app.js"></script>
</head>
<body ng-controller="PhoneListController">

  <ul>
    <li ng-repeat="phone in phones">
      <span>{{phone.name}}</span>
      <p>{{phone.snippet}}</p>
    </li>
  </ul>

</body>
</html>

우리는 여기서 하드코딩 되어있는 휴대폰 리스트를 ngRepeat 지시자와 두개의 Angular 표현식을 사용하도록 변경하였습니다.

  • <li> 태그에 붙어있는 ng-repeat=”phone in phones” 속성은 Angular의 반복 지시자입니다. 이 반복지시자는 Angular로 하여금 리스트에 있는 각각의 휴대폰 정보마다 <li> 태그를 템플릿으로 사용하여 앨리먼트들을 만들도록 해줍니다.
  • 중첩 중괄호로 표현된 {{phone.name}} 과 {{phone.snippet}}은 표현식의 값으로 치환될 것입니다.

우리는 또한 <body> 태그에 ngController 라고 불리는 새로운 지시자를 사용하여 PhoneListController 컨트롤러를 추가하였습니다. 여기서 다음과 같은 점을 알 수 있습니다.

  • PhoneListController는 <body> 엘리먼트를 (포함하여) 하위 DOM 트리를 담당하게 됩니다.
  • 중첩 중괄호로 표현되어있는 표현식은 우리의 PhoneListController 컨트롤러내에서 초기화 되는 어플리케이션 모델을 가르키는 바인딩을 의미합니다.

우리는 여기서 ng-app=”phonecatApp” 이라는 지시자를 사용하여 Angular Module이 phonecatApp 이라는 이름을 가진 우리의 모듈을 로드하도록 특정하였습니다. 이 모듈은 PhoneListController를 포함합니다.

모델과 컨트롤러

데이터 모델(객체 문자 표기법으로 구성된 폰 정보를 담은 간단한 배열)은 PhoneListController 컨트롤러 내에서 인스턴스화 됩니다. 이 컨트롤러는 $scope 파라미터를 갖는 단순한 생성자입니다.

// Define the `phonecatApp` module
var phonecatApp = angular.module('phonecatApp', []);

// Define the `PhoneListController` controller on the `phonecatApp` module
phonecatApp.controller('PhoneListController', function PhoneListController($scope) {
  $scope.phones = [
    {
      name: 'Nexus S',
      snippet: 'Fast just got faster with Nexus S.'
    }, {
      name: 'Motorola XOOM™ with Wi-Fi',
      snippet: 'The Next, Next Generation tablet.'
    }, {
      name: 'MOTOROLA XOOM™',
      snippet: 'The Next, Next Generation tablet.'
    }
  ];
});

여기서 우리는 PhoneListController라고 불리는 컨트롤러를 선언하였고 그것을 phonecatApp이라는 이름의 Angular 모듈에 등록하였습니다. 여기서 우리는 어플리케이션이 부트스트래핑 하는 과정에서 ngApp 지시자가 phonecatApp 모듈 이름을 특정하였음을 알 수 있습니다.

아직 컨트롤러는 많은것을 하고 있지는 않지만 매우 중요한 역할을 하고 있음을 알 수 있습니다. 우리의 데이터 모델을 컨텍스트로 제공함으로써 컨트롤러는 모델과 뷰 사이에 데이터 바인딩을 확립할 수 있도록 허용 해 줍니다. 우리는 프레젠테이션, 데이터, 논리 컴포넌트를 다음과 같은 방법으로 연결하였습니다.

  • <body> 태그에 위치한 ngController 지시자는 우리의 컨트롤러인 PhoneListController를 가르킵니다. (이 컨트롤러의 선언은 app.js 자바스크립트 파일안에 위치하고 있습니다.)
  • PhoneListController 컨트롤러는 휴대폰 데이터를 우리의 컨트롤러 함수를 통해 주입된 $scope에 붙이는 작업을 수행합니다. 이 scope는 어플리케이션이 선언 되었을 때 만들어진 루트 스코프의 하위 영역에 위치합니다. 이 컨트롤러의 스코프(scope)는 <body ng-controller=”PhoneListController”> 하위의 모든 바인딩에서 사용할 수 있습니다.

스코프

Angular 에서의 스코프라는 개념은 중요합니다. 스코프는 템플릿, 모델, 컨트롤러가 모두 함께 동작하도록 하는 접착재로 간주될 수 있습니다. Angular는 스코프를 사용하여 템플릿, 데이터 모델, 컨트롤러에 포함된 정보를 모델과 뷰가 분리된 상태로 공유하도록 합니다. 하지만 이 모든것들이 동기로 수행됩니다.

모델에 어떤 변화가 발생하면 이것은 뷰에 반영됩니다. 또한 어떤 뷰의 변화가 발생하면 모델에 반영하게 됩니다.

Angular Scope에 대해서 더 알아보고 싶다면 Angular Scope Documentation을 확인하시기 바랍니다.

angularjs-phonecat-tutorial-02

Angular 스코프는 구조적으로 어플리케이션의 루트 스코프까지 도달 가능한 그들의 부모 스코프를 상속받도록 되어있습니다. 결과적으로 스코프에 직접 값을 할당하는것은 페이지의 다른 파트들간에 데이터를 공유하거나 상호작용하는 어플리케이션을 만드는것을 쉽게 해줍니다. 이러한 접근 방법은 프로토타입이나 작은 어플리케이션에는 효과가 있지만 우리의 데이터 모델이 강한 결합을 야기하고 우리의 데이터 모델의 변화에 대한 논리적인 어려움을 야기합니다.

다음 과정에서 우리는 “packaging”을 통해 우리의 코드를 좀 더 정리하고 프레젠테이션 로직을 격리하고, 재사용 가능한 형태의 컴포넌트로 만드는 방법을 배워볼 것입니다.

테스트

“Angular 방식”으로 뷰로부터 컨트롤러를 분리하는것은 개발과정에서도 테스트 코드를 만드는것을 쉽게 해줍니다. 우리의 컨트롤러가 글로벌 네임스페이스 위에서 동작하고 있다면 우리는 다음과 같이 간단하게 목업 스코프 오브젝으를 만들어서 테스트 해볼 수 있습니다.

describe('PhoneListController', function() {

  it('should create a `phones` model with 3 phones', function() {
    var scope = {};
    var ctrl = new PhoneListController(scope);

    expect(scope.phones.length).toBe(3);
  });

});

이 테스트는 PhoneListController를 인스턴스화 하고 스코프에 휴대폰 배열이 3개의 레코드를 갖는지 검증합니다. 이 예제는 Angular에서 유닛 테스트 코드를 만드는것이 얼마나 쉬운지를 보여줍니다. 소프트웨어 개발에서 테스팅은 정말 중요한 부분이므로 이러한 장점으로 인해 개발자들에게 테스트를 만드는것을 권장할 수 있습니다.

글로벌 컨트롤러가 아닌경우의 테스트

실전에서는 컨트롤러 함수들을 글로벌 네임스페이스에 선언하고 싶지 않을 수 있습니다. 대신에 phonecatApp 모듈에 생성자 함수를 등록하는것을 볼 수 있습니다.

이경우 Angular에서는 컨트롤러를 이름으로 받아올 수 있는 $controller 서비스를 제공합니다. 다음은 $contoller를 사용하는 같은 테스트입니다.

describe('PhoneListController', function() {

  beforeEach(module('phonecatApp'));

  it('should create a `phones` model with 3 phones', inject(function($controller) {
    var scope = {};
    var ctrl = $controller('PhoneListController', {$scope: scope});

    expect(scope.phones.length).toBe(3);
  }));

});

위의 코드는 다음과 같은 의미를 가지고 있습니다.

  • 각각의 테스트를 수행하기전에 Angular에게 phonecatApp 모듈을 로드할 것을 알립니다.
  • Angular에게 $controller 서비스를 우리의 테스트 함수에 주입하도록 요청합니다.
  • $controller 를 사용하여 PhoneListController의 인스턴스를 생성합니다.
  • 이 인스턴스를 통해 우리는 3개의 레코드를 갖는 스코프가 정상적으로 만들어지는지 검증합니다.

이전에 이미 언급했듯이 유닛 테스트 파일 (specs)들은 어플리케이션 코드들과 함께 보관되어야 합니다. 우리는 어플리케이션 코드의 파일 이름과 명확히 구별이 가능하도록 추가적인 확장자를 추가하여 이 유닛 테스트 파일을 보관하는것이 좋습니다. 테스트 파일은 여전히 일반적인 자바스크립트 파일이므로 .js 확장자를 유지하는것은 좋겠다고 생각됩니다.

이 튜토리얼에서는 우리는 .spec 접미사를 파일명에 추가하였습니다. 그 결과 something.js 파일에 대응되는 테스트 파일은 something.spec.js가 됩니다. (다른 일반적인 컨벤션은 _spec 또는 _test 접미사를 붙이는 방법입니다)

쓰기 및 실행 테스트

수많은 Angular 개발자들은 쓰기 테스트를 할 때 Jasmin의 Behavior-Driven Development (BDD) 프레임워크를 선호합니다. 비록 Angular는 Jasmin 사용을 필요로하지 않지만 우리는 이 튜토리얼의 테스트에서 Jasmin v2.4를 사용하여 모든 테스트를 작성하였습니다. Jasmin에 대해서 더 많은 것을 알아보고 싶다면 Jasmin 홈페이지를 방문하시기 바랍니다.

angular-seed 프로젝트에서는 Karma를 이용하여 유닛테스트를 진행하도록 설정되어있습니다. 만약 Karma를 실행하는데에 필요한 플러그인이 설치되어있다고 확인하지 못할 경우 npm install 명령을 실행하여 설치할 수 있습니다.

테스트를 실행하고, 그리고 파일들의 변화에 따라 테스트를 재시도 하도록 npm test 를 실행합니다.

  • Karma는 크롬이나 파이어폭스 브라우저의 인스턴스를 자동으로 시작합니다. 그들이 백그라운드에서 실행되도록 그냥 무시하면 됩니다. Karma는 이 브라우저들을 이용하여 테스트를 실행합니다.
  • 만약 당신의 머신에 하나의 브라우저만 설치되어있을 경우 테스트를 수행하기전에 Karma 설정파일인 karma.conf.js 파일을 수정해 주십시오. 이 설정파일은 프로젝트의 루트 디렉토리에 위치하고 있으며 browser 항목을 수정하면 됩니다.
  • 당신은 터미널을 통해 다음과 같은 결과를 확인할 수 있습니다. 테스트 수행을 성공하였네요!

INFO [karma]: Karma server started at http://localhost:9876/
INFO [launcher]: Starting browser Chrome
INFO [Chrome 49.0]: Connected on socket … with id …
Chrome 49.0: Executed 1 of 1 SUCCESS (0.05 secs / 0.04 secs)

  • 테스트를 재실행하기 위해서는 단지 소스 또는 테스트 .js 파일에 어떤 변화가 발생하면 됩니다. Karma는 변화가 발생했음을 알리고 당신을 위해서 테스트를 재실행할것입니다. 정말 멋지죠?

Karma가 브라우저를 열었을 때 이를 최소화 하지 마십시오. 특정 OS에서는 최소화된 브라우저에 최소한의 메모리를 할당하여 Karma 테스트가 엄청나게 느려지게 만드는 문제가 있습니다.

실험

index.html 파일에 다른 바인딩을 추가해 봅시다.

<p>Total number of phones: {{phones.length}}</p>

컨트롤러에 새로운 모델 프로퍼티를 만들고 그것이 템플릿에 바인딩 되도록 해봅시다.

// In controller
$scope.name = 'world';

// In template
<p>Hello, {{name}}!</p>

app/app.spec.js 안의 컨트롤러 유닛 테스트를 위의 변화를 반영하도록 수정해봅시다.

expect(scope.name).toBe('world');

index.html 안에 간단한 테이블을 출력하는 반복 지시자를 사용해 봅시다.

<table>
  <tr><th>Row number</th></tr>
  <tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i}}</td></tr>
</table>

expect(scope.phones.length).toBe(3) 대신에 toBe(4)를 사용하여 유닛 테스트가 실패하도록 해봅시다.

참고 :
https://docs.angularjs.org/tutorial/step_01
https://docs.angularjs.org/tutorial/step_02