Tag Archives: Angular

AngularJS PhoneCat 튜토리얼 앱 – 컴포넌트

angularjs-large

이전 단계에서 우리는 컨트롤러와 템플릿이 어떻게 정적인 HTML 페이지를 동적인 뷰로 변화시키는지를 보았습니다. 이것은 단일 페이지 어플리케이션에서 (특히 Angular 어플리케이션들에서) 매우 일반적으로 사용되는 패턴입니다.

이 템플릿(바인딩과 프레젠테이션 로직을 포함하고 있는 뷰의 일부)은 우리의 데이터가 어떻게 정리되고 유저에게 보여지는지를 결정하는 설계도와 같은 역할을 합니다. 그리고 컨트롤러는 바인딩에 의해서 해석되거나 행동이나 로직을 우리의 템플릿에 적용되는 컨텍스트를 제공합니다.

우리는 아직 몇군데를 좀 더 개선할 수 있습니다 :

  1. 우리의 어플리케이션의 다른 부분에서 같은 기능을 하는 부분을 재사용 하고 싶다면 어떻게 해야 할까요? 컨트롤러를 포함한 전체 템플릿을 복제할 수 있습니다. 하지만 이것은 에러를 야기하고 코드의 보수를 어렵게 만들것입니다.
  2. 우리의 컨트롤러와 템플릿을 동적 뷰로 변환시키는데 본드 역할을 하는 스코프는 페이지의 다른 파트들간에 격리되지 않습니다. 이것이 의미하는 것은 페이지의 다른 부분에 있는 예상하지 못한 변경이 (프로퍼티 이름 충돌등) 우리의 뷰에 예상치 못한 디버깅이 어려운 문제를 가져올 수 있습니다. (물론 현재 진행하고 우리의 튜토리얼은 작은 프로젝트이기에 문제가 되지 않겠지만 실 서비스의 큰 규모의 어플리케이션에서는 문제가 될 수 있습니다)

이번에 진행할 튜토리얼을 위해 프로젝트를 단계 3으로 초기화 하겠습니다.

$ git checkout -f step-3

컴포넌트로 구조하자!

템플릿과 컨트롤러 콤비네이션은 일반적이고 순환적인 패턴을 가지기 때문에 Angular는 간단한 방법으로 이것들을 재사용가능하고 격리된 엔티티로 통합해줍니다. 이것을 컴포넌트라고 합니다. 추가로 Angular는 각각의 컴포넌트 인스턴스마다 격리된 스코프를 생성해주며 이는 스코프간의 상속이 발생하지 않고 어플리케이션의 다른 부분의 같은 컴포넌트를 사용하고 있는 부분들끼리 발생할 수 있는 리스크가 없음을 의미합니다.

이 문서는 소개 수준의 튜토리얼이기 때문에 컴포넌트의 모든 기능에 대해서 심도 있는 설명을 하지 못합니다. 만약 당신이 컴포넌트에 대해 더 알아보고 사용 패턴에 대해 공부해보고 싶다면 개발자 가이드의 Components 섹션을 읽어보시기 바랍니다.

사실, 사람들은 컴포넌트를 그들의 더 복잡하고 장확한 (하지만 강력한) 지시자의 자기 중심적이고 경량화된 버전으로 생각할 수 있습니다. 개발자 가이드의 Directives 섹션에서 이부분에 대한 모든것을 확인할 수 있습니다. 지시자는 고급 주제입니다. 이 튜토리얼을 이용해서 공부를 끝낸 뒤에 기초를 마스터하기 위해 한번 읽어보시길 권장합니다.

컴포넌트를 생성하기 위해서 우리는 Angular 모듈의 .component() 를 사용해야 합니다. 우리는 여기서 컴포넌트의 이름과 컴포넌트 정의 오브젝트(CDO – Component Definition Object)를 제공해야 합니다.

컴포넌트 역시 지시자이므로 컴포넌트의 이름은 카멜 케이스(camelCase) 로 표기하도록 합며 HTML에서는 케밥 케이스(kebab-case)로 사용할 것입니다. 우선 간단한 형태로 우리의 CDO는 하나의 템플릿과 하나의 컨트롤러를 포함할 것입니다. (우리는 사실 컨트롤러를 생략할수도 있으며 이경우 Angular는 더미 컨트롤러를 생성하여 제공할 것입니다) 이것은 템플릿에 어떠한 동작도 추가하지 않은 간단하지만 유용한 형태의 프레젠테이션 컴포넌트입니다.

다음의 예제를 봅시다.

angular.
  module('myApp').
  component('greetUser', {
    template: 'Hello, {{$ctrl.user}}!',
    controller: function GreetUserController() {
      this.user = 'world';
    }
  });

이제 우리의 뷰에 <greet-user></greet-user> 를 추가할때마다 Angular는 템플릿을 제공하고 특정 컨트롤러의 인스턴스를 관리함으로써 DOM 하위 트리를 확장합니다.

하지만 여기서 $ctrl 은 어디서 왔고 무엇을 하는것일까요? 이미 이전에 설명 했듯이 이것은 스코프를 직접 접근하는것을 피하는 좋은 예시입니다. 여기서 우리는 우리의 컨트롤러 인스턴스를 사용할 수 있고 사용해야만 합니다. 예로 우리의 컨트롤러에 프로퍼티로 할당할 데이터나 메소드는 컨트롤러 생성자 안에서 스코프에 직접 접근하는것이 아닌 “this”로 접근하여 할당합니다.

템플릿에서 컨트롤러 인스턴스를 별칭을 사용하여 접근할 수 있습니다. 이 방법은 우리의 표현식을 해석할때의 컨텍스트가 훨씬 더 명확해 집니다. 기본적으로 컴포넌트는 $ctrl 을 컨트롤러의 별칭으로 사용합니다. 하지만 필요할 경우 이것을 덮어쓰기 할 수 있습니다.

여기에 사용할 수 있는 옵션은 더 많은것이 있습니다. 우리의 어플리케이션에 .component() 를 사용하기 전에 API 문서를 확인해보시기를 권장합니다.

컴포넌트 사용하기

이제 우리는 컴포넌트를 어떻게 만드는지 알았습니다. 그럼 HTML 페이지를 우리가 새로 배운 스킬을 사용하여 리팩토링 해보겠습니다.

<html ng-app="phonecatApp">
<head>
  ...
  <script src="bower_components/angular/angular.js"></script>
  <script src="app.js"></script>
  <script src="phone-list.component.js"></script>
</head>
<body>

  <!-- Use a custom component to render a list of phones -->
  <phone-list></phone-list>

</body>
</html>
// Define the `phonecatApp` module
angular.module('phonecatApp', []);
// Register `phoneList` component, along with its associated controller and template
angular.
  module('phonecatApp').
  component('phoneList', {
    template:
        '<ul>' +
          '<li ng-repeat="phone in $ctrl.phones">' +
            '<span>{{phone.name}}</span>' +
            '<p>{{phone.snippet}}</p>' +
          '</li>' +
        '</ul>',
    controller: function PhoneListController() {
      this.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.'
        }
      ];
    }
  });

이제 결과물을 실행해보면 결과물은 보기에는 같아보입니다. 하지만 우리가 무엇을 추가했는지 봐보겠습니다.

  • 우리의 휴대폰 리스트는 재사용 가능해졌습니다. 단지 <phone-list></phone-list> 를 페이지 어디든지 넣어주기하면 하면 휴대폰 리스트를 출력할 수 있습니다.
  • 우리의 메인 뷰 (index.html) 가 깔끔해지고 더 명확해졌습니다. 그냥 훑어보기만 해도 어디에 휴대폰 리스트가 출력될지 알 수 있습니다. 더이상 세부적인 구현에 대해서 신경쓰지 않아도 됩니다.
  • 우리의 컴포넌트는 격리되었고 외부의 영향으로부터 안전합니다. 마찬가지로, 어플리케이션의 다른 부분에 의해서 무언가 사고가 발생할 수 있다는 걱정을 할 필요가 없습니다. 컴포넌트 내부에서 무엇이 발생하면 그것은 그 컴포넌트 내부에서 유지됩니다.
  • 격리된 환경에서 우리의 컴포넌트를 테스트하는것이 더 쉽습니다.

angularjs-phonecat-tutorial-03

각기 다른 타입의 엔티티들은 다른 접미사를 사용함으로써 명확하게 구별되도록 하는것이 좋습니다. 이 튜토리얼에서는 .component 접미사를 컴포넌트에 사용하였습니다. someComponent 컴포넌트의 파일명은 some-component.component.js가 될 것입니다.

테스트

우리는 우리의 컨트롤러와 템플릿을 컴포넌트로 통합하였지만 우리는 여전히 어플리케이션 로직과 데이터가 존재하는 컨트롤러를 개별적으로 테스트 할 수 있고 그래야 합니다. 컴포넌트의 컨트롤러를 인스턴스화 하고 가져오기 위해서는 Angular는 $componentController 서비스를 제공합니다.

우리는 기존에 .controller() 메소드를 통해 등록된 컨트롤러의 이름으로 컨트롤러를 인스턴스화 하는 메소드로 $controller 서비스를 사용했었습니다. 만약 원한다면 우리는 또한 이러한 방식으로 우리의 컴포넌트 컨트롤러를 등록할 수 있습니다. 하지만 대신에 우리는 이것을 CDO 내부에 인라인으로 선언하여 지역화를 유지할 수 있도록 하였습니다. 어느쪽이던지 잘 동작합니다.

describe('phoneList', function() {

  // Load the module that contains the `phoneList` component before each test
  beforeEach(module('phonecatApp'));

  // Test the controller
  describe('PhoneListController', function() {

    it('should create a `phones` model with 3 phones', inject(function($componentController) {
      var ctrl = $componentController('phoneList');

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

  });

});

이 테스트는 컨트롤러와 연관된 phoneList 컴포넌트를 가져오고 인스턴스화 한 뒤 휴대폰 배열 프로퍼티가 3개의 레코드를 가지고 있는지 검증합니다. 여기서는 scope가 아닌 컨트롤러 인스턴스 자신이 데이터를 가지고 있는것을 확인할 수 있습니다.

실행 테스트

이전과 동일하게 npm test 를 통해 테스트를 진행하고 파일의 변화를 즉시 추적하도록 할 수 있습니다.

실험

  • 이전 단계로부터 이번에는 phoneList 컴포넌트에 대한 실험을 도전해 봅시다.
  • index.html 에 단지 <phone-list></phone-list> 를 더 추가함으로써 한 페이지 안에 두개 이상의 휴대폰 리스트를 출력하도록 해봅시다. 이제 새로운 바인딩을 phoneList 컴포넌트의 템플릿에 추가해 봅시다.
template:
    '<p>Total number of phones: {{$ctrl.phones.length}}</p>' +
    '<ul>' +
    ...

페이지를 새로고침하고 새로운 기능이 모든 휴대폰 리스트에 퍼져나가는것을 확인해 봅시다. 실제 어플리케이션에서는 이러한 휴대폰 리스트가 다른 여러 페이지에서 보여질 수 있습니다. 또한 어떤 한 장소에서 값이 변경되거나 추가될 수 있으며 이 변화가 어플리케이션 전체에 퍼쳐나가게 됩니다.

참고 : https://docs.angularjs.org/tutorial/step_03

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