Angular.js의 이해

Angular.js를 사용하는 ionic 기반의 웹앱을 구성하는 프로젝트를 다루기 위해서는 프레임워크를 어느정도 이해하여야 코딩에 손을 댈 수 있다.  자바스트립트와 html 문법은 잘 안다는 가정하에 이 프레임워크를 이해하여야 한다. 아직까지 Angular.js가 정말로 유용한 프레임워크인지는 의문이 간다. Angular.js version 2가 나오면서 더욱 더 복잡해졌기 때문이다. 하지만 일단 version 1 에 대한 프로젝트를 커버하기 위해 기본적인 사항을 알아본다.

Angular.js란

Angular.js는 구글이 만든 MV*(Model - View - Whatever) 자바스크립트 프레임워크로 모델과 뷰가 분리되어 동적인 웹 화면을 구성하기에 적합한 구조를 가지고 있다. 

AngularJS의 기본구조
AngularJS의 기본구조

AngularJS의 기본구조를 나타낸 그림으로 AngularJS가 어떻게 로딩되고 시작되는지를 나타내주는 그림으로 순서는 다음과 같다.

  1. 브라우저가 html을 로드 (DOM을 파싱한다.)
  2. Angular.js를 로드한다
  3. DOM Content Loaded Event를 기다린다.
  4. DOM이 모두 로드되면 ng-app 지시자를 찾는다.
  5. ng-app에서는 dependency injection 을 위해 사용되는 $injector를 생성한다.
  6. injector 지시어는 어플리케이션의 모델을 위한 컨텍스트가 되는 루트 스코프를 생성한다.
  7. 최종적으로 ng-app을 기준으로 하위DOM을 컴파일하고 rootScope와 링크시킨다.

 

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

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

See the Pen NdYzMX by youngdeok (@medusakiller) on CodePen.

동작방식은 ng-model 디렉티브로 input이 양방향 바인딩(two-way binding) 되어 데이터를 표현한다. 그러면 user.name는 어디에 저장되는지가 궁금할텐데 user.name$scope에 저장되어 있다. input에 입력할 때마다 스코프의 user.name 객체가 갱신되고 Angular.js의 {{ ... }} 인터폴레이션으로 모델을 출력할 수 있다. 그래서 HTML에서 user.name의 값을 볼 수 있는 것이다. input에 입력할 때 user.name이 스코프에 저장되고 인터폴레이션으로 HTML에서 그 값을 보게 된다.

템플릿에서 $scopeuser.name을 설정하면 컨트롤러에서도 user.name에 접근할 수 있다.

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>

See the Pen angular.js ex02 by youngdeok (@medusakiller) on CodePen.

app 에 컨트롤러를 생성하여 코드내에서 message 에 대한 내용을 받는다. 

body태그에 포함된 ng-app="app" ng-controller="MainCtrl"directive 라고 한다.  ng-app은 body 요소가 Anuglar 어플리케이션에 포함되어 있다고 알려준다. 즉, body 요소내의 모든 것을 Angular 어플리케이션이 관리하도록 한다. ng-controller는 요소의 스코프를 MainCtrl이라는 컨트롤러에 할당하겠다는 것이다.

함수에 바인딩하기

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

app.controller('MainCtrl', function($scope) {
  $scope.greet = function() {
    $scope.message = "Hello, " + $scope.user.name;
  }
});
<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>

See the Pen Angular.js ex03 by youngdeok (@medusakiller) on CodePen.

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

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

디렉티브(Directives)

<body>
  <div id="chart"></div>
</body>

이 코드에서 id가 어떤 역할을 하는지 살펴보려면 구현된 자바스크립트 파일중에 하나를 봐야 한다.

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

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

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

<body>
  <pie-chart width="400" height="400" data="data"></pie-chart>
</body>

파이차트를 추가한다는 간다한 사실뿐만 아니라 크기가 어느정도 이고 어떤 데이터가 할당되었는지를 알 수 있다.

내장 디렉티브

 

이미 ng-app, ng-controller, ng-click, ng-model 과 같은 ng 접두사를 사용하는 다수의 내장 디렉티브가 존재한다. 

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

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

배열목록을 출력해 보자.

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

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

See the Pen angularjs ex04 by youngdeok (@medusakiller) on CodePen.

HTML에서 ng-repeat 디렉티브를 사용했다. ng-repeat는 컬렉션의 아이템마다 새로운 템플릿을 생성할 것이다. 예제에서는 4개의 아이템이 있으므로 ng-repeat가 이 코드 부분을 4번 생성할 것이다.

각 반복된 <li>는 자신만의 스코프를 가지며 person은 developers의 요소에 접근한다.

이처럼 각 디렉티브는 역할이 있으며 자신만의 디렉티브도 만들어낼 수 있다. 

 <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>

이 코드에서 페이지가 로딩 되었을 때 input에 포커스를 주고 싶다면 다음과 같은 디렉티브를 만들어낼 수 있다. 

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

디렉티브는 객체를 반환해야 하고 이 반환 객체에 몇가지 속성을 정의할 수 있지만 이 예제에서는 속성을 사용하지 않았다. 디렉티브는 link 함수를 반환할 수 있는데 이 link함수 안에 템플릿 로직의 대부분을 작성하고 여기서 DOM 리스너를 등록하거나 DOM을 갱신하는 등의 작업을 할 수 있다.

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

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

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

 <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>

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

// hello.js
app.directive('hello', function() {
  return {
    restrict: "E",
    replace: true,
    template: "<div>Hello readers, thank you for coming</div>"
  }
});
  • restrict: 디렉티브는 여러 위치에 사용할 수 있다.
A: Attribute
<div foo> </div>
E: Element
<foo> </foo>
C: Class
<div class="foo"> </div>
M: Comment
<!-- directive: foo -->
  • replace: 이 값을 true로 설정하면 해당 요소는 새로운 템플릿으로 대체될 것이다.
  • template: HTML 요소에 추가할(또는 대체할) 템플릿을 여기에 둔다.

따라서 hello 디렉티브는 다음과 같이 사용 가능하다. 

<hello></hello>

이 코드는 template의 코드인 <div>Hello readers, thank you for coming</div> 같이 변경된다. html을 별도의 파일로 관리하려면 templateUrl에 파일을 지정할 수도 있다. 이때 로드할 html의 상대위치를 정의한다.

  <body>
    <div ng-controller="Ctrl">
      <my-example></my-example>
    </div>
  </body>
// app.js
angular.module('app', [])  
  .controller('Ctrl', function($scope) {
    $scope.person = {
      name: 'Youngdeok',
      address: 'Incheon'
    };
  })
  .directive('myExample', function() {
    return {
      templateUrl: 'my-example.html'
    };
});
<!-- my-example.html -->
<span> Name: {{person.name}} </br> Address: {{person.address}} </span>

replace

디렉티브를 사용한 HTML의 태그에 template 또는 templateUrl에 포함된 태그 내용을 추가할지 교체할지 설정한다. true로 설정할 경우 HTML의 디렉티브를 사용한 태그를 template 또는 templateUrl에 작성된 내용으로 교체된다.

// app.js
angular.module('app', [])  
  .controller('Ctrl', function($scope) {
    $scope.person = {
      name: 'Youngdeok',
      address: 'Incheon'
    };
  })
  .directive('myExample', function() {
    return {
      restrict: 'E',
      template: '<div>Hello AngularJS</div>',
      replace: true
    };
});

그 밖에 transclude 를 사용하면 원본 내용을 포함하는 형태로 교체할 수 있다. 원본은 template나 templateUrl에 <span ng-transclude> 를 사용한 곳에 들어가게 된다. 

필터

기본적인 표현식을 파이프를 사용해 필터를 적용할 수 있다. 예를 들어 ng-repeat에서 개발자 목록을 출력을 이름으로 정렬 한다면,

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

와 같이 orderBy를 사용한다. 만일 -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>

input에 할당된 search는 filter에 전달되어 필요한 정보에 따라 필터링 한다. 좀더 정밀하게 name을 골라내려면,

<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을 사용해 바인딩하여 이름을 전달 하는 것이다.

자신만의 필터를 직접 생성하는 방법을 알아본다. 첫글자를 대문자를 필터를 어떻게 작성하는지 보자.

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

이 예제에서는 인풋의 첫글자를 대문자로 바꿨다. 이제 이 필터를 사용해 보자.

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

 

서비스

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

Angular에는 HTTP 요청을 관리하는 $http나 Promise에 대한 $q와 같은 많은 내장 서비스가 있다. 간단한 서비스를 생성해 이해해 보자.

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

<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.name을 다루려고 할때 안쪽에서 이름을 쓰면 다른쪽에서 똑같이 반영되도록 하고 싶을때 이코드는 제대로 동작하지 않는다. 왜냐면 스코프가 분리되어 있기 때문이다.

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

// 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는 다른 컨트롤러에 주입되기는 하지만 실제로 서비스는 아닌 예외사항 중 하나이다.

 

 

 

 

 

youngdeok's picture

Language

Get in touch with us

"If you would thoroughly know anything, teach it to other."
- Tryon Edwards -