Tag Archives: CDO

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