AngularJS 기반 웹 어플리케이션, 어디서부터 시작해야 할까?
AngularJS를 기반으로 웹 어플리케이션을 개발할 때, AngularJS 사이트에서 제공해주는 문서들은 특정 API 활용 방법을 이해하거나 단순한 수준으로 시작하기에는 적합하지만 어떻게 수백에서 수천줄에 이르는 코드로 어플리케이션을 구성하고 발전시켜야하는지에 대해서는 명확한 가이드라인을 제공해주고 있지 않습니다. AngularJS를 기반으로 수차례의 크고 작은 개발을 진행하며 얻은 경험 지식과, 이를 기반으로 대규모 웹 어플리케이션 개발이 가능한 UI 프레임워크를 만드는 과정에서 고민하고 연구한 결과물을 정리해 보았습니다.
이 글은 대규모 웹 어플리케이션 개발에 있어 정답을 제시하는 글이 아니며, 대규모 웹 어플리케이션을 고려한 UI 프레임워크를 개발하는 과정에서 나온 산출물들 중 일부를 설명과 곁들여 공개하는 것입니다. 따라서 대규모 웹 어플리케이션 개발을 위한 인사이트를 얻는 방향으로 접근하시는 것이 좋습니다.
먼저는 파일과 디렉토리 구조를 어떻게 구성해야 하는지에서부터, 주요 자바스크립트 파일이 어떻게 구동되는지 하나하나 살펴보게 될 것입니다.
파일 및 디렉토리 구성
Brain Ford는 “Building Huuuuuge Apps with AngularJS” 라는 포스팅에서 대규모 어플리케이션을 개발할 때 중요한 것은 작고 집중되고 모듈화된 부분으로 개발해서 점진적으로 확장시켜나가는 것이 중요하다고 조언했습니다. 하지만 이렇게 대규모 어플리케이션을 어떠한 분류로 나누고 또 어떤 구조로 구성해야하는지에 대한 고민이 또다시 생기게 됩니다.
나누어진 부분들은 필연적으로 파일과 디렉토리로 분류되게 되는데, angular seed project 등에서도 권장하는 디렉토리 구조가 있고, 또 웹 상에서 검색해보면 다양한 구조로 파일과 디렉토리를 나누는 방법을 찾을 수 있습니다.
여기에서는 현재까지 여러 프로젝트에서 누적된 경험을 바탕으로, 모듈화된 AngularJS 프로젝트에 가장 적합한 파일과 디렉토리 구조를 소개하고자 합니다.
- 먼저 css와 img에는 각각 어플리케이션에서 공통적으로 사용되는 스타일시트와 이미지 데이터가 들어가게 됩니다.
- lib 폴더에는 angular, jquery 등 어플리케이션에서 사용되는 라이브러리들이 위치합니다.
- partials 폴더에는 ng-view에 뿌려줄 html template 파일들이 위치합니다.
- js 폴더에는 모듈화된 스크립트 파일들이 위치하게 됩니다.
js 폴더와 partials 폴더 안의 각 파일들은 반드시 한 가지의 내용만 담고 있는 것을 권장합니다. 예를 들어, 자바스크립트 파일 하나에는 오직 하나의 컨트롤러 혹은 오직 하나의 다이렉티브만 담겨있어야 한다는 것입니다. 프로젝트의 규모가 커져서 파일이 많아질수록, 더 많은 서브 디렉토리를 두어서 구조를 더욱 체계화하여 관리할 필요가 있습니다.
이러한 파일과 디렉토리 구조는 앞서 이야기한 작고 집중되고 모듈화된 부분으로 개발하려는 방향성을 고민하여 설계된 것입니다. 이렇게 모듈화된 구조를 통해 필요한 부분만 선별적으로 로딩할 수 있게되고 초기 로딩 속도에 크게 영향을 받는 UX 개선에 도움을 얻을 수 있습니다. 모듈화와 동적 로딩은 requireJS를 기반으로 구현되었으며, 이에 관련해서는 이 글에 자세히 설명되어 있습니다.
웹 어플리케이션 구조
전체적인 어플리케이션 구조는 위와 같습니다. document 전체에 대해 myApp 이라는 모듈이 부트스트래핑되고, html 태그에 CommonController 라는 컨트롤러가 자리합니다. CommonController에는 전체 메뉴와 같이 어플리케이션 전반에서 공통적으로 반복해서 사용되는 부분에 대한 컨트롤을 담당하게 됩니다.
ng-view directive가 있는 부분이 동적으로 로딩되는 부분으로, partials 폴더 아래에 있는 partial view 템플릿들을 가져와서 뿌려주게 됩니다. 이렇게 로딩되는 view 마다 각 view에 해당하는 컨트롤러 역시 동적으로 로딩되어 할당됩니다.
이러한 구조에 대한 이해를 바탕으로, 실제 어떤 흐름으로 웹 어플리케이션이 구동되며 모듈들이 관리되는지를 이어서 살펴보겠습니다.
웹 어플리케이션 흐름
웹 어플리케이션의 전체적인 큰 그림은 위와 같습니다.
index.html 파일을 열면 여기에 require.js 파일을 script 태그로 가져오게 됩니다. RequireJS 는 로드된 후 data-main 속성에 따라 가장 먼저 main.js 파일을 동적으로 불러와서 실행하게 됩니다.
main.js 파일은 RequireJS 모듈 형태로 선언되어 있는데, 디펜던시 선언부 및 디펜던시 로드 뒤 실행부로 나뉘어져 있습니다. 실행부에는 myApp Angular 모듈을 부트스트래핑하도록 되어 있지만, 그 전에 디펜던시들이 먼저 로드 되어야 하므로, angularJS, jQuery 등의 라이브러리와 app.js, route-config.js,routes.js 파일이 로드된 뒤에야 부트스트래핑이 진행되게 됩니다.
app.js 파일 역시 RequireJS 모듈 형태로 선언되어 있는데, 실행부에는 myApp 모듈 및 CommonController를 선언하는 내용으로 되어 있습니다. 이 내용 역시 바로 실행되지 않고 그 전에 디펜던시가 로드된 뒤 실행되게 됩니다. 따라서 디펜던시인 route-config.js 파일의 로드를 먼저 처리합니다.
route-config.js 파일 역시 RequireJS 모듈 형태로 동적으로 서비스, 다이렉티브, 필터 등을 로드할 때 필요한 스크립트들이 디펜던시로 등록되어 있습니다. 이 디펜던시들이 로드되면, AngularJS에서 경로(route) 설정 시 사용할 config 함수를 선언합니다.
route-config.js 파일의 처리가 끝나면 다시 역으로 app.js 파일의 실행부가 처리되고, 이어서 route-config.js 파일과 app.js 파일을 디펜던시로 갖고 있던 route.js 파일의 실행부가 처리됩니다. route.js 파일의 실행부에는 AngularJS의 경로 설정 로직이 있습니다.
여기까지 모든 디펜던시들의 처리가 끝나고 나면 다시 main.js 파일의 실행부가 처리되게 됩니다. 이 시점에서 myApp 모듈의 부트스트래핑이 작동되어 Angular Application 이 실행되게 됩니다.
이때, 입력된 route 에 따라서 해당 partial view template 파일과 controller 가 동적으로 로드됩니다.
다시 정리하면, RequireJS 를 통해 설정된 디펜던시들이 로드된 후 비로소 Angular Application이 시작되는 형태로, 또 각 route 마다 RequireJS가 template 과 controller 등을 동적으로 가져와서 추가해주는 방식으로 구현되어 있습니다.
그럼 이제, 각 스크립트 파일 안의 로직이 실제로 어떻게 동작하는지 구체적으로 살펴보겠습니다.
세부 흐름 분석
흐름의 시작은 index.html 파일입니다. require.js 에 대한 포스팅에서 이미 설명한 것과 크게 다르지 않은 구조로 설계되어 있습니다.
index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | <!doctype html> <html lang="en" ng-controller="CommonController"> <head> <meta charset="utf-8"> <title>My AngularJS App</title> <link href="css/bootstrap.css" type="text/css" rel="stylesheet"/> <link href="css/app.css" type="text/css" rel="stylesheet"/> <!-- HTML5 shim, for IE6-8 support of HTML5 elements --> <!--[if lt IE 9]> <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <!-- 꼭 필요한 필수 CSS는 위와 같이 고정해서 붙이고, 일부 페이지마다 필요한 CSS은 아래와 같이 컨트롤러에서 설정해서 로드한다. (IE8 에서도 정상동작) http://plnkr.co/edit/KzjIMN --> <link ng-repeat="stylesheet in stylesheets" ng-href="{{stylesheet}}" type="text/css" rel="stylesheet" /> </head> <body> <div> <a href="#/view1" class="btn">view1</a> <a href="#/view2" class="btn">view2</a> <a href="#/grid" class="btn">grid</a> <a href="#/admin" class="btn" ng-show="isAdmin">admin</a> </div> <hr> <div ng-view class="well well-small"></div> <button ng-hide="isAdmin" ng-click="isAdmin=true;">Become admin</button> <!-- 이 data-main 속성에서 requireJS가 처음 로드해야할 JS를 설정한다. 아래와 같이 쓰면, js 폴더 아래에 main.js 파일을 열게 된다. --> <script data-main="js/main" src="lib/require/require.js"></script> </body> </html> |
기본적으로 반드시 필요한 스타일시트는 link 태그로 먼저 입력하지만, 페이지마다 동적으로 필요한 스타일시트는 하단의 ng-repeat과 ng-href 를 활용해서 동적으로 로드될 수 있도록 설계되었습니다. IE8에서도 정상 동작하는 방식입니다.
하단의 require.js 파일을 로드해주는 부분에 data-main 속성을 설정해서 RequireJS가 로드된 후 바로 로드해서 실행해줄 JavaScript 파일을 지정해줄 수 있는데, 위의 예에서는 require.js 파일이 로드된 후에 바로 js 폴더 아래에 main.js 파일을 불러와서 실행하도록 되어 있습니다.
index.html 파일에서는 구체적으로 어떤 라이브러리들이 사용되는지 감추어짐으로써 전체적인 코드의 가독성도 높아지는 것을 볼 수 있습니다.
main.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | /* user strict 명령은 엄격하게 JavaScript 룰을 적용하라는 의미이다. 일부 브라우저의 경우 use strict 명령을 통해 보다 빠르게 동작하는 경우도 존재하는 것 같다. 잘못된 부분에 대한 검증도 보다 엄격하게 동작한다. 하지만, 일부 라이브러리의 경우 use strict 명령을 사용하면 동작하지 않는 경우도 있으므로 주의해야 한다. */ 'use strict'; //requireJS 기본 설정 부분 requirejs.config({ /* baseUrl: JavaScript 파일이 있는 기본 경로를 설정한다. 만약 data-main 속성이 사용되었다면, 그 경로가 baseUrl이 된다. data-main 속성은 require.js를 위한 특별한 속성으로 require.js는 스크립트 로딩을 시작하기 위해 이 부분을 체크한다. */ baseUrl:'js', /* paths: path는 baseUrl 아래에서 직접적으로 찾을 수 없는 모듈명들을 위해 경로를 매핑해주는 속성이다. "/"로 시작하거나 "http" 등으로 시작하지 않으면, 기본적으로는 baseUrl에 상대적으로 설정하게 된다. paths: { "exam": "aaaa/bbbb" } 의 형태로 설정한 뒤에, define에서 "exam/module" 로 불러오게 되면, 스크립트 태그에서는 실제로는 src="aaaa/bbbb/module.js" 로 잡을 것이다. path는 또한 아래와 같이 특정 라이브러리 경로 선언을 위해 사용될 수 있는데, path 매핑 코드는 자동적으로 .js 확장자를 붙여서 모듈명을 매핑한다. */ paths:{ //뒤에 js 확장자는 생략한다. 'text': '../lib/require/text', //HTML 데이터를 가져올때 text! 프리픽스를 붙여준다. 'jquery': '../lib/jquery/jquery', 'jquery-ui': '../lib/jquery/jquery-ui-1.10.2.min', 'angular': '../lib/angular/angular', 'library': '../lib' }, /* shim: AMD 형식을 지원하지 않는 라이브러리의 경우 아래와 같이 SHIM을 사용해서 모듈로 불러올 수 있다. 참고 : http://gregfranko.com/blog/require-dot-js-2-dot-0-shim-configuration/ */ shim:{ 'angular':{ deps:['jquery'], exports:'angular' }, 'jquery-ui': { deps: ['jquery'] }, 'app':{ deps:['angular'] }, 'routes':{ deps:['angular'] } } }); //requireJS를 활용하여 모듈 로드 requirejs( [ 'text', //미리 선언해둔 path, css나 html을 로드하기 위한 requireJS 플러그인 'jquery', //미리 선언해둔 path, jQuery는 AMD를 지원하기 때문에 이렇게 로드해도 jQuery 또는 $로 호출할 수 있다. 'angular', //미리 선언해둔 path 'jquery-ui', 'app', //app.js 'routes' //routes.js ], //디펜던시 로드뒤 콜백함수 function (text, $, angular) { //이 함수는 위에 명시된 모든 디펜던시들이 다 로드된 뒤에 호출된다. //주의해야할 것은, 디펜던시 로드 완료 시점이 페이지가 완전히 로드되기 전 일 수도 있다는 사실이다. //페이지가 완전히 로드된 뒤에 실행 $(document).ready(function () { //위의 디펜던시 중 myApp이 포함된 app.js가 로드된 이후에 아래가 수행된다. //임의로 앵귤러 부트스트래핑을 수행한다. angular.bootstrap(document, ['myApp']); }); } ); |
require.js 파일이 로드된 뒤 가장 처음 불러오는 파일인 main.js 스크립트는 크게 두 부분으로 나누어져 있습니다.
먼저는 RequireJS 의 환경을 설정하는 부분이고, 설정이 끝난 뒤에는 require 함수를 사용해서 디펜던시를 불러온 뒤 angular module을 부트스트래핑 하게 됩니다. 환경 설정에 대한 부분은 지난 require.js 에 대한 글에 충분하게 설명되어 있습니다.
앞서 흐름에 대해 살펴볼때 이야기했던 것처럼 angular module이 부트스트래핑 되는 것은 나열된 디펜던시들이 모두 로드된 시점이므로 모든 흐름의 끝에 실행됩니다.
나열된 디펜던시 중 ‘text’ 라는 것은 require.js 의 text plug-in 으로 스크립트 파일이 아닌 텍스트 형태의 파일을 동적으로 가져올 수 있도록 해주는 기능을 합니다.
app.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | 'use strict'; //requireJS 모듈 선언 - [myApp 앵귤러 모듈] define([ 'angular', //앵귤러 모듈을 사용하기 위해 임포트 'route-config' //registers에 각 프로바이더를 제공하기 위해 임포트 ], /* 이 부분도 주의깊게 살펴봐야한다. 위의 디펜던시들이 모두 로드된 뒤에 아래의 콜백이 실행된다. 디펜던시들이 리턴하는 객체들을 콜백함수의 파라메터로 받게 되는데, 자세히보면 route-config와 같이 snake case로 된 파일명이, 파라메터로 받을 때는 routeConfig와 같이 camel case로 바뀌는 것을 볼 수 있다. */ //디펜던시 로드뒤 콜백함수 function (angular, routeConfig) { //위의 디펜던시를 가져와서 콜백을 수행하게 되는데, //리턴하는 내용이 실제 사용되는 부분이겠지? //여기서는 myApp이라는 앵귤러 모듈을 리턴한다. //모듈 선언 var app = angular.module('myApp', [], function ($provide, $compileProvider, $controllerProvider, $filterProvider) { //부트스트랩 과정에서만 가져올 수 있는 프로바이더들을 각 registers와 연계될 수 있도록 routeConfig.setProvide($provide); //for services routeConfig.setCompileProvider($compileProvider); //for directives routeConfig.setControllerProvider($controllerProvider); //for controllers routeConfig.setFilterProvider($filterProvider); //for filters }); //공통 컨트롤러 설정 - 모든 컨트롤러에서 공통적으로 사용하는 부분들 선언 app.controller('CommonController', function($scope) { //스타일시트 업데이트 $scope.$on('updateCSS', function(event, args) { //파라메터로 받아온 스타일 시트 반영 $scope.stylesheets = args; }); }); return app; } ); |
AngularJS나 jQuery 와 같은 라이브러리 외에 main.js의 디펜던시로 걸린 파일 중 하나가 app.js 파일입니다. 이 디펜던시 설정으로 인해 app.js 파일이 로드되어 실행 된 후에야 main.js 파일의 실행된다는 것은 앞서 언급한 부분입니다.
app.js 파일에도 역시 디펜던시 설정이 걸려 있는데, AngularJS의 route 설정을 위한 route-config.js 파일입니다. route-config.js 파일이 로드된 뒤에 app.js 에서는 myApp 모듈을 선언합니다. AngularJS는 모듈 선언시에만 접근할 수 있는 provider 들이 있는데 Lazy Loading을 위해서 이 provider 들을 별도로 저장해둡니다. 이 provider 들을 저장하는 부분이 route-config.js 에 구현되어 있기 때문에 route-config.js 파일이 디펜던시로 잡혀있는 것입니다.
또한 app.js 파일에는 앱 전체적으로 공통적으로 사용되는 CommonController가 선언되어 있습니다. 사실 각 partial view마다 또 컨트롤러가 존재하는데, 이 공통 컨트롤러에서는 partial view 외에 부분에서 사용되는 내용들이 위치하게 됩니다. 예를들어, 위의 소스에서는 동적으로 스타일 시트를 업데이트하는 로직이 들어 있는 것을 볼 수 있습니다. 이 외에도 앱 전체 메뉴를 설정하는 부분 등도 이 컨트롤러에 포함될 수 있습니다.
여기에서는 컨트롤러 하나만 myApp 모듈에 추가하고 있지만, 공통적으로 사용되는 directive가 존재한다면 이 app.js 파일에서 선언해서 추가해 주는 것도 가능합니다. 그 외에도 공통적으로 사용되고 동적으로 추가될 필요가 없는 value 값이나 service, filter 등도 여기에서 선언해두 추가해주는 것이 좋습니다.
route-config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | //requireJS 모듈 선언 define([ //디펜던시가 걸려있으므로, 아래의 디펜던시가 먼저 로드된 뒤에 아래 콜백이 수행된다. 'registers/lazy-directives', 'registers/lazy-services', 'registers/lazy-filters' ], //디펜던시 로드뒤 콜백함수 function (lazyDirectives, lazyServices, lazyFilters) { var $controllerProvider; //컨트롤러 프로바이더를 받을 변수 //컨트롤러 프로바이더 설정 함수 function setControllerProvider(value) { $controllerProvider = value; } //컴파일 프로바이더 설정 함수 function setCompileProvider(value) { lazyDirectives.setCompileProvider(value); } //프로바이드 설정 함수 function setProvide(value) { lazyServices.setProvide(value); } //필터 프로바이더 설정 함수 function setFilterProvider(value) { lazyFilters.setFilterProvider(value); } /* 현재 시점에서 services는 오직 value 값을 정할때만 사용할 수 있다. Services는 반드시 factory를 사용해야 한다. $provide.value('a', 123); $provide.factory('a', function() { return 123; }); $compileProvider.directive('directiveName', ...); $filterProvider.register('filterName', ...); */ function config(templatePath, controllerPath, lazyResources) { //컨트롤러 프로바이더가 존재하지 않으면 오류! if (!$controllerProvider) { throw new Error("$controllerProvider is not set!"); } //변수 선언 var defer, html, routeDefinition = {}; //경로 템플릿 설정 routeDefinition.template = function () { return html; }; //경로 컨트롤러 설정 routeDefinition.controller = controllerPath.substring(controllerPath.lastIndexOf("/") + 1); //경로 routeDefinition.resolve = { delay: function ($q, $rootScope) { //defer 가져오기 defer = $q.defer(); //html에 아무런 값이 없는 경우 if (!html) { //템플릿 및 컨트롤러 디펜던시 설정 var dependencies = ["text!" + templatePath, controllerPath]; //리소스들 추가 if (lazyResources) { dependencies = dependencies.concat(lazyResources.directives); dependencies = dependencies.concat(lazyResources.services); dependencies = dependencies.concat(lazyResources.filters); } //디펜던시들 가져오기 require(dependencies, function () { //인디케이터 var indicator = 0; //템플릿 var template = arguments[indicator++]; //컨트롤러 if( angular.isDefined(controllerPath) ) { $controllerProvider.register(controllerPath.substring(controllerPath.lastIndexOf("/") + 1), arguments[indicator]); indicator++; } if( angular.isDefined(lazyResources) ) { //다이렉티브 if( angular.isDefined(lazyResources.directives) ) { for(var i=0; i<lazyResources.directives.length; i++) { lazyDirectives.register(arguments[indicator]); indicator++; } } //서비스(value) if( angular.isDefined(lazyResources.services) ) { for(var i=0; i<lazyResources.services.length; i++) { lazyServices.register(arguments[indicator]); indicator++; } } //필터 if( angular.isDefined(lazyResources.filters) ) { for(var i=0; i<lazyResources.filters.length; i++) { lazyFilters.register(arguments[indicator]); indicator++; } } } //딜레이 걸어놓기 html = template; defer.resolve(); $rootScope.$apply(); }) } else { defer.resolve(); } return defer.promise; } } return routeDefinition; } return { setControllerProvider: setControllerProvider, setCompileProvider: setCompileProvider, setProvide: setProvide, setFilterProvider: setFilterProvider, config: config }; } ); |
앞서 app.js 파일에 이 route-config.js 파일이 디펜던시로 잡혀있었기 때문에 route-config.js 파일이 먼저 실행되게 됩니다.
또 route-config.js 파일에는 각각 directive, service, filter 를 동적으로 등록시켜줄 때 사용되는 lazy-directives, lazy-services, lazy-filters 가 디펜던시로 잡혀있기 때문에 이 파일들이 로드된 뒤에 route-config.js 파일이 실행됩니다.
디펜던시가 로드된 뒤에는 route 설정과 관련된 함수들이 선언됩니다. 각각의 provider를 저장하는 함수들과 path에 따라 lazy-loading 을 구현하는 부분이 config 함수에 선언됩니다.
소스에 주석으로 설명이 되어 있지만 간단하게 전체 로직을 살펴보자면, 템플릿과 스크립트 파일 등을 파라메터로 받아서 차후에 호출이 들어올 경우 requireJS로 이들을 동적으로 가져와 등록하도록 예약하는 로직입니다.
route.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 'use strict'; define([ 'app', //생성한 앵귤러 모듈에 루트를 등록하기 위해 임포트 'route-config' //루트를 등록하는 routeConfig를 사용하기 위해 임포트 ], function (app, routeConfig) { //app은 생성한 myApp 앵귤러 모듈 return app.config(function ($routeProvider) { //view1 경로 설정 $routeProvider.when('/view1', routeConfig.config('../partials/view1.html', 'controllers/first', { directives: ['directives/version'], services: [], filters: ['filters/reverse'] })); //view2 경로 설정 $routeProvider.when('/view2', routeConfig.config('../partials/view2.html', 'controllers/second', { directives: ['directives/version'], services: ['services/tester'], filters: [] })); //grid 경로 설정 $routeProvider.when('/grid', routeConfig.config('../partials/grid.html', 'controllers/grid')); //admin 경로 설정 $routeProvider.when('/admin', routeConfig.config('../partials/admin.html', 'controllers/third')); //기본 경로 설정 $routeProvider.otherwise({redirectTo:'/view1'}); }); }); |
route-config.js 파일과 app.js 파일을 디펜던시로 갖고 있던 route.js 파일의 실행부가 처리됩니다. route.js 파일의 실행부에는 AngularJS의 경로 설정 로직이 있습니다.
모듈의 config 메서드를 사용해서 각각의 경로를 설정해주게 되는데, 여기에서는 4개의 경로만 설정해주었습니다. 실제 프로젝트에서는 $http 서비스 등을 사용해서 메뉴 관련 데이터를 JSON으로 동적으로 받아와서 처리해주는 방법도 고민해볼 수 있습니다.
이렇게 route.js 파일도 로드 및 실행이 완료되고 나면 다시 main.js 파일의 콜백 함수 부분으로 돌아가게 되고 비로소 myApp 모듈이 부트스트래핑되며 Angular Application 이 실행됩니다.
동적으로 로딩되는 컨트롤러 예 – grid.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | 'use strict'; define(['library/pqgrid/pqgrid.dev'], function () { //컨트롤러 선언 function _controller($scope) { //CSS 설정 $scope.$emit('updateCSS', ['lib/jquery/css/base/jquery-ui-1.10.2.min.css', 'lib/pqgrid/pqgrid.dev.css']); /* 보여줄 더미 데이터 생성 */ var array = []; for(var i=0; i<100; i++) { array[i] = [ "Task " + i, "5 days", Math.round(Math.random() * 100), "01/01/2009", "01/05/2009", (i % 5 == 0) + "" ]; } /* Paramquery Grid 설정 */ $("div[pq-grid]").pqGrid({ width: 700, height: 400, editable: false, title: "Basic Grid", colModel: [ { title: "Title", width: 100, dataType: "string" }, { title: "Duration", width: 100, dataType: "string" }, { title: "Complete", width: 50, dataType: "float", align: "right" }, { title: "Start", width: 100, dataType: "string", align: "right" }, { title: "Finish", width: 100, dataType: "string", align: "right" }, { title: "Effort Driven", width: 100, dataType: "string", align: "right"} ], dataModel: { data: array } }); } //생성한 컨트롤러 리턴 return _controller; }); |
path 가 grid 일 경우 grid.js 파일이 컨트롤러로서 동적으로 로드됩니다.
RequireJS module 형태로 선언되어 있는데, 이 grid 메뉴의 화면에는 pqGrid를 사용하므로 디펜던시로 pqGrid 라이브러리를 넣고 있는 것을 볼 수 있습니다.
| define(['library/pqgrid/pqgrid.dev'], function () { |
위와 같이 define으로 모듈을 선언한 뒤에 첫 파라메터로 배열 형태로 필요한 디펜던시를 선언합니다.
디펜던시 로드가 완료되면 아래 콜백 함수가 실행되는데, 콜백함수에서는 Angular Controller 형태로 함수를 선언해서 이 함수를 리턴해주는 것을 볼 수 있습니다.
또 동적으로 CSS를 설정해주기 위해 컨트롤러 내부에 $emit 으로 추가하고자하는 css 의 경로를 보내게 됩니다. 이러한 방식으로 CSS를 동적으로 추가/제거 해줌으로써 너무 많은 CSS 추가로 인해 스타일이 충돌하는 것을 예방할 수 있습니다.
실제 구현 미리보기
여기까지 설명한 내용을 바탕으로 동적으로 템플릿, 컨트롤러, 다이렉티브, 서비스, 필터 등을 로드하는 프로젝트 샘플을 제작해보았습니다. 이 프로젝트 샘플은 아래 프로젝트 샘플 다운로드 부분에서 다운 받을 수 있는 링크를 얻을 수 있습니다. 이 프로젝트 샘플을 좀더 개선하고 발전시켜 적용한 사례가 JCF UI 사이트입니다.
기본 경로인 view1 path로 들어간 모습입니다. CSS, 컨트롤러, 필터, 템플릿 모두 동적으로 로드되어 반영된 것을 볼 수 있습니다.
view2 메뉴를 누르면 역시 마찬가지로 CSS, 컨트롤러, 다이렉티브, 템플릿 모두 동적으로 로드되어 반영된 것을 볼 수 있습니다. 기존의 CSS는 제거되고 새로운 CSS만 반영된 것 역시 확인할 수 있습니다.
grid 메뉴를 누르면 디펜던시 설정된 pqGrid 라이브러리를 동적으로 로드하여 실행되는 것을 확인할 수 있습니다.
프로젝트 샘플 다운로드 – GitHub
지금까지 설명한 내용을 바탕으로 제작된 프로젝트 샘플을 GitHub에서 받으실 수 있습니다. 직접 받아서 수정하고 실행해본다면 전체적인 구조를 이해하고 활용하는데 도움이 될 것입니다.
결론
지금까지 AngularJS를 기반으로 대규모 웹 어플리케이션을 개발할 때 고민하게 되는 모듈화, 리소스의 동적로딩 등에 대해 살펴보았습니다. 또 이를 바탕으로 간단한 샘플 프로젝트를 작성했습니다.
앞서 말씀드린 것처럼 이 글은 대규모 웹 어플리케이션 개발에 있어 정답을 제시하는 글이 아닙니다. route 를 동적으로 추가하는 부분이나, breadcrumb을 관리하는 부분 등 실제 대규모 웹 어플리케이션을 개발할 때 필요한 부분들이 많이 빠져있기 때문입니다. 하지만 부족하나마 대규모 웹 어플리케이션을 개발하는데 있어 기준점은 될 수 있지 않을까 생각해봅니다.
부족한 설명과 부실한 예제였지만, 대규모 웹 어플리케이션 개발을 위한 안목을 얻을 수 있는 시간이었기를 기대합니다. 다음 포스팅에서는 대규모 웹 개발을 위해 좀더 개선된 형태의 seed project를 살펴보도록 하겠습니다.
InputStream
come from? FromURLConnection#getInputStream()
? In a bit decent protocol like HTTP, the enduser should already be instructed somehow that the content is gzipped. – BalusC Jan 27 '11 at 15:47