#405 AngularJS pro
- Download:
- source codeProject Files in Zip (35.4 KB)
- mp4Full Size H.264 Video (33.3 MB)
- m4vSmaller H.264 Video (17.9 MB)
- webmFull Size VP8 Video (22.8 MB)
- ogvFull Size Theora Video (40.1 MB)
AngularJS는 리치 클라이언트 어플리케이션을 개발하기 위한 하나의 프레임워크입니다. 이번 연재에서는 AngularJS가 할 수 있는 것 중에 몇가지에 대한 데모와 레일스 어플리케이션에 통합하는 방법에 대해서 소개할 것입니다. 여기서 보여주고자 하는 어플리케이션은 이름을 추가해서 무작위로 당첨자를 추첨하는 간단한 앱입니다. 이전 연재에서 Backbone과 Meteor를 소개할 때 사용하였던 동일한 앱을 사용했기 때문에 비교하는데 도움이 될 것입니다. 이제 index
액션을 가지는 RaffleController
를 포함하는 레일스 어플리케이션을 만들어서 작업을 시작하겠습니다. 물론 디폴트 index 페이지를 제거할 것입니다.
$ rails new raffler $ cd raffler $ rails g controller raffle index $ rm public/index.html
다음은 이 index 액션을 루트 액션으로 지정하겠습니다.
Raffler::Application.routes.draw do root to: "raffle#index" end
이미 이 어플리케이션에 대한 약간의 CSS를 작성한 것이 있어서 이것을 raffle.css.scss
파일에 추가하겠습니다. 또한 레이아웃 파일에 있는 yield
호출을 div
태그로 감싸고 스타일시트에서 해당 태그를 참조할 것입니다.
<!DOCTYPE html> <html> <head> <title>Raffler</title> <%= stylesheet_link_tag "application", :media => "all" %> <%= javascript_include_tag "application" %> <%= csrf_meta_tags %> </head> <body> <div id="container"> <%= yield %> </div> </body> </html>
이제, 아직 아무런 기능은 없지만, single-page 어플리케이션의 형태를 갖추게 되었습니다. AngularJS 를 여기에 추가해 보겠습니다. 여기에는 여러가지 방법이 있습니다. 한가지는 AngularJS 웹사이트로부터 관련 파일들을 다운로드 받아서 어플리케이션의 /vendor
디렉토리에 추가해 주는 것입니다. 다른 방법으로는, 젬을 추가하는 것인데, 여기서는, gemfile의 asset 그룹에 angularjs-rails
젬을 추가할 것입니다. 이제 bundle
명령을 실행하여 설치합니다.
group :assets do gem 'sass-rails', '~> 3.2.3' gem 'coffee-rails', '~> 3.2.1' # See https://github.com/sstephenson/execjs#readme for more supported runtimes # gem 'therubyracer', :platforms => :ruby gem 'uglifier', '>= 1.0.3' gem 'angularjs-rails' end
아래와 같이, 자바스크립트 manifest 파일에 이 젬이 제공해 주는 assets을 사용할 것입니다.
//= require angular //= require_tree .
AngularJS는 jQuery와 호환되지만 반드시 필요하지 않습니다. 여기서는 manifest 파일에서 jQuery를 제거하여 Angular를 학습하는 동안에 jQuery 코드가 부족한 부분을 대신하지 못하도록 할 것입니다. 이와 같은 셋업과정은 (opening html 태그에 하나의 속성을 추가하여) 어플리케이션에서 Angular를 사용할 수 있도록 함으로써 마무리하게 됩니다. 이와 같이 추가된 속성은 자바스크립트가 인지하게 될 것입니다.
<html ng-app>
이제 index
템플릿에서 Angular를 사용해 보겠습니다. 이 페이지의 상단에 하나의 form을 두고 사용자가 이름을 추가하고 추첨을 할 수 있도록 하기 위해 하나의 텍스트 필드를 추가할 것입니다. 여기서 사용하게 될 Angular의 멋진 기능 중에 하나는 양방향 데이터 바인딩이라는 것입니다. 임의의 이름을 가지는 ng-model
속성을 추가하여 하나의 form 필드를 임의의 모델 데이터로 연결할 수 있게 됩니다.
<h1>Raffler</h1> <form> <input type="text" ng-model="newEntry.name"> </form> {{newEntry.name}}
여기서는 newEntry.name
을 사용했습니다. 여기서 newEntry
는 이 form에서 데이터 조작을 하게 될 객체로, name
은 텍스트 필드를 통해서 유지하게 될 속성에 해당하게 됩니다. 이러한 데이터 연결 효과를 보기 위해서 이중 대괄호를 사용하여 이 값을 나타낼 수 있게 됩니다. 어플리케이션을 재시작하여 홈페이지를 다시 로드하여 테스트할 것입니다. 텍스트 필드 박스에 입력하게 되면 해당 값이 바로 연결되어 나타나게 됩니다.
다음은 텍스트 박스 아래에 이미 작성된 목록을 보이도록 할 것입니다. form에서 새로운 이름을 서브밋하면 바로 이 목록에 추가되어야만 합니다. 현재상태에서는 단지 간단한 연결 데이터만을 볼 수 있지만, 기능을 추가하기 위해서는 하나의 컨트롤러를 사용해서 자바스크립트 코드를 추가해 주어야 합니다. 컨트롤러를 추가하는 방법은, 일반적으로 div
태그에 ng-controller
속성을 추가하고 Ctrl
로 끝나는 이름을 할당해 주면 됩니다.
<h1>Raffler</h1> <div ng-controller="RaffleCtrl"> <form> <input type="text" ng-model="newEntry.name"> </form> {{newEntry.name}} </div>
여기서는 raffle
Coffeescript 파일에 이 컨트롤러를 작성할 것입니다. 이 컨틀로러는 간단한 함수이지만 이 함수에 접근할 수 있도록 하기 위해, AngularJS가 인식할 수 있는 @
심볼을 사용할 것입니다.
@RaffleCtrl = ($scope) -> $scope.entries = [ {name: "Larry"} {name: "Curly"} {name: "Moe"} ]
이 컨트롤러 함수는 하나의 scope를 인수로 가지는데 뷰 파일과 상호작용할 수 있도록 해 주는 객체의 형태를 가집니다. 이 scope 객체에 변수나 함수를 설정하거나 얻을 수 있게 되는데, 여기서는 entries
변수를 지정하여 추첨에 사용할 이름 목록이 들어 있는 배열을 할당했습니다. 뷰 템플릿에서는 이 데이터를 이용하여 아래와 같이 목록을 보여주고자 합니다.
<ul> <li ng-repeat="entry in entries"> {{entry.name}} </li> </ul>
이제 ul
태그를 추가해서 그 안에 이름 목록의 각 항목을 표시할 것입니다. 이를 위해서 li
태그에 ng-repeat
속성을 추가하고 entry in entries
값을 할당해 줍니다. 이것은 entries
배열 루프를 돌면서 각 요소마다 li
태그를 이용하여 이름을 나타낼 것입니다. 페이지를 다시 로드하면 추가한 항목들의 목록을 볼 수 있게 될 것입니다.
다음은 코드를 변경해서 새로운 항목을 텍스트 박스에 입력하고 form을 서브밋하면 목록에 추가되도록 할 것입니다. 이를 위해서 우선, form에 버튼 하나를 추가하고, form 태그에 ng-submit
속성을 추가하여 서브밋 이벤트를 연결해 줍니다. 이렇게 하면 scope에 연결된 특정 함수를 호출할 수 있게 되는데, 여기서는 addEntry
함수를 호출할 것입니다.
<form ng-submit="addEntry()"> <input type="text" ng-model="newEntry.name"> <input type="submit" value="Add"> </form>
Coffeescript 파일에서 이 함수를 작성할 것입니다. form이 만들어 주는 객체인 $scope.newEntry
를 이용해서 배열에 새로운 항목을 push 하도록 할 것입니다. 그리고나서 텍스트 박스를 비우기 위해서 이 객체에 빈 객체를 할당하게 됩니다.
$scope.addEntry = -> $scope.entries.push($scope.newEntry) $scope.newEntry = {}
이제 페이지를 다시 로드하면 버튼이 보이게 되고 새로운 항목을 입력한 후 form을 서브밋하게 되면 페이지가 다시 로드되지 않은 채로 입력한 항목이 목록에 추가될 것입니다.
이를 통해서 Angular의 데이터 바인딩 기능이 얼마나 강력한지 알 수 있게 됩니다. 이러한 데이터 바인딩으로 자바스크립트로 변경한 데이터가 뷰에 즉각적으로 반영되어 표시될 수 있는 것입니다.
추첨하기
다음은 "Draw Winner(추첨)" 버튼을 목록 아래에 두고 무작위로 항목을 선택한 후에 당첨자로 표시할 것입니다. 이를 위해서, 먼저, 목록 아래에 버튼을 추가합니다. 이 버튼의 click
이벤트를 모니터링하기 위해서 ng-click
속성에 임의의 함수를 지정해 줍니다.
<button ng-click="drawWinner()">Draw Winner</button>
이제 이 함수를 작성합니다. 이 함수에서는 무작위로 항목을 뽑아서 해당 항목의 winner
속성을 true
로 설정하게 됩니다.
$scope.drawWinner = -> entry = $scope.entries[Math.floor(Math.random() * $scope.entries.length)] entry.winner = true
뷰에서는 당첨자들을 표시하기 위해서 아래와 같이 코드를 추가합니다.
<li ng-repeat="entry in entries"> {{entry.name}} <span ng-show="entry.winner" class="winner">WINNER</span> </li>
여기에서는 ng-show
속성을 사용하여 true
값을 반환하는 항목만을 표시하도록 합니다. 이제 페이지를 다시 로드하고 "Draw Winner" 버튼을 클릭하면 무작위로 선택된 이름이 winner로 표시되어야 합니다.
여러명의 당첨자를 선택할 수 있으며 가장 마지막에 선택된 당첨자를 빨간색으로 표시하고자 합니다. 이를 위해서, 가장 최근에 선택된 당첨자를 추적할 필요가 있으며, 따라서 scope 객체에 lastWinner
변수를 만들 것입니다.
$scope.drawWinner = -> entry = $scope.entries[Math.floor(Math.random() * $scope.entries.length)] entry.winner = true $scope.lastWinner = entry
이제 최종 당첨자에 대해서 span
태그를 추가하고 class를 추가할 것입니다. 이를 위해서, ng-class
속성을 사용하게 되고 대괄호를 이용하여 하나의 표현식을 지정해 줍니다. 이 표현식은 해당 항목이 최종 당첨자인 경우 highlight
클래스가 추가되도록 합니다.
<li ng-repeat="entry in entries"> {{entry.name}} <span ng-show="entry.winner" ng-class="{highlight: entry == lastWinner}" class="winner">WINNER</span> </li>
이제 페이지를 다시 로드한 후 "Draw Winner" 버튼을 여러번 클릭하면 최종 당첨자는 의도했던 바와 같이 빨간색으로 표시되는 것을 볼 수 있을 것입니다.
다음으로 할 것은, "Draw Winner" 버튼의 기능을 변경해서 현재 선택된 당첨자는 다시 선택되지 못하도록 할 것입니다. 따라서, 아직 당첨되지 못한 항목들로만 구성된 목록 풀을 작성할 필요가 있습니다.
$scope.drawWinner = -> pool = [] angular.forEach $scope.entries, (entry) -> pool.push(entry) if !entry.winner if pool.length > 0 entry = pool[Math.floor(Math.random() * pool.length)] entry.winner = true $scope.lastWinner = entry
이를 구현하기 위해서, Angular의 forEach
를 이용하여 목록에 대해서 루프를 돌면서 당첨되지 못한 항목들을 풀에 추가하는 작업을 하게 될 것입니다. 이렇게 하면 이제 이 풀로부터 당첨자를 뽑을 수 있게 되는 것입니다.
데이터베이스에 변경내용을 저장하기
지금까지 어플리케이션의 클라이언트 사이드에 많은 기능을 추가해 주었지만 추가한 항목이나 무작위로 뽑은 당첨자들은 페이지를 다시 로드할 때마다 초기화 되어 데이터베이스에 저장되지 않게 됩니다. 이제 어플리케이션을 수정하여 레일스 backend와 연결하여 변경된 내용이 데이터베이스로 저장되도록 할 것입니다. 이를 위해서 name과 winner 속성을 가지는 Entry
라는 리소스를 생성한 후에 데이터베이스로 마이그레이션할 것입니다.
$ rails g resource entry name winner:boolean $ rake db:migrate
이 때, 작업할 초기 데이터가 필요할 수 있어서 seed 파일에 항목을 몇개 생성하도록 할 것입니다. 이후에 rake db:seed
명령을 수행하면 데이터베이스에 이 항목들을 손쉽게 추가할 수 있을 것입니다.
Entry.create!(name: "Matz") Entry.create!(name: "DHH") Entry.create!(name: "Jose Valim") Entry.create!(name: "Avdi Grimm") Entry.create!(name: "Steve Klabnik") Entry.create!(name: "Aaron Patterson")
AngularJS와 연결하기 위해서는, 이 데이터를 조작하기 위한 JSON API를 만들어 주어야 합니다. 이를 위해서 EntriesController
에서 이러한 작업을 코딩할 것입니다.
class EntriesController < ApplicationController respond_to :json def index respond_with Entry.all end def show respond_with Entry.find(params[:id]) end def create respond_with Entry.create(params[:entry]) end def update respond_with Entry.update(params[:id], params[:entry]) end def destroy respond_with Entry.destroy(params[:id]) end end
매우 간단하게 작성되었습니다. respond_to :json
을 사용하여 액션마다 응답을 하도록 합니다. 아마도 API 기능을 더 추가하기를 원할 수도 있지만 여기서 간단하게 작성한 것도 작동할 것입니다. 이 API와 연결하기 위해서는 별도의 자바스크립트 파일로 제공되는 Angular Resource를 사용할 수 있는데, 이 때는 어플리케이션의 자바스크립트 manifest 파일에 이것을 포함해 주어야 할 것입니다.
//= require angular //= require angular-resource //= require_tree .
어플리케이션에서 이것을 사용하기 위해서는 CoffeeScript 코드의 구조를 약간 변경해 줄 필요가 있습니다. 해당 리소스를 dependency로 정의할 필요가 있는데 이를 위해서는 angular.module
을 호출한 후 이름을 지정하여 모듈 하나를 만들어 주어야 합니다. 이 모듈에 대해서 ngResource
를 dependency로 추가하여 정의해 주면, 이 리소스를 이용할 수 있게 되는데 이 결과를 하나의 변수로 지정해 줄 것입니다. Angular Resource dependency를 지정해 놓았기 때문에 방금 전에 작성한 컨트롤러에 인수로서 이 리소스를 넘겨줄 수 있게 됩니다. 이 리소스는 하나의 함수로써 호출하면 REST API를 통해서 연결되는 객체를 반환해 주게 됩니다. 이 함수로 넘겨지는 첫번째 인수는 API로 연결되는 URL로써 여기서는 /entries/:id
를 사용하게 되는데 :id
가 파마미터로 전달됩니다. 두번째 인수에는 디폴트 파라미터를 지정하는데 id
를 @id
로 지정하여 현재 객체의 id
값을 사용하도록 할 것입니다. id 값이 할당되어 있는 하나의 Entry 객체를 넘겨줄 경우에는 이것이 URL로 사용되고 그렇지 않을 경우에는 생략되어 entries
가 사용될 것입니다. 세번째 인수를 지정하게 되면, API로 호출하고자 하는 액션들을 추가로 지정할 수 있게 됩니다. 디폴트 값은 update 액션을 제외한 필요로 하는 모든 작업을 처리해 주는데 이 액션을 추가해서 PUT으로 메소드를 지정해 줄 것입니다.
Angular Resource의 작동법에 대해서 자세한 것을 배우고자 한다면 해당 문서참고하기 바랍니다. 이 문서에는 언급했던 인수들에 대한 설명과 get
, save
, query
, remove
, delete
와 같은 디폴트 액션에 대한 설명이 기술되어 있습니다. 이러한 액션들을 함수로서 사용할 수 있는데, 데이터베이스로부터 모든 항목 리스트를 가져오는 API를 호출하는 Entry.query()
함수를 호출하여 정적 목록을 교체할 것입니다.
app = angular.module("Raffler", ["ngResource"]) @RaffleCtrl = ($scope, $resource) -> Entry = $resource("/entries/:id", {id: "@id"}, {update: {method: "PUT"}}) $scope.entries = Entry.query()
어플리케이션에서 모듈이름을 지정했기 때문에 ng-app
속성에 값을 할당하는 방법으로 레이아웃 파일에서 이 모듈의 이름을 지정할 필요가 있습니다.
<div ng-controller="RaffleCtrl">
이제 페이지를 다시 로드하면, seed 데이터를 이용해서 데이터베이스로 추가했던 이름 목록을 볼 수 있어야 합니다.
임의의 항목을 추가할 때 목록에 추가하기 전에 데이터베이스에 저장하고 합니다. 이를 위해서는, POST 요청을 하여 레일스 어플리케이션에서 create
액션을 호출하는 Entry.save
함수를 호출합니다.
$scope.addEntry = -> entry = Entry.save($scope.newEntry) $scope.entries.push(entry) $scope.newEntry = {}
특정 항목을 당첨자로 표시할 때 데이터베이스에 이를 업데이트해 줄 필요가 있는데 Entry.update
를 호출하여 해당 항목을 넘겨주거나 액션을 호출한 resource 객체인 entry를 이용하여 entry.$update
를 호출하므로써 이러한 작업을 구현할 수 있습니다.
$scope.drawWinner = -> pool = [] angular.forEach $scope.entries, (entry) -> pool.push(entry) if !entry.winner if pool.length > 0 entry = pool[Math.floor(Math.random() * pool.length)] entry.winner = true entry.$update() $scope.lastWinner = entry
구현한 내용이 제대로 작동하는지를 알아보기 위해서 페이지를 다시 로드한 후 새로운 항목을 추가하고 항목 몇개를 당첨자로 표시합니다. 페이지를 다시 로드하면 변경된 내용이 계속 유지되어야 합니다.
제대로 동작되어야 합니다. 데이터가 레일스 데이터베이스에 저장된 것처럼 유지되어야 합니다.
서비스
리소스들을 서비스로 refactoring 하고자 합니다. 하나의 서비스란, 컨트롤러에 넘겨 줄 수 있는 것을 말합니다. scope와 resource가 서비스에 해당합니다. 컨트롤러내에서 리소스를 만들지 않고 Entry
리소스를 컨트롤러로 넘겨주고자 합니다. 이를 위해서는, app.factory
를 호출해서 "Entry" 라는 리소스를 생성할 것입니다.
app = angular.module("Raffler", ["ngResource"]) app.factory "Entry", ($resource) $resource("/entries/:id", {id: "@id"}, {update: {method: "PUT"}} ) @RaffleCtrl = ($scope, Entry) -> $scope.entries = Entry.query() # rest of code omitted.
여기서 일어나고 있는 상황을 이해하는 것이 중요합니다. Angular는 dependency injection이라는 것을 수행할 수 있는데, 이것은 해당 함수가 넘겨 받게 되는 인수들을 살펴보고 인수의 이름에 의존하는 서비스들을 연결해 주는 것을 말합니다. 여기서는, Entry 인수가 Entry
factory 함수를 호출하여 그것이 반환하는 어떤 것이라도 사용하게 됩니다. 이것은 두개의 인수의 위치를 바꾸더라도 넘겨 받은 이름에 근거해서 인수들이 지정되기 때문에 여전히 어플리케이션이 동작하게 될 것입니다.
이것은 편리해 보일지 몰라도 어플리케이션을 운영서버로 배포할 경우 큰 문제를 발생시키게 됩니다. 레일스는 자동으로 자바스크립트를 압축하여 인수명들을 더 짧게 바꾸게 되는데, 이로써 dependency injection이 더 이상 동작하지 않게 된다는 것입니다. 이러한 문제를 해결할 수 있는 여러가지 방법이 있습니다. 그 중에 하나는, 아래와 같이, 함수를 배열 속으로 이동시켜서 dependency를 문자열로 지정하는 것입니다.
@RaffleCtrl = ("$scope", "Entry", $scope, Entry) ->
이것은 AngularJS에게 dependency를 알려주게 되며 이름이 변경되더라도 문제가 되지 않도록 합니다. 서비스를 인수로 받는 모든 함수에 대해서 이와 같은 작업을 해주어야 합니다. 예를 들면, 리소스를 생성하기 위한 함수와 같은 것입니다.
app.factory "Entry", ["$resource", ($resource) $resource("/entries/:id", {id: "@id"}, {update: {method: "PUT"}} ) ]
다른 방법은 운영환경 설정을 수정하여 압축법을 지정하는 것입니다.
config.assets.js_compressor = Sprockets::LazyCompressor.new { Uglifier.new(mangle: false)}
이말은, 자바스크립가 압축시 효과적으로 되지 못하더라도 이름이 변경되지 않는다는 것입니다. 어떤 방법을 선택하더라도, 운영환경에서 Angular 어플리케이션을 철저히 테스트하여 문제가 없음을 확인하는 것이 현명한 생각입니다.
이제 어플리케이션은 완벽하게 작동하며 AngularJS는 동적 클라이언트 사이드 어플리케이션을 매우 쉽게 만들어 주었고 backend 레일스와 멋지게 동기화됩니다. 이 연재에서는 AngularJS가 할 수 있는 것에 대한 표면적인 부분만 다루었습니다. AngularJS는 라우터, 뷰 등 훨씬 많은 부분을 가지고 있습니다. 더 많은 것을 알기를 원하면 AngularJS 웹사이트의 learning 메뉴를 찾아 볼 만하고 Egghead 웹사이트에는 Angular를 다루는 무료 스크린캐스트가 몇가지 있어서 이를 참고하면 도움이 될 것입니다. 또한 Angular를 레일스 어플리케이션에서 사용하도록 도와주는 몇가지 루비 젬들이 있으며 AngularJS Scaffold가 그 예입니다.