2014/02/13 13:58

프로트앤드 개발을 하면서 테스트는 어떻게 할 수 있는지 알아본다. 앵귤러에서는 테스트 관련 내용까지도 포함하고 있어서 개발 테스트의 편의성을 함께 제공하고 있다. 


사전 준비

  generator-angular를 설치하게 되면 앵귤러 프레임워크에 카르마(Karma) 테스트 프레임워크와 Mock 객체등이 포함되어 있다. 과거에는 Karma를 통해서 단위(Unit) 테스트와 E2E (End-To-End) 테스트 두가지를 하였으나, 최근에는 Karma로는 단위 테스트만을 수행하고 Protractor라는 새로운 테스트 프레임워크를 통해서 E2E 테스트를 수행한다 


  - karma.conf.js : Unit Test 환경파일. 내부적으로 Jasmine (http://pivotal.github.io/jasmine/) 프레임워크를 사용한다

  - karma-e2e.conf.js : E2E Test 환경파일

  - "grunt test" 를 통해 수행


Karma가 jasmine 테스트 프레임워크를 사용하고, 필요한 files 목록을 정의하며 Chrome 브라우져에서 8080 포트를 사용하여 테스트함

// karma.conf.js

// Karma configuration

// http://karma-runner.github.io/0.10/config/configuration-file.html


module.exports = function(config) {

  config.set({

    // base path, that will be used to resolve files and exclude

    basePath: '',


    // testing framework to use (jasmine/mocha/qunit/...)

    frameworks: ['jasmine'],


    // list of files / patterns to load in the browser

    files: [

      'app/bower_components/angular/angular.js',

      'app/bower_components/angular-mocks/angular-mocks.js',

      'app/bower_components/angular-resource/angular-resource.js',

      'app/bower_components/angular-cookies/angular-cookies.js',

      'app/bower_components/angular-sanitize/angular-sanitize.js',

      'app/bower_components/angular-route/angular-route.js',

      'app/scripts/*.js',

      'app/scripts/**/*.js',

      'test/mock/**/*.js',

      'test/spec/**/*.js'

    ],


    // list of files / patterns to exclude

    exclude: [],


    // web server port

    port: 8080,


    // level of logging

    // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG

    logLevel: config.LOG_INFO,


    // enable / disable watching file and executing tests whenever any file changes

    autoWatch: false,

    // Start these browsers, currently available:

    // - Chrome

    // - ChromeCanary

    // - Firefox

    // - Opera

    // - Safari (only Mac)

    // - PhantomJS

    // - IE (only Windows)

    browsers: ['Chrome'],


    // Continuous Integration mode

    // if true, it capture browsers, run tests and exit

    singleRun: false

  });

};


  Gruntfile.js 의 test Task 내역 : grunt test 

// Test settings

karma: {

   unit: {

      configFile: 'karma.conf.js',

      singleRun: true

  }

}


... 중략 ..

grunt.registerTask('test', [

    'clean:server',

    'concurrent:test',

    'autoprefixer',

    'connect:test',

    'karma'

  ]);



1. Jasmine 테스트 프레임워크 

  describe ~ it 으로 BDD를 표현한다. BDD는 TDD의 2세대 버전 정도로 이해하자. 보다 자세한 사항은 위키 참조 (http://en.wikipedia.org/wiki/Behavior-driven_development

 describe("A suite", function() {

  it("contains spec with an expectation", function() {

    expect(true).toBe(true);

  });

});


 describe로 시작하는 것을 Suite (슈트)라고 부르고, it 으로 시작하는 것을 spec (스펙)이라고 부른다. 슈트안에 여러개의 스펙이 있을 수 있다. beforeEach(), afterEach()를 통해서 스펙 수행시 마다 환경 초기값 셋업(Setup)이나 마무리 작업(TearDown)을 수행할 수 있다. 스펙 안에는 expect 기대값에 대한 다양한 메서드를 제공한다.

toEqual()

toBe(), not.toBe()

toBeTruthy(), toBeFalsy()

toContain(), not.toContain()

toBeDefined(), toBeUndefined()

toBeNull()

toBeNaN()

toBeGreaterThan()

toBeLessThan()

toBeCloseTo() : 소수점

toMatch() : 정규표현식

toThrow()

 ...


  보다 자세한 사항은 Jasmine 문서를 참조한다 (http://jasmine.github.io/2.0/introduction.html)



2. 앵귤러 Service 테스트 준비

  yo를 통하여 앵귤러 코드를 완성하게 되면 자동으로 테스트 코드까지 생성된다. SessionService에 대한 테스트 코드는 /test/spec/services 폴더 밑에 동일한 session-service.js 파일이 존재한다. module 호출 후에 inject 가 올 수 있으나 inject 후에 module 호출은 올 수 없다. 

describe('Service: SessionService', function () {

  // load the service's module

  beforeEach(module('meanstackApp'));


  // instantiate service

  var SessionService;

  beforeEach(inject(function (_SessionService_) {

    SessionService = _SessionService_;

  }));


  it('should do something', function () {

    expect(!!SessionService).toBe(true);

  });


});

 

 grunt test 명령을 수행하면 에러가 발생할 것이다. 그것은 index.html에 필요에 의해 추가한 .js파일을 Karma 환경파일에 추가하지 않았기 때문이다. controller 쪽에서도 에러가 발생하는데 기본 테스트 코드에서 발생하므로 불필요한 코드를 삭제한다. 

 // karma.conf.js 

  files: [

      'app/bower_components/jquery/jquery.js',

      'app/bower_components/bootstrap/dist/js/bootstrap.js',

      'app/bower_components/angular/angular.js',

      'app/bower_components/angular-mocks/angular-mocks.js',

      'app/bower_components/angular-resource/angular-resource.js',

      'app/bower_components/angular-cookies/angular-cookies.js',

      'app/bower_components/angular-sanitize/angular-sanitize.js',

      'app/bower_components/angular-route/angular-route.js',

      'app/bower_components/angular-bootstrap/ui-bootstrap-tpls.js',

      'app/bower_components/angular-ui-router/release/angular-ui-router.js',

      'app/scripts/*.js',

      'app/scripts/**/*.js',

      'test/mock/**/*.js',

      'test/spec/**/*.js'

    ],


  singleRun: true


// Controller 테스트 파일에서 불필요한 하기 코드 삭제 

  it('should attach a list of awesomeThings to the scope', function () {

    expect(scope.awesomeThings.length).toBe(3);

  });



3. 앵귤러 Service 테스트를 위한 Mock 사용

  SessionService 테스트 수행전에 공통적으로 사용하고 있는 Factory나 Service에 대한 테스트 코드를 작성한 후 SessionService를 최종 테스트 한다  


  - msRequestFactory : 원하는 JSON 객체가 생성되는지 테스트 

  - msRestfulApi : $httpBackend 이용하여 요청 처리 테스트

  - SessionService : $httpBackend 이용하여 요청 처리하고 SessionInfo의 정보 유무 테스트 


msRequestFactory 통하여 만들어진 JSON 객체가 원하는 key:value 값을 가지고 있는지 확인한다. expect().toEqual()을 사용한다 

 'use strict';


describe('Service: msRequestFactory', function () {


  // load the service's module

  beforeEach(module('meanstackApp'));


  // instantiate service

  var msRequestFactory;

  beforeEach(inject(function ($injector) {

    msRequestFactory = $injector.get('msRequestFactory');

  }));


  it('should exist', function () {

    expect(!!msRequestFactory).toBe(true);

  });


  it('should make JSON', function () {

    var request = msRequestFactory.createRequest('user', 'login', '');

    expect(request).toEqual({

      'area' : 'user',

      'resource' : 'login',

      'id' : '',

      'request' : {}

    });

  });


});

  

msRestfulApi는 $resource를 통해 서버에 요청을 보낸다. 서버 요청에 대한 Mock을 만들어서 응답을 줄 수 있는 기능으로 앵귤러는 $httpBackend를 제공한다. 즉, 실서버가 없을 경우 XHR 또는 JSONP 요청없이도 실서버에 요청한 것과 같은 응답을 줄 수 있다. $httpBackend의 유형은 두가지 이다


  - request expectation : 요청 JSON 데이터의 검증 원하는 결과가 왔는지 설정. expect().respond()

  - backend definition : 가상의 응답정보를  설정. when().respond()


서버의 요청은 모두 async 처리가 되므로 테스트코드 작성하기가 애매하다. 따라서 $httpBackend.flush()를 호출하는 시점에 request가 전달되므로, 테스트 코드 작성시 적절한 위치에 놓아야 한다. API 참조 (http://docs.angularjs.org/api/ngMock.$httpBackend)

// 로그인 서비스 테스트에서 계속 에러가 발생하여 Re-Thinking 중이다. 

// 에러 코드 

'use strict';


describe('Service: msRestfulApi', function () {


  // load the service's module

  beforeEach(module('meanstackApp'));


  // instantiate service

  var msRestfulApi, httpBackend, scope, resource;

  beforeEach(inject(function ($injector) {

    msRestfulApi = $injector.get('msRestfulApi');

    httpBackend = $injector.get('$httpBackend');

    scope = $injector.get('$rootScope');

    resource = $injector.get('$resource');


    httpBackend.when('POST', '/api/v1/user/login').respond(200, {'id': 1});

    

  }));


  it('should call test', function() {

    var request = {

      'area' : 'user',

      'resource' : 'login',

      'id' : '',

      'request' : {}

    };


    msRestfulApi.login(request, function(response){

      expect(response.id).toBe(1);

      console.log(response);

    }, function(error){

      console.log(error);

    });


    httpBackend.flush();

    expect(r.id).toBe(1);


  });


});

  


<참조>

  - Jasmine 문법

  - Jasmine 소개

  - Protractor Seed 소개 : Protractor WebDriver 업데이트 

  - WebDriverJS 소개 

저작자 표시 비영리 변경 금지
Posted by 이뜸으로 이끄는 이 윤영식
2014/02/04 10:43

메뉴의 우측에 로그인을 생성하고 로그인 화면을 트위터 부트스트랩을 이용하여 만들고 로그인 처리를 한다. 


사전 준비 

  트위터 부트스트랩을 이용하여 먼저 화면 프로토 타입을 만든다. 그리고 앵귤러 Controller를 코딩한 후 서버와 통신을 위한 앵귤러 Service를 만들자. 그리고 필요에 따라 Framework 요소로 필요한 것을 만들어 간다. 하나의 업무처리를 위하여 HTML+Controller+Service 를 기억억한다.  


  - 비즈니스 HTML View 작성

  - 비즈니스 Controller 코딩

  - 비즈니스 Service 코딩



1. 로그인 메뉴 추가하기

  메뉴는 menu.html 로 별도 구성하였고, 맨 하단에 로그인이 안되었을 때와 로그인이 되었을 때를 나타내는 <li> 태그를 넣어준다. currentUser는 로그인 성공후 $rootScope에 저장되는 사용자 객체이다. <li> 태그는 ng-hide/ng-show에의해 보여졌다 안보여졌다 한다.

     ... 중략 ...

          <li class="divider"></li>

          <li class="dropdown-header">Members</li>

          <li data-ng-class="{ active: $state.includes('gurumember') }">

            <a ui-sref="gurumember"><i class="fa fa-users "></i> 멤버소개</a>

          </li>

        </ul>

      </li>

    </ul>


    <ul class="nav navbar-nav navbar-right">

      <li ng-hide="false">

        <a href="#" ui-sref="login">Login</a>

      </li>

      <li ng-show="false">

        <a href="#" ng-click="logout()">

          [{{ currentUser.name }}] Logout

        </a>

      </li>

    </ul>


  </div>

</div>



2. 로그인 화면 구성

  ng-hide="false" 이므로 현 상태에서는 "Login" 메뉴가 나오고, 메뉴 클릭시 ui-sref="login" 에 따라 views/login.html 화면으로 이동을 한다.  Bootsnipp에서 login 을  (http://bootsnipp.com/search?q=login) 검색하면 다양한 로그인 창 디자인이 나오므로 활용해 본다.


  - 로그인을 할 때 컨트롤러에서 처리할 메서드를 정의한다 

    submitLogin()

  - 정보 객체를 정의한다  

    login = { email: 'xxx', password: 'xxx' }

  - type 과 required 지정을 통해 필수항목 유효성 체크를 한다 

<div class="row">

  <div class="col-md-4 col-md-offset-4">

    <h3 class="text-center">LOGIN</h3>

    <form name="loginForm" ng-submit="submitLogin()">

      <div class="form-group">

        <div class="input-group">

          <span class="input-group-addon">

            <i class="fa fa-envelope-o"></i>

          </span>

          <input id="login_email" type="email" name="email" class="form-control input-lg" placeholder="Email" ng-model="login.email" required>

        </div>

      </div>


      <div class="form-group">

        <div class="input-group">

          <span class="input-group-addon">

            <i class="fa fa-asterisk"></i>

          </span>

          <input id="login_password" type="password" name="password" class="form-control input-lg" placeholder="Password" ng-model="login.password" required>

        </div>

      </div>


      <button type="submit" class="btn btn-primary btn-lg btn-block btn-shadow">Login</button>

    </form>

  </div>

</div>


  우측 상단의 "Login" 메뉴 클릭시 출력되는 최종화면이다 



3. 로그인 앵귤러 Controller 개발 

  로그인 화면에 대한 처리를 하는 컨트롤러는 LoginCtrl (app/scripts/controllers/login.js) 이다. 앵귤러의 컨트롤러는 뷰와 모델 객체를 주고 받으며 처리하는 역할만을 담당하도록 만든다. 즉, 서버와 연결하고 처리하는 것은 앵귤러 서비스를 만들고 주입(DI)을 받아 사용한다. 로그인이 성공했다고 가정하여 UI-Router의 $state를 이용하여 main 화면으로 이동하는 코드를 넣었다. 

 'use strict';


angular.module('meanstackApp')

  .controller('LoginCtrl', [

  '$scope', 

  '$state', 

  function ($scope, $state) {

   $scope.submitLogin = function() {

    console.log($scope.login);

     $state.go("main");

   }

  }]);



로그인 앵귤러 Service 개발 

  로그인 정보를 받아서 서버와 통신하는 모듈을 만들도록 한다. Node.js와 RESTful 방식으로 요청을 처리한다. 앵귤러에서는 HTTP 요청 처리를 위하여 $http와 이를 보다 추상화한 $resource를 제공한다. 하지만 클라이언트와 서버사이의 통신을 위하여 몇가지 공통모듈을 만들 필요가 있다. 


  - 서버와 주고받는 요청/응답을 추상화한 객체 

  - $resource를 추상화한 서비스   



1. 서버 응답/요청 객체 추상화 

  자바의 J2EE 스펙중 Servlet 스펙을 보면 요청/응답을 처리하는 서블릿은 HTTPServletResponse/HTTPServletRequest가 있다. 이와 유사하게 앵귤러와 Node.js에 사이에 주고 받는 JSON 객체를 추상화 한다. MS를 MEANStack의 약어로 공통모듈의 경우 앞에 붙여 사용한다. 요청객체는 앵귤러 Factory 로 개발한다.  


  - 요청객체 : MSRequest 를 통해 정형화된 형태의 JSON 객체를 생성한다 

  - 응답객체 : MSResponse 는 서버로 부터 전달되는 정형화된 형태의 JSON 객체이다 


  RESTful API를 만들기 위한 몇가지 원칙을 지키고 그외에는 변형하여 사용토록 한다. URI 정의시 몇가지 원칙만을 가지고 진행토록한다. 복잡하게 하지 말고 Stateless하면서 Resource 접근에 대해서 명사를 사용하며 동사적 의미는 HTTP POST, GET, PUT, DELETE를 사용하며, Collection의 경우는 복수를 사용한다. 즉 더 복잡한 부분이 나오면 그때 가서 확장하거나 별도 구성하여 사용하고 몇가지 원칙을 정해서 사용토록 한다 


  - prefix 로 버전을 반드시 붙인다 : api/v1

  - 3단계의 정의를 한다 : /:area/:resource/:id 

    + area : authentication, person, tech, guru 등의 resource 영역 구분

    + resource : login, mylevel(myfollowing), angularjs(nodejs, expressjs, mongodb), month(member)같은 좀 더 구체적인 구분

    + id : GET, PUT, DELETE의 경우 옵션적으로 사용

    + 그외의 데이터는 Key=Value 파라미터 값으로 넘긴다 


// MSRequest 생성 및 개발 

$ yo angular:factory ms-factory

   create app/scripts/services/ms-factory.js

   create test/spec/services/ms-factory.js


// ms-factory.js

angular.module('meanstackApp')

  .factory('msRequestFactory', function () {

  

  var createRequest = function(area, resource, id, request) {

    if (!request)

      request = {};


    return {

      "area" : area,

      "resource" : resource,

      "id" : id,

      "request" : request

    };

  };


  return {

    createRequest : createRequest

  };


});


// 공통 호출 팩토리 생성

$ yo angular:factory ms-restful-api

   create app/scripts/services/ms-restful-api.js

   create test/spec/services/ms-restful-api.js


// ms-restful-api.js 

angular.module('meanstackApp')

  .factory('msRestfulApi', ['$resource', function ($resource) {

  var prefixUrl = '/api/v1';


  return $resource(

      prefixUrl + '/:area/:resource/:id', 

     {

       area : "@area",

       resource : "@resource",

       id : "@id"

     },

     {

      'get':    {method:'GET', isArray:true},

      'save':   {method:'POST'},

      'update': {method:'PUT'},

      'delete': {method:'DELETE'},

      'login':  {method:'POST'}

    });

  }]);


  $resource의 파라미터는 첫번째 URI 두번째 URI의 구분자에 맵핑되는 JSON, 세번째는 사용자 정의 메소드이다. 



2. 로그인 Service 개발 

  컨트롤에서 로그인 관련 서버호출을 위하여 MSRequest 팩토리와 RESTful하게 호출할 수 있는 팩토리를 만들었다. 이를 활용하는 앵귤러 Service 코딩해 보자. 


  - 사용자 로그인을 처리하는 서비스를 만든다.

  - 서비스는 View에서 Controller를 통하여 로그인 정보(params)를 받아서 Service로 넘긴다.

  - 성공적으로 완료되면 Controller에서 넘겨준 펑션(successCallback)을 수행한다. 


Service는 순수하게 서버와의 요청 처리만을 담당하고 Controller는 HTML View에서 넘어온 Model 값의 핸들링과 이후 Service에서 성공을 하면 실행할 successCallback을 갖는다. successCallback은 UI Routing 또는 View의 변경을 수행한다. Service와 Controller 서로의 역할을 정확히 나눔으로써 유지보수성을 높이도록 하였다. 


// 로그인 관련 서비스를 만든다 

$ yo angular:service session-service

   create app/scripts/services/session-service.js

   create test/spec/services/session-service.js


// session-service.js

angular.module('meanstackApp')

  .service('SessionService', [

  'msRequestFactory', 

  'msRestfulApi', 

  '$log',

  function SessionService(msRequestFactory, msRestfulApi, $log) {

    

    this.login = function(paramssuccessCallback) {

    // 1) create request

    var request = msRequestFactory.createRequest('authentication', 'login', '');

   

    // 2) set params

    request.params = {

    email: params.email,

    password: params.password

    }


    // 3) call ajax 

    msRestfulApi.login(request, 

    function(response) {

    // success

    successCallback(response);

    }, 

    function(error){

    // fail

    $log.error('Server Exception is ', error);

    })

  };


  }]);


  사용자가 로그인을 한후 사용자 정보를 담는 SessionInfo 객체를 관리하는 서비스를 만들어 보자. 

  - 사용자 정보 저장소로 HTML5의 localStorage를 우선 사용한다 

  - 현재 사용자 정보를 $rootScope.currentUser에 담아서 애플리케이션 전체 영역에서 사용토록 한다 

// 세션 정보 생성 

$ yo angular:service session-info

   create app/scripts/services/session-info.js

   create test/spec/services/session-info.js


// session-info.js

angular.module('meanstackApp')

  .service('SessionInfo', ['$rootScope', function SessionInfo($rootScope) {

    this.localStorageKey = "__SESSION_INFO";

    try {

      $rootScope.currentUser = JSON.parse(localStorage.getItem(this.localStorageKey) || "{}");

    } catch(e) {

      $rootScope.currentUser = {};

    }


    this.getCurrentUser = function() {

      return $rootScope.currentUser;

    }


    this.isUserSignedIn = function() {

      if(this.getCurrentUser() && this.getCurrentUser().id) {

        return true;

      } else {

        return false;

      }

    };


    this.setUserInfo = function(info) {

      angular.extend($rootScope.currentUser, info);

      localStorage.setItem(this.localStorageKey, JSON.stringify($rootScope.currentUser));

    };


    this.reset = function() {

      $rootScope.currentUser = {};

      localStorage.setItem(this.localStorageKey, JSON.stringify($rootScope.currentUser));

    };

  }]);



  SessionService에서 로그인 요청이 성공하면 SessionInfo를 호출하는 코드를 넣도록 한다. 

// SessionInfo를 사용하는 SessionService 수정 

angular.module('meanstackApp')

  .service('SessionService', [

   'msRequestFactory', 

   'msRestfulApi', 

   'SessionInfo',

   '$log',

   function SessionService(msRequestFactory, msRestfulApi, SessionInfo, $log) {

    

    this.login = function(params, successCallback) {

     // 1) create request

     var request = msRequestFactory.createRequest('authentication', 'login', '');

    

     // 2) set params

     request.params = {

     email: params.email,

     password: params.password

     }


     // 3) call ajax 

     msRestfulApi.login(request, 

     function(response) {

       // success

        SessionInfo.reset();

                   SessionInfo.setUserInfo(response);

        successCallback(response);

     }, 

     function(error){

       // fail

                  alert('Login Failed');

       $log.error('Server Exception is ', error);

     })

   };


  }]);



3. 로그인 Controller 개발

  이제 앵귤러 Service가 만들어 졌으니 Service를 사용하는 Controller를 코딩해 보자. 로그인 Service 가 성공하면 메인화면으로 이동한다.  

// login.js 의 기존 코드 

angular.module('meanstackApp')

  .controller('LoginCtrl', [

  '$scope', 

  '$state', 

  function ($scope, $state) {

   $scope.submitLogin = function() {

    console.log($scope.login);

     $state.go("main");

   }

  }]);


// SessionService를 사용하는 수정 코드 

angular.module('meanstackApp')

  .controller('LoginCtrl', [

  '$scope', 

  '$state', 

  'SessionService',

  'SessionInfo',

  '$log',

  function ($scope, $state, SessionService, SessionInfo, $log) {


   $scope.submitLogin = function() {

    SessionService.login($scope.login, changeState);

   }


   function changeState(response){

    alert(SessionInfo.getCurrentUser().name + '님 반갑습니다 :)');

        $state.go("main");

   }

  }]);


최종적으로 Chrome 개발자 도구의 console에 아무런 에러가 없으면 모든 서비스와 컨트롤러가 앵귤러 초기화가 잘 되었다는 신호이다. 로그인을 해보면 아마도 404에러가 날 것이다. 아직 서버가 준비되지 않았기 때문인데, 이는 다음장에서 테스트 코드를 작성해서 확인해 보록한다. 다음과 같인 나오면 성공이다. 



4. 로그인 html 수정

  SessionInfo에는 사용자가 로그인 상태인지 체크하는 메소드가 존재한다. 이를 menu.html 에 적용하자 

<ul class="nav navbar-nav navbar-right">

<li ng-hide="SessionInfo.isUserSignedIn()">

<a href="#" ui-sref="login">Login</a>

</li>

<li ng-show="SessionInfo.isUserSignedIn()">

<a href="#" ng-click="logout()">

[{{ currentUser.name }}] Logout

</a>

</li>

</ul>


위의 코드는 해당 브랜치에 존재한다. 

https://github.com/MEAN-STACK/MEANStack-Bookwork/tree/angular_step03_login-ctrl-service

저작자 표시 비영리 변경 금지
Posted by 이뜸으로 이끄는 이 윤영식
2014/01/30 11:15

UI-Router를 이용한 화면 라우팅 설정을 app/scripts/app.js 메인 애플리케이션 파일에 설정하였다. 설정 내용중 각 라우팅되는 HTML View의 제어를 담당하는 Controller를 yeoman을 사용해서 생성해 보자 



HTML과 Controller 

  앵귤러는 MVW 프레임워크라고 하는데 여기서 M은 model 이고 V는 View, W은 Whatever를 의미하여 무엇이든지 올 수 있다는 의미이다. Whatever에는 싱글톤패턴의 서비스, 팩토리 또는 필터, 디렉티브(Directive)등이 올 수 있다. 


  - Model : Controller의 $scope를 통한 자동 two-way binding 을 한다 

  - View : HTML에 $scope의 모델들을 설정하거나 앵귤러의 디렉티브를 이용하여 직관적인 설정한다  


  앵귤러의 데이터 모델은 바로 Scope에 의해서 연결이 되며, app.js 가 최초 수행 되면 $rootScope가 생성되고 그 하위로 $scope가 생성된다. 이들은 계층적으로 연결되어 상위의 scope 데이터에 접근할 수 있다. 만일 Chrome 브라우져를 사용한다면 scope 객체들의 계층구조를 보기 위하여 별도의 크롬도구를 설치해야 한다. 


 1) AngularJS Batarang: https://chrome.google.com/webstore/detail/angularjs-batarang/ighdmehidhipcmcojjgiloacoafjmpfk 설치 

 2) 크롬 개발자도구 : 윈도우(Ctrl+Shift+i), 맥(Command+Option+i) 연다 

 3) 맨 우측의 AngularJS를 선택하고 Enable 옵션을 체크한다 

 4) < Scope (003) 에서 003은 앵귤러가 Scope를 생성하고 부여한 아이디이고 클릭하면 우측에 Scope에 저장된 정보를 볼 수 있다.



1. 화면별 컨트롤러 생성하기 

  Yeoman에서 앵귤러 프레임워크의 기능을 자동 생성해 주는 것은 yo 명령이다. 우리는 meanstack 애플케이션 생성을 위하여 "generator-angular"를 설치하였는데 yo 뒤의 명령은 관례에 따라 "yo angular:<기능> <명칭>"으로 사용할 수 있다. 


  - yo의 generator가 만일 generator-mean 이고 geneartor-mean안에 <기능>으로 controller가 있다면 

  - yo mean:controller sample 을 수행하면 보통은 컨트롤러 코드와 컨트롤러 테스트 코드가 자동 생성된다

  -즉 "yo <generator- 두의 명칭>:<genertor가 제공하는 기능명칭>  <사용자정의 명칭>" 으로 명령은 구성된다 

  - <사용자정의 명칭> 에서 이름을 지정할 때는 "user-biz" 라고 명칭을 주면 index.html에는 "user-biz.js" 파일이 추가 되고, 기능의 명칭은 "UserBiz<기능약어>" 식으로 명칭이 만들어 진다.


  generator-angular 기능 목록은 홈페이지를 참조한다 (https://github.com/yeoman/generator-angular) 컨트롤러 생성시 테스트 코드 파일이 자동으로 생성된다 

// yo angular:controller <명칭> 으로 각 메뉴의 컨트롤러를 생성한다.

$ yo angular:controller my-level

   create app/scripts/controllers/my-level.js

   create test/spec/controllers/my-level.js


$ yo angular:controller my-qna

   create app/scripts/controllers/my-qna.js

   create test/spec/controllers/my-qna.js


$ yo angular:controller my-following

   create app/scripts/controllers/my-following.js

   create test/spec/controllers/my-following.js


$ yo angular:controller tech-area

   create app/scripts/controllers/tech-area.js

   create test/spec/controllers/tech-area.js


$ yo angular:controller ranking

   create app/scripts/controllers/ranking.js

   create test/spec/controllers/ranking.js


$ yo angular:controller member

   create app/scripts/controllers/member.js

   create test/spec/controllers/member.js


$ yo angular:controller login

   create app/scripts/controllers/login.js

   create test/spec/controllers/login.js


// index.html에 자동으로 추가된 것을 볼 수 있다.

<!-- build:js({.tmp,app}) scripts/scripts.js -->

<script src="scripts/app.js"></script>

<script src="scripts/controllers/main.js"></script>

<script src="scripts/controllers/my-level.js"></script>

<script src="scripts/controllers/my-qna.js"></script>

<script src="scripts/controllers/my-following.js"></script>

<script src="scripts/controllers/tech-area.js"></script>

<script src="scripts/controllers/ranking.js"></script>

<script src="scripts/controllers/member.js"></script>

<script src="scripts/controllers/login.js"></script>

<!-- endbuild -->


  컨트롤러를 생성했는데 - 또는 . 를 사용하면 앞뒤의 문자들의 첫글자가 대문자로 변형되어 설정이 되고 뒤에 "Ctrl" 이 자동으로 붙는다 

// app/scripts/controllers/my-level.js 소스

'use strict';


angular.module('meanstackApp')

  .controller('MyLevelCtrl', function ($scope) {

    $scope.awesomeThings = [

      'HTML5 Boilerplate',

      'AngularJS',

      'Karma'

    ];

  });

  

  이제 app/scripts/app.js에 컨트롤러를 설정해 보자 

$stateProvider

      .state('main', {

        url: '/',

        templateUrl: 'views/main.html',

        controller: 'MainCtrl'

      })

      .state('login', {

        url: '/login',

        templateUrl: 'views/login.html',

        controller: 'LoginCtrl'

      })

      .state('mylevel', {

        url: '/mylevel',

        templateUrl: 'views/mylevel.html',

        controller: 'MyLevelCtrl'

      })

      .state('myqna', {

        url: '/myqna',

        templateUrl: 'views/myqna.html',

        controller: 'MyQnaCtrl'

      })

      .state('myfollowing', {

        url: '/myfollowing/{userId:[0-9]{1,4}}',

        templateUrl: 'views/myfollowing.html',

        controller: 'MyFollowingCtrl'

      })

      .state('techarea', {

        url: '/techarea/{techId:[0-9]{1,4}}',

        templateUrl: 'views/techarea.html',

        controller: 'TechAreaCtrl'

      })

      .state('gururanking', {

        url: '/gururanking',

        templateUrl: 'views/gururanking.html',

        controller: 'RankingCtrl'

      })

      .state('gurumember', {

        url: '/gurumember',

        templateUrl: 'views/gurumember.html',

        controller: 'MemberCtrl'

      });


  my-following이나 tech-area를 보면 아이디를 설정하였고 myfollowing.html에서 $stateParam을 사용하기위해 meanstackApp 모듈의 run() 에서 $rootScope에 $stateParams를 저장한다. 그리고 페이지가 전환 되기 시작할 때와 전환하여 로딩 완료되었을 때의 이벤트를 처리하기 위한 Listener를 등록한다. 

// myfollowing.html 

<div class="row">

  <div class="col-md-12">

    <h1>myfollowing.html</h1>

     User ID: {{$stateParams.userId}}

  </div>

</div>


// app/scripts/app.js

.run(['$rootScope', '$state', '$stateParams', 

  function ($rootScope, $state, $stateParams) {

    $rootScope.$state = $state;

    $rootScope.$stateParams = $stateParams;

  }]);


  - html에서 $stateParams 를 찾기위하여 자신의 $scope.$stateParams를 찾는다 없으면 다시 상위의 $rootScope.$stateParams을 찾게 된다. 따라서 클라이언트 애플리케이션 전역에서 사용할 내용들이 있다면 $rootScope에 저장한다. 

  - myfollowing 메뉴를 선택하면 Scope (009) 의 아이디값이 변경된다. 메뉴를 누를 때마다 Scope의 아이디가 변경되는데 이는 클라이언트 화면이 변경될 때 이전 Scope는 destroy되고 새로운 Scope가 생성되기 때문이다. 즉, 앵귤러가 화면 라우팅이되고 해당 화면의 컨트롤러에 신규 $scope 을 생성하여 DI (Dependency Injection)해준다 



2. 컨트롤러 코드 변경하기 

  애플리케이션 배포시에는 "grunt build"를 수행하는데 이때 .js에 대한 단축화(minification)이 수행되어 변수명을 알 수 없는 코드로 바꾸어준다. 앵귤러에서는 $로 시작하는 명칭이 변경이 되면 안되기 때문에 yo로 생성된 코드를 그대로 사용할 수 없다.  '$scope' 문자를 지정해 주면 function($scope) {} 가 최소화 되어 function($ab){} 로 바뀌어도 DI시에 '$scope'로 지정된 $scope 객체가 주입된다.

 angular.module('meanstackApp')

 .controller('MyFollowingCtrl', [

  '$scope',

  function ($scope) {

    ... 중략 ...

  }]);

  

  다른 컨트롤러도 해당 방식으로 전부 수정한다. 소스는 anuglar_setp02_insert-ctrl-menu 브랜치를 체크아웃한다 

https://github.com/MEAN-STACK/MEANStack-Bookwork/tree/anuglar_setp02_insert-ctrl-menu

저작자 표시 비영리 변경 금지
Posted by 이뜸으로 이끄는 이 윤영식
2014/01/29 23:56

CSS Framework과 클라이언트 UI Routing을 위한 라이브러리 설치가 끝났으니 이제 본격적으로 메인화면을 만들어 보도록 하죠. 메인 화면의 디자인은 CSR에서 내용을 참조하여 Twitter Bootstrap의 반응형 웹 디자인 메뉴로 구성한다. 



전체 구조 이해하기 

  우선 메뉴를 만들기 전에 index.html의 전체 구조를 이해하자. SPA 방식의 싱글페이지 애플리케이션 개발이란 index.html의 일부 DOM을 변경함으로써 화면을 전화하여 보여주는 것이다. 초기 index.html 레이아웃은 단순화 하여 사용토록 한다. 


  - 메뉴 html을 별도 파일로 분리 

  - 메뉴 html에 링크 설정하기 

  - 메뉴에 링크된 화면의 html 파일 생성 


// index.html 기존 설정

<body ng-app="meanstackApp">


    <!-- Add your site or application content here -->

    <div class="container" ng-view=""></div>


</body>



1. 메뉴관련 html 파일 분리

  우선 메뉴관련 파일을 분리해서 시작한다. app/views/menu.html 파일을 생성하고 index.html 파일에 include 한다.

<!-- Add your site or application content here -->

<!-- menu -->

<div ng-include="' /views/menu.html '"></div>

  menu.html 안에 이제 부트스트랩의 반응형 웹 디자인 메뉴를 만들어 보자. 반응형 웹 디자인 메뉴란 해상도에 따라 메뉴의 형태가 자동으로 변하는 것을 말한다. 주로 데스크톱과 모바일기기의 해상도에 맞게 메뉴 형태가 변한다. 처음에 부트스트랩을 직접 수작업을 해보는 것이 도움이 되지만 여기서는 부트스트랩용 저작도구인  Bootply(http://bootply.com/)를 사용해서 만들어 본다.  


  - Bootply의 "Drag-and-Drop" 가운데 메뉴를 선택한다

  - Bootstrap 3.0 에서 "Basic starter"를 선택한다 

  - html에서 메뉴에 대한 부분만 취한다 



 menu.html 에서 <div class="navbar navbar-inverse navbar-fixed-top"> 내용은 index.html의 ng-include가 있는 <div>에 class설정한다. 

// index.html

<div class="navbar navbar-inverse navbar-fixed-top" ng-include="' /views/menu.html '"></div>


// menu.html 

  <div class="container">

    <div class="navbar-header">

      <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">

        <span class="icon-bar"></span>

        <span class="icon-bar"></span>

        <span class="icon-bar"></span>

      </button>

      <a class="navbar-brand" href="#">Brand</a>

    </div>

    <div class="collapse navbar-collapse">

      <ul class="nav navbar-nav">

        <li class="active"><a href="#">Home</a></li>

        <li><a href="#about">About</a></li>

        <li><a href="#contact">Contact</a></li>

      </ul>

    </div><!--/.nav-collapse -->

  </div>

  

  "grunt serve"로 수행을 해보면 입력한 메뉴 구조를 볼 수 있다. 화면의 사이즈를 줄이면 메뉴형태가 변화는 것 또한 볼 수 있다. 

  - 데스크톱 해상도

  - 모바일 해상도


2. 메뉴의 링크 설정하기 

  menu.html 파일안에 링크에 대해 설정한다. 요구사항 정의를 보면 MEANStack을 위한 Q&A 서비스를 만드는 것이다. 하기와 같은 대분류 메뉴와 소분류 메뉴를 링크설정해 본다 

  

  - 나의 Q&A : 나의 질문 답변, 내가 팔로잉한 사람

  - Tech 글 : MEAN Stack 관련 메뉴

  - 명예의 전당 : 이번달 명예의 전당(인기글), 멤버소개 


부트스트랩은 기본적인 사항은 홈페이지를 통해 몇시간 정도 공부하거나 오픈 튜토리얼 사이트에서 익히고 시작하는 것이 좋다. 그리고 응용에 관련된 부분은 Bootsnipp (http://bootsnipp.com/) 에서 유용한 형태를 취하여 사용해 본다. 여기서는 부트스트랩의 기본적인 Navigation bar 구조를 참조한다.

참조: http://getbootstrap.com/components/#navbar


// menu.html 변경 내용

<div class="container">

  <div class="navbar-header">

    <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">

      <span class="icon-bar"></span>

      <span class="icon-bar"></span>

      <span class="icon-bar"></span>

    </button>

    <a class="navbar-brand" href="#"> MEANStack.net</a>

  </div>

  <div class="collapse navbar-collapse">

    <ul class="nav navbar-nav">

      

      <li class="dropdown">

        <a href="" class="dropdown-toggle" data-toggle="dropdown"> My Area <b class="caret"></b></a>

        <ul class="dropdown-menu">

          <li class="dropdown-header">Status</li>

          <li>

            <a href="#"> My Level: 평민</a> 

          </li>

          <li>

            <a href="#"> 질문: 3, 답변: 1</a>

          </li>

          <li class="divider"></li>

          <li class="dropdown-header">Following</li>

          <li>

            <a href="#"> 윤영식</a>

          </li>

        </ul>

      </li>


      <li class="dropdown">

        <a href="" class="dropdown-toggle" data-toggle="dropdown"> Tech Area <b class="caret"></b></a>

        <ul class="dropdown-menu">

          <li class="dropdown-header">SPA</li>

          <li>

            <a href="#"> Angular.js</a>

          </li>


          <li class="divider"></li>

          <li class="dropdown-header">Middleware</li>

          <li>

            <a href="#"> Node.js</a>

          </li>

          <li>

            <a href="#"> Express.js</a>

          </li>


          <li class="divider"></li>

          <li class="dropdown-header">NoSQL</li>

          <li>

            <a href="#"> MongoDB</a>

          </li>


          <li class="divider"></li>

          <li class="dropdown-header">Eco Tools</li>

          <li>

            <a href="#"> Yeoman</a>

          </li>

        </ul>

      </li>


      <li class="dropdown">

        <a href="" class="dropdown-toggle" data-toggle="dropdown"> GuruGuru <b class="caret"></b></a>

        <ul class="dropdown-menu">

          <li class="dropdown-header">Ranking</li>

          <li>

            <a href="#"> 명예의 전당</a>

          </li>

          <li class="divider"></li>

          <li class="dropdown-header">Members</li>

          <li>

            <a href="#"> 멤버소개</a>

          </li>

        </ul>

      </li>


    </ul>

  </div><!--/.nav-collapse -->

</div>


 다시 "grunt serve"를 통하여 메뉴가 정상적으로 만들어 졌는지 확인한다 



3. 메뉴 링크에 라우팅 설정하기 

  메뉴구조를 만들었으니 이제 UI-Router(https://github.com/angular-ui/ui-router) 방식에 따라 라우팅을 설정해 보자. 앵귤러 라우팅 순서는 먼저 서버에서 HTML파일을 불러온 후 앵귤러에서 HTML파일을 파싱하여 앵귤러 코드를 앵귤러 컨텍스트에 포함시키고 DOM을 변경한다. 

  

  - 서버에 HTML 파일 요청하기 

  - HTML 파일 파싱하여 앵귤러 컨텍스트에 포함시키기

  - DOM을 변경하여 화면에 표현하기 


먼저 "My Level: 평민" 메뉴를 대상으로 설정을 하면 나머지는 동일 과정을 반복하게 된다. 

1) mylevel.html 파일을 app/views 폴더 밑에 생성한다 

<div class="row">

  <div class="col-md-12">

    <h1>mylevel.html</h1>

  </div>

</div>


2) <li> 태그에 현재 선택이 되면 class="active" 부트스트랩 클래스를 추가하기 위하여 앵귤러의 ng-class를 사용한다. 

  - 라우킹 명칭을 "mylevel" 로 정한다 

  - (data-)ng-class 안에 표현식을 넣어서 현재 라우팅 명칭이 mylevel (true)이면 active 클래스를 적용하고 false이면 적용하지 않는다

  - ui-sref 는 ui-router에서 사용하는 라우팅 변경 링크 속성으로 href를 대체한다 

// 기존 설정

<li>

    <a href="#"> My Level: 평민</a> 

</li>


// 변경 설정 

<li data-ng-class="{ active: $state.includes('mylevel') }">

   <a ui-sref="mylevel"> My Level: 평민</a> 

</li>

  

3) 이제 HTML 파일의 위치와 기타 정보를 설정한다

  - app/scripts/app.js 가 meanstack 서비스의 메인 애플리케이션 파일이 된다. 앵귤러가 최초 수행이 되면 애플리케이션에서 사용하는 $rootScope를 만들게 되는데 app.js 메인 애플리케이션 레벨에서 사용하는 Global Scope 이다. 단 브라우져를 재로딩하면 초기화 된다. 

  - ui-router 설정은 홈페이지(https://github.com/angular-ui/ui-router)를 참조한다.

'use strict';


angular.module('meanstackApp', [

  'ngCookies',

  'ngResource',

  'ngSanitize',

  'ngRoute',

  'ui.bootstrap',

  'ui.router'

])

.config(['$stateProvider', '$urlRouterProvider',  function ($stateProvider, $urlRouterProvider) {


    $urlRouterProvider.otherwise("/");


    $stateProvider

      .state('main', {

        url: '/',

        templateUrl: 'views/main.html',

        controller: ''

      })

      .state('mylevel', {

        url: '/mylevel',

        templateUrl: 'views/mylevel.html',

        controller: ''

      });

  }])


  - $urlRouterProvider.otherwise('/') 는 설정되지 않은 uri 요청이 들어올 경우를 처리한다. 여기서는 main 으로 간다 

  - main.html은 meanstack을 만들 때 생성된 html이다 

  - 메뉴에서 "나의 레벨"을 클릭하면 "mylevel" state의 url로 변경되고 templateUrl에 설정한 html을 서버에 요청한다 

  - HTML을 파싱하여 앵귤러 컨텍스트에 포함시키고 앵귤러 코드에 대한 처리는 controller에서 담당하나 아직은 설정하지 않았다

  - mylevel.html의 파싱된 DOM은 index.html의 "<div ui-view class="container"></div>" ui-view 하위 <div>에 자동 주입된다. 


설정이 완료되었으면 "나의 레벨" 메뉴를 클릭해 본다 


ui-view 속성이 있는 <div>에 mylevel.html 파일의 내역의 표현이 약간 위로 올라갔다. index.html과 main.css를 수정한다 

// app/index.html 기존 

<div class="navbar navbar-inverse navbar-fixed-top" ng-include="' /views/menu.html '"></div>


// app/index.html 수정

<div class="navbar navbar-default navbar-static" ng-include="' /views/menu.html '"></div>


// app/views/main.html 수정 

<div class="row">

  <div class="col-md-12">

    <h1>main.html</h1>

    <h2> Welcome to MEANStack.net</h2>

  </div>

</div>


// app/sytles/main.css 의 모든 내역을 삭제함 


  최종 결과 화면




4. 메뉴에 적절한 아이콘 설정하기 

 다른 메뉴들도 라우팅 명칭을 정하고 menu.html과 app.js에 라우팅 설정을 하고 각 메뉴의 기본 파일을 app/views/ 폴더 밑에 생성하여 연결한다. 그리고 메뉴에 FontAwesome의 아이콘을 설정한다. 


  - FontAwesome 사이트에서 원하는 아이콘을 선택한다 : http://fortawesome.github.io/Font-Awesome/icons/

  - 아이콘 태그를 넣는다 : 예) <i class="fa fa-check-square"></i> fa-check-square

// app/views/menu.html

<div class="container">


  <div class="navbar-header">

    <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">

      <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span>

    </button>

    <a ui-sref="main" class="navbar-brand"><i class="fa fa-th-large"></i> MEANStack.net</a> 

  </div>

  

  <div class="collapse navbar-collapse">

    <ul class="nav navbar-nav">

      <li class="dropdown">

        <a href="" class="dropdown-toggle" data-toggle="dropdown">

          <i class="fa fa-spinner fa-spin"></i> My Area <b class="caret"></b>

        </a>

        <ul class="dropdown-menu">

          <li class="dropdown-header">Status</li>

          <li data-ng-class="{ active: $state.includes('mylevel') }">

            <a ui-sref="mylevel"><i class="fa fa-eye"></i> My Level: 평민</a> 

          </li>

          <li data-ng-class="{ active: $state.includes('myqna') }">

            <a ui-sref="myqna"><i class="fa fa-comments"></i> 질문: 3, 답변: 1</a>

          </li>

          

          <li class="divider"></li>

          <li class="dropdown-header">Following</li>

          <li data-ng-class="{ active: $state.includes('myfollowing') && $stateParams.userId == '1' }">

            <a ui-sref="myfollowing({userId:'1'})"><i class="fa fa-thumb-tack"></i> 박유진</a>

          </li>

          <li data-ng-class="{ active: $state.includes('myfollowing') && $stateParams.userId == '2' }">

            <a ui-sref="myfollowing({userId:'2'})"><i class="fa fa-thumb-tack"></i> 이규원</a>

          </li>

          <li data-ng-class="{ active: $state.includes('myfollowing') && $stateParams.userId == '3' }">

            <a ui-sref="myfollowing({userId:'3'})"><i class="fa fa-thumb-tack"></i> 윤영식</a>

          </li>


        </ul>

      </li>


      <li class="dropdown">

        <a href="" class="dropdown-toggle" data-toggle="dropdown">

        <i class="fa fa-pencil-square-o"></i> Tech Area <b class="caret"></b></a>

        <ul class="dropdown-menu">

          <li class="dropdown-header">SPA</li>

          <li data-ng-class="{ active: $state.includes('techarea') && $stateParams.techId == '1' }">

            <a ui-sref="techarea({techId:'1'})"><i class="fa fa-file-text-o"></i> Angular.js</a>

          </li>


          <li class="divider"></li>

          <li class="dropdown-header">Middleware</li>

          <li data-ng-class="{ active: $state.includes('techarea') && $stateParams.techId == '2' }">

            <a ui-sref="techarea({techId:'2'})"><i class="fa fa-file-text-o"></i> Node.js</a>

          </li>

          <li data-ng-class="{ active: $state.includes('techarea') && $stateParams.techId == '3' }">

            <a ui-sref="techarea({techId:'3'})"><i class="fa fa-file-text-o"></i> Express.js</a>

          </li>


          <li class="divider"></li>

          <li class="dropdown-header">NoSQL</li>

          <li data-ng-class="{ active: $state.includes('techarea') && $stateParams.techId == '4' }">

            <a ui-sref="techarea({techId:'4'})"><i class="fa fa-file-text-o"></i> MongoDB</a>

          </li>


          <li class="divider"></li>

          <li class="dropdown-header">Eco Tools</li>

          <li data-ng-class="{ active: $state.includes('techarea') && $stateParams.techId == '5' }">

            <a ui-sref="techarea({techId:'5'})"><i class="fa fa-file-text-o"></i> Yeoman</a>

          </li>

        </ul>

      </li>


      <li class="dropdown">

        <a href="" class="dropdown-toggle" data-toggle="dropdown">

        <i class="fa fa-smile-o"></i> GuruGuru <b class="caret"></b></a>

        <ul class="dropdown-menu">

          <li class="dropdown-header">Ranking</li>

          <li data-ng-class="{ active: $state.includes('gururanking') }">

            <a ui-sref="gururanking"><i class="fa fa-sitemap"></i> 명예의 전당</a>

          </li>

          <li class="divider"></li>

          <li class="dropdown-header">Members</li>

          <li data-ng-class="{ active: $state.includes('gurumember') }">

            <a ui-sref="gurumember"><i class="fa fa-users "></i> 멤버소개</a>

          </li>

        </ul>

      </li>

    </ul>

  </div>

</div> 



// app/scripts/app.js 

angular.module('meanstackApp', [

  'ngCookies',

  'ngResource',

  'ngSanitize',

  'ngRoute',

  'ui.bootstrap',

  'ui.router'

])

.config(['$stateProvider', '$urlRouterProvider',  function ($stateProvider, $urlRouterProvider) {


    $urlRouterProvider.otherwise("/");


    $stateProvider

      .state('main', {

        url: '/',

        templateUrl: 'views/main.html',

        controller: ''

      })

      .state('login', {

        url: '/login',

        templateUrl: 'views/login.html',

        controller: ''

      })

      .state('mylevel', {

        url: '/mylevel',

        templateUrl: 'views/mylevel.html',

        controller: ''

      })

      .state('myqna', {

        url: '/myqna',

        templateUrl: 'views/myqna.html',

        controller: ''

      })

      .state('myfollowing', {

        url: '/myfollowing/{userId:[0-9]{1,4}}',

        templateUrl: 'views/myfollowing.html',

        controller: ''

      })

      .state('techarea', {

        url: '/techarea/{techId:[0-9]{1,4}}',

        templateUrl: 'views/techarea.html',

        controller: ''

      })

      .state('gururanking', {

        url: '/gururanking',

        templateUrl: 'views/gururanking.html',

        controller: ''

      })

      .state('gurumember', {

        url: '/gurumember',

        templateUrl: 'views/gurumember.html',

        controller: ''

      });


  }])


  - ui-router에서 myfollowing({userId:'1'}) 는 파라미터를 넘기는 방식이다 형식) ui-sref='stateName({param:value, param:value})

  - ui-router API : https://github.com/angular-ui/ui-router/wiki/Quick-Reference


현재까지 진행된 내역을 GitHub에 올려 놓았다. 다음은 router에 앵귤러 controller를 만들고 router에 설정해 보자 

https://github.com/MEAN-STACK/MEANStack-Bookwork/tree/angular_step01_making-index


저작자 표시 비영리 변경 금지
Posted by 이뜸으로 이끄는 이 윤영식
2014/01/29 07:48

Git, Node.js, Yeoman등 기본적인 개발환경을 설치하였다면 소프트웨어 요구사항 정의서 (Software Requirements Specification, SRS) 에서 정의한 화면을 CSS Framework을 사용하여 만들어 보자. 여기서는 가장 많이 사용하고 있는 Twitter Bootstrap을 사용토록 한다. 



사전준비

  이전 글에서 yeoman의 yo 명령을 통하여 angular 프로젝트를 "meanstack"이름으로 생성하였다. meanstack 하위에 app 폴더가 앵귤러 기반의 SPA (Single Page Application) 프론트앤드 애플리케이션의 ROOT 폴더가 된다. 


  - 반응형 웹 디자인 기반의 메뉴구조를 구성한다 

  - 메뉴를 클릭하면 해당하는 html 파일로 라우팅한다 

  - 메뉴에 아이콘을 추가한다 




1. Twitter Bootstrap 설치

 Twitter Bootstrap은 meanstack을 생성하면서 app/index.html에 자동 추가된다. 

  1) <!-- build:css ... --> : 주석은 제거하면 안된다. grunt build 시에 css 압축할 때 사용하는 주석이다. (build:js 도 동일) 

  2) <!-- bower:css .. --> : 주석은 제거하면 안된다. grunt build 시에 자동으로 bower install 한 내역을 추가한다. (bower:css 도 동일) 

// index.html

    <!-- build:css styles/vendor.css -->

    <!-- bower:css -->

    <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css" />

    <!-- endbower -->

    <!-- endbuild -->


   .. 중략 ..


    <!-- build:js scripts/vendor.js -->

    <!-- bower:js -->

    <script src="bower_components/jquery/jquery.js"></script>

    <script src="bower_components/angular/angular.js"></script>

    <script src="bower_components/bootstrap/dist/js/bootstrap.js"></script>

    <script src="bower_components/angular-resource/angular-resource.js"></script>

    <script src="bower_components/angular-cookies/angular-cookies.js"></script>

    <script src="bower_components/angular-sanitize/angular-sanitize.js"></script>

    <script src="bower_components/angular-route/angular-route.js"></script>

    <!-- endbower -->

    <!-- endbuild -->

  * "yo angular meanstack" 명령으로 twitter bootstrap이 자동 설치가 되면 twitter bootstrap이 사용하는 fonts 폴더를 app/fonts로 자동 복사해 준다. 이것은 grunt build시에 모든 css가 app/styles/*.css로 압축되고 bootstrap의 css가 사용하는 font는 ../fonts를 참조하기 때문이다. 따라서 배포를 위해서는 app/fonts 폴더가 존재 해야한다.




2. Angular UI-Bootstrap 설치

  Twitter Bootstrap을 html에 적용하려면 class 정보를 입력해야 한다. 이를 좀 더 직관적인 html tag 형식으로 쓸 수 있도록 AngularUI 팀에서 Directives를 만들었다. 예로 tab을 적용하가 위하여 <tab> 태그를 html에 사용하면 angular-bootstrap 모듈이 <tab> 를 class="tab" 형태로 바꿔서 DOM에 적용하는 것이다. html을 좀 더 직관적으로 작성할 수 있다는 장점이 있다. 

$ bower install angular-bootstrap --save

angular-bootstrap#0.10.0 app/bower_components/angular-bootstrap

└── angular#1.2.10


  bower를 통해 angular-bootstrap을 설치 후 "grunt build" 또는 "grunt serve" 를 실행하면 index.html에 필요 파일이 자동 추가된다. 자동 추가되는 파일의 정보는 bower_components/angular-bootstrap/bower.json 파일에 지정되어 있다.

// index.html 파일 

   <!-- build:js scripts/vendor.js -->

    <!-- bower:js -->

    <script src="bower_components/jquery/jquery.js"></script>

    <script src="bower_components/angular/angular.js"></script>

    <script src="bower_components/bootstrap/dist/js/bootstrap.js"></script>

    <script src="bower_components/angular-resource/angular-resource.js"></script>

    <script src="bower_components/angular-cookies/angular-cookies.js"></script>

    <script src="bower_components/angular-sanitize/angular-sanitize.js"></script>

    <script src="bower_components/angular-route/angular-route.js"></script>

    <script src="bower_components/angular-bootstrap/ui-bootstrap-tpls.js"></script>

    <!-- endbower -->

    <!-- endbuild -->


  angular-bootstrap은 별도의 앵귤러 모듈이므로 애플리케이션에서 사용하려면 모듈 의존성을 설정해야 한다. meanstack 서비스의 메인 소스는 app/scripts/app.js 이다. 여기에 모듈 의존성을 설정한다. 

  - 형식 : angular.module('<ApplicationName>', [<의존성 모듈 열거>]);

// app/bower_components/angular-bootstrap/ui-bootstrap-tpl.js 파일 상단

angular.module("ui.bootstrap", ["ui.bootstrap.tpls", "ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdownToggle","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]);



// app/scripts/app.js 안에 추가 

'use strict';

angular.module('meanstackApp', [

  'ngCookies',

  'ngResource',

  'ngSanitize',

  'ngRoute',

  'ui.bootstrap'

])

  .config(function ($routeProvider) {

    .. 중략 ..

  });



3. Angular UI-Router 설치 

클라이언트 화면 전환을 위한 라우팅 기능은 앵귤러에서 제공하는 기본적인 라우터를 사용하지 않고 AngularUI팀에서 역시 제공하는 ui-router (https://github.com/angular-ui/ui-router)를 사용한다. ui-router는 한 화면에서 멀티 view 설정으로 원하는 부분들의 DOM을 변경처리할 수 있다. bower를 통해 ui-router v0.2.7 버전을 설치한다. ui-router v0.2.8 버전설치에 오류가 존재하기 때문이다. 또한 설치전에 Angular.js 버전을 현재 (2014.1.28기준) 최신버전인 v1.2.10 으로 업데이트한다.

// bower.json 파일에서 angular 관련 버전을 1.2.10 으로 지정한다 

$ vi bower.json

{

  "name": "meanstack",

  "version": "0.0.0",

  "dependencies": {

    "angular": "1.2.10",

    "json3": "~3.2.6",

    "es5-shim": "~2.1.0",

    "jquery": "~1.10.2",

    "bootstrap": "~3.0.3",

    "angular-resource": "1.2.10",

    "angular-cookies": "1.2.10",

    "angular-sanitize": "1.2.10",

    "angular-route": "1.2.10",

    "angular-ui-router": "0.2.7"

  },

  "devDependencies": {

    "angular-mocks": "1.2.10",

    "angular-scenario": "1.2.10"

  }

}


// angular 버전 업데이트

$ bower update 

angular-resource#1.2.10 app/bower_components/angular-resource

└── angular#1.2.10

.. 중략..

angular-scenario#1.2.10 app/bower_components/angular-scenario

└── angular#1.2.10


// ui-router 설치 1번을 선택한다 

$ bower install angular-ui-router#0.2.7 --save

    1) angular-ui-router#0.2.7 which resolved to 0.2.7

    2) angular-ui-router#~0.2.8 which resolved to 0.2.8 and has meanstack as dependants

[?] Answer: 1

angular-ui-router#0.2.7 app/bower_components/angular-ui-router

└── angular#1.2.10


   bower를 통해 ui-router를 설치 후 "grunt build" 또는 "grunt serve" 를 실행하면 index.html에 필요 파일이 자동 추가된다. 자동 추가되는 파일의 정보는 bower_components/angular-bootstrap/bower.json 파일에 지정되어 있다. 

    <!-- build:js scripts/vendor.js -->

    <!-- bower:js -->

    <script src="bower_components/jquery/jquery.js"></script>

    <script src="bower_components/angular/angular.js"></script>

    <script src="bower_components/bootstrap/dist/js/bootstrap.js"></script>

    <script src="bower_components/angular-resource/angular-resource.js"></script>

    <script src="bower_components/angular-cookies/angular-cookies.js"></script>

    <script src="bower_components/angular-sanitize/angular-sanitize.js"></script>

    <script src="bower_components/angular-route/angular-route.js"></script>

    <script src="bower_components/angular-bootstrap/ui-bootstrap-tpls.js"></script>

    <script src="bower_components/angular-ui-router/release/angular-ui-router.js"></script>

    <!-- endbower -->

    <!-- endbuild -->


  ui-router 또한 별도의 모듈이므로 app/scripts/app.js 안에 모듈 의존성을 설정한다 

'use strict';


angular.module('meanstackApp', [

  'ngCookies',

  'ngResource',

  'ngSanitize',

  'ngRoute',

  'ui.bootstrap',

  'ui.router'

])

  .config(function ($routeProvider) {

  .. 중략 ..

  });



4. FontAwesome Icon 설치

  다양한 아이콘을 사용하기 위하여 bootstrap과 별도로 fontawesom(http://fortawesome.github.io/Font-Awesome/) 라이브러리를 설치한다. 텍스트 정보를 좀 더 직관적으로 인지할 수 있도록 다양한 아이콘을 적용할 수 있다. 

$ bower install font-awesome --save

bower font-awesome#*            cached git://github.com/FortAwesome/Font-Awesome.git#4.0.3

font-awesome#4.0.3 app/bower_components/font-awesome


  설치 후 app/bower_components/font-awesome/ 폴더 밑으로 bower.json 이 존재하지 않으므로 필요한 파일을 index.html에 직접 기입해야 한다. index.html에 font-awesome.css 파일을 추가하자. 추가시 <!-- bower:css --> 영역에 넣으면 grunt build(grunt serve)시에 직접 넣은 정보는 자동 삭제되므로 <!-- bower:css --> 가 없는 <!-- build:css --> 안에 추가한다. 

     <!-- build:css styles/vendor.css -->

    <!-- bower:css -->

    <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css" />

    <!-- endbower -->

    <!-- endbuild -->


    <!-- build:css({.tmp,app}) styles/main.css -->

    <link rel="stylesheet" href="bower_components/font-awesome/css/font-awesome.css" />

    <link rel="stylesheet" href="styles/main.css">

    <!-- endbuild -->


  'grunt build'를 하게 되면 font-awesome의 css는 app/styles/main.css 파일로 묶이게 된다. font-awesome.css 소스를 보면 css 상위의 fonts 폴더의 파일을 참조한다. 따라서 font-awesome의 fonts 폴더안의 파일들을 app/fonts 밑으로 복사한다.

Twitter Bootstrap과 Font awesome의 css가 참조하는 파일들 목록

$ cd app/fonts

$ ls 


fontawesome-webfont.woff

fontawesome-webfont.ttf

fontawesome-webfont.svg

fontawesome-webfont.eot

FontAwesome.otf

glyphicons-halflings-regular.woff

glyphicons-halflings-regular.ttf

glyphicons-halflings-regular.svg

glyphicons-halflings-regular.eot


이제 필요한 라이브러리들이 index.html에 추가되었다. 다음장에서는 Twitter Bootstrap을 이용하여 반응형 웹 디자인(Responsive Web Design : RWD) 메뉴와 전체 화면 레이아웃을 만들어 보자.

저작자 표시 비영리 변경 금지
Posted by 이뜸으로 이끄는 이 윤영식