최용수 인스웨이브시스템즈 연구개발본부 팀장

▲ 최용수 인스웨이브시스템즈 연구개발본부 팀장

[컴퓨터월드] 지난 시간에는 JavaScript와 JSON을 이용해서 Server-Side를 구성한 데 이어 이번 편에서는 Front-End를 구현해 보겠다. Front-End에서는 이미 JavaScript와 JSON이 널리 사용되고 있기 때문에 새로운 것이 없을 수 있다. 


Front-End 개발/빌드 환경에 JavaScript가 어떻게 사용되고 패키지와 종속성 관리는 무엇으로 하는지에 대해, 그리고 개발에 유용한 수많은 라이브러리 중 하나를 살펴보도록 하겠다. Node.js가 Server-Side를 위해 탄생했지만 아래 살펴볼 개발/빌드 환경과 패키지/종속성 관리 모듈들도 Node.js로 만들어져 있다.

Bower 
프로그램이나 프로젝트의 모든 기능을 직접 구현할 수도 있겠지만 검증된 패키지를 이용함으로써 빠르고 견고하게 구현할 수 있다. 이를 도와주는 도구로 Bower(http://bower.io)가 있다.

지난 글들에서 살펴본 NPM이 Node.js로 만들어진 모듈을 관리했다면 Bower는 Front-End 패키지 관리자라고 할 수 있다. Bower는 Front-End 패키지들을 검색하고 설치, 제거, 업데이트, 종속성 관리 등을 수행한다. Bower 자체도 Node 패키지이므로 NPM을 이용해서 설치하면 된다. 


$ npm install -g bower 
  

사용방법도 NPM과 유사하다. 아래는 `backbone` 라이브러리를 설치하는 예이다.


$ bower install backbone 
  

패키지는 명령을 수행한 디렉토리 하위의 `components` 폴더(없는 경우 생성됨)에 종속성이 있는 패키지들과 함께 설치된다. backbone은 'underscore'라는 패키지에 종속성이 있으므로 components 폴더에 두 개의 패키지가 모두 설치된 모습이다. 

 

NPM이 `package.json` 파일로 패키지를 관리한다면 Bower는 `bower.json`라는 파일이 그 역할을 한다. 아래는 backbone의 bower.json의 예이다. dependencies에 필요한 패키지를 명시하고 ignore에 설치 시 무시할 파일들을 지정한다. 



"name" : "backbone", 
"version" : "1.1.2", 
"main" : "backbone.js", 
"dependencies" : { 
"underscore" : ">=1.5.0" 
}, 
"ignore" : ["backbone-min.js", "docs", "examples", "test", "*.yml", "*.map", ".html", "*.ico"]

물론 자신이 구현한 패키지도 등록해서 다른 사람들에게 유용하게 사용될 수 있다.

Grunt 
Front-End가 Server-Side의 개발/빌드 환경에 비해 단순한 것 같지만 반복적이고 번거로운 작업이 의외로 많다. 예를 들면 품질을 위한 문법오류 체크와 Unit 테스트를 수행하고 실행 시 성능을 위해 파일들을 Minify하며 배포용 파일의 버전과 생성일을 자동으로 관리하는 등의 작업이 필요하다. 도구를 이용해 이런 작업들을 손쉽게 수행할 수 있는데 가장 잘 알려진 것으로 `Grunt`(http://gruntjs.com)가 있다. 

1) 설치 
Grunt 또한 Node.js 모듈로 NPM을 이용해 설치하면 된다. 


$ npm install grunt 
  

Grunt가 빌드 시스템이라면 위에 나열했던 작업들은 각각의 플러그인으로 구현되어 있어서 NPM을 이용해 프로젝트에 필요한 플러그인들을 설치해서 사용한다. 


$ npm install grunt-contrib-uglify 
  

모두 Node.js의 모듈이기 때문에 `node_modules` 폴더에 설치되고 'package.json' 파일로 관리된다.

 

2) 실행 
Grunt를 실행하기 위해서는 `Gruntfile.js` 파일이 필요하다. 아래는 그 예이다. `module.exports`는 지난 편에서도 살펴봤듯이 할당된 함수를 외부에 제공하고 이를 `require`를 이용해서 참조한다. 이렇게 Server-Side 개발과 Front-End 빌드 환경을 동일한 패턴으로 작성할 수 있다. 


module.exports = function(grunt) { 

require('load-grunt-tasks')(grunt); 

function process( code ) { 
return code 
.replace( /@VERSION/g, grunt.config( "pkg" ).version ) 
.replace( /@HOMEPAGE/g, grunt.config( "pkg" ).homepage ) 
.replace( /@DATE/g, ( new Date() ).toISOString().substr(0, 10) ); 

grunt.initConfig({ 
pkg: grunt.file.readJSON('package.json'), 
concat: { 
"basic": { 
options: { process: process }, 
src: [ "src/intro.js", "src/init.js", "src/data.js", 
"src/api.js", "src/formatter.js", "src/outro.js" ], 
dest: "js/w5.js" 

}, 
uglify: { 
options: { 
banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */n'
}, 
build: { 
src: ['js/w5.js'], 
dest: 'dist/js/<%= pkg.version %>/<%= pkg.name %>.min.js' 

}, 
copy: { 
js: { 
files: [ 

expand: true, 
cwd: 'js', 
src: ['*.js'], 
dest: 'dist/js', 
rename: function ( dest, src ) { 
return dest + '/<%= pkg.version %>/' + src.substring( 0, src.indexOf('.js') ) + '.js';

}, 

expand: true, 
cwd: 'dist/js', 
src: ['*.js'], 
dest: 'www-root/javascripts/' 


}, 
resources: { 
files: [ 

expand: true, 
cwd: 'resources', 
src: 'images/*', 
dest: 'dist/' 



}, 
jshint: { 
js: { 
src: ['js/w5.js'], 
options: { curly: true, eqeqeq: true, immed: true, latedef: true, newcap: true, noarg: true,
sub: true, undef: true, unused: true, boss: true, eqnull: true, node: true, browser: true }

}, 
less: { 
miniCss: { 
options: { 
cleancss: true 
}, 
files: { 
"dist/css/w5.min.css": "resources/less/*.less" 

}, 
css: { 
files: { 
"dist/css/w5.css": "resources/less/*.less" 


}, 
watch: { 
files: [ 'src/*.js', 'resources/less/*.less' ], 
tasks: [ 'default' ] 
}, 
clean: ['js/w5.js', 'www-root/javascripts'] 
}); 

grunt.registerTask('default', ['clean', 'concat:basic', 'jshint:js', 'uglify', 'copy:js', 'copy:resources', 'less']);
grunt.registerTask('startup', ['connect:server']); 
grunt.registerTask('clear', ['clean']); 
}; 

  

함수가 실행될 때 grunt 객체가 파라미터로 전달된다. 


function(grunt) { }; 
  

이 객체의 initConfig 메소드를 이용해서 필요한 Task를 정의하고 registerTask 메소드를 통해 Task를 등록한다. 이 등록된 Task를 실행해서 필요한 작업들을 수행한다. Grunt에서는 수행할 작업을 `Task`라고 부르고 각각 플러그인으로 제공된다. 사용하기 위해서 이를 로딩해야 하는데 일반적으로 `loadNpmTasks`를 이용한다.


grunt.loadNpmTasks('grunt-contrib-uglify'); 
  

하지만 사용할 플러그인이 많은 경우 개별적으로 로딩하는 것보다 `load-grunt-tasks`를 이용하면 한 번에 모듈들을 로딩할 수 있다. load-grunt-tasks 모듈을 require로 가져온 후 grunt 객체를 파라미터로 전달해서 실행하면 package.json 파일의 dependencies/devDependencies/peerDependencies 에 정의된 Grunt Task들을 일괄적으로 로딩한다. 


require('load-grunt-tasks')(grunt); 
  

3) Task 정의 
필요한 모듈들을 로딩했으면 'initConfig'로 Task를 정의한다. initConfig의 파라미터로 JSON 객체를 구성해 주면 된다. package.json 파일을 읽어서 JSON 객체로 참조할 수 있게 `pkg` 프라퍼티에 할당한다. 주로 package.json에 정의된 버전과 패키지명을 참조하는데 사용한다. 


pkg: grunt.file.readJSON('package.json'), 
  

`concat` Task는 여러 파일들을 합쳐서 하나의 파일로 만든다. 기능이나 프로그램 구조에 따라 파일을 구분해서 개발한 후 실행에 사용될 하나의 파일로 묶을 때 사용된다. Task(concat) 하위에 Target(basic)을 정의할 수 있다.[1] Target은 개발/운영 등 실행환경에 따라 조합해야 할 파일 대상이나 순서를 조정할 때 유용하다.

Task가 실행되면 'src'의 배열에 정의된 파일들이 순서대로 'dest'에 정의된 하나의 파일로 합쳐진다. 정의된 파일 path는 명령이 수행된 디렉토리 기준으로 상대경로이다. options는 Task 레벨이나 Target 레벨로 정의할 수 있는데 아래는 Target 레벨의 예이다. Task 레벨로 정의된 options는 모든 Target에 적용된다. 동시에 존재하는 경우 Target 레벨의 options가 Task 레벨의 것을 override한다. 


concat: { 
"basic": { 
options: { process: process }, 
src: [ "src/intro.js", "src/init.js", "src/data.js", 
"src/api.js", "src/formatter.js", "src/outro.js" ], 
dest: "js/w5.js" 


  

options의 process 프라퍼티에 함수를 할당해서 전처리를 수행할 수 있다. 아래 함수는 각 파일 내용 중 '@VERSION'을 'package.json'에 정의된 버전으로 대체한다. JavaScript의 replace 함수를 이용하고 있다. Chain으로 연결된 것들도 동일하게 수행된다. 


function process( code ) { 
return code 
.replace( /@VERSION/g, grunt.config( "pkg" ).version ) 
.replace( /@HOMEPAGE/g, grunt.config( "pkg" ).homepage ) 
.replace( /@DATE/g, ( new Date() ).toISOString().substr(0, 10) ); 

  

'intro.js' 파일에 아래와 같이 작성되었다고 가정한다. 


/*! 
* w5 @VERSION 
* [@HOMEPAGE] 
* Date: @DATE 
*/ 
  

합쳐진 'w5.js' 파일에는 다음과 같이 표현된다. 


/*! 
* w5 1.0.0 
* [http://w5.io] 
* Date: 2014-11-23 
*/ 
  

'less' Task는 less[2] 파일을 css 파일로 컴파일하고 minify한다. 

위에 정의된 모든 작업들을 지정한 파일이 변경된 경우 자동으로 수행할 수 있다. 'watch' Task는 'src' 경로의 js 파일이나, 'resources/less' 경로의 less 파일이 수정된 것을 감지하여 'default' Task를 자동으로 수행한다.


watch: { 
files: [ 'src/*.js', 'resources/less/*.less' ], 
tasks: [ 'default' ] 

  

지면상 모든 Task를 설명하기는 어렵기 때문에 각 Task의 자세한 내용은 각 플러그인 사이트에서 참조하기 바란다.

4) Task 실행 
Task들이 정의됐다면 Command Line(터미널이나 명령 창)에서 실행한다. 아래와 같이 실행하면 'w5.js' 파일을 'w5.min.js' 파일로 minify한다. 


$ grunt uglify 
  

일련의 작업들을 묶어서 Task로 등록하면 편리하게 사용할 수 있다. 'registerTask'로 Task들의 조합을 'Alias'로 등록할 수 있다. 아규먼트 없이 'grunt'를 실행하면 `default` Task가 실행된다. `:` 를 이용해서 Task의 특정 Target을 실행할 수 있다. Target을 지정하지 않은 경우, Task에 정의된 모든 Target이 수행된다.

'default' Task를 실행하면 이전 결과물들을 삭제하고 개발파일들을 하나의 파일로 합친 후 문법 검사를 하고 minify해서 필요한 위치로 js 파일과 리소스들을 옮기며 less 파일을 컴파일과 minify해서 지정한 위치에 css 파일을 생성한다. 


grunt.registerTask( 'default', ['clean', 'concat:basic', 'jshint:js', 'uglify', 'copy:js', 'copy:resources', 'less'] ); 
  

Backbone 
웹 애플리케이션을 개발하다 보면 UI(DOM)와 Data, 자바스크립트 로직 들이 서로 결합되어서 특정 기능이나 부분만 수정하려고 해도 전체가 영향을 받아 큰 위험을 감수해야 한다거나 손을 대기 어려운 상황이 벌어지곤 한다. 이를 해결하기 위해 DOM과 Data, 로직, Event 처리 간의 결합도를 낮춰야 하는데 여기에 프레임워크나 라이브러리를 이용할 수 있다. 이중 하나인 Backbone.js(http://backbonejs.org)를 살펴보겠다.

흔히 Backbone.js를 MVC framework이라고 한다. Backbone은 Controller가 없기 때문에 MVC가 아니라고도 하는데 요즘은 'MVVM', 'MVP', 'MVC' 등을 통칭해서 'MV*(Whatever)'라고 하니 'MV*'라고 부르는 것이 적절할 것 같다.[3] Framework은 모듈이 개발 코드를 호출하고 Library는 개발자가 모듈을 호출한다는 관점에서 본다면 Backbone은 라이브러리에 가깝다. 

Backbone은 크게 Events, Router, History, View, Model, Collection, Sync로 분류할 수 있다. 이 중에 Router, History는 SPA[4]를 위한 것이고 View가 MV*에서 View, Model과 Collection이 MV*에서 Model에 해당한다. Events로 이벤트를 제어하고 Custom Event를 생성할 수 있으므로, 이를 이용해서 ViewModel이나 Controller를 구성한다. Sync는 서버와의 통신과 동기화를 담당한다. 

Backbone은 다음에 살펴 볼 Underscore.js[5] 에 종속성이 있다. 실제로는 Sync에서 통신과 DOM Selector와 Event 처리에 jQuery(http://jquery.com/)를 이용하기 때문에 jQuery에도 종속성이 있다.[6] Backbone에서 jQuery를 'Backbone.$'로 참조할 수 있다. 

Backbone의 Model을 Table 구조로 봤을 때 컬럼의 헤더를 Key로, Row의 데이터를 그 Key의 Value로 가지는 Object이다. 이 Model을 논리적인 그룹으로 관리하는 것이 Backbone의 Collection이다.
아래의 테이블을 보면 Collection은 두 개의 Model을 갖고, 각 모델은 `{ first_name: 'Art', last_name: 'Venere', city: 'Bridgeport', phone1: '856-636-8749' }` 형태로 표현될 수 있다.

 

Underscore 
Underscore.js는 자바스크립트 Utility를 모아 놓은 라이브러이다. 자바스크립트 배열과 객체, 함수를 처리할 수 있는 유용한 기능들을 제공하고 간단한 Template과 체이닝도 제공한다. 
지난 호에서 Server-Side 로직을 구현할 때도 이 라이브러리를 사용했었다. 동일한 자바스크립트 라이브러리가 Front-End와 Server-Side에서 사용되는 것이다. 

'ECMAScript 5'[7] 를 지원하는 브라우저에서는 Array.isArray, Object.keys, Function.prototype.bind 같은 네이티브 함수를 이용하고 그렇지 않은 경우에는 Fallback 처리한다. 

DataGrid 
서버에 저장되어 있는 사용자 정보를 DataGrid로 표현하고 간단한 CUD 조작을 해 보자. 이를 위해 지난 번 Server-Side 프로그램을 이용한 W5 Grid 예제를 살펴보겠다. 

개별 사용자 정보를 담을 model을 정의한다. MongoDB와 동기화시킬 것이므로 Model의 id로 사용될 프라퍼티를 `_id` 로 지정해 준다. 방금 전 정의한 Model Constructor를 model 프라퍼티에 지정하여 Collection을 정의한다. url 프라퍼티에 지난 편 라우팅에 사용해던 '/users' 경로를 지정해 준다.

View가 표현될 Element를 el 프라퍼티에 'selector'로 지정해 주고 Model로 정의한 Collection을 collection 프라퍼티에 지정한다. 이 밖에 컬럼 ID 와 그리드의 넓이, 표시될 row의 개수 등을 설정해 준다.


<div id='grid1'></div> 

var model = Backbone.Model.extend( { idAttribute: "_id" } ), 
List = Backbone.Collection.extend( { model: model, url: '/users' } ),
grid1 = new w5.Grid({ 
el : "#grid1", 
option: { 
width : "800px", 
caption : "사용자 정보", 
rowNum : 10 
}, 
collection: new List(), 
colModel : [ { id: 'first_name' }, { id: 'last_name' }, { id: 'company_name' }, { id: 'address' },
{ id: 'city' }, { id: 'county' }, { id: 'state' }, { id: 'zip' }, 
{ id: 'phone1' }, { id: 'phone2' }, { id: 'email' }, { id: 'web' } ] 
}).render(); 

grid1.fetch(); 
  

w5.Grid Constructor 함수를 이용해서 'grid1' 인스턴스를 생성한 후 render 메소드를 호출하면 View가 그려진다. 'fetch'를 호출하면 Collection에 지정한 url로 restful 서비스를 요청하고 응답으로 받은 JSON 데이터가 Collection과 바인딩된 후 그리드로 표현된다.[8] 아래는 여기까지 실행한 모습이다.

 

사용자 정보를 추가하기 위해 입력할 JSON Object를 준비한다. 객체의 프라퍼티는 w5.Grid의 colModel로 정의한 id들이다. 삽입할 위치와 데이터를 파라미터로 addRow를 호출한다. save 옵션을 주어 화면뿐만 아니라 서버와 동기화시킨다. success에 지정한 callback 함수는 서버와 동기화 성공 후 호출된다.


var data = { "first_name": "YoungSoo", "last_name": "Choi", "company_name": "inswave",
"address": "guro", "city": "seoul", "county": "korea", "state": "guro", "zip": "1234",
"phone1": "123-456-1234", "phone2": "789-123-4567", 
"email": "maninzoo@inswave.com", "web": "http://www.inswave.com" };

grid1.addRow( 0, data, { save: true, success: function ( model, response, options ) {
console.log( 'create a Row ', _.omit( response, '_id' ) ); 
}} ); 
  

 

추가한 Row의 두 개 전화번호 컬럼을 수정한다. 

data = { phone1: '111-111-1111', phone2: '222-222-2222' }; 
grid1.row(0).set( 'data', data, { save: true, saved: true } ); 

 

removeRow를 호출해서 추가했던 Row를 삭제한다. destroy 옵션을 주어 서버와 동기화 시킨다. success에 지정한 callback 함수는 addRow와 마찬가지로 서버와 동기화 성공 후 호출된다.


grid1.removeRow( 0, { destroy: true, success: function ( model, response, options ) {
console.log( 'removed the Row ', response.result ); 
}}); 
  

fetch하고 CUD한 작업을 브라우저의 개발자도구에서 Network을 확인한 모습이다.

 

각 CRUD에 적절한 Request Method(GET/POST/PUT/DELETE)가 지정되었고 '/users' url 뒤에 자동으로 '_id' 가 붙어서 Express.js에서 라우팅과 MongoDB에서 Update/Delete가 정상 수행되었다.

마치며 
개발/빌드 환경을 구축하고 패키지 매니저들을 이용해서 필요한 라이브러리를 설치(종속성 관리 포함)하고 DB에 쿼리하고 클라이언트 요청을 라우팅하고 MV* 라이브러리를 이용해서 Front-End, Server-Side 프로그램을 구현해 보았다. 이 모든 작업들이 JavaScript(Node.js)와 JSON으로만 이뤄졌다. 단순함에서 오는 강력함을 경험해 보시기 바란다. 

[1] 이 예제에서는 Target이 하나이므로, 굳이 선언하지 않고 options, src, dest 프라퍼티들이 Task 레벨에 정의되는 것이 일반적이다. 
[2] LESS는 CSS 전처리기(Pre-processor)로 Variables, Mixins, Nested Rules, Functions & Operations을 지원해서 Style을 구조적으로 작성할 수 있게 해 준다.
[3] MVC, MVP and MVVM(http://tomyrhymond.wordpress.com/2011/09/16/mvc-mvp-and-mvvm/) 
[4] Single-page application(http://en.wikipedia.org/wiki/Single-page_application)
[5] Backbone.js와 Underscore.js는 모두 DocumentCloud(https://www.documentcloud.org/home)의 라이브러리들이다.
[6] Underscore.js와 jQuery를 Lo-Dash(http://lodash.com/)와 Zepto(http://zeptojs.com/) 등으로 일정 부분을 대체할 수 있다. 
[7] ECMAScript 3 버전이 우리가 흔히 알고 있는 자바스크립트라고 이해하면 편리하다.(http://ko.wikipedia.org/wiki/ECMA스크립트) 

 

저작권자 © 컴퓨터월드 무단전재 및 재배포 금지