Angular 1을 배워야 하나요 2를 배워야 하나요?

😢 이 페이지는 다음 주소로 변경될 예정입니다.

Todd Motto의 글 Should you learn Angular 1.x or 2?를 번역했다.


Angular 1을 배워야 하나요 2를 배워야 하나요?

“Angular 1을 배워야 하나요 2를 배워야 하나요?”라는 질문은 정말 자주 받는다. 그 질문에 답하는 성격의 글로 도움과 안내가 될 수 있는 통찰을 줄 수 있었으면 한다. 이 질문은 누구도 쉽게 답할 수 없는데 바로 질문자에 따라 답이 다르기 때문이다. 내 생각을 정리해봤다.

흔한 질문

다음 같은 질문을 정말 자주 받는다.

  • “Angular를 새로 시작하는데 버전 1을 할까요 2를 할까요?”
  • “아직 Angular 2를 배우면 안되나요?”
  • “Angular 1과 2를 모두 배워야 하나요?”

먼저 기억해둬야 할 점은 이 질문에 “공식” 답변은 없다는 점이다. (물론 짧고 간결하게 답하면 어떤 도구든 가장 최신의 안정적인 버전을 선택하는 것이 바람직하다.) 하지만 어느 프레임워크를 배워야 할지, 어떤 프레임워크가 더 나은지 생각해본다면 몇 가지 큰 요인을 고려해야 할 것이다. 이 글에서는 그 질문에 대해 어떻게 스스로 답을 내릴지 고민할 수 있도록 몇 조언을 제공하려고 한다.

코드 기반과 팀

혼자서 일을 하든 팀으로 일을 하든 현재 일을 하고 있다면 질문에 답하기 위해 이런 생각을 해볼 수 있다.

먼저 AngularJS (1.x)는 프레임워크 세계에서 지배적인 포식자라고 할 수 있다. 그만큼 실무에서 가장 규모가 크고 현재 가장 많이 사용하는 프레임워크에 해당한다. 만약 AngularJS를 일에서 사용하고 있다면 전혀 문제가 아니다. 회사에서 고객을 위해 단일 프로덕트/어플리케이션을 개발한다면 고개를 숙여 프로젝트를 완성해 전달하는데 집중하고 프로젝트를 꾸준히 진행하길 바랄 것이다.

둘째로 코드 기반을 업그레이드하기 위해서 잠재적으로 여러 해의 수고가 들어간 코드를 다시 작성하고 싶은지 생각해봐야 한다. 이 질문에 답하기 위해서는 따져봐야 할 여러 요인이 있을 것이다. 2009년에 나온 AngularJS는 생산성을 극대화한 빠른 프레임워크긴 하지만 몇 가지 한계점이 존재하고 프로젝트의 생애를 잠재적으로 방해하는 요소가 될 수 있다. 말은 그렇지만 탈출 버튼을 누르고 당장에 Angular (v2+)로 넘어갈 만큼의 요소로 보기엔 어렵다.

어떻게 결정하든 상관 없이 이 고민은 “업그레이드”나 “마이그레이션”이 아니라 근본적으로 프레임워크를 교체하는 작업이다. 즉 완전히 다른 코드 기반으로 옮겨가게 된다는 것이다. (한번에 전부 옮기든, 점진적으로 옮기든 말이다.) CEO/CTO라면 견고하고 명확한 이유 없이는 사업에 영향을 주는 이런 결정을 쉽게 내리지 않을 것이다. 의사 결정권자는 고객에게 전달할 중점적 사안이 중요하지 버전 번호가 중요한 것이 아니다.

시나리오: 단일 상품 회사

Github에서 AngularJS를 사용한다고 가정해보자. 코드 기반은 아마 몇 년 정도 오래 되었을 것이다. 이제 코드 기반을 모바일에, 또는 데스크탑 클라이언트에도 배포하려고 한다. 이런 상황에서는 몇 가지 선택할 만한 경우가 있다. 단일 모바일 어플리케이션 (안드로이드, iOS)를 만드는 방법, 또한 네이티브 데스크탑 클라이언트를 만드는 방법이 있겠다. 이런 기술 차이는 더 큰 금전적 투자를 필요로 한다.

내 의견으로는 비지니스 목표를 달성하기 위해서 Angular로 옮겨 위에서 필요로 했던 모든 작업을 단일 프레임워크 내에서 수행할 수 있도록 권할 것이다. Angular를 사용하면 NativeScript를 사용해 네이티브 모바일 코드로 컴파일이 가능하며 모바일에 배포하기 위해 Ionic을 사용하거나 데스크탑 환경을 위해 Electron을 사용할 수 있을 것이다. 단일한 코드 기반에서 말이다.

하지만 한 걸음 물러나 다시 생각해봐야 한다. 무엇이 가장 중요한 웹 어플리케이션인가? 단일 페이지 앱(SPA)는 코드를 잘 나누고 작게 만들었다면, 제대로 된 성능 전략을 선택하고 사용자에게 컨텐츠를 전달하기 위한 가능한 가장 빠른 방법을 사용했다면 빠를 수도 있다. 하지만 더 빠르게도 가능하다. Angular는 Angular Universal을 사용해서 서버측 렌더링(SSR)이 가능하다. 이런 전략은 Angular 1.x에서 사용할 수 없다. 이 특징도 Angular를 배워야 할지 말아야 할지 결정할 때 참고할 중요한 부분이다.

시나리오: 다양한 프로덕트와 푸른 초원

내 경우에는 AngularJS로 작성된 단일 프로덕트 어플리케이션도 작업해봤고 그만큼 1.x를 사용하는 회사를 위해서 여러 프로젝트로 개발했었다. 그래서 두 경우 모두 경험해본 경력이 있다. 만약 대단한 클라이언트 10 곳과 10개의 Angular 1.x 앱이 있고 11번째 클라이언트가 푸른 초원에서 새로 시작하는 프로덕트를 제안했다고 하자. 무엇을 할 것인가?

이런 상황이라면 위에서 살펴봤던 이유들 때문에라도 미래를 보장받는 선택인 Angular를 고려할 것이다. Angular 2는 바닥부터 다시 작성되어 단일 방향 데이터 흐름과 컴포넌트 아키텍처와 같이 모범 사례를 적용하는데 집중했다. 이런 기능은 AngularJS에서도 사용할 수 있지만 가장 최신 버전에만 적용되어 있다. 즉, 기존에 존재하는 코드를 1.6+에서 사용하려면 코드 기반을 리팩토링하고 .component API를 사용해야 한다.

AngularJS가 언젠가 “중단(discontinued)”될 운명인건 알지만 그건 AngularJS 뿐만 아니라 모든 앱과 버전이 그러한 것 아닐까? 꼭 가장 최신에 가장 좋은 도구를 쓸 필요는 없지만 앞으로 3년 정도 어려운 시기 후에 Angular가 최종적으로 출시되면 투자할 가치가 있을 만큼 엄청난 힘이 있을 것이다.

만약 11번째 클라이언트가 당신에게 새로운 어플리케이션을 원한다면 시도해라. 하지만 그 전에 생명주기 훅, 상태 저장과 비저장(stateful and stateless) 컴포넌트와 단방향 데이터 흐름과 이벤트를 이해할 필요가 있다.

당신, 개인적으로

여기도 몇 가지 시나리오가 있으며 당신이 무엇을 하는지에 따라 다를 것이다. 하나의 답변으로 모든 상황에 딱 맞을 수는 없을 것이다.

시나리오: AngularJS를 사용해서 취업함

만약 AngularJS를 일에서 사용하고 있다면 아마도 이미 Angular를 둘러보고 문서를 살펴봤을 것이다. 그런 중에 이 괴물은 AngularJS나 기존에 알고 있는 MVC 패턴과는 사뭇 거리감이 있다는 것을 알게 되었을 것이다. 이 상황에서는 전적으로 본인에게 달렸다. Angular에 더 깊게 빠져들고 싶다면 도전해라. 그렇지 않아도 물론 괜찮다. 누군가에게 충고할 때 꼭 해라 하지 마라 하는건 별로 의미가 없다. 질문에 특별한 이유가 있는 경우가 아니고서는 말이다. (예를 들어, 서버에서 렌더링이 가능한가요? 아니면 이러이런 일을 할 수 있나요?) 이런 질문은 마치 “포르쉐를 사야 할까요 페라리를 사야 할까요?” 같고 답은 질문한 사람 머릿속에만 존재한다.

그렇다고 질문이 일을 벗어난 것은 아니다. Angular를 배우지 않고서는 사장에게 가서 Angular 사용하자고 할 수 없을 것이다. 자기 시간에 배워서 마음에 드는지 살펴보자. 그렇게 간단한 일이다.

시나리오: Angular를 처음 한다면

Angular를 전혀 본 적이 없다면 조금 어려운 질문이다. AngularJS가 갖고 있는 단일 시장 지배력과 Angular로 넘어가는 회사의 비율을 고려해보면 결국 둘 다 배워야 할 것이다. 만약 AngularJS를 .component() API로 배우고, 컴포넌트 기반 구조에서 “MVC 접근 방식으로” 어떻게 돌아가는지만 이해할 수 있다면 내일 당장이라도 AngularJS를 사용하는 회사에 취업할 수 있을 것이다.

“Angular만 하는” 직업을 찾고 있다면 위에서 언급했던 이유로 지금 당장은 조금 어려울 수 있다. 만약 Angular를 막 시작했다면 둘 다 배워야 할 것 같다. 하지만 앞서와 같이 이 결정은 자기 자신이 어떤 삶, 어떤 커리어를 선택하느냐에 기반하게 된다.

Angular가 급격하게 성장하고 있고 경이로운 성장 추이를 보여주고 있지만 새 이력서에 “Angular 2+”만 적어 놓고는 회사 문을 두드리기는 쉽지 않을 것이다. 대다수의 회사는 여전히, 앞으로 다년 간 AngularJS를 사용할 것이기 때문이다. 이런 경우에는 어떤 직업과 어떤 스킬을 원하는지, 어디에 취업하고 싶은지에 따라 결정할 필요가 있다. 이 “취업” 란에서는 최대한 일반적인 상황을 이야기하고 있다. 하지만 나처럼 자영업자를 하는 사람도 많을 것이다. 물론 그렇다고 이런 질문을 피할 수는 없다.

만약 자영업 엔지니어라면 더 땅을 파서 생계 유지에 집중하는 것이 당연하다. AngularJS에 대한 요청이 50회고 Angular 앱에 대한 요청은 한 번만 들어왔다면 어디에 더 시간을 집중해야 할까? AngularJS에 집중해야 할 것이다.

뒤집어서 새로운 일이든, 컨설팅이든, 무슨 일을 하든 50/50 비율로 요청을 받는 위치라면 둘 다 배워야 할 것이다. 개인적으로 아는 Angular 개발자는 대부분 AngularJS도 잘 알고 있었고 다 년 간 사용한 경험이 있었다.

시나리오: 다른 일로 취업함

아마도 React, Ember, Backbone, nockout 같은 프레임워크로 취업했지만 Angular를 고려하고 있는 경우일 것이다. 먼저 Angular 2가 무슨 이득이 있을 지 먼저 조사해볼 필요가 있다. Ahead-of-Time 컴파일은 브라우저에 배포하기 전 코드 크기를 극적으로 줄여서 앱을 전달 할 수 있는데 Angular를 살펴볼 때 주요하게 고려할 만한 부분이다.

마무리하며

빠르게 정리하자면 자신과 자신의 직업에 따라 답이 달라진다. Angular 직업은 많아질 것이고 AngularJS는 여전히 주변에 있을 거란 점에 의심하지 않는다. 사실 기업을 대상으로 한 AngularJS 지원은 더욱 높아질 것이다. (새 버전으로 마이그레이션 하기로 결정하기 전까지 말이다.)

요약하면 AngularJS를 사용하고 있다면 프로젝트 또는 회사의 미래 목표로 고려해보자. 만약 Angular를 처음 배우고 직업으로 삼고 싶다면 시장과 다니고 싶은 회사를 조사해보고 어떤 기술 스택을 요구하는지 살펴보자.

옳은 답은 없지만 이 글을 통해 조금이나마 고려에 도움이 되는 통찰이 생겼으면 좋겠다. 모두 잘 되길 바란다!

Angular에서 디렉티브 간 `require`를 사용해 소통하기

😢 이 페이지는 다음 주소로 변경될 예정입니다.

Todd Motto의 글 Directive to Directive communication with “require”를 번역한 글이다. 짧은 글이지만 디렉티브의 계층 관계에서 require를 활용해 값을 주고 받는 방법을 살펴볼 수 있다. 다른 디렉티브의 컨트롤러에 정의된 메소드를 어떻게 접근해서 사용할 수 있는지 탭 디렉티브를 작성하는 예제를 통해 설명한다.


디렉티브 간 소통은 여러 방법이 있지만, 계층 관계를 갖고 있는 디렉티브를 다룰 때는 디렉티브 컨트롤러를 활용해 서로 소통할 수 있다.

이 글에서는 탭 디랙티브를 작성한다. 탭을 추가하기 위한 다른 디랙티브의 함수를 활용하며, 디렉티브 정의 개체의 require 프로퍼티를 사용해 만들려고 한다.

HTML을 먼저 정의한다:

<tabs>
  <tab label="Tab 1">
    Tab 1 contents!
   </tab>
   <tab label="Tab 2">
    Tab 2 contents!
   </tab>
   <tab label="Tab 3">
    Tab 3 contents!
   </tab>
</tabs>

이 시점에서 tabstab 두 디렉티브를 만들 것을 예상할 수 있다. tabs를 먼저 만들면:

function tabs() {
  return {
    restrict: 'E',
    scope: {},
    transclude: true,
    controller: function () {
      this.tabs = [];
    },
    controllerAs: 'tabs',
    template: `
      <div class="tabs">
        <ul class="tabs__list"></ul>
        <div class="tabs__content" ng-transclude></div>
      </div>
    `
  };
}

angular
  .module('app', [])
  .directive('tabs', tabs);

tabs 디렉티브에서는 transclude를 사용해 각각의 tab을 전달하고 개별적으로 관리하도록 구성하고 있다.

tabs 컨트롤러 내에서 새로운 탭을 추가할 때 사용할 함수가 필요하다. 이 함수를 사용해 부모/호스트 디렉티브에 동적으로 탭을 추가할 수 있게 된다:

function tabs() {
  return {
    ...
    controller: function () {
      this.tabs = [];
      this.addTab = function addTab(tab) {
        this.tabs.push(tab);
      };
    },
    ...
  };
}

angular
  .module('app', [])
  .directive('tabs', tabs);

이제 컨트롤러에 addTab 메소드가 연결되었다. 하지만 탭을 어떻게 추가할 것인가? 자식 tab 디렉티브를 추가하고, 이 디렉티브가 컨트롤러의 기능으로서 필요로 한다:

function tab() {
  return {
    restrict: 'E',
    scope: {
      label: '@'
    },
    require: '^tabs',
    transclude: true,
    template: `
      <div class="tabs__content" ng-if="tab.selected">
        <div ng-transclude></div>
      </div>
    `,
    link: function ($scope, $element, $attrs) {

    }
  };
}

angular
  .module('app', [])
  .directive('tab', tab)
  .directive('tabs', tabs);

require: '^tabs'를 추가하는 것으로 부모로 tabs 디렉티브의 컨트롤러에 포함했으며 이제 link 함수를 통해 접근할 수 있게 되었다. link 함수의 4번째 인자인 $ctrl을 주입해서 작성한 컨트롤러의 참조를 받아오자:

function tab() {
  return {
    ...
    link: function ($scope, $element, $attrs, $ctrl) {

    }
  };
}

여기서 console.log($ctrl);을 넣어보면 다음과 비슷한 객체를 볼 수 있다:

{
  tabs: Array,
  addTab: function addTab(tab)
}

addTab 함수를 활용해서 새로운 탭을 생성할 때, 부모 디렉티브의 컨트롤러로 정보를 보낼 수 있게 되었다:

function tab() {
  return {
    ...
    link: function ($scope, $element, $attrs, $ctrl) {
      $scope.tab = {
        label: $scope.label,
        selected: false
      };
      $ctrl.addTab($scope.tab);
    }
  };
}

이제 새로운 tab 디렉티브를 사용할 때마다 이 $ctrl.addTab 함수를 호출하고 tabs 컨트롤러 내에 있는 this.tabs 배열에 디렉티브 정보를 전달한다.

3개의 탭이 존재한다면 $ctrl.addTab 함수가 3번 호출 될 것이고 배열은 3개의 값을 갖고 있게 된다. 그 후 배열을 반복해서 살펴보고 제목과 선택되어 있는 탭이 있는지 확인한다:

function tabs() {
  return {
    restrict: 'E',
    scope: {},
    transclude: true,
    controller: function () {
      this.tabs = [];
      this.addTab = function addTab(tab) {
        this.tabs.push(tab);
      };
      this.selectTab = function selectTab(index) {
        for (var i = 0; i < this.tabs.length; i++) {
          this.tabs[i].selected = false;
        }
        this.tabs[index].selected = true;
      };
    },
    controllerAs: 'tabs',
    template: `
      <div class="tabs">
        <ul class="tabs__list">
          <li ng-repeat="tab in tabs.tabs">
            <a href="" ng-bind="tab.label" ng-click="tabs.selectTab($index);"></a>
          </li>
        </ul>
        <div class="tabs__content" ng-transclude></div>
      </div>
    `
  };
}

selectTabtabs 컨트롤러에 추가된 것을 확인할 수 있을 것이다. 이 함수는 특정 탭의 컨텐츠를 보여주기 위해 초기 색인을 지정할 수 있게 한다. this.selectTab(0);를 호출하면 작성한 코드에 따라 배열의 인덱스를 확인해 첫번째 탭의 컨텐츠를 표시하게 된다.

Angular의 컴파일링 과정에 따라 controller는 가장 먼저 인스턴스가 생성되고, link 함수는 디렉티브가 컴파일되고 엘리먼트에 연결될 때 한 번 호출된다. 즉, 초기화 된 탭을 볼 수 있을 때, 디렉티브 컨트롤러를 $ctrl와 그 메소드를 사용하기 위해 주입되어야 한다:

function tabs() {
  return {
    ...
    link: function ($scope, $element, $attrs, $ctrl) {
      // 첫번째 탭을 가장 먼저 보여줌
      $ctrl.selectTab(0);
    },
  };
}

하지만 다음처럼 어트리뷰트로 초기 탭을 지정할 수 있다면, 개발자에게 더 많은 선택권을 제공할 수 있다:

<tabs active="2">
  <tab>...</tab>
  <tab>...</tab>
  <tab>...</tab>
</tabs>

이 코드는 배열 인덱스를 동적으로 2로 지정하며 배열에서 3번째 엘리먼트를 보여주게 된다. link 함수에서는 어트리뷰트의 존재를 $attrs가 포함하고 있는데 이 인덱스를 바로 지정하거나 $attrs.active가 존재하지 않거나 잘못된 값일 경우 (false0으로 평가되니 어쨌든 0이므로 안전하겠지만) 초기 인덱스를 다음처럼 폴백(fallback)으로 지정할 수 있다.

function tabs() {
  return {
    ...
    link: function ($scope, $element, $attrs, $ctrl) {
      // `active` 탭 또는 첫번째 탭을 지정
      $ctrl.selectTab($attrs.active || 0);
    },
  };
}

그리고 require를 이용해 새로운 tab 정보를 부모 디렉티브에 전달하는 라이브 데모는 아래에서 확인할 수 있다:

AngularJS의 서비스와 팩토리

😢 이 페이지는 다음 주소로 변경될 예정입니다.

AngularJS의 서비스 Services는 여러 코드에서 반복적으로 사용되는 코드를 분리할 때 사용하는 기능으로, 해당 서비스가 필요한 곳에 의존성을 주입해 활용할 수 있다. 서비스는 다음과 같은 특성이 있다.

  • 지연 초기화(Lazily instantiated): 의존성으로 주입하기 전까지는 초기화가 되지 않음.
  • 싱글턴(Singletons): 각각의 컴포넌트에서 하나의 인스턴스를 싱글턴으로 참조함.

AngularJS에서 서비스(Service)와 팩토리(factory)는 서로 상당한 유사성을 갖고 있기 때문에 쉽게 혼동할 수 있다. 특히 JavaScript의 유연한 타입으로 인해 라이브러리의 의도와는 다르게 그냥 동작하는 경우가 많다. 이 두 가지의 차이는 코드에서 확인할 수 있다. Angular의 코드를 보면 service는 factory를 사용해서 구현하고 있다.

  function service(name, constructor) {
    return factory(name, ['$injector', function($injector) {
      return $injector.instantiate(constructor);
    }]);
  }

위 코드를 보면 $injector.instaniate()에 생성자를 넣어 반환하는데 이 함수에서 Object.create()로 해당 생성자를 인스턴스화 한다. 이렇게 얻은 인스턴스를 factory에 넣어 나머지는 factory와 동일하게 처리하는 것을 확인할 수 있다. 그래서 라이브러리의 실제 의도와는 다른 구현도 문제 없이 구동될 수 있는 것이다.

Todd Motto의 AngularJS 스타일 가이드 중 Service and Factory을 살펴보면 이 구현의 차이를 다음과 같이 정리한다.

서비스와 팩토리

Angular의 모든 서비스는 싱글턴 패턴이다. .service()메소드와 .factory() 메소드의 차이는 객체를 생성하는 방법에서 차이가 있다.

서비스: 생성자 함수와 같이 동작하고 new 키워드를 사용해 인스턴스를 초기화 한다. 서비스는 퍼블릭 메소드와 변수를 위해 사용한다.

function SomeService () {
  this.someMethod = function () {
    // ...
  };
}
angular
  .module('app')
  .service('SomeService', SomeService);

팩토리: 비지니스 로직 또는 모듈 제공자로 사용한다. 객체나 클로저를 반환한다.

객체 참조에서 연결 및 갱신을 처리하는 방법으로 인해 노출식 모듈 패턴(Revealing module pattern) 대신 호스트 객체 형태로 반환한다.

function AnotherService () {
  var AnotherService = {};
  AnotherService.someValue = '';
  AnotherService.someMethod = function () {
    // ...
  };
  return AnotherService;
}
angular
  .module('app')
  .factory('AnotherService', AnotherService);

왜?: 노출식 모듈 패턴을 사용하면 초기값을 변경할 수 없는 경우가 있기 때문이다. 1


서비스와 팩토리에서 가장 두드러진 차이점을 꼽는다면, 서비스에서는 초기화 과정이 존재하기 때문에 자연스럽게 prototype 상속이 가능하다. 그래서 일반적으로 상속이 필요한 데이터 핸들링이나 모델링 등의 경우에는 서비스를 활용하고, helper나 정적 메소드와 같이 활용되는 경우는 팩토리로 구현을 많이 하는 것 같다.

물론 앞서 살펴본 것과 같이 둘은 아주 유연한 관계이기 때문에 서비스에서 일반 호스트 객체를 반환하면 팩토리와 다를 것이 없게 된다. 그래서 각각의 특징에 맞게 구현하기 위해 가이드라인을 준수하는게 바람직하다. 가이드라인을 따르지 않는다면 적어도 프로젝트 내에서 일정한 프로토콜을 준수할 수 있도록 합의가 필요하다.

서비스와 팩토리처럼 구현의 제한성이 있는 것이 싫다면 강력한 기능을 제공하는 프로바이더(Provider)를 사용할 수 있다. (factory는 provider를 쓴다.) AngularJS에서 흔히 사용하는 $http가 대표적이며 많은 기능이 프로바이더로 구현되어 있다.


  1. 팩토리를 작성하는 방법을 설명하는 글을 보면 노출식 모듈 패턴을 활용하는 경우가 종종 있어서 왜? 부분이 추가된 것 같다. 이 패턴은 일부 구현(메소드, 변수)에 대해 외부에서 접근할 수 있는지 없는지 명시적으로 지정할 수 있다는 특징이 있는데 그 특징으로 외부에서 접근할 수 없는 코드에 대해서는 값을 변경할 방법이 없다. 그런 특징 때문에 가이드에서는 호스트 객체로 반환할 것을 권장하고 있다.