[AngularJS] Route

Posted in 모바일/Javascript // Posted at 2016. 7. 14. 14:57
728x90

AngularJS는 URL기반 경로 탐색을 지원하는 라우트(route) 모듈을 제공한다.

라우팅 기능은 SPA(Single Page Application)를 지향하는 프레임워크에서 빠져서는 안되는 중요한 기능이며, 이미 그전의 많은 모바일 자바스크립트 프레임워크(jQTouch, jQuery Mobile, Sench Touch 등)에서도 제공되는 유용한 기능이다.

라우팅 기능은 다수의 사용자 화면(UI)을 가진 AngularJS 응용프로그램 구현 시 특히 유용한 기능으로 개별 사용자 화면을 서로 다른 HTML 파일로 만들고, URL주소에 따라 특정 화면으로로 이동시켜 주는 기능이다. 특히 SPA환경에서의 페이지 교체는 새로고침 없이 단일 페이지에서 HTLM Element의 교체로 이뤄져야 하는데 AngularJS는 이와 같은 기능을 route 모듈과 ng-view 디렉티브의 조합으로 가능케 한다.

라우팅 구현은 아주 심플하며, w3schools.com의 예제는 이를 잘 설명해 주고 있다.

<!DOCTYPE html>
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular-route.js"></script>
<script>
var app = angular.module("myApp", ["ngRoute"]);
app.config(function($routeProvider) {
    $routeProvider
    .when("/", {
        templateUrl : "main.htm"
    })
    .when("/red", {
        templateUrl : "red.htm"
    })
    .when("/green", {
        templateUrl : "green.htm"
    })
    .when("/blue", {
        templateUrl : "blue.htm"
    });

});
</script>
<body ng-app="myApp">
 <p><a href="#/">Main</a></p>

 <a href="#red">Red</a>
 <a href="#green">Green</a>
 <a href="#blue">Blue</a>

 <div ng-view></div>
</body>
</html>

먼저 라우팅 모듈이 정의된 angular-route.js에 대한 스크립트 참조를 추가하고, 메인모듈에 ngRoute서브모듈을 주입한다. 그리고 응용프로그램이 실제 동작하기 전에 수행되어야 할 설정옵션을 정의할 수 있는 module.config 메서드에 라우팅 기능을 추가한다.

$routeProvider는 라우팅 기능을 위해 AngularJS에서 기본적으로 제공하는 서비스이며, URL조건과 이에 대응하는 리소스 경로는 $routeProvider.when 메서드에 라우팅 설정 객체를 정의하는 방식으로 구현한다.

ngRoute 모듈을 이용한 라우팅은 ng-view 디렉티브와 연결된다. 라우팅 조건에 맞는 URL 요청이 들어오면 이에 대응하는 리소스이 내용(HTML페이지)을 불러와서 ng-view 디렉티브 동적으로 삽입시킨다.

각각의 HTML 페이지는 새로운 URL요청에 따라 네트워크를 통해 다운로드 받게 되는데, 이때 대상 페이지가 새로고침(reload)되는 것이 아니라, 페이지의 특정 영역(ng-view)에 불러온 HTML 내용을 삽입하는 방식으로 동작하기 때문에 SPA(Single Page Application) 구현이 가능하게 된다.

URL 링크는 #red와 같은 형태로, 해시(#) 기호를 이용하여 작성한다.

* 대체 라우트 지정
- 요청된 URL에 대한 라우팅 정의가 없을 경우, $routeProvider 서비스의 otherwise 메서드로 대체 라우트를 정의할 수 있다. 다음의 코드는 일치하는 URL이 없을 경우 루트(/)로 이동하도록 구현한 것이다.

app.config(function($routeProvider) {
    $routeProvider  
    .when("/red", {
        templateUrl : "red.htm"
    })
   
.otherwise({redirectTo: '/'})
});

 

* Controller와 함께 사용하기
- 라우팅된 페이지 역시 AngularJS의 영역에 포함된 HTML영역이기에 일반적인 Controller 사용형태를 그대로 구현할 수 있다. 다음 예제는 라우팅에서 Controller를 사용해 각 뷰에 해당하는 컨트롤러를 정의하고 Controller-as 문법을 사용한 코드이다.

<!DOCTYPE html>
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular-route.js"></script>
<script>
var app = angular.module("myApp", ["ngRoute"]);
app.config(function($routeProvider) {
    $routeProvider   
    .when("/Page1", {
        templateUrl : "Page1.html",
        controller : "Page1Controller",
        controllerAs: "page1Ctrl"
    })
    .when("/Page2", {
        templateUrl : "Page2.html",
        controller : "Page2Controller",
        controllerAs: "page2Ctrl"
    });
});
app.controller("Page1Controller", function(){
 var page1Ctrl = this;
    page1Ctrl.msg = "Page1 Controller Property";
});
app.controller("Page2Controller", function(){
 var page2Ctrl = this;
    page2Ctrl.msg = "Page2 Controller Property";
});
</script>
<body ng-app="myApp">
 <a href="#Page1">Page1</a>
 <a href="#Page2">Page2</a>

 <div ng-view></div>
</body>
</html>

- Page1.html
<p>{{page1Ctrl.msg}}</p>

- Page2.html
<p>{{page2Ctrl.msg}}</p>


* 매개변수와 함께 사용하기
- 지금까지 구현한 예제에서는 실제 리소스 URL만 지정해서 라우팅을 구성하였다. 실무 환경에서는 번번히 URL에 매개변수가 따라 붙는다. 예를 들어 다음과 같이 특정 게시물 ID를 매개변수로 지정해서 해당 게시물로 바로 접근하는 URL을 만들 수 있다.

- http://yourdomain/board/10 (10번 게시물에 대한 URL 스킴)

AngularJS 라우팅 시스템에서는 URL의 매개변수 정보를 얻기 위해서 $routeParams 를 제공한다.
라우팅 경로 설정시 ':(콜론)' 을 사용해서 매개변수명을 추가하고, 컨트롤러에 $routeParams 주입하여 scope객체에 매개변수 값을 노출시킬 수 있다.

다음 코드는 바로 직전 예제에서 매개변수 처리를 추가한 코드이다.

<!DOCTYPE html>
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular-route.js"></script>
<script>
var app = angular.module("myApp", ["ngRoute"]);
app.config(function($routeProvider) {
    $routeProvider   
    .when("/Page1/:param", {
        templateUrl : "Page1.html",
        controller : "Page1Controller",
        controllerAs: "page1Ctrl"
    })
    .when("/Page2/:param", {
        templateUrl : "Page2.html",
        controller : "Page2Controller",
        controllerAs: "page2Ctrl"
    });
});
app.controller("Page1Controller", function($routeParams){
    var page1Ctrl = this;
    page1Ctrl.msg = "Page1 Controller Property";
    page1Ctrl.param = $routeParams['param'];
});
app.controller("Page2Controller", function($routeParams){
    var page2Ctrl = this;
    page2Ctrl.msg = "Page2 Controller Property";
    page2Ctrl.param = $routeParams['param'];
});
</script>
<body ng-app="myApp">
 <a href="#Page1/param1">Page1</a>
 <a href="#Page2/param2">Page2</a>

 <div ng-view></div>
</body>
</html> 

- Page1.html
<p>{{page1Ctrl.msg}}</p>
<p>{{page1Ctrl.param}}</p>

- Page2.html
<p>{{page2Ctrl.msg}}</p>
<p>{{page2Ctrl.param}}</p>

URL에서 매개변수 정보를 추출하기 위해 보다 직관적인 $routeParams를 사용했지만, 다음과 같이 $route를 이용해도 동일하게 처리 가능하다

app.controller("Page1Controller", function($route){
 var page1Ctrl = this;
    page1Ctrl.msg = "Page1 Controller Property";
    page1Ctrl.param = $route.current.params['param'];
});


* 로딩 중 이미지 표시하기
- 이쯤되면, 왠만한 웹개발자들은 한가지 부가기능이 필요하다는 것을 느낄 것 이다. 바로 '로딩 중' 처리이다. 라우팅을 통해 HTML페이지를 내부적으로 요청할 때 지연이 발생하지 않는다는 보장을 할 수 없다. 페이지 로딩의 지연은 페이지 자체의 크기(무게) 때문일 수도 있고, 인터넷 네트워크 통신지연 때문일 수도 있다. 또한 라우팅 시 뷰가 표시되기 전에 반드시 먼저 선행되어야 하는 라이트 의존성 작업이 클 경우에도 지연이 발생할 수 있다.

UX 측면에서 보면, 발생 가능한 정상적 지연상황을 사용자에게 알려주는 것이 좋다. 즉 우리가 흔히들 사용하는 '로딩중' 이미지를 표시해 주면 좋을 것이다.

AngularJS 라우트에서는 이러한 처리를 지원하기 위해 라우트 이벤트를 제공한다. 라우트 이벤트를 통해 라우트가 시작되는 시점과 종료하는 시점에 특별한 작업을 추가할 수 있다. 우리는 여기에 로딩중 이미지를 표시해 볼 것이다.

먼저 다음과 같이, 메인모듈의 run 메서드를 작성한다. 로딩중 이미지는 응용프로그램 전역적으로 적용되어야 하는 기능이기 때문에 모듈의 run 메서드를 이용한다.

app.run(function($rootScope){
 $rootScope.$on('$routeChangeStart', function(e, curr, prev){
   $rootScope.IsLoading = true;
 });
 $rootScope.$on('$routeChangeSuccess', function(e, curr, prev){
   $rootScope.IsLoading = false;
 });
});

$rootScope 객체를 통해, routeChangeStart 이벤트와 routeChangeSuccess 이벤트 리스터를 등록한다.
이 이벤트 핸들러 함수들은 이벤트 객체와 현재페이지, 이전페이지 객체가 매개변수로 제공된다.
우리는 단순히 로딩중 이미지만 표시하면 되기에 특별히 매개변수를 사용하지는 않지만, $rootScope 객체에 IsLoading라는 임의의 속성을 추가시킨다. 이렇게 하면 응용프로그램의 뷰 어느 영역에서든 IsLoading 속성에 접근할 수 있게 된다.

다음으로, HTML 내용을 다음과 같이 작성한다. 로딩중 이미지가 표시될 영역을 추가하고 ng-class에 해당 CSS 클래스와 앞서 추가한 $rootScope 객체의 Loading 속성 값(true or false)을 매칭시킨다.

<body ng-app="myApp" ng-class="{loading:IsLoading}">
 <a href="#Page1/param1">Page1</a>
 <a href="#Page2/param2">Page2</a>

 <div ng-view></div>
 <div class="loadingModal"></div>
</body>
</html>

마지막으로, CSS를 다음과 같이 작성한다. loader.gif 이미지는 미리 준비해 둔다.

body.loading {
    overflow: hidden;
}
body.loading .loadingModal {
    display: block;
}
.loadingModal {
    display: none;
    position: fixed;
    z-index: 1000;
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
    background: rgba(244, 249, 251, 0.80) url(loader.gif) 50% 50% no-repeat;
}

다음은 로딩중 샘플의 전체 코드이다.

<!DOCTYPE html>
<html>
<style>
body.loading {
    overflow: hidden;
}
body.loading .loadingModal {
    display: block;
}
.loadingModal {
    display: none;
    position: fixed;
    z-index: 1000;
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
    background: rgba(244, 249, 251, 0.80) url(loader.gif) 50% 50% no-repeat;
}
</style>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular-route.js"></script>
<script>
var app = angular.module("myApp", ["ngRoute"]);
app.config(function($routeProvider) {
    $routeProvider   
    .when("/Page1/:param", {
        templateUrl : "Page1.html",
        controller : "Page1Controller",
        controllerAs: "page1Ctrl"
    })
    .when("/Page2/:param", {
        templateUrl : "Page2.html",
        controller : "Page2Controller",
        controllerAs: "page2Ctrl"
    });
});
app.controller("Page1Controller", function($route){
 var page1Ctrl = this;
    page1Ctrl.msg = "Page1 Controller Property";
    page1Ctrl.param = $route.current.params['param'];
});
app.controller("Page2Controller", function($routeParams){
 var page2Ctrl = this;
    page2Ctrl.msg = "Page2 Controller Property";
    page2Ctrl.param = $routeParams['param'];
});

app.run(function($rootScope){
 $rootScope.$on('$routeChangeStart', function(e, curr, prev){
  $rootScope.IsLoading = true;
 });
 $rootScope.$on('$routeChangeSuccess', function(e, curr, prev){
  $rootScope.IsLoading = false;
 });
});
</script>
<body ng-app="myApp" ng-class="{loading:IsLoading}">
 <a href="#Page1/param1">Page1</a>
 <a href="#Page2/param2">Page2</a>

 <div ng-view></div>
 <div class="loadingModal"></div>
</body>
</html>


* 페이지 호출 대신, 내부 템플릿 사용
- 대부분 복잡한 사용자화면은 HTML를 분리시키고 라우트를 통해 페이지 호출하는 형태로 작성될 것이다. 그러나 간단한 내용이라면 굳이 HTML 페이지로 분리시키지 않고 바로 HTML값을 템플릿으로 지정할 수 있다. templateUrl 대신 template를 사용하면 된다.

 

* ngRoute 모듈의 한계
- 지금까지 AngularJS의 ngRoute 서브모듈을 이용해 라우팅 기능을 구현해 보았다. ngRoute 모듈은 라우팅을 위해 AngularJS에서 제공하는 기본 모듈로 한 페이지에 오직 한 개의 ng-view 디렉티브만 선언가능하다. 이는 내부적으로 라우트와 뷰 사이에 일대일 관계만이 성립되는 제약때문이다.

만일 여러개의 뷰가 중첩되거나 복잡한 레이아웃(다중뷰, 중첩뷰 등)으로 구성된 페이지에서 고릅 라우팅을 구현하고자 할 경우에는, ui-router 라이브러리를 이용할 것을 권장한다. 

- https://github.com/angular-ui/ui-router

 

'모바일 > Javascript' 카테고리의 다른 글

[AngularJS] REST API 통신(with Node.js)  (0) 2016.07.15
[Node.js] socket.io를 활용한 웹채팅 구현  (5) 2016.07.15
[AngularJS] Directive  (0) 2016.07.13
[AngularJS] $scope과 controller-as 문법  (0) 2016.07.12
[AngularJS] Module  (0) 2016.07.12

[AngularJS] Directive

Posted in 모바일/Javascript // Posted at 2016. 7. 13. 15:16
728x90

AngularJS는 정적인 HTML을 확장하여 동적인 응용프로그램으로써의 웹을 구현할 수 있도록 도와주는 자바스크립트 프레임워크이다.

MVC(MVVM) 아키텍처 기반, 양방향 데이터바인딩, SPA 개발, 모듈화 프로그래밍, 라우팅 시스템 등 단순한 웹 페이지를 응용프로그램으로 승격시키기 위한 다양한 기반 기술을 제공하는데, 이 중 Directive(디렉티브)는 HTML 요소 즉 뷰(View)에 대한 확장기능을 제공하는 AngularJS의 중요한 기능 중 하나다.

Directive를 사용하면, 자신의 응용프로그램에 최적화된 뷰와 기능을 캡슐화하여 재사용 가능한 컴포넌트를 만들 수 있다. 즉 Directive를 통해 HTML 태그 및 특성을 입맛에 맞게 재정의 가능하며 이를 통해 '사용자 정의 뷰'를 제작 및 필요에 의해 재사용할 수 있게 된다.

* AngularJS 내장 디렉티브
- AngularJS 내부적으로 HTML 확장을 위해 많은 디렉티비를 정의하고 있다. AngularJS 적용을 위해 HTML Element에 추가로 부여했던 특성들, na-app, ng-init, ng-model 등이 대표적 내장 디렉티브이다.

내장 디렉티비의 목록은 다음 url에서 확인 가능핟.
- http://www.w3schools.com/angular/angular_ref_directives.asp

 

* 사용자 정의 디렉티브
- AngularJS 디렉티브를 사용해서 HTML을 확장시킬 수 있다고 했는데, 먼저 간단한 예제부터 살펴보자

<!DOCTYPE html>
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script>
var app = angular.module("myApp", []);
app.directive("userdirective", function() {
    return {
        template : "<h1>사용자 정의 디렉티브</h1>"
    };
});
</script> 
<body ng-app="myApp">

   <userdirective></userdirective>

</body>
</html>

가장 기본적인 형태로 구현된 코드형태이지만, 너무나도 간단하다.
디렉티브 역시, AngularJS의 Module에 추가하는 형태로 구현되며, 사용자 구미에 맞는 새로운 HTML Element를 정의하고 페이지에서 새로운 요소를 사용하고 있다.

기존에 정해진 HTML 요소 외에, 사용자가 원하는 이름을 새롭게 정의하고 사용할 수 있다는 측면에서 AngualrJS가 HTML을 '도메인 특화 언어(domain-specific language)'로써의 기능을 부여했다고 할 수 있다.

* 디렉티브 Naming
- 여기서 한가지 주의할 점이 있는데, 디렉티브 이름을 카멜표기법(단어와 단어사이의 첫 글자를 대문자료 표기)으로 지정했을 땐, 각 단어연결문자로 '-'를 사용해야 한다는 것이다. 즉 다음과 같이 user와 Directive사이에 '-' 문자로 분리해야 한다.

app.directive("userDirective", function() { ... }
...
<user-Directive></user-Directive> 

 

* 디렉티브 구현 문법
- 앞서 예제에서는 가장 단순한 형태의 디렉티브 사용법을 보여준 것이다. 디렉티브는 다음과 같이 세 가지 구성요소를 기반으로 동작한다.

1) 디렉티브 정의 객체(DDO, Directive Definition Object)
- 디렉티브 구현을 위한 필수요소로, 디렉티브의 HTML 템플릿, scope, 컴파일 주기 등을 설정한 객체를 정의한다. 앞서 예제에서도 리터럴 객체를 리턴하고 있는데, 이것이 바로 DDO이다.

2) 컨트롤러
- AngularJS의 컨트롤러와 동일한 방식으로 동작한다. 디렉티브를 위한 상태와 기능을 정의하고 외부와의 각종 상호작용을 구현한다. DDO와는 달리 반드시 컨트롤러를 정의해야 하는 것은 아니지만, 제대로 된 실무용 디렉티브에서는 필수로 구현해야 할 것이다.

3) 링크
- 디렉티브에서 정의한 DOM을 조작하는 코드를 구현한다. DOM 요소에 발생하는 이벤트를 가로채서 각종 처리를 수행한다. DDO와는 달리 선택요소로 반드시 구현하지 않아도 된다.

이와 같은 구성요소들이 포함된 디렉티브 정의를 위한 뼈대를 살펴보자.

var app = angular.module("myApp", []);
//디렉티브 생성
//필요한 경우, 생성자 함수에 서비스를 주입해서 사용
//ex: function($rootScope, myService, ..., ) .. 이후 컨트롤러에서 사용가능
app.directive("userdirective", function() {
    var linker = function(scope, element, attrs){
        //DOM 조작에 관한 코드를 구현한다
        //scope: 현재 작업중인 디렉티브에 할당된 scope객체. 
        //컨트롤러함수의 $scope와 동일. 이를 통해 여기서 컨트롤러 함수 호출 가능
        //element: 디렉티브가 선언된 요소에 대한 참조를 jQuery 객체로 감싸서 제공
        //attr: 디렉티브가 선언된 요소에 정의된 모든 특성을 배열로 제공 

    };

    var controller = function($scope){
       //디렉티브의 뷰에서 사용할 비즈니스 로직(상태와 메서드) 구현
       //링크함수와 동일한 scope객체 공유
    };

    return { //DDO객체
       restrict: 'A', //이 디렉티브를 HTML요소의 특성(attribute)으로만 사용하도록 제한
       controller: controller, //디렉티브의 컨트롤러함수 지정
       controllerAs: 'userdirective' //userdirective라는 이름으로 controller 참조 
       link: linker //링크함수 지정
     
  //이 디렉티브가 HTML Element로 사용될 경우 다음과 같이 템플릿 지정

       //template : "<h1>사용자 정의 디렉티브</h1>" //뷰를 위한 HTML 템플릿 지정
    };
});

DDO(디렉티브정의객체)에 보면 restrict 속성이 있는데 이것은 해당 디렉티브를 HTML에서 사용할 때 특정 용도로 제한하기 위해 사용한다. 코드예제에서는 'A'라고 지정했는데 이는 HTML요소의 특성(Attribute)로만 이 디렉티브를 사용하도록 제한한 것이다. 다음은 restrict로 지정가능한 옵션이다.

그리고 template로 HTML을 직접 작성했는데, 외부에 이미 존재하는 html 파일을 지정할 수도 있다.
이때는 template 대신 templateUrl: 'directive.html' 형태로 사용한다.

 

* 사용자 정의 디렉티브 샘플
- 이제 디렉티브의 구성요소를 대부분 사용해서 만들어보자

<!DOCTYPE html>
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script>
var app = angular.module("myApp", []);

app.directive("mybutton", function(){
    var linker = function(scope, element, attrs){   
       //마우스 over/out 이벤트에 반응하여 투명도를 조절한다    
       element.bind('mouseover', function(e) {
         element.css({'opacity': 0.5});       
       });
       element.bind('mouseout', function(e) {
         element.css({'opacity': 1.0});       
       });
    };

    var controller = function($scope){
       var ctrlMyBtn = this;
       ctrlMyBtn.text = "My Button";
       ctrlMyBtn.getText = function(){
           alert(ctrlMyBtn.text);
       }
    };

    return {
       restrict: 'A', 
       controller: controller,
       controllerAs: 'ctrlMyBtn',
       link: linker
    };
});
</script>
<body ng-app="myApp">
 <button mybutton ng-click="ctrlMyBtn.getText();">
   {{ctrlMyBtn.text}}
 </button>
</body>
</html>

- 예제코드가 그다시 쓸모있는 디렉티브라고는 할 수 없지만, 디렉티브의 구성요소가 어떤식으로 상호작용하여 동작 하는지 파악하기에는 더없이 좋을 것이다.

HTML Element의 특성으로만 사용가능한 디렉터리를 만들고 마우스 오버/아웃에 따른 DOM 투명도 조작과 컨트롤러에 정의된 속성과 메서드와 상호작용하는 예를 보여준다.

결과화면은 다음과 같다.

 

 

* 참고자료
- https://docs.angularjs.org/guide/directive 
-
http://www.w3schools.com/angular/angular_directives.asp

 

'모바일 > Javascript' 카테고리의 다른 글

[Node.js] socket.io를 활용한 웹채팅 구현  (5) 2016.07.15
[AngularJS] Route  (0) 2016.07.14
[AngularJS] $scope과 controller-as 문법  (0) 2016.07.12
[AngularJS] Module  (0) 2016.07.12
[AngularJS] Two-way Data Binding  (0) 2016.07.10

[AngularJS] $scope과 controller-as 문법

Posted in 모바일/Javascript // Posted at 2016. 7. 12. 20:22
728x90

* Scope 객체
AngularJS의 $scope은 뷰(View)와 컨트롤러(Controller)를 연결하는 객체이다.
HTML 페이지(뷰)는 자기자신이 포함된 컨텍스트의 $scope객체를 통해 컨트롤러에서 관리하는 데이터와의 양방향 동기화가 가능하며 컨트롤러가 제공하는 메서드도 호출할 수 있게 된다.


- 출처: https://github.com/Capgemini/ngTraining/wiki/AngularJS-Templates-101 

그림에서 보는것과 같이, Scope은 비지니스 로직과 사용자 인터페이스간 연결을 담당하며 Controller는 Scope에 속성과 메서드를 추가하여 뷰에 노출시킨다.

scope 객체를 통해 컨트롤러와 뷰가 상호작용하는 간단한 코드를 보자.

<!DOCTYPE html>
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script>
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope) {
    $scope.name = "Park";
    $scope.changeName = function(){
       $scope.name = "Kim";
    }
});
</script>
<body>
 <div ng-app="myApp" ng-controller="myCtrl">
   <h1 ng-click="changeName();">{{name}}</h1>
 </div>
</body>
</html>

컨트롤러에서 $scope객체에 name속성을 추가하고 값을 할당하였다. 또한 changeName이라는 메서드를 추가하고 name값을 변경하는 코드를 작성했다.
그리고 HTML 페이지에서는 {{name}} 표현식을 사용하여 scope에 정의된 name값을  출력하고 값이 클릭되면 changeName() 함수를 호출하도록 하였다.

이렇듯 컨트롤러에서 $scope 객체를 통해 속성과 함수를 노출하면 뷰에서는 이와 상호작용 할 수 있게 되는 것이다.

* controller-as 문법
AngularJS 1.3부터 추가된 것으로, 뷰에서 컨트롤러를 선언하는 방식에 대한 문법이다.
기존까지는 ng-controller="controllerName" 이라고 선언하는 반면, 이 문법을 사용하면
ng-controller="controllerName as ctlName"과 같이 선언하는 방식이다. as 뒤에 오는 것은 일종의 별칭으로 뷰에서 ctlName이라는 키워드를 통해 컨트롤러에 접근할 수 있게 되는 것이다.

다음 코드는 앞선 예제에서 $scope 객체를 사용하는 대신, controller-as 문법으로 교체한 것이다.

<!DOCTYPE html>
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script>
var app = angular.module('myApp', []);
app.controller('myCtrl', function() {
    var main = this;
    main.name = "Park";
    main.changeName = function(){
       main.name = "Kim";
    }
});
</script>
<body>
 <div ng-app="myApp" ng-controller="myCtrl as main">
   <h1 ng-click="main.changeName();">{{main.name}}</h1>
 </div>
</body>
</html>

controller-as 문법의 사용으로 더 이상 컨트롤러 생성자 함수로 $scope 객체를 주입하지 않아도 된다. 그리고 HTML페이지(뷰)에서는 main이라는 이름으로 컨트롤러에 접근할 수 있게 되었다.

내부적으로는 controller-as 문법으로 정의한 컨트롤러의 이름은 여전히 $scope 객체 내부에 해당 일므의 변수로 추가된다.

그리고 참고로, 컨트롤러 코드에서 var main = this 하고 한 것은 필수 사항은 아니다. 단지 자바스크립트이 함수 컨텍스트 상의 this의 의미 모호성을 제거하기 위해 main이라는 임의의 이름으로 this(자기자신)를 미리 할당한 것이다. 이때 이왕이면 뷰에서 사용하는 이름과 똑 같이 하면 가독성이 좋아질 것이다.

사실 AngularJS 팀이 $scope 대신 controller-as 문법을 사용하기를 권장하고 이 문법을 추가한 것은 두 가지 이유 때문이다.
1. 뷰가 참조하는 컨트롤러의 모호성 제거
- $scope 객체를 통해 추가시킨 속성이나 메서드를 뷰에서 접근할 경우, 단지 속성명(메서드명)만 명시하면 된다. 이는 여러 컨트롤러가 복합적으로 사용되는 환경일 경우, 마크업에서 바인딩에 사용한 속성이 정확히 어디에 정의된 것인지에 대한 모호함을 유발시키며 이는 곧 코드의 가독성 및 유지보수성을 저해시킨다.

2. 묵시적 scope 상속의 나쁜 습관 방어
- 모든 scope 객체는 자신의 직계부모로부터 $rootScope에 이르기까지 계층 구조상 모든 부모 객체의 프로토타입을 상속받는다. 뷰에서 scope의 속성이나 메서를 참조할 경우 자신이 속한 컨텍스트의scope에 해당 속성(메서드)가 존재하지 않을 경우, scope 프로토타입 체인을 거슬러 올라가며 부모 객체를 계속 탐색해 나간다. 이런 동작은 프로토타입 기반의 상속 매커니즘을 가진 자바스크립트의 매커니즘과도 일치하며, 어떤 경우에는 편한 기능이기도 하다. 다만 게으른 개발자라면 부모 scope에 존재하는 속성이나 메서드를 그대로 사용하려고 할 것이며, 여러 곳에서 응용프로그램 전체에 걸쳐 공유되는 데이터에 접근하여 마구잡이로 변경하면 예측하지 못하는 문제들이 발생한다. 

controller-as 문법은 더 나은 코딩을 위한 권고사항일 뿐이다. 우리는 여전히 $scope를 사용할 수 있다.

AngularJS 팀은 뷰를 더욱 명확하게 구현할 수 있고, 컨트롤러로부터 $scope에 대한 의존성을 제거할 수 있으므로 이 문법을 사용할 것을 권장하고 있다.


* Scope Hierarchies(Scope의 계층)

- 모든 AngularJS 응용프로그램은 하나의 root scope를 가지며, 이 root scope를 기준으로 컨트롤러나 디렉티브 내의 scope가 DOM 구조의 계층구조와 유사하게 child scope로 계층화 된다.
(단, DOM 계층구조가 모든 scope 계층구조를 결정하는 것은 아님에 주의하자)

즉 하나의 root scope와 하나 이상의 chind scope 형태의 계층구조로 이뤄지며, 이런 구조는 뷰가 scope 객체를 참조할 경우 자동으로 scope 프로토타입 체인을 거슬러 올라가며 탐색하도록 동작한다.

다음의 코드는 Scope의 계층구조 구성을 보여주며, 하위 계층에서는 상위계층의 속성에 접근할 수 있다는 것을 보여주고 있다.

<!DOCTYPE html>
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script>

var app = angular.module('myApp', [])
app.controller('ctrl_A', function($scope, $rootScope) {
    $scope.name = 'name-A';
    $scope.nameA= 'name-A-only';
    $rootScope.masterName = 'master';
});

app.controller('ctrl_B', function($scope) {
    $scope.name = 'name-B'
    $scope.nameB = 'name-B-only'

});

</script>
<body>
 <div ng-app="myApp">
  <div ng-controller="ctrl_A">
    <h1>Controller A</h1>
      {{name}}, {{nameA}} , {{nameB}} {{masterName}}
    <div ng-controller="ctrl_B">
       <h1>Controller B</h1>
       {{name}}, {{nameA}} , {{nameB}}, {{masterName}}
    </div>
  </div>
 </div>
</body>
</html>

결과는 다음과 같다.
B컨트롤러는 A컨트롤러 하위계층에 속하며, B컨트롤러에 정의되지 않은 nameA속성과 rootScope의 masterName 속성에 접근할 수 있다. 반면 A컨트롤러는 B컨트롤러에 정의된 nameB 속성에 접근하지 못한다. 이로써, scope는 계층구조로 구성되며(묵시적으로 상속된다) 하위 scope는 자신에게 정의되지 않은 속성(메서드)를 찾기 위해 상위계층으로 거슬러 올라가며 자동으로 탐색한다는 것을 알 수 있다.

 

이번엔, 앞의 코드를 controller-as 문법을 사용하여 수정해 보자

<!DOCTYPE html>
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script>

var app = angular.module('myApp', [])
app.controller('ctrl_A', function($rootScope) {
    var ctrlA = this;
    ctrlA.name = 'name-A';
    ctrlA.nameA= 'name-A-only';
    $rootScope.masterName = 'master';
});

app.controller('ctrl_B', function() {
    var ctrlB = this;
    ctrlB.name = 'name-B'
    ctrlB.nameB = 'name-B-only'
});

</script>
<body>
 <div ng-app="myApp">
  <div ng-controller="ctrl_A as ctrlA">
    <h1>Controller A</h1>
      {{ctrlA.name}}, {{ctrlA.nameA}} , {{ctrlB.nameB}}, {{masterName}}
    <div ng-controller="ctrl_B as ctrlB">
       <h1>Controller B</h1>
       {{ctrlB.name}}, {{ctrlA.nameA}} , {{ctrlB.nameB}},  {{masterName}}
    </div>
  </div>
 </div>
</body>
</html>

코드의 결과는 앞의 $scope 객체를 사용한 것과 동일하다. 하지만 뷰에서 참조하는 속성(메서드)이 정확히 어떤 컨트롤러에 정의된 것인지는 더욱 분명해 졌다.

* 다이제스트 주기(Digest cycle)
- AngularJS는 모델과 뷰의 내용을 자동으로 동기화 시킨다. 그 중심에 scope가 있다. scope는 MVVM 아키텍처 패턴에서 ViewModel에 해당하는 핵심 객체로 이해해도 무방하다.

AngularJS는 scope에 저장된 값이 변경되면 자동으로 뷰도 갱신시킨다. 이것은 AngularJS를 강력하게 만든 양방향 동기화이다. 그런데 이와같은 자동 동기화는 어떤 메커니즘으로 구현될 것일까?

AngularJS는 다이제스트 주기라는 순환적 매커니즘 하에 모델과 뷰를 자동으로 동기화 시킨다.
이 동작은 더티체크(dirty check)라는 개념 위에 구현된 것이며, 이것은 단순히 angular.equals 메서드를 이용하여 속성값이 이전 값과 현재 값을 비교하여 동기화 여부를 결정하는 것이다.

더티 체크: 단순히 현재 값을 이전 값과 비교하여 값이 변경되었으면 이벤트를 발생시키는 과정

다음 그림은 AngularJS 공식 사이트에 게제된 다이제스트 주기를 표현한 그림이다.

아래 설명은 AngularJS in action의 설명을 요약(조금 수정)한 것이다.

AngularJS는 $digest 객체가 관리하는 다이제스트 주기를 통해 더티 체크를 수행한다. 만일 다이제스트 주기를 수동으로 동작시키려면 $apply객체를 사용하면 된다. 이 객체는 $digest 객체를 호출하며 오류처리 메커니즘을 함께 탑재하고 있다.

AngularJS는 컴파일 과정을 거치면서 $scope 객체에 정의된 모든 속성에 대한 감시(watch) 표현식을 생성한다. 감시 표현식을 통해 $scope객체의 속성이 변경된 것을 알게 되면 리스너 함수가 호출된다.

간혹 AngularJS가 속성값이 변경되었다는 사실을 알아내지 못하는 경우도 있다. 이런 경우 $apply 객체를 통해 다이제스트 주기를 직접 실행할 수 있다. 대부분 API 호출이나 서드 파트 라이브러리가 수행한 작업을 AngualrJS에게 알리고자 할 때 이 기법을 활용한다.

AngularJS의 자동 바인딩 메커니즘의 개략적인 설명이며, 보다 자세한 이해를 위해서 다음의 블로그에 정리된 글을 읽어 보기를 권장한다.

=> http://www.nextree.co.kr/p8890/

설명이 아주 쉽고 자세히 잘 되어 있으며, 특히 실무에서 자주 맞주칠 수 있는 3rd Party 라이브러리 사용시 자동 바인딩을 위한 $scope.$apply() 함수 사용부분을 빼먹지 않고 봐두기를 바란다.

AngularJS는 기본적으로 위에서 설명한 메커니즘 하에, scope 속성이 변경되면 뷰에 자동으로 변경을 반영시킨다. 하지만 AngularJS의 관리 밖의 영역에서 발생한 이벤트는 인식하지 못하기 때문에 scope 속성값의 변화를 감지하지 못하게 된다. 따라서 $scope.$apply() 함수를 이용해 수동으로 다이제스트 주기를 시작하도록 할 수 있다.

'모바일 > Javascript' 카테고리의 다른 글

[AngularJS] Route  (0) 2016.07.14
[AngularJS] Directive  (0) 2016.07.13
[AngularJS] Module  (0) 2016.07.12
[AngularJS] Two-way Data Binding  (0) 2016.07.10
[AngularJS] MVC Pattern in Angular  (0) 2016.07.10

[AngularJS] Module

Posted in 모바일/Javascript // Posted at 2016. 7. 12. 13:57
728x90

AngularJS의 Module(모듈)은 평범한 HTML 페이지를 AngularJS 응용프로그램으로 확장시키기 위한 진입점이다. AngularJS는 Module을 통해 controller, service, filter, directive 등을 추가하는 방식으로 동작한다.

또한 전역네임스페이스를 오염시키지 않는 전형적인 모듈패턴을 지향하며 나아가 모듈화 프로그래밍을 가능케 하는 아키텍처링 방법으로 설계되었다.

AngularJS 모듈을 그 역할 관점에서 다음과 같이 다양하게 정의해 볼 수 있을 것이다.

- HTML 페이지를 AngularJS 응용프로그램으로 확장시키기 위한 진입점

- HTML 페이지와 AngularJS가 상호작용하기 위한 매개체

- 연관된 각종 컴포넌트를 포함하는 컨테이너

- AngularJS 응용프로그램의 구성을 논리적으로 조직화하는 단위

- 모듈화 프로그래밍을 지원하는 AngularJS의 아키텍처 지원 기능

 
* Module 생성과 로딩
- 모듈 생성과 로딩은 모두 angular.module 함수를 통해 이뤄진다. 첫 번째 매개변수는 모듈 이름을 지정하며 두 번째 매개변수는 해당 모듈과 연관된 서브모듈을 배열으로 지정할 수 있다.
서비모듈 지정은 모듈을 생성할 때만 지정 가능하다.

//모듈 생성
angular.module("myApp", []);

//생성한 모듈 가져오기
var app = angular.module("myApp");


* HTML페이지를 AngularJS 앱으로 확장시키기
- 이렇게 생성한 모듈을 HTML 페이지의 특정 Element에 지정하면 그 Element하위 요소들은 AngularJS 응용프로그램으로 동작가능하게 된다. 이때 ng-app 지시자를 통해 해당 앱이 사용할 모듈이름을 지정한다. (참고로 한 페이지에 모듈은 하나만 지정이 가능한데 만일, 두 개 이상의 서로다른 모듈을 지정한 경우 HTML 페이지 기준으로 가장 상단에 지정된 모듈만이 정상 동작할 것이다.)

<script>
 var app = angular.module("myApp", []);
</script>
<div ng-app="myApp">...</div>

 

* 모듈에 컴포넌트 추가하기
- 모듈은 연관된 각종 컴포넌트를 포함하는 컨테이너 역할을 하며, 이렇게 필요에 의해 추가된 컴포넌트들을 이용하여 HTML페이지를 동적인 MVC 어플리케이션으로 동작시킬 수 있다.

다음 그림은 모듈을 통해 구성가능한 컴포넌트를 보여준다

(출처: http://www.c-sharpcorner.com/article/module-and-controller-in-angularjs/)

실제로 AngularJS 학습의 대부분은 그림에서 보는 것과 같은 컴포넌트의 사용법을 익히는 것이다. 각 컴포넌트의 상세한 설명은 개별 주제를 다룰 때 보기로 하고 여기서는 대략 다음과 같은 코드로 모듈에 필요한 컴포넌트를 추가할 수 있다는 것만 알고 넘어가자

var app = angular.module("myApp", []);   //모듈 생성

app.controller("MyCtrl", function() { } ); //모듈에 컨트롤러 정의

app.service("MySvc", function() { } ); //모듈에 서비스 정의

app.directive("MyDir", function() { } ); //모듈에 디렉티브 정의

패턴은 거의 동일하다. 각 컴포넌트에 해당하는 메서드를 호출하고 컴포넌트의 이름과 익명함수를 전달하는 방식으로 동작한다. 모듈 메서드는 다음 그림을 참고하기 바란다.

참고) 모듈의 로딩(설정블럭과 실행블럭)
- AngularJS의 모듈 로딩은 설정단계와 실행단계라는 두 단계를 거치며 각 단계를 위한 코딩블럭이 제공된다.

(설정블럭, Configuration blocks)
- 모듈 로딩의 첫 단계로, 모든 프로바이더를 연결하고 등록한다. 오직 프로바이더와 상수만이 설정블록에 주입될 수 있다.

(실행블럭, Run blocks)
- Injector가 생성된 후에 실행되며, 응용프로그램을 시동하는데 사용된다. 이 시점 이후에 추가적인 시스템 설정이 이뤄지는 것을 방지하기 위해, 실행블록에는 오직 인스턴스와 상수만을 주입할 수 있다.
 

AngularJS는 응용프로그램을 실행할 때, 해당 모듈의 설정영역을 먼저 실행한 후 실행영역을 실행시킨다. 실행영역은 일종의 main 메소드 같은 역할로 모든 서비스가 설정되고 인젝터가 생성된 후에 실행된다.

설정블럭과 실행블럭에 대한 자세한 내용은 다음 블로그를 참고하기 바란다.
- AngularJS의 설정단계(configuration phase)와 실행단계(run phase)


* 모듈화 프로그래밍(서브 모듈 사용하기)
- AngularJS의 Module은 응용프로그램의 구성을 논리적으로 조직화하는 단위로써도 역할을 한다. 아마 이것이 AngularJS Module의 핵심가치일 것이다.
이로써 모듈화 프로그래밍이 가능해 지는 것인데, 모듈화를 하면 연관된 것끼리는 묶고 서로 다른 것끼리는 분리해서 조직화 가능하며 필요한 모듈을 그때그때 조합해서 하나의 전체 응용프로그램을 완성시킬 수 있다. 이 역시 '관심사의 분리'라는 대원칙이 적용된 SW설계 기법이며 모듈화가 잘 이뤄진 프로그램은 재사용성/생산성/유지보수성이 향상되고 테스트용이성도 높아진다.
(공학적으로 모듈화가 잘 되었다는 것은 '응집도'는 높고 하게 '결합도'는 낮게 구성하는 것이다)

모듈화의 가치는 응용프로그램의 규모에 따라 달라진다.
아주 작고 간단한 프로그램 환경에서는 모듈화가 복잡성만 증가시키는 별 필요없는 기법이 된다.
하지만 프로그램의 크기가 상당하고 여러명의 개발자가 참여하는 개발환경이라면 모듈화는 거의 필수적인 기법이라 할 수 있다.

 간단한 시나리오를 가정해 보면, ToDo 리스트를 관리하는 앱을 개발할 경우 다음과 같이 조직화 가능할 것이다.
- 메인모듈, 공용모듈, 사용자모듈, 인증모듈, ToDo 아이템 모듈, ....

순전히 개인적인 구분이지만, 프로그램의 성격과 규모에 따라 모듈화의 기준은 달라질 것이다.

다음 코드예제는 AngularJS Module을 이용해 모듈을 역할별로 분리하고 이를 조합하여 구성한 즉 모듈화 프로그램을 작성한 예이다.

먼저 MyApp이라는 메인 모듈을 생성하고 이 모듈이 의존하는 서브모듈(MyApp.User, MyApp.Common)을 배열로 전달한다.
MyApp.User 모듈은 사용자정보를 추상화한 모듈이며, MyApp.Common 모듈은 전체 응용프로그램에 공통적으로 사용될 상수를 정의한 모듈이다. 이 각각의 모듈은 메인모듈의 서브모듈로 등록되어 메인모듈의 컨트롤러의 생성자 함수에 서브모듈의 컴포넌트가 주입되는 방식으로 동작한다.

<!DOCTYPE html>
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>

<script>
var myApp = angular.module("MyApp", ["MyApp.User", "MyApp.Common"]);
myApp .controller("MainCtrl", function(userlist, APPLICATION_CONFIG){
 var main = this;
 main.users = userlist.getUsers();
 main.appConfig = APPLICATION_CONFIG;
});

var userModule = angular.module("MyApp.User", []);
userModule.service('userlist', function(){ 
   var user = this;
   var mock = [
       {name: 'Park', city: 'Pusan'},
       {name: 'KIM', city: 'Seoul'},      
     ];    
   user.getUsers = function(){
     return mock;
   };
})

var commonModule = angular.module("MyApp.Common", []);
commonModule.constant("APPLICATION_CONFIG", {config1: "value1", config2: "value2"});

</script>

<body>
<div ng-app="MyApp" ng-controller="MainCtrl as main">
<h1>Users</h1>
  <ul>
     <li ng-repeat="user in main.users">
        {{user.name + ', ' + user.city}}
     </li>
   </ul>
   <div>{{ main.appConfig.config1 + ", " + main.appConfig.config2 }}</div>
</div>
</body>
</html>

 

* AngularJS 내장 모듈과 3rd-Party 모듈
- 앞서 모듈화 코드에서 구현한 모듈은 일종의 사용자 정의 모듈이다. 즉 개발자가 필요에 의해 임의로 생성한 모듈이다.
AngularJS는 유용한 기능을 미리 모듈화 시켜두고 이를 사용할 수 있도록 하고 있다. 이것을 AngularJS 내장모듈이라고 하는데 대표적으로 다음과 같은 것들이 있다.
- ngRoute: URL 라우팅 기능 제공
- ngAnimate: 애니메이션 기능 제공
- ngCookies: 웹 쿠키 기능 제공

더 많은 내장 모듈이 있지만, 차차 알아보기로 한다.

또한 인터넷의 제 3자에 의해 유용한 기능들을 모듈화하여 제공하기도 한다. 마치 플러그인 처럼 필요한 모듈을 가져다가 자신의 응용프로그램에 적용할 수 있다.

다음 사이트는 AngularJS 모듈을 검색하거나 새로운 모듈을 등록할 수 있다.
- http://ngmodules.org/

 

 * 참고자료
- https://docs.angularjs.org/guide/module
- http://www.w3schools.com/angular/angular_modules.asp

 

 

'모바일 > Javascript' 카테고리의 다른 글

[AngularJS] Directive  (0) 2016.07.13
[AngularJS] $scope과 controller-as 문법  (0) 2016.07.12
[AngularJS] Two-way Data Binding  (0) 2016.07.10
[AngularJS] MVC Pattern in Angular  (0) 2016.07.10
[AngularJS] 일주일 탐방기  (0) 2016.07.06

[AngularJS] Two-way Data Binding

Posted in 모바일/Javascript // Posted at 2016. 7. 10. 18:44
728x90

처음 AngularJS를 접했을 때 가장 눈에 띄는 특징이 바로 양방향 데이터 바인딩(Two-way Data Binding)이었다.

개인적으로, 자동 바인딩의 참신함은 knockout.js에서 그 놀라움을 처음 발견하였는데, 어느덧 더욱 발전된 형태의 양방향 자동바인딩을 AngularJS에서 어여쁘게 제공하고 있지 않은가...

이는 단연 AngularJS의 가장 큰 장점이며, 이 프레임워크를 채택하는데 있어, 가장 주요한 기준일 것이다.

AngularJS의 양방향 바인딩은, 응용프로그램의 '모델'과 이를 표시하는 '뷰'간의 자동 동기화를 지원하는 기능이다. 조금 다른 측면에서 설명하자만 자바스크립트 영역과 HTML 영역간의 데이터 동기화를 지원하는 것이다.

AngularJS 공식 사이트에서는 다른 전형적인 자바스크립트 템플릿 시스템과 AngularJS의 바인딩을 다음과 같이 그림으로 비교하고 있다.

Data Binding in Classical Template Systems

- 여타 프레임워크는 단방향 바인딩만 지원
- 동기화를 위한 추가코딩 필요

 Data Binding in Angular Templates

- AngularJS는 양방향 바인딩으로 동작
- 뷰와 모델간 동기화가 자동으로 유지됨

가장 보편적으로 사용되는 jQuery의 경우를 보면,
jQuery 탐색 기능을 사용하여 DOM에서 특정 HTML요소를 찾아내어 이벤트를 리스닝하고, DOM요소의 값을 파싱해서 이 값을 가지고 필요한 작업을 수행하고 했다.
AngularJS에서는 단순히 자바스크립트 속성을 정의하고 이것을 HTML에 바인딩만 하면 된다. 그것으로 끝이다.(by AngularJS in Action)

jQuery의 DOM 탐색, 이벤트 리스닝, 수동 동기화 코드로 가능한 바인딩 작업을 AngularJS에서는 극도로 단순화 시킨 것이다.

* 자동 데이터 바인딩

자동 데이터 바인딩을 위한 아주 간단한 코드를 한번 살펴보자

<!DOCTYPE html>
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<body>

<div ng-app="">
    <p>Input something in the input box:</p>
   <p>Name : <input type="text" ng-model="name" placeholder="Enter name here"></p>
   <h1>Hello {{name}} </h1>
</div>

</body>
</html>

모델과 뷰의 동기화를 위해 필요한 작업이 이것이 전부이다.
- ng-app 선언: 해당 영역이 AngularJS 응용프로그램으로 동작하도록 선언한다. 여기서는 모듈의 이름을 생략했는데, 실제 규모있는 앱을 제작하는 경우에는 적절한 이름을 지정하는 것을 권장한다.

- ng-model 지정: 모델로 사용할 값을 지정하는 것으로, 여기서는 input 박스에 name이라는 이름으로 모델을 지정했다. ng-model 디렉티브는 모델로부터 뷰로 데이터를 바인딩 시키기 위해 사용하며 이렇게 하면 AngularJS는 자동으로 $scope 객체의 속성으로 추가되어 뷰와 상호작용이 가능해진다.

- {{ }} 표현식: 모델로 지정한 값을 HTML로 바인딩하기 위한 표현식을 기술한다. 모델값이 변경되면 HTML에 표시된 값이 자동으로 즉각 반영될 것이다.

결과화면은 다음과 같댜. input박스의 값을 변경시키면 그 즉시 HTML에 표시된 값도 같이 변경된다.

 

* 양방향 (자동) 데이터 바인딩

그럼 이제, 좀더 일반적인 형태의 AngularJS 예제를 보자.
아래 코드는, AngularJS에 컨트롤러를 정의하고 이 컨트롤러를 통해 모델 정보에 접근하도록 했다.
이것이 보편적인 AngularJS 코딩 방법이다.

<!DOCTYPE html>
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script>
var app = angular.module('myApp', []);
app.controller('myCtrl', function() {
    main = this;
    main.name= "mkex";
//모델로 사용될 정보. HTML영역에 바인딩되어 자동 동기화 됨
   
    //자바스크립트 영역의 값의 변화를 보기 위한 메서드
    main.getName = function(){
      return alert(main.name);
    }
});
</script>
<body>
  <div ng-app="myApp" ng-controller="myCtrl as main">
    Name: <input ng-model="main.name">
    <h1>{{main.name}}</h1>
    <button ng-click='main.getName()'>check</button>
  </div>
</body>
</html>

input 박스의 데이터 변화는 HTML에 바인딩 된 값 뿐만 아니라, 자바스크립트 영역에 정의된 컨트롤러의 main.name 속성의 값도 자동으로 반영된다. 이것이 양방향 자동 바인딩 인 것이다.
(데이터 바인딩은 사용자가 속성값을 직접 조작할 수 있는 HTML 폼 같은 특별한 경우에는 양방향으로 동작한다 - by AngularJS in Action)

결과화면은 다음과 같다.

 

이 얼마나 극강의 생산성을 보여주는 기능인가? 자바스크립트 영역에 정의된 각종 데이터와 이 데이터를 투영하는 HTML요소의 자동 바인딩은 웹 응용프로그램의 무한한 가능성을 쉽게 시도할 수 있도록 할 것이다. 자바스크립트 객체만 잘 관리하면 HTML에 표시되는 것은 신경쓰지 않아도 되며 반대로 HTML로 부터 발생한 변경사항 역시 자바스크립트 객체로 바로 반영될 수 있어 SPA 실현은 쉬운 죽 먹기가 될 수 있다는 것이다. 놀라웁다~

* 단방향 데이터 바인딩(One-way Data Binding)
- 항상 그렇듯이, 훌륭한 기능도 필요치 않을 때가 종종 생긴다. AngularJS의 자동 양방향 데이터 바인딩은 진정 휼륭하지만, 어떤 경우에는 이 기능이 오버스펙일 경우가 있다.
또한 양방향 데이터 바인딩은 내부적으로 변경사항을 감시하는 메커니짐을 내포하고 있어 오버헤드가 생길 수 밖에 없다. (세상에 공짜는 없다)

이에, AngularJS 1.3부터는 one-time binding 이라는 일회성 바인딩 기능을 제공한다.
일회성 바인딩을 적용하기 위해 필요한 것은 단지, '더블클론(::)'의 지정 뿐이다.

이전의 예제에서 단방향 데이터 바인딩으로 변경해 보자.
아래 코드를 보면, 바뀐 것이라곤 고작 두 개의 콜론을 추가한 것 뿐이다. 이로써 최초 한번만 바인딩 되고 이후 변경사항은 HTML로 전파되지 않는다.

<!DOCTYPE html>
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script>
var app = angular.module('myApp', []);
app.controller('myCtrl', function() {
    main = this;
    main.name= "mkex";
  
    main.getName = function(){
      return alert(main.name);
    }
});
</script>
<body>
  <div ng-app="myApp" ng-controller="myCtrl as main">
    Name: <input ng-model="::main.name">
    <h1>{{::main.name}}</h1>
    <button ng-click='main.getName()'>check</button>
  </div>
</body>
</html>

결과를 보면 더욱 명확하다.
최초 mkex라는 자바스크립트 영역의 속성 값이 HTML요소로 동기화 되었으며 이후 input 박스의 변화는 더 이상 HTML요소로 동기화 되지 않는다. 
다만 alert 창을 보면 input 박스의 변경은 자바스크립트 속성의 변화만 일으킨다.

 

'모바일 > Javascript' 카테고리의 다른 글

[AngularJS] $scope과 controller-as 문법  (0) 2016.07.12
[AngularJS] Module  (0) 2016.07.12
[AngularJS] MVC Pattern in Angular  (0) 2016.07.10
[AngularJS] 일주일 탐방기  (0) 2016.07.06
knockout.js  (0) 2013.11.14

[AngularJS] MVC Pattern in Angular

Posted in 모바일/Javascript // Posted at 2016. 7. 10. 11:41
728x90

MVC(Model-View-Controller)는 아주 오래된 SW아키텍처패턴이다.
(이 패턴이 소개된 시기는 1979년으로, 길지 SW공학분야에서는 고전적인 패턴이라 할 수 있다.)

이 패턴은 백엔드개발자(웹서버 사이드 개발자)에게는 이미 익숙한 패턴일 것이다.

자바의 JSP(스트럿츠), 서블릿, Bean 등의 상호연동 구조가 MVC 모델에 기반하고 있으며, 닷넷은 자사 웹개발플랫폼 기술 이름 자체를 ASP.NET MVC로 하여 MVC 구조를 지향하고 있다.

그러니, 그들(웹서버 사이드 개발자)이 이 패턴에 익숙한 것은 당연한 것이다.

반면, 웹 프론트엔드 개발자에게는 조금 낯설수도 있다.
물론 자바스크립트를 코어하게 학습하거나, ExtJS/Sench Touch와 같은 MVC 모델에 기반한 프레임워크를 다뤄본 적이 있다면, 역시 익숙한 개념일 것이다. (그래.. 중요한 것은 익숙함의 차이인 것이다.)

이 패턴의 핵심은, 모델이 어쩌구.. 뷰가 어쩌구가 아니다. 바로 기저에 깔린 사상이다.
그 사상이 바로 '관심사의 분리(관점의 분리)를 통한 상호 독립성 보장'인 것이다.

웹 프론트개발자 관점에서도 '관심사의 분리'라는 사상 자체는 너무나도 자연스러운 개념일 것이다.
바로 HTML과 CSS 그리고 Javascript의 역할 분리를 빗대어 보면 알 수 있다.

흔히 이 셋의 역할을 'HTML(구조)-CSS(표현)-Javascript(동작)'로 정의한다. 이것이 바로 역할의분리, 다시말해 관심사의 분리이며 이렇게 분리된 구조를 통해 상호 독립적인 개발이 보장되어 생산성과 유지보수성 등이 좋아지게 되는 것이다.

MVC 모델 역시 이와같은 관심사의 분리라는 SW공학적 원리가 적용된 아키텍처 패턴인 것이다.

* MVC 패턴
- 대부분의 응용프로그램은 데이터가 사용되며 이 데이터를 기반으로 사용자 인터페이스를 제공한다. MVC패턴은 데이터를 조작하는 비즈니스 로직과 사용자인터페이스를 서로 분리하여 상호 영향 없도록 하는 것에 초점을 맞춘 패턴이다. 이 둘(데이터와 인터페이스)의 상호연동을 위해 컨트롤러라는 새로운 계층을 두고 있는 것이다.

- Model: 응용프로그램의 핵심 로직과 데이터를 캡슐화 수행
- View: 사용자가 보게 되는 인터페이스의 총칭. 모델의 정보를 표시하는 역할수행
- Controller: 모델과 뷰의 연결을 위한 계층으로 사용자 입력을 받아 중계 수행

MVC 패턴에 대한 이론적 내용은 많은 인터넷 자료에서 쉽게 찾아 볼 수 있으니 여기서는 그만 다루도록 한다. 혹여 MVC라는 용어가 어색한 웹 프론트 개발자들이 반드시 짚고 넘어가야 할 점은, MVC는 관점의 분리라는 공학적 원리가 응용프로그램의 구조와 상호작용 부분에 적용된 모범사례라는 것 뿐이다.

* Server Side MVC? Client Side MVC?
- 웹개발 측면에서 보면, MVC 패턴은 웹 서버 사이드 개발에서의 응용프로그램의 구조 패턴으로 먼저 보편화되었다고 할 수 있다. 즉 JSP나 ASP.NET과 같은 서버 측 응용구조로 채택되어 지금까지도 많이 활용되고 있는 패턴이다.

그러나 오늘날 진보적인 웹 클라이언트 개발에 훌륭한 자바스크립트 라이브러리들이 등장하고 이 들이 추구하는 아키텍처 기반 구조 역시 MVC에 근거하고 있다.

전통적인 서버측 개발에서의 View의 영역은 (최종 랜더링되는) HTML 페이지이다. 즉 Controller와 Model은 서버측에 존재하며 (서버가 사용자에게 반환하는) HTML페이지가 바로 View의 영역이 되는 것이다.

이 말은 웹 클라이언트에 존재하는 웹 클라이언트 자체가 View가 된다는 것인데, 웹 클라이언트 영역에서도 이 부분을 다시 MVC로 나누고 있는 것이다. 뭔가 혼란스럽지 않은가?

웹 응용프로그램이란 것이 결국 서버측과 클라이언트 측이 결합되어 하나의 결과물이 되는 것인데, 각각의 영역에서 MVC 패턴을 따로 다룬다면, 전체의 MVC모습은 어떻게 된다는 말인가?

이것은 어떻게 보면 중요한 문제가 될 수 있다. 분업화된 작업방식(서버개발자/클라이언트 개발자)에서 각기 나름의 MVC 모델을 추구한다면 응용프로그램의 전체 구조는 가히 우스운 꼴이 될 수도 있다.

오히려 이 부분은 웹 프론트앤드 개발자에게는 익숙한 것일지도 모른다. 그것은 웹 응용프로그램의 구현모델의 차이를 이해하면 되는 것이다.

* SPA(Single Page Application)에서의 MVC
- 서버측 MVC, 클라이언 측 MVC라는 말 자체가 조금 억지스럽다. 그것은 응용프로그램의 구현모델상의 차이가 날 수 있는 부분이지, 서버와 클라이언트로 구분되지는 않을 것이다.

AngularJS는 SPA 모델을 위한 자바스크립트 프레임워크이다.
SPA는 전통적인 웹 구조와 상반되는 개념으로, 기존의 웹 페이지는 데이터를 전송하고 서버가 이를 처리한 후 완전히 새로운 페이지를 랜더링하는 구조였다.(이를 '호출 후 새로고침' 방식이라 한다)

그러나 현대의 웹 페이지는 단순히 정적 페이지라는 개념을 넘어 사용자의 이벤트에 반응하고 적절한 상호작용을 제공하며 애니메이션과 네비게이션을 자체적으로 해결하는 응용프로그램의 면모를 보여줄 수 있다. 그리고 원격지 서버와의 통신은 더이상 완전히 새로운 페이지의 '새로고침'이 아니라 단일 페이지에서 필요한 데이터만 비동기로 가져와서 사용자의 작업을 방해하지 않으면서 데이터를 갱신시킨다.

결론적으로 전통적인 새로고침 방식의 웹과 SPA 방식의 웹에서의 MVC의 역할 모델이 조금 상이할 수 있다고 할 수 있다.

다시 말하지만, AngularJS는 SPA 실현을 가능케 하는 자바스크립트 프레임워크이다.
SPA모델에서는 대부분의 작업이 클라이언트 측에서 이루어진다. 그리고 서버측 데이터 연동은 Ajax와 같은 통신 모델을 이용하여 비동기로 그때그때 필요한 데이터만 가져와서 사용자 화면을 갱신시킨다. 이런 모델에서의 MVC패턴의 각 영역은 다음과 같다.

- Model: View가 사용할 데이터 또는 공통되고 주요한 비즈니스 로직을 정의한 영역으로 실제 데이터는 대체로 서버측에 존재하게 된다.
- View: 사용자 화면을 구성하는 HTML 영역
- Controller: 뷰와 모델을 연결해 주는 영역으로 SPA모델에서는 클라이언트 측에 존재하게 된다. 좀 더 면밀히 따져보자면, SPA 모델에서는 Controller의 영역을 ViewModel(뷰모델)로 설계되기도 하며, 이는 SPA뿐만 아니라 전통적인 윈도우 응용프로그램(요청과 응답 구조의 웹구조가 아닌) 환경에서 뷰와 모델간 의존성을 제거하고 뷰의 상태와 뷰에 투영될 데이터의 변화에 따른 자동 바인딩을 가능케 하는 구조로써 이와 같은 모델을 MVC의 변형된 모델로 MVVM 패턴이라고 불리운다.

큰 틀에서 정리하자면, Model은 그 역할이, 응용프로그램 전체에 걸쳐 필요한 데이터와 비즈니스 로직을 제공하게 되는데, SPA 구조에서는 이 부분을 클라이언트 측에 추상화 하여 일종의 공용서비스 형태로 구현하며, 실제 데이터는 서버로부터 비동기 형태로 (백그라운드에서) 받아와서 추상화된 서비스계층에 할당하고 뷰모델이라는 중간계층을 경유해 뷰와 바인딩시키는 구조인 것이다.

결국 SPA 에서의 서버측은 응용프로그램에 필요한 데이터를 비동기로 서비스 해 주는 기능만 존재하면 된다는 말이다.(이부분은 대체로 REST API로 구현된다)

SPA모델에서의 MVC는 기존의 새로고침 모델에서의 MVC와는 그 역할에 따른 컴포넌트의 위치와 통신구조에서 차이점을 보이며, 어떤 형태의 모델로 웹 응용프로그램을 구현하는지에 따라 '모델-뷰-컨트롤러'의 실질적 구현레벨의 모습이 약간 다를 수 있다는 것이다.

하지만 관점의 분리라는 사상과 비즈니스 로직(모델)과 뷰를 분리하는 아키텍처 구조는 SPA이든 새로고침방식이든 다르지 않다는 것은 원론적으로 중요하다.

* AngularJS의 MVC 컴포넌트
- AngularJS에서는 MVC 패턴을 기반으로 응용프로그램 구성을 가능케 한다. 어떤이는 MVVM이라 예기하고 싶겠지만 여기서는 상황을 일반화 시켜서 글을 이어 나가도록 한다. MVVM역시 MVC에서 필요에 의해 일부 변형시킨 패턴이며 이 글에서는 그 차이가 그리 중요하지 않기 때문이다.

(실제로 AngularJS는 자신의 아키텍처 기반을 MVW(Model-View-Whatever)라고 부르며 Whatever란 어떤것이라도 가능하다는 뜻으로, MVC/MVP/MVVM 모두 가능하다는 의미이다. 아키텍처 패턴간 발전과정과 특징 및 구조적 차이는 분명이 존재하며 이 들의 관계는 다른 글에서 다시 언급하도록 한다.)

MVC 아키텍처를 위해 AngualrJS가 제공하는 컴포넌트를 알아보자. 다음은 AngularJS 공식 사이트에서 설명하는 내용이다.

* Model — Models are the properties of a scope; scopes are attached to the DOM where scope properties are accessed through bindings.
* View — The template (HTML with data bindings) that is rendered into the View.
* Controller — The ngController directive specifies a Controller class; the class contains business logic behind the application to decorate the scope with functions and values

 AngularJS가 MVC 구현을 위해 제공하는 것중 핵심은, ngController 디렉티브이며 이를 이용하여 뷰를 위한 컨트롤러 클래스를 생성할 수 있다.

Model은 scope의 프로퍼티라고 설명하고 있다. AngularJS에서 $scope은 뷰와 컨트롤러의 연동을 위해 제공되는 객체이며 컨트롤러 객체에서 뷰로 데이터를 노출하기 위한 일반적인 방법을 제공한다.
scope이 모델이다라고 애매한 해석을 하기 보다는, 컨트롤러에서 모델 정보를 뷰로 연결하기 위해 제공되는 객체로 이해하면 될 것이다.

실제 예제를 한번 보자.

<div ng-app ng-init="qty=1;cost=2">
  <b>Invoice:</b>
  <div>
    Quantity: <input type="number" min="0" ng-model="qty">
  </div>
  <div>
    Costs: <input type="number" min="0" ng-model="cost">
  </div>
  <div>
    <b>Total:</b> {{qty * cost | currency}}
  </div>
</div>

코드는 뷰와 모델간 가장 기본적인 상호작용을 설명하기 위해 작성되었으며, 데이터에 해당하는 Model정의를 위해 ng-model 디렉티브를 사용했다. ng-model 디렉티브가 지정된 입력박스의 값은 자동으로 $scope객체의 프로퍼티로 정의되며, 더블중괄호로 표현되는 '{{ property of scope }}' 표현식을 통해 모델의 정보를 View로 연결키실 수 있다.

다음 이에 대한 설명으로 AngularJS 공식 사이트에서 제공하는 그림이다.

 

이제 컨트롤러를 등장시켜 보자.

angular.module('myModule', [])
.controller('InvoiceController', function() {
  this.qty = 1;
  this.cost = 2;
  this.total = function total(outCurr) {
    return this.qty * cost;
  };
  this.pay = function pay() {
    window.alert("Thanks!");
  };
});

<div ng-app="myModule" ng-controller="InvoiceController as invoice">
  <b>Invoice:</b>
  <div>
    Quantity: <input type="number" min="0" ng-model="invoice.qty" required >
  </div>
  <div>
    Costs: <input type="number" min="0" ng-model="invoice.cost" required >
  </div>
  <div>
    <b>Total:</b>
    <span>{{invoice.total}}</span>
    <button class="btn" ng-click="invoice.pay()">Pay</button>
  </div>
</div>

컨트롤러는 뷰가 바인딩하고 다룰 수 있는 속성과 메서드를 정의하기 위한 객체로써 AngularJS 모듈에 연결한다. 코드를 보면 $scope 객체를 찾아볼 수 없는데 이는 controller-as 문법을 사용하여 $scope객체를 명시적으로 사용하지 않아도 되기 때문이다. 중요한 것은 View와 Scope 그리고 Controller의 상호작요이며 다음 그림에 도식화하여 설명하고 있다.

 

* 일반적인 MVC 패턴 구현
- 이제 AngularJS에서 일반적 MVC 패턴 구현의 전체 모습을 살펴보자. Model의 경우 원격지 서버에 존재하는 데이터를 기반으로 하는게 일반적이며 이와같은 공통적인 데이터를 AngularJS 어플리케이션 전체에 공유하기 위해서는 AngularJS의 Service(서비스) 객체로 생성하는 것이 좋다.

사실 이전까지의 글보다 다음의 코드를 이해하고 이 코드를 기반으로 AngularJS MVC 패턴 구현을 이해하는 것이 보다 현실적이다.

먼저index.html을 다음과 같이 작성한다

<!DOCTYPE html>
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script src="app.js"></script>
<body>
 <div ng-app="myModule" ng-controller="MainCtrl as main">
  <h1>Persons</h1>
  <ul>
     <li ng-repeat="person in main.persons">
        {{person.name + ', ' + person.city}}
     </li>
   </ul>
 </div>
</body>
</html>

AngularJS 관련 자바스크립트 파일을 app.js라는 별도의 파일로 분리했다.
그리고 AngularJS의 모듈과 컨트롤러를 매핑시킨다. 이때 $scope객체를 사용하는 대신, controller-as 문법을 사용한다. 그리고 배열로 반환되는 persons 정보를 ng-repeat 디렉티브로 순환시켜 <ul>태그의 <li>항목으로 각각 매핑시킨다.

그리고 다음은 app.js 파일의 코드이다.

//모듈 생성
var app = angular.module('myModule', []);
 
//컨트롤러 생성(컨트롤러 생성자에 PersonModel서비스 전달=>의존성주입(생성자 주입)패턴)
app.controller('MainCtrl', function(PersonModel){
 var main = this;
 //Personmodel서비스에 정의된 getPersons()함수를 호출
 main.persons = PersonModel.getPersons();
});

//서비스 생성
//서비스는 전체 어플리케이션에서 사용하게 될 데이터나 공통 기능을 작성하기 좋은 곳이다
//보통 MVC의 Model 정보를 원격지에서 불러오는 역할을 하도록 한다.
 app.service('PersonModel', function(){
   var service = this;

  //실제환경에서는 persons 정보는 원격지 서버로 부터 받아오는 것이 일반적이지만,
  //여기서는 코드의 단순화를 위해 직넙 객체 리터럴로 생성한다.
   var persons = [
       {name: 'Park', city: 'Pusan'},
       {name: 'KIM', city: 'Seoul'},
       {name: 'Jung', city: 'suwon'}
     ];
    
   service.getPersons = function(){
     return persons;
   };

   service.setPersons = function(){
     //서버로 부터 받아서 업데이트

   };
});

Model 정보를 제공하는 서비스 객체(PersonModel)를 생성하여 컨트롤러 생성자 함수에 주입시킨다. 이렇게 함으로써 Controller와 Model을 물리적으로 분리가능하며 의존성 주입패턴으로 이 둘을 의존성을 관리한다. 컨트롤러는 이제 PersonModel의 속성과 메서드를 사용할 수 있게 되었다.

참고로 서비스와 컨트롤러에서 this키워드를 변수에 저장시키는 것은, 자바스크립트의 동적언어 특징에서 기인하는 문맥(Context)상 this의 해석이 모호해 지는 점을 예방하기 위해 명시적으로 할당해 두는 것이다. 이는 예외적인 동작에 대한 예방차원에서 필요한 코드로 자바스크립트 특징에 기반한 코드사례이다.

결과 화면은 다음과 같다.

 

지금까지 AngularJS의 MVC 패턴 구현에 대해 장황하게(?) 알아보았다.

대부분의 AngularJS 응용프로그램의 코드 구조는 여기에 살을 더 붙여 나가는 식으로 작성하면 된다. MVC 패턴에 기반한 응용프로그램 구조는 영역간 분리를 통해 상호 독립적 개발과 유지, 단위테스트를 가능케 하여 전체적인 생산성과 유지보수성을 향상시킨다.

중요한 것은 MVC의 사상을 이해하고 AngularJS가 MVC를 위해 제공하는 컴포넌트를 적절히 조합하여 응용프로그램을 구성하는 것이다.

이 글을 바탕으로 좀더 세말한 치장을 해 나가면 될 것이다.

'모바일 > Javascript' 카테고리의 다른 글

[AngularJS] Module  (0) 2016.07.12
[AngularJS] Two-way Data Binding  (0) 2016.07.10
[AngularJS] 일주일 탐방기  (0) 2016.07.06
knockout.js  (0) 2013.11.14
OOP in Javascript  (0) 2013.08.16

[AngularJS] 일주일 탐방기

Posted in 모바일/Javascript // Posted at 2016. 7. 6. 11:23
728x90

오랜만에, 웹 클라이언트 기술에 다시 불을 지펴보고 싶어졌다.

일주일 전, AngularJS를 본격적으로 알아보고자 책 한권 구입해서 대략 전체를 한번 훑어봤다.


기술사 공부할때, Node.JS와 함께 기출 예상되었던 핫(hot)한 자바스크립트 프레임워크 중 하나여서 대략 느낌(?) 정도만 알고 있었는데, 알면 알 수록 매력 떵어리가 아닌가...

특히 CRUD기반의 SPA(Single Page Application) 개발을 위한 최고의 생산성을 보장하는 아주 훌륭한 물건이라 하겠다.

더불어 이젠 전통에 가까워진 MVC(Model View Controller) 아키텍처 사상에 기반을 둔 프로그램 구조를 채택하고 있어 분할과 정복/관심사의 분리라는 소프트웨어 기본 원리가 적용되어 구현의 독립성, 단위테스트의 용이성, 느슨한 결합으로 연결된 전체적인 아키텍처는 유지보수성을 향상시켜 엔트프라이즈급 웹 응용개발에 아주 적합해 보인다.

참고로 AngularJS는 MVW(Model View Whatever)라는 용어로 MVC를 넘어 MVP, MVVM. 뭐든지 다... 라는 발찍한(?) 도발마저 하고 있다.

View와 Model간의 관계성과 중간 연결계층인 Controller의 View와의 차수성이 이들 아키텍처를 나누는 주요 기준이 되는데, 이 부분은 좀 더 면밀히 따져 봐야 할 것 같다.

AngularJS는 아주 유명한 크로스브라우저 라이브러리인 jQuery와 자주 비교되는 사실을 발견했다.

음.. 그 목적과 쓰임새가 분명 다르다는 느낌을 지울 수 없지만, 이 둘이 많은 자료에서 매번 비교되는 것은 jQuery가 이미 산업표준으로 자리매김을 해 버린 웹클라 개발환경에 불현듯(?) 나타나 상승세를 타버린 AngularJS의 유명세와의 시기적 마찰이 그 시발점인 듯 하며 실제로 jQuery로 구현된 많은 부분이 AngularJS로 대체가능 하다는 사실도 한 몫했을 터이다.

앞서, 구매한 책에서는 다음과 같이 이 둘을 언급하고 있다.

수많은 프레임워크들 중에서도 jQuery가 압도적인 지지를 받는다는 점에 대해서는 많은 개발자가 동의할 것이다. jQuery가 수많은 브라우저의 각기 다른 부분을 모조리 추상화하여 개발자들이 브라우저에 관계없이 웹사이트에 단인 API를 사용할 수 있게 해주었다는 사실 때문이다.

그 후로는 애플리케이션과 유사하게 동작하는 웹사이트를 개발할 수 있도록 구현된 프레임워크들이 등장했다. 이들은 완전히 새로운 방식을 도입했다. 예를 들어, jQuery는 DOM을 조작하는 데 필요한 탁월한 도구들을 제공하지만, 애플리케이션 구조에 맞게 코드를 정리하는 것에 대한 실질적인 가이드라인을 제공하지는 않았다. 'jQuery 애플리케이션'을 구현한 코드가 오히려 유지보수가 어렵고 확장성이 떨어지는 괴상한 코드로 변하고 말았다는 안타까운 소식ㅇ르 듣게 되는 것은 바로 이점 때문이다.

이후 유지보수가 쉬운 대규모 자바스크립트 애플리케이션 개발에 대한 수요가 증가하면서 자바스크립트 프레임워크는 전성시대를 맞이한다. 최근 2~3년 사이에는 수많은 프레임워크가 등장하고 또 아무도 모르는 사이에 사려져 갔다. 그러나 몇몇 프레임워크는 유지보수가 쉽고 확장 및 테스트가 수월한 대규모 웹 애플리케이션을 개발하기 위한 필수 옵션으로 자리매김했다. 그중에서도 첫 손가락에 꼽히지는 않지만 매우 대중적으로 알려진 프레임워크가 바로 구글이 개발한 AngualrJS다.

그리고 '웹 엔지니어의 교과서'라는 책에서는 다음과 같이 말하고 있다.

jQuery로 프런트엔드 개발이 매우 편해졌다는 사실을 알았습니다. 그런데 왜 Backbone.js, AngualrJS, Ember.js, Vue.js와 같은 Javascript 프레임워크가 등장한 걸까요? jQuery만으로는 부족했을까요? 그 이유는 크게 두가지로 들 수 있습니다.

1) DOM 변경에 약한 jQuery

- DOM구조에 기반한 탐색으로 인해 DOM 구조변경에 오히려 취약함

- $("$target").parent().next().find(".default").hide();
- 리팩토링이나 수정, 디자인 변경 등으로 DOM구조가 바뀌어 지금까지 next()로 찾았던 형제 요소를 더는 찾지 못하는 상황 등은 충분히 예상할 수 있습니다. 그때마다 짜증을 내면서 jQuery를 수정한 경험이 많을 것으로 생각합니다.

2) 대규모화된 프런트 엔트 개발
- 서버사이드 개발에서 이전부터 MVC라는 디자인패턴을 이요하여 비즈니스 로직은 모델에, 디자인은 뷰에 책임을 분산했습니다. 최근 프런트 엔드 개발 규모가 커지나 서버사이드 개발과 마찬가지로 프런트 엔드 개발에서도 MVC 디자인 패턴으로 비즈니스 로직과 디자인을 따로 분리하려는 경향이 보입니다.

두 책에서 언급한 내용을 요약하자면, 브라우저 호환성을 보장하면서 DOM 조작에 탁월한 능력을 보여주는 jQuery는 DOM 탐색에 기반한 구조가 오히려 유지보수성을 떨어뜨리는 면이 있으며, 보다 큰 차이점은 간단한 웹페이지들의 모음이 아닌 응용프로그램으로써의 규모있는 SPA 환경일 경우 MVC 아키텍처가 가져다 주는 유지보수성과 테스트용이성, 확장성이란 점이다.

물론 AngularJS를 만나면 가장 먼저 눈에 띄는 장점인 '양방향 바인딩'도 jQuery에서는 제공되지 않는 기능이다.
(웹 페이지의 양방향 바인딩은 knockout.js를 첨 접했을 때 깜놀한 기억이 난다. => http://m.mkexdev.net/268)

결론적으로 다시 말하지만, 이 둘은 완전히 서로를 대체한다고 보기는 힘들다는 것이다.

두 프레임워크의 사상과 추구하는 목적이 다르다는 점, 그에 따라 쓰임새 역시 다를 수 밖에 없는것이다. 정말 고무적인 것은 이 둘을 같이 사용할 수 있다는 것이며, AngularJS는 내부적으로 jQuery를 포함하고 있다는 점이다. 이 점은 그야말로 더욱 훌륭하다 하겠다.

* AngularJS의 빠른 학습

앞서 소개한 책은 AngularJS를 실제 SPA 응용프로그램을 작성해 보면서 배울 수 있는 좋은 기회를 제공한다. 또한 AngularJS의 내부동작 등 심도있는 내용도 같이 포진되어 있어 많은 걸 배울 수 있다.

그리고 깊이는 좀 덜하지만 빠르게 테스트 해 볼 수 있는 학습도구로, w3cshools의 다음 강좌를 참조하면 좋다.

http://www.w3schools.com/angular/

마지막으로 AngularJS 자체에서 제공하는 api 문서는 늘 곁에 함께...

https://docs.angularjs.org/api

'모바일 > Javascript' 카테고리의 다른 글

[AngularJS] Two-way Data Binding  (0) 2016.07.10
[AngularJS] MVC Pattern in Angular  (0) 2016.07.10
knockout.js  (0) 2013.11.14
OOP in Javascript  (0) 2013.08.16
함수에 대하여  (5) 2013.07.31

knockout.js

Posted in 모바일/Javascript // Posted at 2013. 11. 14. 14:16
728x90

방안의 코끼리 마냥 본다본다 하던게 오래되어, 이제야 맘먹고 제대로 살펴 보게 되었다.

 

'넉아웃' 이라는 흥미로운 이름의 자바스크립트 라이브러리인데, 이놈 물건이네!

 

동적 웹 UI 구현의 생산성과 안정성이 매우 향상될 것으로 보인다.

점점 훌륭한 오픈소스 라이브러리들이 출현하고, 개발자는 점점 코어 구현에서 멀어지고...

 

빠르고 안정적이면서 유연성마저 좋은 스마트한 응용프로그램을 쉬이 개발할 수 있는 장점 이면에는, 소프트웨어 구조 설계와 코어 구현의 고민과 경험을 빌려 쓰게 되면서 궁극의 개발 스킬 향상을 저해하는 면이 있다 하겠다. 현 시대가 빠른 출시, 잦은 변경, 복잡한 비즈니스에 초점이 맞춰 지다 보니 이러한 라이브러리에 의존하는 것이 당연하게 받아 들여지지만 한편으로는 비즈니스가 아닌 소프트웨어의 코어 수준의 고민은 여전히 필요한게 아닌가 한다.

 

그럼... 잡설은 집어치우고.

 

knockout.js의 실행 흐름을 도식화 해봤다.

 

 

 

MVVM (모델, 뷰, 뷰모델) 패턴을 기반으로 뷰를 위한 뷰모델 계층이 존재하고, 이 뷰모델의 변경을 관찰하여 뷰와 자동 상호작용이 가능토록 구현된 라이브러리이다.

 

뷰모델 역할을 하는 객체를 knockout.js에 바인딩하면 knockout.js는 뷰모델 객체의 프러퍼티를 관찰/감시하게 된다. 만일 관찰 대상으로 지정된 뷰 모델 객체의 프로퍼티가 변경될 경우 연결된 HTML Element로 변경 내용이 전파된다. 또한 HTML Element에 연결된 프로퍼티는 HTML Element의 이벤트에 반응해 자동으로 값이 변경되기도 한다.

 

대부분의 예제에서 HTML Element의 이벤트에 기반해 뷰모델의 프로퍼티가 변경되는 것을 보여주지만, HTML Element에 의존없이 뷰 모델 프로퍼티만 변경되어도 자동으로 뷰가 갱신되도록 할 수 있어, 매우 참신하다.

 

간단한 예를 들어, 1초마다 시간을 갱신시켜 주는 코드를 다음과 같아 쉽게 구현할 수 있다.

데이터 변경에 따라 UI를 갱신하는 따위(?)의 코드는 존재하지 않는다.

<p>Time: <span data-bind="text: time" /></p>

 

 

   var viewModel = { time : ko.observable(new Date().toString()) };
   
   function onLoad(){             
       ko.applyBindings(viewModel);
       setInterval(clock,1000);
   }
   
   function clock(){
       viewModel.time(new Date().toString()); 
   }
  
   window.addEventListener('load', onLoad);

 

knockout.js 공식 사이트에 보면 주요 예제가 일목요연하게 정리되어 있어 적절한 활용처를 판단하는데 도움을 얻을 수 있다.

: http://knockoutjs.com/examples/

 

그리고 닷넷 기술과 함께 knockout.js를 활용하여  SPA(Single Page Application)를 구현한 좋은 사례를 다음의 MSDN 매거진에서 확인할 수 있다.

: http://msdn.microsoft.com/ko-kr/magazine/dn463786(en-us).aspx

 

 

'모바일 > Javascript' 카테고리의 다른 글

[AngularJS] MVC Pattern in Angular  (0) 2016.07.10
[AngularJS] 일주일 탐방기  (0) 2016.07.06
OOP in Javascript  (0) 2013.08.16
함수에 대하여  (5) 2013.07.31
null과 undefined 그리고 NaN  (0) 2013.07.29

[HTML5 Game] Shooting Down

Posted in 모바일/HTML5 // Posted at 2013. 10. 16. 22:37
728x90

앞서 전개한 글들에서 충돌처리, 총알발사, 오브젝트 이동 처리 등을 구현해 보았다.

이번 글에서는, 이러한 개별 개념을 조합하여 총알로 적(enemy) 비행체를 격추하는 샘플을 구현해 보자.

 

간략한 시나리오

- 적(enemy) 비행체는 캔버스 상단에서 좌/우로 방향을 바꿔가며 계속 이동한다.

- 키보드의 스페이스 키를 누르면 총알이 발사된다.

- 총알이 적 비행체에 명중하면 폭발한 듯한 이미지 효과를 준다.

 

격추라는 시나리오에만 충실하기 위해 아군 비행기는 등장시키지 않았으며, 총알의 출발 위치도 캔버스 하단 중앙에서만 출발하도록 한다. 또한 지속적인 테스트를 위하여 적 비행체가 격추되어도 충돌을 표현하는 이미지 효과만 줄 뿐 비행체는 계속 살아서 움직이도록 할 것이다.

 

먼저 적 비행체와 총알, 그리고 충돌 처리를 위한 객체 기반을 작성할 것인데 그전에, 주요 코드부터 간략히 살펴보자.

 

총알 발사

이전 글에서는, 과도한 연사 속도를 조절하기 위해 FPS를 기반의 조절값을 사용한 반면 여기서는 총알 발사의 전/후 시간을 속도 조절의 기준 값으로 사용했다. 즉 직전 총알 발사 시간과 현재 총알 발사 시간의 간격이 0.1초 이상 되어야만 새 총알이 배열에 추가되도록 처리한다. 그리고 총알의 발사 위치는 캔버스의 하단 중앙으로 설정한다.

if(Date.now() - this.lastShootTime >= 100){        
   this.bullets.push({x: (this.canvasSize.width / 2)
                       - (this.gameAssets.bullet.width / 2), y: this.canvasSize.height});
   this.lastShootTime = Date.now();  
  } 

 

 

오브젝트 이동

적 비행체와 총알의 이동을 위해 좌표값을 변경하는 코드이다. 게임 루프의 update() 메서드에서 매 프레임마다 호출하는 부분으로, 총발 위치는 위쪽 방향으로 이동하도록 Y 좌표값을 변경하고 적 비행체는 캔버스의 좌/우우를 계속해서 왔다갔다 하도록 방향값을 기준으로 X 좌표값을 변경시켜준다.

for(var i = 0; i < this.bullets.length; i++){    
   this.bullets[i].y -= this.speedBullet; 
}      

 

if(this.directionEnemy == 1
        && this.enemyPostion.x + this.gameAssets.enemy.width  > this.canvasClientRect.right)

{
   this.directionEnemy = -1;
}  


if(this.directionEnemy == -1
        && this.enemyPostion.x + 5 < this.canvasClientRect.left)

{   
   this.directionEnemy = 1;
}
    
this.enemyPostion.x += (this.directionEnemy) * 5; 

 

 

오브젝트 그리기 및 충돌처리

객체의 마지막 코드로, 게임 오브젝트들을 캔버스에 그린다. 게임 루프의 display() 메서드에서 매 프레임마다 호출하는 부분으로, 먼저 캔버스의 모든 내용을 지우고 적 비행체를 그린다. 다음으로 총알을 배열 수 만큼 그려주는데 총알이 캔버스 영역 밖으로 이동했다면 배열에서 제거하고 루프를 빠져나간다. 그리고 총알과 적 비행체와의 충돌처리를 통하여 격추되는 이미지를 그려줄지 판단한다. 

this.canvasContext.clearRect(0, 0, this.canvasSize.width, this.canvasSize.height); 
  
this.canvasContext.drawImage(this.gameAssets.enemy, this.enemyPostion.x, this.enemyPostion.y);
       
for(var i = 0; i < this.bullets.length; i++){
   if(this.bullets[i].y <= 0) {
      this.bullets.splice(i,1); continue;
   }
   
   this.canvasContext.drawImage(this.gameAssets.bullet, this.bullets[i].x, this.bullets[i].y);
   
   if(this.bullets[i].x +  this.gameAssets.bullet.width > this.enemyPostion.x 
       && this.bullets[i].x < this.enemyPostion.x + this.gameAssets.enemy.width    
       && this.bullets[i].y > this.enemyPostion.y 
       && this.bullets[i].y < this.enemyPostion.y + this.gameAssets.enemy.height)
    {           
      this.canvasContext.drawImage(this.gameAssets.pow, this.enemyPostion.x, this.enemyPostion.y);
    }                
}

 

 

여기까지 해서 객체기반 작성이 완료되었다. 나머지 코드는 각종 초기화 및 이미지 리소스 다운로드, 위의 객체를 기반으로 게임 루프를 구현하는 것이다. 다음 코드는 객체를 포함한 전체 소스이다.

 function ShootingDownObj(gameAssets,canvasElement){
    this.canvasSize = {width: canvasElement.width, height: canvasElement.height};
    this.canvasContext = canvasElement.getContext('2d');
    this.canvasClientRect = canvasElement.getBoundingClientRect();     
    this.enemyPostion = {x: 10, y:10}; 
    this.gameAssets = gameAssets;
    this.bullets = [];
    this.speedBullet = 15;   
    this.directionEnemy = 1;
    this.lastShootTime = Date.now();
 
    this.addBullet = function(){   
        if(Date.now() - this.lastShootTime >= 100){        
         this.bullets.push({x: (this.canvasSize.width / 2)
             - (this.gameAssets.bullet.width / 2), y: this.canvasSize.height});
         this.lastShootTime = Date.now();  
        }  
    }
 
    this.movePosition = function(){
        for(var i = 0; i < this.bullets.length; i++){    
           this.bullets[i].y -= this.speedBullet;
        }      
        if(this.directionEnemy == 1
          && this.enemyPostion.x + this.gameAssets.enemy.width  > this.canvasClientRect.right){
            this.directionEnemy = -1;
        }  
        if(this.directionEnemy == -1
         && this.enemyPostion.x + 5 < this.canvasClientRect.left){   
            this.directionEnemy = 1;
        }
    
        this.enemyPostion.x += (this.directionEnemy) * 5;   
    }
   
    this.shotBullets = function(){  
       this.canvasContext.clearRect(0, 0, this.canvasSize.width, this.canvasSize.height); 
       this.canvasContext.drawImage(this.gameAssets.enemy,
                                                                        this.enemyPostion.x, this.enemyPostion.y);
       
      for(var i = 0; i < this.bullets.length; i++){
          if(this.bullets[i].y <= 0) {
             this.bullets.splice(i,1);
             continue;
          }
   
          if(this.bullets[i].x +  this.gameAssets.bullet.width > this.enemyPostion.x 
           && this.bullets[i].x < this.enemyPostion.x + this.gameAssets.enemy.width    
           && this.bullets[i].y > this.enemyPostion.y 
           && this.bullets[i].y < this.enemyPostion.y + this.gameAssets.enemy.height){           
            this.canvasContext.drawImage(this.gameAssets.pow,
                                                         this.enemyPostion.x, this.enemyPostion.y);
          }           
            this.canvasContext.drawImage(this.gameAssets.bullet, this.bullets[i].x, this.bullets[i].y);
       }    
    } 
}

var fps = 30;
var canvasElement;
var gameContext; 
var shootingDownObj;   
var gameAssets;  
var currentAssetImageLoadCount = 0;    
var isKeyDown = [];

 

function init(){
  canvasElement = document.getElementById('GameCanvas');
  gameContext = canvasElement.getContext('2d');
   
  var bulletImage = new Image();  
  bulletImage.src = 'image/bullet.png';       
  bulletImage.onload = onAssetImageLoadComplete; 
 
  var enemyImage = new Image();  
  enemyImage.src = 'image/enemy.png';       
  enemyImage.onload = onAssetImageLoadComplete; 
 
  var powImage = new Image();  
  powImage.src = 'image/pow.png';       
  powImage.onload = onAssetImageLoadComplete;  
 
  gameAssets = {bullet: bulletImage, enemy: enemyImage, pow: powImage};     
}

 

function onAssetImageLoadComplete(){ 
  if(++currentAssetImageLoadCount >= 3){  
    shootingDownObj = new ShootingDownObj(gameAssets,canvasElement);      
    setInterval(gameLoop, 1000 / fps);
  } 
}

 

function gameLoop(){
  update();
  display();
}

 

function update(){   
  if(isKeyDown[32]){      
    shootingDownObj.addBullet();
  } 
  shootingDownObj.movePosition();
}

 

function display(){
  shootingDownObj.shotBullets();

}

 

function onKeyDown(e){  
  isKeyDown[e.keyCode] = true;   
}

 

function onKeyUp(e){
  isKeyDown[e.keyCode] = false;
}

 

window.addEventListener("load", init, false);
window.addEventListener("keydown",onKeyDown,false);
window.addEventListener("keyup",onKeyUp,false);

 

 

 

브라우저를 통해 확인해보면, 다음 그림과 같이 총알이 명중하면 폭발한듯한 이미지 효과를 확인할 수 있다.

 


 


 

 

'모바일 > HTML5' 카테고리의 다른 글

[HTML5 Game] Sound Effect  (2) 2013.10.11
[HTML5 Game] Firing Bullet  (0) 2013.10.08
[HTML5 Game] Moving Object Ⅱ  (1) 2013.10.02
[HTML5 Game] Moving Object  (0) 2013.10.02
[HTML5 Game]Calculating FPS  (0) 2013.09.30

[HTML5 Game] Sound Effect

Posted in 모바일/HTML5 // Posted at 2013. 10. 11. 21:43
728x90

이번 글에서는 게임에 사운드(Sound) 효과를 추가해 볼 것이다.

 

이전 글(http://m.mkexdev.net/252)에서 스페이스 키를 누르면 총알이 발사되도록 해 봤으니, 여기서는 총알이 발사되는 소리를 구현해 보도록 하겠다.

 

사운드 처리는 HTML5 스펙에 새로이 추가된 Audio 요소를 이용할 것이다.(참고: http://m.mkexdev.net/63)

 

 

 

단순히 하나의 사운드를 재생하는 것은 매우 심플한 코드로 구현이 가능하다. 하지만 게임 사운드는 한 번에 하나의 사운드만 재생되는 것이 아니라 여러 사운드가 동시에 재생되어야 하는 경우가 많고 또한 하나의 사운드라 해도 그 사운드가 종료되기 전에 같은 사운드가 다시 재생되어야 하는 경우도 있다. 이러한 대표적인 경우가 총알이 발사되는 경우이다. 예를들어 총알 사운드의 플레이 타임이 3초인 경우, 첫번째 총알이 발새되고 3초가 지나기 전에 두 번째 총알이 발사되는 경우이다. 이를 위해서는 하나의 사운드를 여러개로 분리해서 각각 재생되도록 하는 기법이 필요하다.

 

바로 구현해 들어가 보자. 다음과 같이 총알 사운드를 추상화환 객체 기반을 준비한다.

여러 개의 총알 소리가 거의 동시에 같이 재생될 수 있도록 사운드 배열을 관리하고 플레이 가능한 사운드를 찾으면 그 사운드를 재생하고 바로 루프를 빠져 나간다. 또한 특정 사운드의 재생이 완료되면 명시적으로 paused(일시정지)처리를 하는데 이 함수에서는 이 속성값을 검사하여 재생가능한 사운드 파일을 탐색한다. 

function FireShot(sounds){
  this.sounds = sounds

  this.playSound = function(){   
    for(var i = 0; i < this.sounds.length; i++){  
      if(this.sounds[i].audio.paused){             
         this.sounds[i].audio.play();
         break;    
      }    
    }   
  }
}

 

이어지는 코드는 FireShort 객체를 이용하여 스페이스 키가 눌러졌을 때 사운드 재생을 처리하는 코드이다.

총알 소리로 사용되는 사운드 파일은 한개이지만, (앞서 설명한대로) 동시 재생을 위해 여러개의 Audio 요소를 동적으로 생성한다. 코드에서는 50개를 생성했는데 이것은 사운드 파일의 재생시간과 동시 재생 환경등을 고려하여 적절한 값을 지정해야 한다. 필자가 사용하는 (인터넷에서 다운받는) 사운드 파일의 재생시간은 대략 4초 가량 되며(너무 길다. 짧막한 총성소리에 비해 너무 긴 재생 시간이다. ㅡ,ㅡ;) 스페이스 키를 계속 누르고 있을 경우 소리가 끊임없이 나도록 해야 하기 때문에 50이라는 좀 과한(?) 값을 지정했다. (이렇게 해도 플레이타임이 길어서 매끄럽지 못하다.) 예제에서는 사운드 재생만이 목적이기 때문에 이러한 과도한 값을 지정했지만 실제 게임 개발시에는 자원의 낭비 및 성능의 저해가 없는 수준에서 적절한 선택을 해야 할 것이다.

 

참고로 크롬 브라우저의 경우 한번 재생한 Audio를 재사용 하기 위해서는 다시 로드해야 하는것을 주의하기 바라며 기타 코드는 HTML5의 Audio 요소를 사용하는 코드와 FireShot 객체를 호출하는 부분으로 이뤄져있다.

var sounds = []; 
var fireShot; 
var currentSoundLoadCount = 0;   


function init(){
 for(var i = 0; i < 50; i++){  
    var gunAudio = new Audio();
    gunAudio.src = 'audio/gun.mp3'
        
    gunAudio.addEventListener("canplaythrough", onSoundReadyComplete, false);  
    gunAudio.addEventListener("ended", function(){
       if(window.chrome) this.load();                    

         this.pause(); 
     },false); 
      
    document.body.appendChild(gunAudio);
  
    sounds.push({audio: gunAudio});  
 }               
}

 

function onSoundReadyComplete(){    
   if(++currentSoundLoadCount >= 50){         
     fireShot = new FireShot(sounds);             
 }
}

 

function onKeyDown(e){   
 if(e.keyCode = 32){        
    fireShot.playSound(); 
 }
}

 

window.addEventListener("load", init, false);
window.addEventListener("keydown",onKeyDown,false);

 

브라우저로 실행하여 사운드 효과를 감상(?)해 보자.

다시한번 말하지만, 사운드 파일의 재생시간과 동시 재생의 환경에 따라 적절히 수정을 가하며 테스트 해 보는 것이 좋다.

 

'모바일 > HTML5' 카테고리의 다른 글

[HTML5 Game] Shooting Down  (0) 2013.10.16
[HTML5 Game] Firing Bullet  (0) 2013.10.08
[HTML5 Game] Moving Object Ⅱ  (1) 2013.10.02
[HTML5 Game] Moving Object  (0) 2013.10.02
[HTML5 Game]Calculating FPS  (0) 2013.09.30

[HTML5 Game] Firing Bullet

Posted in 모바일/HTML5 // Posted at 2013. 10. 8. 22:30
728x90

이전 글(http://m.mkexdev.net/247)에서 게임 루프를 기반으로 게임 오브젝트 이동에 대해 알아 보았다.

키보드의 방향키에 반응해 전투기가 이동하는 샘플을 작성해 봤는데 여기서 덧붙여 전투기에서 총알이 발사되도록 해 볼 것이다. 키보드의 스페이스 바를 누르면 총알이 발사되며 키를 계속 누르고 있으면 총알 다발이 연속해서 발사되는 시나리오이다.

 

총알 이미지

총알으로 사용할 이미지는 총알을 제외한 배경이 투명한 형태로 만드는 것이 좋다. 그러나 투명 이지미를 수급하기 쉽지 않아,  여기서는 캔버스 배경과 동일한 흰색 배경의 총알 이미지를 사용할 것이다. (ppt로 간단히 만들고 캡쳐해서 이미지로 변환하자 ㅎㅎ)

 

 

데모 실행 화면

실제 구현에 앞서 데모 실행화면을 먼저 확인해 보자. 이전 글의 예제와 같이 전투기는 방향키에 반응해 이동을 하며, 여기서 더해 스페이스 키를 누르면 총알이 위쪽 방향으로 발사된다.

 

 

 

구현 내용 정리

구현해야 할 내용을 큰 맥락에서만 정리해 보자.

- 스페이스 키를 누르면 총알 이미지가 비행기 중앙에서 출발하도록 한다

- 총알은 연속적으로 발사되기 때문에 배열로 관리한다.

- 발사된 총알이 위쪽 방향으로 계속 이동하게끔 하기 위해 Y 좌표 값을 업데이트 한다

 

대략 이 정도만 정리하고 자세한 건 구현하면서 살펴 보도록 하자.

 

이전 글에서는 전투기 객체를 Character로 명명했었는데, 맘에 들지 않아 Fighter로 변경하고 메서드 이름도 drawFighter 로 변경하였다. 그리고 총알을 그리기 위해 추가된 코드는 파란색으로 표시했다.

 

총알은 하나 이상 발사될 수 있기 때문에 배열로 관리하며 발사된 총알 수 만큼 배열 요소가 생성된다. drawFighter 메서드에서는 전투기를 그리기 전에 배열에 담긴 총알을 그려준다. 이때 캔버스의 맨 위쪽까지 도달했을 경우(Y 좌표가 '0'일 경우) 배열에서 제거하고 루프를 건너뛴다. 이렇게 하지 않으면 캔버스를 벗어난 총알도 배열에 계속 잔존하게 되고 이 배열은 게임 실행 중 계속 커져 버려서 성능에 좋지 못하다. 따라서 반드시 총알 제거를 해 주어야 한다.(참고로 캔버스의 위쪽 끝 Y 좌표가 오프셋 값 등으로 인해 0보다 클 수 있어나 큰 의미 없으므로 그냥 0 값을 기준으로 한다) 

function Fighter(assets, x, y, canvasElement){ 
  this.canvasSize = {width: canvasElement.width, height: canvasElement.height};
  this.canvasContext = canvasElement.getContext('2d'); 
  this.assets = assets;
  this.position = {x: x, y: y}  
  this.bullets = [];
 
  this.drawFighter = function(){  
    this.canvasContext.clearRect(0, 0, this.canvasSize.width, this.canvasSize.height);  
  
    //draw Bullet
    for(var i = 0; i < this.bullets.length; i++){
       if(this.bullets[i].y <= 0) {
        this.bullets.splice(i,1);
        continue;
       }       
      this.canvasContext.drawImage(this.assets.bulletAsset, this.bullets[i].x, this.bullets[i].y);
    }

  
   //draw Fighter
   this.canvasContext.drawImage(this.assets.fighterAsset, this.position.x, this.position.y);
  }
}

 

 

이어지는 코드 역시 이전 글의 샘플 코드에서 파란색 부분의 주요 변경이 이뤄졌다. 파란색으료 표시하지 않는 부분도 일부 변경되 되었는데 이것은 일부 변수명 변경, 이미지를 두 개(전투기, 총알) 다운 받기 위한 코드, 게임 루프 메서드 분리 등 총알 발사와 관련된 로직과 크게 관련되어 있지 않아 별도 표시를 하지 않았다.

 

변경된 부분을 대략 설명하면, 전투기와 총알 이미지 객체를 담기 위한 assets 객체와 스페이스 바를 누를때 전투기 객체의 총알 배열에 (총알이 발사될 최초 x,y 지점을 지정하여) 하나의 총알 요소를 추가하고 있다. 그리고 매 업데이트마다 Y 값을 점점 줄여나가면서 위쪽 방향으로 이동시킨다.

var fps = 30;
var canvasElement;
var gameContext;
var fighter;         
var assets;  
var currentAssetLoadCount = 0;    
var speedFighter = 10;
var speedBullet = 15;
var isKeyDown = [];
var bulletTime = 0;
   
function init(){
 canvasElement = document.getElementById('GameCanvas');
 gameContext = canvasElement.getContext('2d');
 
 var fighterImage = new Image();  
 fighterImage.src = 'image/fighter.png';        
 fighterImage.onload = onAssetLoadComplete;  
  
 var bulletImage = new Image();  
 bulletImage.src = 'image/bullet.png';        
 bulletImage.onload = onAssetLoadComplete; 
 
 assets = {fighterAsset: fighterImage, bulletAsset: bulletImage};    

}

 

function onAssetLoadComplete(){ 
 if(++currentAssetLoadCount >= 2){     
    fighter = new Fighter(assets, 200, 200, canvasElement);      
    fighter.drawFighter();   
    setInterval(gameLoop, 1000 / fps);
 } 
}

function gameLoop(){
  update();
  display();
}

 

function update(){  
  bulletTime += 1000 / fps;  
  if(bulletTime >= 100){
    if(isKeyDown[32]){ //space      
       fighter.bullets.push({x: fighter.position.x + 6, y: fighter.position.y});  
       bulletTime = 0;
    }
  }

  if(isKeyDown[37]){
    fighter.position.x -= speedFighter;
  }
  if(isKeyDown[38]){
    fighter.position.y -= speedFighter;
  }
  if(isKeyDown[39]){
    fighter.position.x += speedFighter;
  }
  if(isKeyDown[40]){
    fighter.position.y += speedFighter;
  }
 
  for(var i = 0; i < fighter.bullets.length; i++){    
    fighter.bullets[i].y -= speedBullet;
  }  
}

 

function display(){
 fighter.drawFighter();
}

 

function onKeyDown(e){  
 isKeyDown[e.keyCode] = true;  
}

 

function onKeyUp(e){
 isKeyDown[e.keyCode] = false;
}

 

window.addEventListener("load", init, false);
window.addEventListener("keydown",onKeyDown,false);
window.addEventListener("keyup",onKeyUp,false);

 

추가로 위의 코드에서 다음의 부분을 주의깊게 보자. 이 코드가 필요한 이유는 30프레임이라는 빠른 속도는 스페이스 키를 한번만 눌러도 두 세번 누른 것처럼 되기도 하고, 스페이스 키를 계속 누르고 있을 경우 그 속도가 너무 빨라 총알이 겹쳐서 발사되어 시각적으로 완전하기 못하게 된다. 따라서 총알이 한번 발사된 후 0.1초 정도 지나야만 다음 총알이 발사될 수 있도록 약간의 딜레이(delay) 시간을 주는 것이 좋다.


bulletTime += 1000 / fps;
if(bulletTime >= 100){
   if(isKeyDown[32]){ //space
      fighter.bullets.push({x: fighter.position.x + 6, y: fighter.position.y});
      bulletTime = 0;
   }
}

 

'모바일 > HTML5' 카테고리의 다른 글

[HTML5 Game] Shooting Down  (0) 2013.10.16
[HTML5 Game] Sound Effect  (2) 2013.10.11
[HTML5 Game] Moving Object Ⅱ  (1) 2013.10.02
[HTML5 Game] Moving Object  (0) 2013.10.02
[HTML5 Game]Calculating FPS  (0) 2013.09.30

[HTML5 Game] Moving Object Ⅱ

Posted in 모바일/HTML5 // Posted at 2013. 10. 2. 21:30
728x90

이전 글(http://m.mkexdev.net/246)에서 키보드에 반응해 게임 오브젝트가 이동하는 예를 살펴 보았다.

당시 제기했던 문제점을 해결해서 완성도를 높여 보자.

 

이전 글에서 제기한 문제점을 다시 가져와 본다.

브라우저로 결과를 확인해 봤다면 뭔가 완전하지 않다는 것을 느꼈을 것이다. 전투기가 방향키에 반응해 원하는 곳으로 이동 하는 것은 분명하지만 완성도 있는 게임으로써는 뭔가 부족하다.

 

먼저 제일 문제가 되는 것은 '동시 키 입력'이 되지 않는다는 것이다.

예를 들어 대각선 방향으로 이동하기 위해 '→, ↑' 두 키를 동시 누르고 있더라도 뒤에 눌러진 키에만 반응하여 한쪽 방향으로만 이동하게 된다. 이것은 PC에서 Sift, Alt 키와 같은 보조키를 제외하고는 동시키 입력을 지원하지 않기 때문에 발생하는 현상이다. 따라서 이에 대한 처리를 별도로 해 줘야 한다.

 

두 번째 문제는, 전투기 이동의 시작이 매끄럽지 못하다는 것이다. 전투기를 끊김 없이 이동 시키기위해 키를 떼지 않고 계속 누르고 있을 경우 처음 이동이 끊기는 현상을 볼 수 있다. 이것은 키 입력이 지속될 때 발생하는 최초 지연 현상 때문인데 실제로 키 입력의 초당 실행수를 측정해 보면 최초 시작시점에 1~5사의 값이 나오고 이후부터는 최대 30정도로 유지되는 것을 확인할 수 있었다.

 

 

두 가지 문제의 해결책은 바로 게임 루프이다.

키보드의 키 입력 이벤트가 발생할 때 UI를 갱신하는 것이 아니라 게임 루프 상에서 빠르고도 지속적으로 UI를 갱신하도록 하며 키 이벤트는 키 입력 상태 값을 업데이트 해 주는 용도로만 그치게 하는 것이 핵심이다.

이렇게 하면 동시 키 입력이나 초기 지연 현상을 극복할 수 있게 되는 것이다.

 

 

 

 

 

Character 객체는 이전 코드와 동일하다.

function Character(asset, x, y, canvasElement){ 
  this.canvasSize = {width: canvasElement.width, height: canvasElement.height};
  this.canvasContext = canvasElement.getContext('2d');
 
   this.asset = asset;
   this.position = {x: x, y: y} 

   this.drawCharacter = function(){  
      this.canvasContext.clearRect(0, 0, this.canvasSize.width, this.canvasSize.height);    
      this.canvasContext.drawImage(asset, this.position.x, this.position.y);    
  }
}

 

 

이어지는 코드는 이전 코드이 문제점을 개선한 것인데, 먼저 UI를 갱신시키는 부분을 키 이벤트가 아닌 게임 루프에서 처리하는 것을 주의깊게 보자. 키 이벤트에서는 isKeyDown이라는 배열에 키 입력 여부를 삽입하고 게임루프에서 이 값을 기반으로 오브젝트 이동을 처리한다. 키 입력을 해제하기 위해 keyup 이벤트도 사용하고 있다.

var fps = 30;
var canvasElement;
var gameContext;
var character;         
var asset;  
var speed = 10;
var isKeyDown = [];
   
function init(){
  canvasElement = document.getElementById('GameCanvas');
  gameContext = canvasElement.getContext('2d');
  
  asset = new Image(); 
  asset.src = 'image/fighter.png';        
  asset.onload = onAssetLoadComplete;      
}

function onAssetLoadComplete(){ 
  character = new Character(asset, 200, 200, canvasElement);
  character.drawCharacter();
 
  setInterval(gameLoop, 1000/fps);
}

 

function gameLoop(){ 
  if(isKeyDown[37]){ 
    character.position.x -= speed;
  }
  if(isKeyDown[38]){ 
    character.position.y -= speed;
  }
  if(isKeyDown[39]){ 
    character.position.x += speed;
  }
  if(isKeyDown[40]){ 
    character.position.y += speed;
  }
 
 character.drawCharacter();
}

 

function onKeyDown(e){ 
  isKeyDown[e.keyCode] = true;
}

 

function onKeyUp(e){
  isKeyDown[e.keyCode] = false;

}

 

window.addEventListener("load", init, false);
window.addEventListener("keydown",onKeyDown,false);
window.addEventListener("keyup",onKeyUp,false);

 

브라우저로 결과를 확인해 보면 이전 샘플에서 발생했던 동시키 입력 문제나 초반 지연 현상이 개선된 것을 확인할 수 있다.

 

 

 

'모바일 > HTML5' 카테고리의 다른 글

[HTML5 Game] Sound Effect  (2) 2013.10.11
[HTML5 Game] Firing Bullet  (0) 2013.10.08
[HTML5 Game] Moving Object  (0) 2013.10.02
[HTML5 Game]Calculating FPS  (0) 2013.09.30
[HTML5 Game] Game State  (0) 2013.09.27

[HTML5 Game] Moving Object

Posted in 모바일/HTML5 // Posted at 2013. 10. 2. 11:08
728x90

이번 글에서는 게임 오브젝트를 움직여 이동시키는 방법에 대해 알아보자.

키보드의 방향키에 반응하여 상,하,좌,우로 움직여 볼텐데 뻔한 개념이니 바로 코드를 살표보자.

 

먼저 다음과 같이 Character 객체 기반을 작성한다. 지금까지 작성해오던 코딩 패턴과 유사하기에 별도의 설명은 생략하며, 위치 정보를 위한 position 객체 변수와 이 값을 기준으로 이미지를 그리고 있는 것을 확인하자.

function Character(asset, x, y, canvasElement){ 
  this.canvasSize = {width: canvasElement.width, height: canvasElement.height};
  this.canvasContext = canvasElement.getContext('2d');
 
  this.asset = asset;
  this.position = {x: x, y: y}; 
  
  this.drawCharacter = function(){  
      this.canvasContext.clearRect(0, 0, this.canvasSize.width, this.canvasSize.height);    
      this.canvasContext.drawImage(asset, this.position.x, this.position.y);  
   }
}

 

이어지는 코드에서는 키보드의 방향키에 반응하여 앞서 생성한 Character 객체의 위치 값(position)을 변경하면서 그리기 작업을 호출하고 있다. speed라는 전역변수는 이동 속도 즉 간견을 위한 값이다. 여기서 사용한 캐릭터는 전투기 이미지인데 구글에서 다운 받아서 무단으로 사용한 것이다 ㅡ,ㅡ;

var fps = 30;
var canvasElement;
var gameContext;
var character;         
var asset;  
var speed = 10;
   
function init(){
  canvasElement = document.getElementById('GameCanvas');
  gameContext = canvasElement.getContext('2d');
  
  asset = new Image(); 
  asset.src = 'image/fighter.png';        
  asset.onload = onAssetLoadComplete;      
}

 

function onAssetLoadComplete(){
 var frameCounter = new FrameCounter();  
 character = new Character(asset, 200, 200, canvasElement,frameCounter);
 character.drawCharacter();
}

 

function onKeyDown(e){ 
 if(e.keyCode == 37){                //left
   character.position.x -= speed;

 }

 if(e.keyCode == 38){                //up
   character.position.y -= speed;

 }

 if(e.keyCode == 39){                //right
   character.position.x += speed;

 }
 if(e.keyCode == 40){                //down
   character.position.y += speed;

 }
  
 character.drawCharacter();
}

 

window.addEventListener("load", init, false);
window.addEventListener("keydown", onKeyDown, false);

 

코드는, 전형적인 자바스크립트 키 이벤트 처리 로직이며 브라우저로 실행해 보면 전투기가 방향키대로 움직이는 것을 확인할 수 있다.

 

 

문제점

브라우저로 결과를 확인해 봤다면 뭔가 완전하지 않다는 것을 느꼈을 것이다. 전투기가 방향키에 반응해 원하는 곳으로 이동 하는 것은 분명하지만 완성도 있는 게임으로써는 뭔가 부족하다.

 

먼저 제일 문제가 되는 것은 '동시 키 입력'이 되지 않는다는 것이다.

예를 들어 대각선 방향으로 이동하기 위해 '→, ↑'  두 키를 동시 누르고 있더라도 뒤에 눌러진 키에만 반응하여 한쪽 방향으로만 이동하게 된다. 이것은 PC에서 Sift, Alt 키와 같은 보조키를 제외하고는 동시키 입력을 지원하지 않기 때문에 발생하는 현상이다. 따라서 이에 대한 처리를 별도로 해 줘야 한다.

 

두 번째 문제는, 전투기 이동의 시작이 매끄럽지 못하다는 것이다. 전투기를 끊김 없이 이동 시키기위해 키를 떼지 않고 계속 누르고 있을 경우 처음 이동이 끊기는 현상을 볼 수 있다. 이것은 키 입력이 지속될 때 발생하는 최초 지연 현상 때문인데 실제로 키 입력의 초당 실행수를 측정해 보면 최초 시작시점에 1~5사의 값이 나오고 이후부터는 최대 30정도로 유지되는 것을 확인할 수 있었다.

 

결국 이 두가지 문제를 해결해야 정상적인 이동 처리가 가능하며 다음 글에서 해결된 내용을 살펴볼 것이다.

 

 

'모바일 > HTML5' 카테고리의 다른 글

[HTML5 Game] Firing Bullet  (0) 2013.10.08
[HTML5 Game] Moving Object Ⅱ  (1) 2013.10.02
[HTML5 Game]Calculating FPS  (0) 2013.09.30
[HTML5 Game] Game State  (0) 2013.09.27
[HTML5 Game] Game Loop  (0) 2013.09.25

[HTML5 Game]Calculating FPS

Posted in 모바일/HTML5 // Posted at 2013. 9. 30. 23:30
728x90

게임 루프(http://m.mkexdev.net/242)를 다루는 글에서 초당 프레임 수인 FPS의 개념과 샘플을 작성해 보았다. 이번 글에서는 실제 초당 얼마의 프레임률을 보이는지 파악할 수 있는 초당 프레임 수 계산기를 작성해 보자.

 

초당 프레임 수는 게임 실행환경이나 가변적인 연산처리 등으로 일정하지 않거나, 목표한 프레임 률(framerate) 보장하지 못하는 등의 문제가 발생하기 때문에 게임 개발과 디버깅 시 관심있게 살펴봐야 하는 데이터이다.

 

특히 가변 프레임 방식이라면 초당 몇 프레임이 나오는지에 대한 실제적 자료가 필요하며 고정 프레임 방식일지라도 항상 고정값을 보장하지 않는 환경이 될 수도 있으니 실제 프레임 수를 살펴보는 것이 여전히 도움이 된다.

 

또한 만들고자 하는 게임이, 초당 몇 프레임에서 가장 원활한 동작을 보이는 지를 확인할 때도 유용하다 하겠다.

 

아래 사이트는 tree.js의 3D 샘플인데, 이 샘플에서도 좌측 상단에 초당 프레임 수를 보여주고 있다.

http://threejs.org/examples/ (이 사이트의 예제는 날 감탄스럽게 만들어 버렸다)

 

 

초당 프레임 수 계산 방식

초당 프레임수를 계산하기 위해서는 크게 두 가지 접근방식을 취할 수 있다.
먼저 밀리세컨드 단위의 이전 프레임의 실행 시간과 현재 프레임의 실행 시간을 비교해서 그 간격을 보고 프레임 수를 역으로 계산하는 방식과 실제 프레임 실행시마다 카운팅하는 방식이 그것이다. 두 방식 모두 큰 의미에서는 동일하다 할 수 있으나, 두 번째 방식이 좀 더 직관적이고 명료한 듯 하여 이 글에서도 두번째 방식으로 샘플을 작성해 볼 것이다.

 

예제가 복잡하지 않으니 바로 작성해 보자. 더 이상 HTML 파일은 언급하지 않겠다.(너무 심플하뉘...)

 

다음과 같이 초당 프레임 수를 계산하기 위한 객체 기반을 작성한다. 코드의 핵심은 현재 시간과 직전 시간을 (밀리세컨드 단위로) 비교하여 1000 즉, 1초가 지났을 경우 계속 증가시켜오던 callCount 값을 framePerSecond에 대입하는 부분이다. 참고로 자바스크립트의 Date.now()는 1970년 1월 1일 자정으로부터 현재 시간 사이의 밀리초를 반환하는 함수인데 new Date().getTime()으로도 동일한 결과를 받을 수 있으나 검색한 자료에 의하면 성능상 Date.now()가 더 좋은 효율을 보인다고 한다.

function FrameCounter(){
  this.callCount = 0;
  this.framePerSecond = 0; 
  this.beforeTime = 0; 
 
  this.countFps = function(){    
  //(Date.now() is returns the number of milliseconds elapsed since 1 January 1970 00:00:00 UTC)  
  var nowTime =  Date.now(); //1970년 1월 1일 자정과 현재 날짜 및 시간 사이의 밀리초 값입니다
  
   //If one second has passed
   if(nowTime - this.beforeTime >= 1000){
      this.framePerSecond = this.callCount;        
      this.beforeTime = nowTime;

      this.callCount = 0; 
   }
  
   //Increase frame count per second
   this.callCount++;
 }
}

 

이어지는 코드는 앞서 생성한 FrameCounter 인스턴스를 생성해서 게임루프마다 fps를 증가시키기 위해
countFps 함수를 호출하는 것이다. 초당 프레임 수는 좌측 상단에 표시하며 전체 누적 프레임 수는 중앙에 표시하도록 한다.

var fps = 30;
var canvasElement;
var gameContext;
var totalFrameCount = 0;
var frameCounter;

 

function init(){
 canvasElement = document.getElementById('GameCanvas');
 gameContext = canvasElement.getContext('2d');
 
 frameCounter = new FrameCounter();
  
 setInterval(gameLoop, 1000/fps); 
}

 

function gameLoop(){ 
  update();
  display();
}

 

function update(){  
  frameCounter.countFps();  //Call countFps function
  ++totalFrameCount;              //Increase total frame count
}

function display(){ 
 gameContext.clearRect(0, 0, canvasElement.width, canvasElement.height); 
   
 gameContext.textBaseline = 'top'; 
 gameContext.font = '10pt Arial';           
 gameContext.fillText(frameCounter.framePerSecond + '/second' ,5, 5);
 
 gameContext.font = '18pt Arial';           
 gameContext.fillText(totalFrameCount ,130, 80); 
}

window.addEventListener('load', init, false);

 

 

브라우저로 실행하면 가운데 누적 프레임 수와 좌측 상단의 초당 프레임 수를 확인할 수 있다 

 

 

'모바일 > HTML5' 카테고리의 다른 글

[HTML5 Game] Moving Object Ⅱ  (1) 2013.10.02
[HTML5 Game] Moving Object  (0) 2013.10.02
[HTML5 Game] Game State  (0) 2013.09.27
[HTML5 Game] Game Loop  (0) 2013.09.25
[HTML5 Game] Collision Detection  (2) 2013.09.23

[HTML5 Game] Game State

Posted in 모바일/HTML5 // Posted at 2013. 9. 27. 21:00
728x90

게임은 상태의 집합이라 할 수 있다. 보통 우리가 게임을 할 때 대략 다음과 같은 수순의 단계를 만나게 된다.

 

게임 준비 -> 게임 실행 -> 게임 종료

 

물론 복잡한 게임에서는 더 다양한 단계가 있을 수 있지만, 개념을 이해하는 수준에서 위와 같이 간단히 세 가지 단계이 있고 한 번에 하나의 단계가 활성화되어 게임이 실행된다는 것을 이해하는게 중요하다.

 

이러한 게임 흐름의 각 단계를 '게임 상태'라 하며 HTML5 Game 개발에서도 이러한 상태의 전이를 통해 전반적인 게임 흐름을 관리하게 된다.

 

다음 그림은 게임 루프상에서 실행되는 게임 상태를 표현한 것이다. 게임 상태는 특정한 이벤트에 의해 서로 교체되며 한 번에 하나의 상태가 게임 루프 상에서 동작하여 게임이 진행된다.

 

 

 

게임 상태를 잘 분리해서 독립화시켜 적절히 모듈화한다면 개발과 유지보수에 좋은 영향을 미치게 된다. 따라서 게임 개발 시 각각의상태를 객체화 시켜서 관리하는 것이 권장된다.

 

 

그럼 이제 게임 상태와 상태의 변경을 통한 게임 흐름을 체험해 볼만한 간단한 샘플을 작성해 보자.

총 3가지 게임상태(ready, running, end)를 객체로 관리하고 키보드의 스페이스 바를 누르면 다음 상태로 이동하는 예인데, running 상태에서는 캔버스에 랜덤한 사격형을 그리는 이전 글(Game Loop, http://m.mkexdev.net/242)의 샘플을 그대로 사용할 것이다.

 

먼저 다음과 같이 HTML 파일을 준비한다.

<!DOCTYPE html>
<html lang="en">
 <head>  
  <script type="text/javascript" src="js/gameState.js"></script>
 </head> 
 <body>  
  <canvas id="GameCanvas" width="500" height="400" style="border: 1px solid #000;">
    HTML5 Canvas를 지원하지 않습니다. 크롬 또는 사파리와 같은 HTML5 지원 브라우저를 이용해 주세요
  </canvas>        
 </body>
</html>

 

 

다음으로 '준비/실행/종료'에 해당하는 각각의 게임상태를 객체로 관리하기 위한 3개의 생성자 함수를 정의하는데 공통 속성을 재사용하기 위해 프로토타입의 상속을 활용한다. 3가지 상태는 모두 게임 루프상에서 실행되기 위한 update(), display() 함수를 가지고 있으며 각 상태에 맞는 업데이트와 화면 갱신을 처리하도록 한다.

 

/* Define Base Game State Class */
function GameState(canvasElement){
 if(canvasElement != undefined){
     this.canvasSize = {width: canvasElement.width, height: canvasElement.height};  
     this.canvasContext = canvasElement.getContext('2d');                                    
   }
}

 

/* Define Ready State Class */
function Ready(canvasElement){
   //Call Parent Constract Function
   GameState.call(this,canvasElement);
}

 

Ready.prototype = new GameState(); //inherit

 

Ready.prototype.update = function(){ 
  this.canvasContext.fillStyle = '#000000'; 
}

 

Ready.prototype.display = function(){ 
   this.canvasContext.font = '18pt Arial';        
   this.canvasContext.textBaseline = 'top';
   this.canvasContext.fillText("Ready..." ,200, 150);
}

 

/* Define Running State Class */
function Running(canvasElement){
  //Call Parent Constract Function
  GameState.call(this,canvasElement);

  this.position = {};
}

 

Running.prototype = new GameState(); //inherit

 

Running.prototype.update = function(){ 
  this.position.x = Math.floor(Math.random() * (canvasElement.width - 20));
  this.position.y = Math.floor(Math.random() * (canvasElement.height - 20));
  
  this.canvasContext.fillStyle = 'rgb(' + Math.floor(Math.random() * 255) + ','
                    + Math.floor(Math.random() * 255) + ',' + Math.floor(Math.random() * 255) + ')'; 
}

 

Running.prototype.display = function(){ 
   this.canvasContext.fillRect(this.position.x, this.position.y, 20, 20);
}

 

/* Define End Class */
function End(canvasElement){
   //Call Parent Constract Function
   GameState.call(this,canvasElement);
}

 

End.prototype = new GameState(); //inherit

 

End.prototype.update = function(){ 
   this.canvasContext.fillStyle = '#000000'; 
}

 

End.prototype.display = function(){ 
   this.canvasContext.font = '18pt Arial';        
   this.canvasContext.textBaseline = 'top';
   this.canvasContext.fillText("End!!!" ,200, 150);
}

 

 

 

게임 상태가 준비되었으니, 각 상태가 실행되도록 다음과 같이 작성한다. 게임상태를 저장하기 위한 gameState 배열과 이 배열에서 특정 상태를 선택하기 위한 인덱스 값을 저장하는 currentGameStateIndex 변수를 선언한다. 키보드의 스페이스 바를 누르면 이 변수 값을 차례대로(0~2) 변경되도록 하여 게임 상태가 순환되도록 한다. 나머지 코드는 지금까지 학습했던 내용과 동일하므로 설명을 생략한다.

 

var fps = 10;
var canvasElement;
var gameContext;
var gameState = [];
var currentGameStateIndex = 0;

 

function init(){
   canvasElement = document.getElementById('GameCanvas');
   gameContext = canvasElement.getContext('2d'); 
 
   //Create Game State Instance & Push in gameState Array
   gameState = [new Ready(canvasElement),new Running(canvasElement),new End(canvasElement)];
 
   setInterval(gameLoop, 1000/fps);
}

 

function gameLoop(){ 
   gameState[currentGameStateIndex].update();
   gameState[currentGameStateIndex].display();
}

 

function ChangeGameState(e){
   if(e.keyCode == 32){ //32: Space Key
      gameContext.clearRect(0, 0, canvasElement.width, canvasElement.height);  
      //Change Game State Index(0 ~ 2) 
      currentGameStateIndex = (currentGameStateIndex + 1) % 3; 
   }
}

 

window.addEventListener('load', init, false);
window.addEventListener("keydown", ChangeGameState, false);

 

샘플을 브라우저로 확인해 보면 스페이스 키로 인해 다음의 3 단계가 순환되는 것을 확인할 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

'모바일 > HTML5' 카테고리의 다른 글

[HTML5 Game] Moving Object  (0) 2013.10.02
[HTML5 Game]Calculating FPS  (0) 2013.09.30
[HTML5 Game] Game Loop  (0) 2013.09.25
[HTML5 Game] Collision Detection  (2) 2013.09.23
[HTML5 Game] Parallxing Background Animation  (0) 2013.09.21