Angular 기반 SPA를 위한 Push 환경은 Node 기반의 Socket.io를 사용한다. Angular.js의 장점중 하나가 DI(Dependency Injection)이고 이를 Node에서 구현한 프레임워크인 express-train (축약, train)을 사용한다. train은 특정 폴더에 .js 파일을 놓으면 자동으로 DI되어 사용을 할 수 있게 해준다. 일정한 규약만을 지키면 개발 코드를 모듈화 하여 관리할 수 있고 테스트 할 수 있다. train은 express에 DI 기능을 접목해 놓은 것으로 보면 된다
1. Express-Train 설치하기
- 설치 : npm install -g express-train
- 새로운 Node 환경 프로젝트 생성
// train이 사용하는 기본 템플릿을 기반으로 자동으로 palette.fronend.node 디렉토리에 기본 폴더와 파일을 생성해 준다
$ train new palette.frontend.node
// 수행 http://localhost:4000 호출
$ train run
- 디렉토리 구조
+ app : auto-injected 로 표현된 것 .js 파일을 해당 디렉토리에 놓으면 다른 곳에서 파일 명칭으로 불러와서 (function parameter DI 방식) 사용을 할 수 있다
+ bin : 이것은 node_modules/express-train 밑에 존재한다
+ test : Mocha를 이용한다
+ public : Angular.js 기반으로 작성한 모든 static 파일들이 이곳에 놓일 것이다. 엄밀히 따지면 Distributed된 Frontend파일이 놓인다
app
/controllers -- application controllers (**autoinjected**)
/lib -- application specific modules (**autoinjected**)
/middleware -- application middleware (**autoinjected**)
/models -- application models (**autoinjected**)
/public -- static content (html, js, css, etc) : SPA모든 파일을 해당 폴더 밑에 복사할 것이다
/views -- view templates (loaded into express' view engine) : 서버단의 파일 jade, ejs, hbs 같은 서버 template 파일
index.js -- index file exports the express train application : train run 시에 수행되는 파일 based on Node.js
bin -- executable scripts :
doc -- documentation
config -- environmental configuration files : train 환경 설정 파일
test -- tests : 서버 테스트 파일
package.json -- npm package.json (needs to have express-train as a dependency)
- train의 환경파일 : development와 production 환경파일을 구분함
{
"cookie_secret": "boggle at the situation",
"session":{
"key": "boggle at the situation",
"secret": "boggle at the situation"
},
"client_port": 3000,
"request_timeout": 1000,
// RESTful 호출 접두사 Restanuglar 참조
"restful_api_version": "/api/v1",
"server_name": "first",
// REDIS 연결 Client Library 환경 설정
"data_broker": {
"port" : 6379,
"host" : "localhost",
"auth" : ""
},
// MongoDB 환경 설정
"mongodb":{
"uri": "mongodb://localhost/playhub_palette"
},
"playhub_channel_file": "channels.json",
// Winston 에러 로그가 쌓일 MongoDB 정보
"winston_mongo": {
"level": "error",
"db": "playhub_palette",
"host": "localhost",
"port": 27017,
"collection": "sd_logs"
},
// Winston 로그 파일의 명칭 설정
"winston_file": {
"filename": "sd.log",
"exception": "sd_exception.log",
"maxSize": 20480,
"maxFiles": 12,
"colorize": true
},
"character_encoding":"utf-8"
}
2. Express-Train 파일 사용하기
- auto-injected 되는 폴더에 모듈로 운용하고 싶은 .js 파일을 놓는다. 주로 MVC 파일과 Utility 또는 Service 파일들이다
- auto-inject 환경내역은 node_modules/express-train/lib/app.js 파일안에 이미 설정되어 있다.
// app.js 내역중 일부
// autoinject라는 옵션을 주면 해당 지정된 폴더의 .js파일을 메모리에 로딩하고 DI를 한다
// 이때 주의할 것은 상호 참조가 되지 않도록 하기 위한 폴더 전략이 중요하다. 위에서 부터 아래순서로 로딩되어 DI 된다
var LOCATIONS = createApplication.locations = {
pkg:{
path: '../package.json'
},
config:{
path: '../config'
},
logs:{
path: '../logs'
},
models: {
path: 'models',
autoinject: true,
aggregateOn: 'models'
},
views: {
path: 'views'
},
lib: {
path: 'lib',
autoinject: true
},
controllers: {
path: 'controllers',
autoinject: true
},
pub: {
path: 'public'
},
middleware: {
path: 'middleware',
autoinject: true
},
// 만약 app밑에 services폴더를 만든다면!
services: {
path: 'services',
autoinject: true
},
};
// app.js 에서 express를 생성하고 views와 public 폴더를 강제 설정하고 있다
// _.each에서 autoinject 폴더의 모든 파일을 읽고 DI를 수행해 준다. 이때 DI되는 .js 파일은 Singleton 패턴이다
// tree.constant로 저장된 객체를 DI 받을 수 있다
function createApplication(dir, locs) {
catchErrors();
var app = express(),
locations = _.defaults((locs || {}), LOCATIONS);
tree = new nject.Tree();
app.set('views', path.join(dir, locations.views.path));
app.set('public', path.join(dir, locations.pub.path));
var config = loadConfig(path.join(dir, locations.config.path));
tree.constant('config', config);
_.each(_.where(locations, {autoinject: true}), function(location) {
traverseAndRegister(path.join(dir, location.path), tree, location.aggregateOn)
});
//allow override of app
if(!tree.isRegistered('app')) {
tree.constant('app', app);
}
//log path 하나 추가함
var logPath = path.join(dir, locations.logs.path);
tree.constant('logPath', logPath);
return tree.resolve();
}
- 폴더 전략 : app.js에 설정된 순서에-위에서 아래로- 따라 로딩됨, 참조가 많은 것은 주로 service가 되지 않을까 함
+ lib/1level : 가장 기본적인 파일 참조하는 것이 없는 것들 - util, logger, db connection, constant
+ lib/2level : express-train의 파일들 - route, views, middleware 설정
+ lib/3level : 1, 2level의 .js를 사용하는 파일들
////////
// 형식
module.exports = function(app, config, logger, ...) {
// app, config, logger 등 파일이름을 대소문자 구분하여 넣으면 자동 DI를 해준다. 객체는 train에서 Singleton으로 관리한다
}
/////////////////////////
// 예) lib/1level/logger.js
// MongoDB에 winston logger를 이용하여 에러발생 내역을 저장하거나 파일로 떨굼
var winston = require('winston'),
path = require('path');
module.exports = function(app, config, logPath) {
winston.info('[1level: log config] init');
require('winston-mongodb').MongoDB;
var dbOpts = {
level: config.winston_mongo.level,
db: config.winston_mongo.db,
collection: config.winston_mongo.collection,
host: config.winston_mongo.host,
port: config.winston_mongo.port
};
winston.info('[1level: log config] DB option is ' + JSON.stringify(dbOpts) );
var fileName = path.join(logPath, config.winston_file.filename);
winston.info('[1level: log config] general log : ' + fileName);
var exceptionFileName = path.join(logPath, config.winston_file.exception);
winston.info('[1level: log config] exception log : ' + exceptionFileName);
var fileOpts = {
filename: fileName,
maxsize: config.winston_file.maxSize,
maxFiles: config.winston_file.maxFiles,
colorize: config.winston_file.colorize
}
var exceptionOpts = {
filename: exceptionFileName,
maxsize: config.winston_file.maxSize,
maxFiles: config.winston_file.maxFiles,
colorize: config.winston_file.colorize
}
var logger = new winston.Logger({
transports: [
new winston.transports.Console(),
new winston.transports.File(fileOpts),
new winston.transports.MongoDB(dbOpts)
],
exceptionHandlers: [
new winston.transports.Console(),
new winston.transports.File(exceptionOpts),
new winston.transports.MongoDB(dbOpts)
]
});
// testing
// logger.log(config.winston_mongo.level, '[log config] Test logging module completely');
// logger.info('==============>> hi info');
// logger.warn('==============>> hi warn');
// logger.error('==============>> hi error'); <- MongoDB에도 쌓인다
return logger;
}
////////////////////////////////////////////////
// 예) lib/2level/ 또는 lib/2level에서 logger사용하기
// lib/2level/views.js 파일에서
// logger.js 파일에서 파일명칭을 - 대소문자 구분 - Function Parameter DI 해준다
module.exports = function (app, logger) {
logger.info('[2level: view engine - jade] init');
app.set('view engine', 'jade');
};
방법-1. Angular 프로젝트 배포후 Express-Train 프로젝트로 통합
- 엄밀히 따지면 합치는 시점은 개발이 다 된후 수행한다. 즉, SPA와 Node 파트의 Git 저장소를 틀리게 가져감을 전제로 한다
+ SPA 저장소 : palette.frontend
+ Node저장소 : palette.frontend.node
- 개발이 된후 SPA와 Node 통합은 다음 순서로 하자
+ Yeoman을 통하여 생성된 Angular 스케폴딩 파일은 별도로 개발한다 -> grunt build 수행 -> dist 폴더에 배포파일이 생성된다
+ palette.frontend.node/app/public/ 와 palette.frontend.node/app/views 폴더 밑의 모든 파일을 삭제한다
+ palette.frontend.node/app/public/ 밑으로 dist/* 모든 파일을 복사한다
// 폴더구조
playhub
|_ palette.frontend (별도 git repository)
|_ palette.frontend.node (별도 git repository)
// Angular 기반 frontend 파일
playhub$ cd palette.frontend
playhub$ grunt build
// Express-train에서 사용하지 않을 파일 삭제
playhub$ cd ../palette.frontend.node/app/public
playhub$ rm -rf *
playhub$ cd ../views
playhub$ rm -rf *
// Angular 기반 SPA frontend파일을 Express-train의 app/public/ 폴더 밑으로 복사
playhub$ cp -rf ../../../palette.frontend/dist/* ../public/
- 테스트 하기
$ cd palette.frontend.node
$ train run
// http://localhost:4000/ 으로 호출한다
방법-2. Express-Train 프로젝트를 Angular 프로젝트로 통합
- Angular 프로젝트(palette.frontend)의 grunt, bower, yo, karma 기능을 그대로 사용한다
- Express-Train 프로젝트(palette.frontend.node)는 express-train기반으로 이미 스케폴딩된 서버단 애플리케이션이다.
즉 palette.frontend.node의 디렉토리 구조를 변경시켜보자
// express-train 프로젝트로 이동하기
$ cd palette.frontend.node
// express-train의 bin 디렉토리를 palette.frontend.node로 이동하기
$ cd node_modules/
$ cd express-train/
$ ls -arlt
total 80
drwxr-xr-x 3 nulpulum staff 102 9 17 17:20 test
-rw-r--r-- 1 nulpulum staff 90 9 17 17:20 index.js
-rw-r--r-- 1 nulpulum staff 82 9 17 17:20 .npmignore
-rw-r--r-- 1 nulpulum staff 13393 9 26 19:13 ReadMe.md
drwxr-xr-x 4 nulpulum staff 136 11 8 14:14 bin
drwxr-xr-x 5 nulpulum staff 170 11 8 14:14 boilerplates
-rw-r--r-- 1 nulpulum staff 14976 11 8 14:14 package.json
drwxr-xr-x 4 nulpulum staff 136 11 8 14:14 lib
drwxr-xr-x 12 nulpulum staff 408 11 8 14:14 node_modules
drwxr-xr-x 11 nulpulum staff 374 11 8 14:14 .
drwxr-xr-x 13 nulpulum staff 442 11 8 14:14 ..
// bin을 palette.frontend.node 루트로 이동시킨다
$ mv bin ../../
// bin으로 들어가서 train run 명령을 수행 - 내부적으로 commander 모듈을 사용한다
$ train run
module.js:340
throw err;
^
Error: Cannot find module 'commander'
at Function.Module._resolveFilename (module.js:338:15)
at Function.Module._load (module.js:280:25)
at Module.require (module.js:364:17)
at require (module.js:380:17)
at Object.<anonymous> (/Users/nulpulum/development/playground/playhub/palette.frontend.node/bin/train:3:15)
at Module._compile (module.js:456:26)
at Object.Module._extensions..js (module.js:474:10)
at Module.load (module.js:356:32)
at Function.Module._load (module.js:312:12)
at Function.Module.runMain (module.js:497:10)
// 에러가 발생하므로 commander, underscore, handlebars, boilerplate 모듈을 설치한다
$ cd .. && npm install commander --save
$ npm install underscore --save
$ npm install handlebars --save
$ npm install boilerplate --save-dev
$ cd bin && train run
module.js:340
throw err;
^
Error: Cannot find module '../../lib/app'
at Function.Module._resolveFilename (module.js:338:15)
at Function.Module._load (module.js:280:25)
at Module.require (module.js:364:17)
at require (module.js:380:17)
at Object.<anonymous>(/Users/nulpulum/development/playground/playhub/palette.frontend.node/bin/commands/console.js:4:13)
at Module._compile (module.js:456:26)
at Object.Module._extensions..js (module.js:474:10)
at Module.load (module.js:356:32)
at Function.Module._load (module.js:312:12)
at Module.require (module.js:364:17)
// 에러 다시 수정 하기
$ cd ../node_modules/express-train/
$ ls -alrt
total 80
drwxr-xr-x 3 nulpulum staff 102 9 17 17:20 test
-rw-r--r-- 1 nulpulum staff 90 9 17 17:20 index.js
-rw-r--r-- 1 nulpulum staff 82 9 17 17:20 .npmignore
-rw-r--r-- 1 nulpulum staff 13393 9 26 19:13 ReadMe.md
drwxr-xr-x 5 nulpulum staff 170 11 8 14:14 boilerplates
-rw-r--r-- 1 nulpulum staff 14976 11 8 14:14 package.json
drwxr-xr-x 4 nulpulum staff 136 11 8 14:14 lib
drwxr-xr-x 12 nulpulum staff 408 11 8 14:14 node_modules
drwxr-xr-x 10 nulpulum staff 340 11 9 15:19 .
drwxr-xr-x 16 nulpulum staff 544 11 9 15:29 ..
// express-train의 lib밑의 모든파일을 bin 밑으로 이동시킨다
$ mv lib/* ../../bin/
// index.js 파일을 이동한 bin 디렉토리에 같이 놓는다
$ mv index.js ../../bin/
// express-train 은 삭제한다
$ cd .. && rm -rf express-train
$ cd ../../bin/
// 최종 lib이 bin 밑으로 들어갔다
$ ls -alrt
total 32
-rw-r--r-- 1 nulpulum staff 64 9 17 17:20 runner.js
-rw-r--r-- 1 nulpulum staff 3594 9 27 14:06 app.js
drwxr-xr-x 11 nulpulum staff 374 11 9 15:19 ..
-rw-r--r-- 1 nulpulum staff 89 11 9 15:56 index.js
-rwxr-xr-x 1 nulpulum staff 785 11 9 16:02 train
drwxr-xr-x 7 nulpulum staff 238 11 9 16:02 .
drwxr-xr-x 3 nulpulum staff 102 11 9 16:08 commands
// commands 폴더에서 runt.js를 제외한 사용하지 않는 나머지 파일은 삭제하거나 나중을 위해서 다른 위치로 백업한다
$ cd commands && rm console.js cycle.js new.js boilerplate.js
// commands 폴더의 run.js 안에 내역을 수정 한다
// 기존 내역 : var appPath = path.join(process.cwd(), '../app/index.js');
// 수정 내역
var appPath = path.join(process.cwd(), './index.js');
// lib밑에 있는 index.js가 가르키는 파일 수정
// 기존 내역 : app: require('./lib/app')
// 수정 내역
// module.exports = {
// app: require('./app.js')
// };
var paletteApp = require('./app.js');
module.exports = paletteApp(__dirname);
// palette.frontend.node 루트의 app 디렉토리명칭을 api 라고 변경한다
$ cd ../../ && mv app api
// 다음으로 직접 app.js안의 인식하는 디렉토리 경로를 상대경로로 변경한다
$ cd bin && vi app.js
//위치 변경하기
var LOCATIONS = createApplication.locations = {
pkg:{
path: '../package.json'
},
config:{
path: '../config'
},
logs:{
path: '../logs'
},
models: {
path: '../api/models',
autoinject: true,
aggregateOn: 'models'
},
views: {
path: '../api/views'
},
lib: {
path: '../api/lib',
autoinject: true
},
controllers: {
path: '../api/controllers',
autoinject: true
},
// 향후 palette.frontend의 grunt build시에 dist 폴더 밑으로 되는 것을 public 폴더로 바꿀 것이다
pub: {
path: '../public'
},
middleware: {
path: '../api/middleware',
autoinject: true
}
};
// api 디렉토리 밑에 있는 public 디렉토리를 루트로 옮겨서 테스트 해본다
$ mv app/public .
// bin 디렉토리 밑에 있는 train 쉘 실행하기
$ train run
[express train application listening on 4000]
- Express-Train기반의 서버구조를 만들었고, 이제 이것을 palette.frontend 쪽으로 옮겨보자
// palette.frontend.node 프로젝트의 package.json 명칭을 변경 - 나중에 palette.frontend쪽의 package.json과 합치기 위함
$ mv package.json packate.json.node
// public 폴더는 삭제한다
$ rm -rf public
// 옮기기
$ mv * ../palette.frontend/
mv: rename node_modules to ../palette.frontend/node_modules: Directory not empty
mv: rename test to ../palette.frontend/test: Directory not empty
$ cd node_modules
$ mv * ../../palette.frontend/node_modules/
// palette.frontend 에서 dist를 public으로 명칭 변경하여 테스트
$ cd ../../palette.frontend
$ mv dist public
$ cd bin
$ train run
[express train application listening on 4000]
// 이제 grunt build를 수행할 때 dist 디렉토리를 만드는 것이 아니라 public 으로 만들어 보자
$ cd .. && rm -rf public
// grunt 환경파일의 내역 수정
$ vi Gruntfile.js
// 변경내역
grunt.initConfig({
yeoman: {
// configurable paths
app: require('./bower.json').appPath || 'app',
dist: 'public' // 'dist'를 'public'으로 변경한다
},
// 다시 grunt로 빌드를 하면 public 폴더가 생성된다
$ grunt build
$ cd bin && train run
[express train application listening on 4000]
// 최종 palette.frontend의 합친 구조
- api : Node 서버
- app : SPA Angular
- public : app 배포 디렉토리이면서 bin/train run 수행시 인식되는 서비스 폴더
// palette.frontend git 저장소에 push하기전에 package.json.node의 내용을 package.json에 통합한다
{
"name": "palette",
"version": "0.0.1",
"dependencies": {
"express": "3.2.x",
"express-train": "1.0.x",
"connect-timeout": "latest",
"connect-mongodb": "latest",
"mongoose": "latest",
"async": "latest",
"express-hbs": "latest",
"passport": "latest",
"winston": "latest",
"commander": "~2.0.0",
"underscore": "~1.5.2",
"handlebars": "~1.1.2",
"nject": "~0.1.6"
},
"devDependencies": {
"grunt": "~0.4.1",
"grunt-contrib-copy": "~0.4.1",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-coffee": "~0.7.0",
"grunt-contrib-uglify": "~0.2.0",
"grunt-contrib-compass": "~0.5.0",
"grunt-contrib-jshint": "~0.6.0",
"grunt-contrib-cssmin": "~0.6.0",
"grunt-contrib-connect": "~0.5.0",
"grunt-contrib-clean": "~0.5.0",
"grunt-contrib-htmlmin": "~0.1.3",
"grunt-contrib-imagemin": "~0.2.0",
"grunt-contrib-watch": "~0.5.2",
"grunt-autoprefixer": "~0.2.0",
"grunt-usemin": "~0.1.11",
"grunt-svgmin": "~0.2.0",
"grunt-rev": "~0.1.0",
"grunt-concurrent": "~0.3.0",
"load-grunt-tasks": "~0.1.0",
"grunt-google-cdn": "~0.2.0",
"grunt-ngmin": "~0.0.2",
"time-grunt": "~0.1.0",
"karma-ng-scenario": "~0.1.0",
"grunt-karma": "~0.6.2",
"karma-script-launcher": "~0.1.0",
"karma-chrome-launcher": "~0.1.0",
"karma-firefox-launcher": "~0.1.0",
"karma-html2js-preprocessor": "~0.1.0",
"karma-jasmine": "~0.1.3",
"karma-requirejs": "~0.1.0",
"karma-coffee-preprocessor": "~0.1.0",
"karma-phantomjs-launcher": "~0.1.0",
"karma": "~0.10.4",
"karma-ng-html2js-preprocessor": "~0.1.0",
"mocha": "latest"
},
"engines": {
"node": ">=0.8.0"
},
"main": "./app/index.js",
"scripts": {
"test": "grunt test",
"start": "cd bin && train run",
"server-test": "mocha test"
}
}
// npm 명령으로 scripts 설정 내역 호출하기
$ npm start
[express train application listening on 4000]
- 최종 완성본 저장소 : MyPlayHub GitHub Repository
// git clone 하여 train run 수행시 다음과 같이 오류가 발생할 경우
$ train run
env: node\r: No such file or directory
// dos2unix 패키지를 설치하여 수정하여 준다
$ brew install dos2unix
==> Installing dos2unix dependency: gettext
==> Downloading http://ftpmirror.gnu.org/gettext/gettext-0.18.3.tar.gz
.. 중략 ..
$ dos2unix train
dos2unix: converting file train to Unix format ...
$ train run
[express train application listening on 4000]
방법-3. Yeoman Generator FullStack을 통한 통합
- Express + Angular의 통합
- Angular는 클라이언트의 파일 전송과 암호화를 위하여 Lint와 압축을 위해 Grunt를 사용함
- Express는 서버단이므로 압축이 필요없음
// FullStack generator 설치
$ sudo npm install -g generator-angular-fullstack
// FullStackTest라는 애플리케이션 명칭으로 스케폴딩 생성
$ yo angular-fullstack FullStackTest
// 빌드하여 클라이언트 파일 Lint 및 압축
// public 폴더밑으로 배포본이 만들어짐
$ grunt build
... 중략 ...
Elapsed time
useminPrepare:html 25ms
concurrent:dist 5s
autoprefixer:dist 113ms
concat:public/scripts/modules.js 23ms
copy:dist 640ms
cdnify:dist 21ms
ngmin:dist 263ms
cssmin:public/styles/main.css 33ms
uglify:dist 28ms
uglify:public/scripts/plugins.js 373ms
uglify:public/scripts/modules.js 134ms
usemin:html 482ms
usemin:css 152ms
Total 8s
<참조>
- Angular-Express-Train-Seed
- Yeoman : generator-fullstack with angular and express
- dos2unix 설치하여 사용하기