블로그 이미지
윤영식
Full Stacker, Application Architecter, KnowHow Dispenser and Bike Rider

Publication

Category

Recent Post

2015. 11. 14. 17:54 Angular/Concept

Angular2 구현 언어는 TypeScript이다. TypeScript를 배우기 위한 준비과정을 알아본다. 








Transpiler


  ES2015(EcmaScript 6) 스펙은 모던 브라우저에서 완벽히 구현되어 있지 않기 때문에 최소한 ES5기준에 맞춰서 사용을 해야 한다. TypeScript는 ES2015를 포함해 정적 타입 시스템과 ES7에서 나오는 데코레이터 @을 지원하는 슈퍼셋이다. 따라서 Babel로 ES2015를 ES5로 트랜스파일(Transpile) 하지 않고 TypeScript만의 트랜스파일러를 사용해야 한다.  


$ sudo npm install -g typescript 


TypeScript 파일의 확장자는 .ts파일로 한다. 그리고 자바스크립 파일로 트랜스파일하기 위해 tsc 명령을  사용한다. 결과물로 동일 명칭의 .js 파일이 생성된다.


$ tsc  <fileName>.ts 






Editor 


  Microsoft에서 mac에서도 사용할 수 있는 전용 Code Editor(Visual Studio Code)를 내놓았다. 설치 후 어느 위치에서든 code . 하면 현재 폴더의 파일 구조를 가지고 에디터가 열린다. 그리고 .ts 파일에 대한 스마트 위저드 헬퍼가 자동으로 뜨기 때문에 MS Visual Studio에서 코딩하는 착각을 불러 일으킨다. 필자는 최근 Sublime 3에서 Atom으로 코딩 툴을 옮겼는데 참 흥미롭게 사용중이다. 큰 파일의 경우 약간의 성능저하를 견딜 수 있다면 사용해 보길 권한다. 




Atom을 사용한다면 atom-typescript 패키지 설치를 통해 Visual Studio Code에서의 .ts 에 대한 스마트한 기능(Auto Complete, Type Info on hover...)을 그대로 사용할 수 있다. 설치후 F6를 클릭하면 tsc <fileName>.ts 명령을 수행하고 결과를 Notify UI 창으로 성공 실패여부를 알려준다. 


 





tsconfig.json


  tsconfig.json 파일은 TypeScript를 위한 프로젝트 단위 환경 파일이다. Exclude 하고 싶은 폴더나 파일, Compile 옵션, File 옵션등을 설정 할 수 있다. TypeScript의 컴파일러인 tsc 명령 수행시 참조한다. tsconfig.json의 전체 스키마를 참조한다.  또는 TypeScript Deep Dive 깃북의 설명을 참조한다.


{

    "exclude": [

        "node_modules",

        "bower_components"

    ],


    "compilerOptions": {

        "target": "es5",

        "sourceMap": true,

        "module": "commonjs",

        "declaration": false,

        "noImplicitAny": false,

        "removeComments": true,

        "noLib": false

    }

}






TypeScript 배우기 


TypeScript를 배우기 위해 ES2015의 문법을 먼저 익히는게 좋다. 

  - ES2015-features 사이트에서 새로운 기능들이 ES5 스펙과 어떤 차이가 있는지 눈여겨 보자. 

  - 동영상 강좌를 본다. 필자는 egghead.io의 동영상을 먼저 참조한다. (Pro는 유료이다)


공식홈페이지는 http://www.typescriptlang.org/ 이고, TypeScript 구현체 자체도 오픈소스로 깃헙에 공개되어 있다. 

  - 공식홈페이지에서 제공하는 TypeScript 스펙 배우기

  - TypeScript Deep Dive 깃북






TSD


TypeScript Definition Manager의 약어이다. 이미 나와있는 프레임워크(angular), 라이브러리(jquery) 같은 것을 사용하면서 애플리케이션을 TypeScript로 작성할 때 이들 구현체의 정의 내역을 미리 알고 코드에서 가이드를 주기위한 파일이다. 확장자는 angular.d.ts 또는 jquery.d.ts처럼 <name>.d.ts 가 붙는다. 예로 $('.awesome').show(); 라고 하면 타입스크립트는 $를 알지 못 한다. 먼저 declare var $:any; 라고 해주어야 사용할 수 있다.  


$ npm install -g tsd


tsd를 설치하고 tsd install <name> 으로 설치하면 수행위치에 typings 폴더 밑으로 파일을 다운로드한다. 


$ tsd init

// tsd.json 파일 

{

  "version": "v4",

  "repo": "borisyankov/DefinitelyTyped",

  "ref": "master",

  "path": "typings",

  "bundle": "typings/tsd.d.ts",

  "installed": {

    "jquery/jquery.d.ts": {

      "commit": "efd40e67ff323f7147651bdbef03c03ead7b1675"

    },

    "angularjs/angular.d.ts": {

      "commit": "efd40e67ff323f7147651bdbef03c03ead7b1675"

    }

  }

}


$ tsd install jquery

$ tsd install angular



이미 만들어져 있는 d.ts 파일을 이곳에서 http://definitelytyped.org/tsd/ 찾아 볼 수 있다. DefinitelyTyped 공식 홈페이지를 참조하자.






WorkFlow 


 Atom 에디터와 기본 툴을 설치했으니, 프로젝트를 위한 워크플로우를 만들어 보자.   


  - 프로젝트 폴더 만들기 

  - NodeJSGit 설치 

  - "npm install gulp -g" 설치 

  - npm 초기화 및 develop dependencies 설정 

$ npm init 


// package.json 첨부

"devDependencies": {

        "gulp": "^3.8.11",

        "gulp-debug": "^2.0.1",

        "gulp-inject": "^1.2.0",

        "gulp-sourcemaps": "^1.5.1",

        "gulp-tslint": "^1.4.4",

        "gulp-typescript": "^2.5.0",

        "gulp-rimraf": "^0.1.1",

        "del": *,

        "superstatic": *,

        "browser-sync": *

    }


// 의존 라이브러리 설치

$ npm install 


  - tsd 설치하고, init 초기화 명령으로 tsd.json 파일과 typings 폴더에 tsd.d.ts 파일을 자동 생성한다.

$ npm install -g tsd 


$ tsd init 



  - angular 의 TypeScript Definition 파일을 설치해 보자. 더 많은 라이브러리는 이곳에서 http://definitelytyped.org/tsd/ 에서 찾을 수 있다.

$ tsd install angular --save

 - angularjs / angular

   -> jquery > jquery


>> running install..

>> written 2 files:

    - angularjs/angular.d.ts

    - jquery/jquery.d.ts



$ tsd install jquery --save

 - jquery / jquery


>> running install..

>> written 1 file:

    - jquery/jquery.d.ts



  - gulpfile.config.js와 gulpfile.js를 https://github.com/DanWahlin/AngularIn20TypeScript 에서 복사해서 만든다. Gulp Task 설명


  - ts 파일 lint를 위해 tslint.json파일 만든다.

{

    "rules": {

        "class-name": true,

        "curly": true,

        "eofline": false,

        "forin": true,

        "indent": [true, 4],

        "label-position": true,

        "label-undefined": true,

        "max-line-length": [true, 140],

        "no-arg": true,

        "no-bitwise": true,

        "no-console": [true,

            "debug",

            "info",

            "time",

            "timeEnd",

            "trace"

        ],

        "no-construct": true,

        "no-debugger": true,

        "no-duplicate-key": true,

        "no-duplicate-variable": true,

        "no-empty": true,

        "no-eval": true,

        "no-imports": true,

        "no-string-literal": false,

        "no-trailing-comma": true,

        "no-trailing-whitespace": true,

        "no-unused-variable": false,

        "no-unreachable": true,

        "no-use-before-declare": true,

        "one-line": [true,

            "check-open-brace",

            "check-catch",

            "check-else",

            "check-whitespace"

        ],

        "quotemark": [true, "single"],

        "radix": true,

        "semicolon": true,

        "triple-equals": [true, "allow-null-check"],

        "variable-name": false,

        "whitespace": [true,

            "check-branch",

            "check-decl",

            "check-operator",

            "check-separator"

        ]

    }

}


  - Atom에서 atom-typescript를 설치해서 사용한다면 tsconfig.json도 만든다. 아래와 같이 설정값을 입력한다. 설정 값이 있어야 atom-typescript 기능을 사용할 수 있다. 보다 자세한 tsconfig.json 작성 문법을 참조한다.

{

    "compilerOptions": {

        "target": "es5",

        "module": "commonjs",

        "declaration": false,

        "noImplicitAny": false,

        "removeComments": true,

        "noLib": false

    },

    "filesGlob": [

        "./**/*.ts",

        "!./node_modules/**/*.ts"

    ],

    "files": [

        "./globals.ts",

        "./linter.ts",

        "./main/atom/atomUtils.ts",

        "./main/atom/autoCompleteProvider.ts",

        "./worker/messages.ts",

        "./worker/parent.ts"

    ]

}


  - gulp 명령을 수행하면 .js 파일과 .js.map 파일이 생성된다. 


  - TypeScript 소개 영상과 소스를 확인해 보자.






Start Kit 


  yeoman의 generator 처럼 TypeScript프로젝트 관련한 스타트 깃을 사용해 보자. 물론 angular2 기반으로 프로젝트를 가정한다. 

  - AngularClass에서 제공하는 angular2-webpack-starter 

  

   


  - AngularClass에서 제공하는 Angular2의 Server Rendering을 접목한 Universal Starter





참조 


  - TypeScript Workflow with Gulp

  - TypeScript in Awesome Angular2

 - Atom TypeScript 패키지 : atom-typescript



posted by 윤영식
2015. 10. 22. 10:41 AngularJS



본 서적은 자바스크립트와 앵귤러 프레임워크를 프로젝트에 도입해 보려는 개발자 또는 프로토타입을 해보고 싶은 개발자 또는 사용해 본 경험이 있는 개발자를 대상으로 한다. 앵귤러에 대해 처음 공부하는 분에게는 다소 어려울 수 있으므로 기초서적을 권장드린다. 책 제목처럼 3장이후 부터 실전 프로젝트 실습을 위한 상세 소스를 GitHub에서 제공한다. 1~2장은 개념 정리이므로 읽고 나중에 참조하는 장으로 사용한다. 참고로 2장은 실습이 없이 개념을 설명하는 장이기에 소스를 제공하지 않고 있다. (소스의 Git Branch는 장이 끝나는 시점에 언급하고 있다. 먼저 전체 소스를 보고 싶다면 GitHub 내용을 참조하자)








출판의 여정 


8개월의 원고작성, 3개월의 편집기간을 거쳐 드디어 책이 출간되었다. 책을 통해 개발자들과 공유하자는 의기투합은 2013년 겨울로 거슬러 올라간다. 이강훈님을 통해 알게된 이규원님, 김충섭님, 김대권님, 박유진님, 구교준님과 함께 MEAN 스택을 기획했었다. 그 당시 미국에 있었던 지라 구글 행아웃을 통해 주말마다 화상미팅을 하며 깃헙과 페이스북을 통해 서로의 의견과 결과물을 공유했다. 2014년 봄 귀국후 함께 오프라인에서 헤커톤도 하면서 책의 샘플을 만들며 진행했지만 각자의 일정으로 인해 MEAN 스택 집필을 흐지부지 되어갔다. 2014년 7월쯤 결단을 내리고 이렇게 진행하면 끝나지 않겠다 싶어서 다음 기회를 기약하였고, 홀로 책을 쓰기로 결심했다. 


2014년 8월 이강훈님 회사인 스터디지피에서(https://bagle.io)의 서비스를 개발하면서 미국 플젝에서 얻은 AngularJS 경험과 개념정리가 덜 된 부분을 하나하나 집어가며 집필을 시작했다. 처음에는 한 페이지를 나가는데 하루의 시간이 걸리더니 조금씩 속도가 붙으면서 한달만에 100p 가량을 썼다. 하지만 다시 가을 미국 프로젝트로 2달간 집필 중단 후 한국에 돌아와 마음을 다잡고 12년전 내 책을 출판해 보자는 꿈을 다시한번 상기했다. 위키북스의 박찬규 대표님도 은근히 압력을 넣어주시며 언제쯤 나올까요 물어볼때마다 곧 나올겁니다 대답하곤 하루 1~2시간씩 짬을 내어 집필을 계속했다. 2015년 3월 집필을 완료하고 출판사에 전달했지만 피드백으로 날아온 것은 맞춤법과 문장 난해함에 대한 엄청난 지적질의 빨간줄이었다. 


한달간 주말마다 빨간 선생님과 싸움하며 "그래 글쓰는 것도 배우는 거다"라고 생각하고 나의 국어실력을 한탄하며 글을 풀어서 이야기식으로 고쳐나갔다. 사실 블로그를 쓰다보니 블릿(점, 점으로 요약한 내용)으로 내용을 요약해 표현하다 보니 책도 그렇게 썼드랬다. 블릿 내용을 다 풀어쓰는 것만도 한달. 다시 피드백받아 또 고치고 피드백받아 또 고치고, "아 무한루프의 피드백". 점점 스트레스와 화가 살짝 났다. 인생 목표 10가지중 1가지가 1년에 책한권씩 내자였던 것이 여지없이 깨지는 순간이었다. 이렇게 하다가는 2년에 한권내는 것도 힘들겠다 싶었다. 하지만 일에는 끝이 있는 법. 5월초 거의 마무리 피드백을 보내고, 책 추천사를 받고 인덱스를 보내고, 몇가지 피드백을 마무리하며 3개월의 수정작업은 6월 5일 마무리되었고, 예약 판매가 모든 온라인 서점에서 시작되었다. 그렇게 나온 것이 "실전 프로젝트로 배우는 AngularJS" 이다. 


본 서적은 AngularJS가 하도 유행이라 하여 한번쯤은 기초서적을 구매하거나 빌려서 본적이 있고, 온라인 튜토리얼을 통해 프로토타입핑을 해보았지만 당췌 AngularJS로 서비스를 어떻게 만들어야 할지 모르는 개발자를 위한 책이다. "나도 내가 원하는 서비스 뚝딱 만들어 보고 싶어 근데 어떻게 시작해야는 거야?"라고 생각하는 모든 개발자를 위한 책이다. AngularJS는 생태계를 가지고 있고 인식의 전환을 해야하는 새로운 용어들이 존재한다. 


처음 1장에서는 간단한 ToDo 서비스를 만들면서 주변의 생태계 도구와 개념을 이해하고 2장에서는 AngularJS의 장점들과 그중에 지시자(Directive)에 대해 좀 더 심도있게 설명을 했다. 3장, 4장에서 서비스를 바로 만들 줄 알았다면 오산이다. 이제 서비스를 만들 준비운동단계이다. 먼저 요건정의를 하고 이에 맞는 코딩 컨벤션 부터 AngularJS를 이용한 SPA 개발시 준비할 것들을 프론트앤드 아키텍처로 접근해 보았다. 5장에서 이제 좀 개발들어가려 했더니 그래도 요즘 많이 사용하는 NodeJS, MongoDB는 알고가자는 취지에서 개념과 실습코드를 넣었다. 1장~5장이 준비운동과 스타트 단계였다면 6장, 7장은 엄청난 스피도로 골인점을 향해가는 단계이다. 올초 2개월간 TossLab의 JANDI(http://www.jandi.com)에서 AngularJS에 대한 컨설팅 경험을 적용한 장이다. 





목차


▣ 01장: 단일 페이지 애플리케이션 개발 준비
1-1. 개발 도구 설치 
   - 깃 설치 
   - 노드 설치 
   - 요맨 설치 
   - 서브라임 텍스트 편집기 설치 

1-2. 단일 페이지 애플리케이션 생성 
   - yo generator 선택과 설치 
   - Yo를 이용한 ToDo 애플리케이션 생성 
1-3. 애플리케이션 컴포넌트 생성 
   - 앵귤러를 위한 index.html 설정 이해하기 
   - yo를 이용한 앵귤러 컨트롤러 추가 
   - bower를 이용한 앵귤러 지시자 추가 
1-4. 애플리케이션 테스트 및 빌드 
   - grunt를 이용한 테스트 
   - grunt를 이용한 배포 
정리 

▣ 02장: AngularJS 프레임워크 이해
2-1. MV* 프레임워크 
2-2. 양방향 데이터 바인딩 
   - 스코프 내부와 상속 관계  
   - MyToDo 애플리케이션에서 양방향 데이터 바인딩 
   - 스코프 생명 주기(Life Cycle) 
   - 그 외 $scope 객체 메서드 
2-3. 의존성 주입(DI, Dependency Injection) 
2-4. 클라이언트 템플릿 
2-5. 지시자(Directive) 
   - 지시자가 DOM에 적용되는 순서 
   - 지시자 정의 
   - 지시자의 스코프 객체의 범위 종류 
   - Template, TemplateUrl, TemplateCache, replace와 ng-template 사용 
   - compile, link의 $watch 등록을 이용한 양방향 데이터 바인딩 
   - controller, require와 link 네 번째 파라미터와의 관계 
   - transclude, ng-transclude 사용 
2-6. 테스트 프레임워크(단위, E2E) 
   - 카르마 기반 단위 테스트 
   - 프로트랙터 기반 E2E 테스트 
정리 

▣ 03장: 싱글 페이지 애플리케이션 기획및 생성
3-1. 애플리케이션 기획
   - 메인 페이지 
   - 그룹 정보 페이지 
   - 그룹 활동 페이지 
   - 설문 생성 페이지 
3-2. 애플리케이션 제너레이터 설계 
   - 애플리케이션의 폴더 구조 전략 
   - 애플리케이션 제너레이터 선정 
   - 앵귤러 코드 스타일 전략 
   - 스타일 가이드에 따른 제너레이터 템플릿 수정 방법 
   - IE8 지원을 위한 index.html 설정 
3-3. SPA 생성 
   - 애플리케이션의 모듈 구성 
   - 라우팅 설정 방식 
3-4. 단위 업무를 위한 앵귤러 컴포넌트 조합 
   - $resource를 통한 REST 모델 사용 
   - promise와 $q Async 호출에 대한 이해 
정리

▣ 04장: 애플리케이션을 위한 공통 프레임워크 개발
4-1. 공통 프레임워크 모듈 개발 
   - 다국어 처리 
   - 메시지 처리 
   - 팝업 메시지창 지시자 
   - HTTP 에러 처리 
   - 사용자 정의 Bower 컴포넌트 등록 
   - 로컬 저장소 서비스 
   - 유틸리티 지시자 
4-2. 로그인 화면 개발 
   - 트위터 부트스트랩 기반의 화면 디자인 및 폰트 사용 
   - 폼 유효성(Form Validation) 검사 
   - 인증을 위한 토큰과 쿠키 
4-3. OAuth를 이용한 인증 처리 
   - 백엔드에서 Passport 모듈을 이용한 인증 처리 
   - 페이스북 인증 처리 
   - 크롬 브라우저 개발자 도구를 이용한 클라이언트 디버깅 
   - 노드 인스팩터를 이용한 서버 디버깅 
정리

▣ 05장: 메인 페이지 개발
5-1. 백엔드 API 개발 
   - REST API 별 서버 모듈 조합 
   - 노드 모듈의 exports 이해 
   - 몽고디비와 몽구스 이해 
   - 서버 모델 개발 
   - 그룹 REST API 개발 
   - 포스트맨을 이용한 REST API 검증 
   - 백엔드 단위 테스트 수행 
5-2. 메인 화면 개발 
   - 공통 컴포넌트 재구성 
   - 메인 화면 레이아웃 개발 
   - 그룹 생성 
5-3. 그룹 목록 및 정보 표현 
정리 

▣ 06장: 그룹 페이지 개발
6-1. 그룹 정보 페이지 
   - 그룹 상세 정보 조회 
   - 그룹 프로필 이미지 변경 
   - 그룹 가입, 탈퇴 
6-2. 그룹 활동 페이지 
   - 그룹 활동 화면 레이아웃 개발 
   - 그룹 멤버 목록 표현 
6-3. 설문 카드 생성 
   - 설문 카드 생성 
   - 카드 지시자 개발 
6-4. 설문 종류별 카드 표현 
6-5. 설문 응답 및 결과 표현 
정리

▣ 07장: 실시간 반응 개발
7-1. Socket.IO 기반 실시간 연동 
   - 노드 기반 백엔드 Socket.IO 
   - AngularJS 기반 프런트엔드 Socket.IO 
   - 상단 알림 메뉴 추가 
7-2. 카드 목록 UX 개선 
   - 카드에 동영상 추가 
   - 무한 스크롤 적용 
   - 애니메이션 효과 적용 
7-3. AngularJS 성능 옵션 
   - 일회 바인딩 
   - ngModelOptions 지시자 
   - 디버깅 정보 비활성화 
   - $applyAsync 적용 

정리




구매


예스24 온라인 서점


알라딘 온라인 서점


인터파크 온라인 서점


교보문고 온라인 서점


G마켓 온라인 서점


현재는 예약 판매로 10% 싸게 구매 할 수 있다. 6월 17~19일간 배송을 시작한다. 당초 높은 가격이 책정되었지만 위키북스 대표님에게 가격이 높아 개발자에게 부담이 될 것 같다하였더니 파격적인(?) 가격에 판매를 시작해 주셨다. 지금까지 독자로 책을 사보면서 철자하나 틀리면 왠지 기분이 나쁘고 지적을 해주고 싶었는데 이제부터는 너그러이 용서해 줄것 같다. 정말 많은 공력과 시간이 투여되는 시간이었다. 하지만 새로운 경험을 하게 되었고 이렇게 해서 책 한권 한권이 출판된다는 생각을 하니 한권의 책을 사도 감사히 볼 마음가짐이 생긴다.  


이번달 중으로 출판사의 양해를 얻어 1장과 2장은 블로그에 공개하려 한다. 2장은 AngularJS v1.* 이 계속 사용되는 한 유용한 레퍼런스가 되리라 생각한다. 이렇게 생애 첫 책 출판을 자축하며... 내년에는 "Data Visualization using D3.js" 이고, 여기에 ReactJS 또는 AngularJS v2.* 와 Meteor를 접목한 경험을 출판할 계획이다. 글쓰기도 가끔은 중독이다. 자전거 처럼 잘 타지는 못하지만 타고 있는 동안은 자유다. 나를 표현하고 느낄 수 있는 시간이랄까~~~




소스 


책의 소스는 모두 깃헙에 있습니다. 책 챕터마도 브랜치에 대한 정보가 담겨 있습니다. 


책 소스를 위한 깃헙 그룹 : https://github.com/AngularJS-SPA-Development



posted by 윤영식
2015. 5. 15. 11:38 React

ReactJS(리액트)는 자바스크립트로 UI 컴포넌트를 만드는 프레임워크이다. 만들어진 컴포넌트를 사용할 수도 있고, 수정할 수 있고, 조합할 수 있으며 자신만의 UI 컴포넌트를 직접 만들 수 있다. 즉 프론트앤드 UI의 화면을 컴포넌트 조합으로 만들고, 화면의 데이터를 앵귤러처럼 자동 업데이트 해주지만 앵귤러와 틀리게 단방향 데이터 바인딩이다. 리액트는 앵귤러(AngularJS)의 지시자(Directive)와 비교되지만 훨씬 간단하고 성능 또한 월등하다. 이제 배우고 익혀 사용할 때가 되었다. 







개념잡기 


  - 쿼라(Quora)에서 이야기하는 장/단점을 살펴보자. 

     장점

       + Virtual DOM 을 JS로 구현해서 UI 컴포넌트의 속도가 엄청 빠르다. 

       + Component의 재사용과 복잡한 UI 컴포넌트 조합이 가능하다.

       + Uni-direction(단방향) 방식하에 데이터가 변경되면 관련된 모든 UI 컴포넌트가 변경된다.

       + commonJS / AMD 패턴과 잘 조합되고, 크롬 익스텐션을 통해 디버깅할 수 있다. 

     단점

       + 초보자들에겐 역시 러닝 커브가 존재한다.

       + 다른 프레임워크가 동작하기 위해선 부가적인 작업들이 필요하다. 

       + 앵귤러처럼 router, model등 Frontend Full Framework을 제공하지 않아서 꼭 다른 프레임워크와의 조합이 필요하다. 


  - 처음에는 무조건 공식 홈페이지의 가이드를 보자. 

      + Quick Start

      + Advanced Guides

      + Reference


  - 프리젠테이션으로 정리해 보자. 재미있게 진행되니 WTF는 날리지 말자.

     

  - egghead.io 에서 리액트에 관련하여 ReactJS, React Native, Flux Architecture를 동강으로 제공한다. 물론 무료가 있다. 
    * 유료강좌를 듣길 추천한다. 앵귤러와 리액트에 대한 좋은 강좌가 많다. 한달 대략 15달러
    + Render MethodProperties, State 사용하기

  - 제이쿼리를 리액트로 바꾸는 과정을 잘 설명해 주고 있다. 글을 읽고 다시 egghead.io 를 보면 이해가 될 것이다. 

    + jQuery to React 가이드

       내용중에 앵귤러를 사용하고 있는 개발자라면 앵귤러의 지사자에서 scope: { createXXX: '&' } 방식으로 자식의 부모의 함수를 호출 할 수 있다

       그리고 자식, 부모간에 scope 객체의 상속을 통해 scope 속성을 접근할 수 있다는 것도 알 수 있다. 

    + Flux 소개 아래 영상을 보면 react가 Virtual DOM을 통해 더 낳은 성능과 단순성을 통한 버그 가능성 제거를 이룰 수 있는지 가늠할 수 있다

       * Flux 사이트

      


  - 이제 다시 설치부터 기초 개념을 문서를 통해 쭉 살펴보자. 

  - ng-conf 말고 react-conf도 있다. 동영상을 통해 향후 방향을 점쳐보자. 
    

  - 리액트 컴포넌트를 찾고 싶을 경우 


  - 리액트 자료 

    + Awesome 자료 목록들

    + 앵귤러와 결합해서 사용하기 : NGREACT 


  - 리액트 컴포넌트 프레임워크 : 트위터 부트스트랩같은 조합

   + reapp.io


  - 새 술은 새 푸대에 넣자. 리액트가 UI 컴포넌트라면 Webpack 또는 Browserify 를 이용해 모듈화 하자
    + 리액트를 위한 웹팩의 starter kit

  - 디버깅은 Chrome Extention 설치하고 시작
  - 필요에 맞는 툴과 환경을 선택한다.

 


Immutable Data에 대한 이해 

불변 데이터에 대해 알아 두어야 한다. 예로 리스트를 생성하고 거기에 새로운 값을 넣으면 리스트를 가르키는 레퍼런스는 같을까 틀릴까? 기존 방식이라면 동일한 레퍼런스이겠지만 Immutable의 세계에 오면 값이 변경 되면 새로운 객체가 된다. 리액트에서는 성능향상을 위해 Immutable.js를 사용토록 권장하고 있다. 이것에 대해 발표한 영상을 보고 영상을 요약한 블로그 글을 보도록 한다. 
  - 영상 설명 블로그 [1], [2], [3]
  - 발표 영상

  - 최근 보고 있는 Clojure는 기본적으로 Immutable Data이다. 클로져 관련 블로그를 읽어보자.



자바스크립트 개념 장착

리액트를 하다 보면 this와 bind(this)를 자주 사용할 때가 있고, ES6로 전환하다보면 좀 더 빈번히 보게 된다. 제대로 이해하고 React 코드를 보자 
  - bind를 해주는 이유 Callback 때문 그렇다면 Callback을 이해하자
  - Callback은 자바스크립트가 Funtional Programming이 가능하게 해주고 Closure이다. 클로저를 이해하자
  - React에서 Object만드는 것이 많이 나오니 자바스크립트의 Object에 대해 알아보자
  - http://javascriptissexy.com/ 에서 다른 글도 참조하시라


'React' 카테고리의 다른 글

[React] 다시 시작하기  (0) 2018.09.14
[React] Semantic-UI를 React 컴포넌트로 만들기 - 1  (0) 2015.08.23
[React] CSS Framework 선정하기  (0) 2015.08.15
[Flux] Flux 배우는 방법  (0) 2015.07.04
[React] AngularJS와 ReactJS 비교  (0) 2015.07.01
posted by 윤영식
2015. 4. 16. 17:50 My Services/Smart Analytics

AngularJS SPA 형태 개발을 진행할 때 AMD(Asychronous Module Definition)에 대한 고려를 하지 않고 있다. 계속해서 확장 되어야 하는 애플리케이션을 개발한다면 AMD를 염두에 두자. 하지만 AMD가 아니라 앵귤러 스러운 모듈단위의 동적 로딩을 하고 싶을 경우 어떻게 하는지 Smart Analytics 관점에서 컨셉을 테스트해 보자. 



개념 


  - CommonJS : exports, require, module을 통한 서버사이드 자바스크립트 모듈화 스팩이고 Node.js에서 많이 사용함

  - AMD : 브라우져에서 모듈화에 대한 스팩. 가장 많이 쓰는 구현체 require.js  를 통한 자세한 사용예(NHN)

    + 특징 : 동적 로딩, 의존성 관리, 모듈화 

    + define을 통한 모듈 패턴을 적용하여 모듈을 만든다

    + require를 통해 의존관계 모듈들을 주입받아 사용한다. 

  - AngularJS는 모듈화와 DI 즉, 의존성 주입을 가능하게 하고, RequireJS는 JS 파일의 의존성을 관리하고 동적 로딩을 통한 성능 최적화를 얻는다. (참조)





Smart Analytics에서 바로 보는 동적 로딩 시나리오

  

  ElasticSearch의 Kibana는 엘라스틱서치의 데이터를 자유롭게 표현할 수 있는 대시보드 소프트웨어이다. AngularJS + Require.js를 사용하고 있다. 하지만 좀 더 AngularJS 스러운 방식으로 앵귤러 모듈을 필요한 시점에 동적으로 할 수는 없을까? 화면이 라우팅될 때 또는 필요한 시점에 필요한 모듈만 로딩할 수 있도록 지원하는 앵귤러 컴포넌트로 ocLazyLoad 모듈이 있고 이를 이용하여 대시보드 화면에 국한하여 시나리오를 써보자


  - 사용자가 로그인을 하면 사용자마다 할당 된 대시보드 목록을 가져오고 첫번째 대시보드를 브라우저 영역 전체화면으로 보여준다.

  - 대시보드에는 사용자가 직접 제작한 차트가 보이고 해당 차트는 별도의 파일로 존재하고 앵귤러의 독립 모듈로 존재한다. 따라서 대시보드안에 들어가는 차트들은 플로그인 방식으로 동적으로 로딩되어 대시보드 일부분에 포함되어 들어가야 한다. 

  - 그러나 사용자가 직접 제작하지 않고  저작도구를 통해 제작한 차트는 별도의 파일로 존재하지 않고 환경값을 서버로 부터 받아 차트 컨테이너에 자동으로 해석되어 표현된다. 


  여기서 동적으로 로딩되어 표현할 것은 직접 별도로 제작된 차트를 대상으로 하고, 별도의 파일로 존재한다. 해당 파일은 로컬 또는 네트워크를 통해 로딩 될 수 있어서 다양한 차트 화면을 대시보드로 수용할 수 있다. 마치 플러그인 방식으로 대시보드 운영 서버의 재기동 없이도 플러그인을 설치하기만 하면 자동으로 대시보드의 특정위치에 차트가 표현되는 것이다. 

  

   - chart-config.json + chart-container를 통해 차트를 자동으로 표현하거나

   - 로컬에서 사용자가 직접 제작한 bizChart01.js 파일을 동적으로 로딩해서 표현하거나, bizGrid01.js를 원격에서 동적 로딩하는 개념이다. 







ocLazyLoad 기반 앵귤러 모듈의 동적 로딩


  ocLazyLoad 모듈을 이용해 파일 로딩하는 예를 보자. 설치하고 모듈 의존성 설정

// survey-gorilla 설문조사 오픈소스를 위해 만들어 놓은 제너레이터로서 generator-angular-fullstack을 기반으로 수정한 것이다. 

$ npm install generator-sg --save

$ mkdir prototyping & cd prototyping


// 애플리케이션 생성 : mongodb는 선택안하고, ui-bootstrap과 sass를 선택함

$ yo sg SAProto 

// oclazyload를 설치함 

$ bower install oclazyload --save

bower install       oclazyload#0.6.3


  ocLazyLoad 모듈을 위한 별도의 서비스를 /app/components/base/pluginer 폴더에 만들어 보자. pluginer 앵귤러 서비스를 base 모듈에 포함시키기 위해 먼저 base.module.js를 만든다. 

// base.module.js 를 만들고, oc.lazyLoad 모듈에 대한 의존관계를 설정한다. 

$ cd client/components

$ mkdir base & cd base

$ vi base.module.js 


(function() {

  'use strict';


angular 

    .module('sa.base', ['oc.lazyLoad']);


})();



// pluginer 서비스를 만들어 보자 

$ mkdir pluginer & cd pluginer

$ vi pluginer.service.js 


(function() {

  'use strict';


  angular

    .module('sa.base')

    .service('pluginer', pluginer);


  /* @ngInject */

  function pluginer($q, $ocLazyLoad) {

    this.load = load;


    function load(moduleName, filePath) {

      var deferred = $q.defer();


      $ocLazyLoad

        .load({

          name: moduleName,

          files: [ filePath ]

        })

        .then(function() {

          console.log('loaded plugin: ' + moduleName + ' successfully.');

          deferred.resolve('ok');

        }, function() {

          console.log('failed plugin: ' + moduleName + ' successfully.');

          deferred.reject('fail');

        });


      return deferred.promise;

    }

  }


})();


// 최종 폴더 구조 


  pluginer.service.js는 $ocLazyLoad를 통해 지정한 파일의 모듈을 로딩해서 사용할 수 있고 promise 패턴을 통해 비동기적인 상황에 대응한다. 다음으로 플러그인을 하나 만들어 보자. 우선 client 밑에 plugins 폴더를 만들고 index.html에 포함되지 않는 *.js / *.css / *.jpg(png) 등을 놓는다. plugin.bizchart01이라는 별도의 모듈을 만들고 bizchart01 서비스를 만든다. 

// client 밑에 plugins 폴더를 생성한다.

$ mkdir plugins & cd plugins

$ mkdir bizchart01 & cd bizchart01

$ vi bizchart01.js 


(function() {

  'use strict';


  angular

    .module('plugin.bizchart01', [])

    .service('bizchart01', bizchart01);


  function bizchart01() {

    this.draw = draw;


    function draw() {

      return 'drawing....';

    }

  }


})();


// plugins 폴더 

 

  이제 pluginer 서비스를 통해 plugin.bizchart01 모듈을 필요한 시점에 로딩을 하고 bizchart01 서비스의 draw() 를 호출해서 출력해 보자. sa.base 모듈을 app.js에 의존관계를 설정하고 app/main/main.html 를 바꿔보자. 

// /client/app/app.js 의 sa.base 모듈 의존 설정 

  angular

    .module('saprotoApp', [

 'ngCookies',

 'ngResource',

 'ngSanitize',

 'ui.router',

 'ui.bootstrap',

                  'sa.base'

])

    .config(config)

    .factory('authInterceptor', authInterceptor)

    .run(run);


// main.html 과 main.controller.js를 수정하여 테스트 코드 작성

// main.html 

 <li><a href="#" ng-click="lazyloading()">Load Module</a>: {{lazyloadingMsg}}</li>


// main.controller.js 

(function () {

  'use strict';


  angular

    .module('saprotoApp')

    .controller('MainCtrl', MainCtrl);


  /* @ngInject */

  function MainCtrl($scope, $injector, pluginer) {

    $scope.lazyloading = lazyloading;


    function lazyloading() {

      pluginer

        .load('plugin.bizchart01', 'plugins/bizchart01/bizchart01.js')

        .then(function() {

          var bizchart01 = $injector.get('bizchart01');

          $scope.lazyloadingMsg = 'lazyloading message: ' + bizchart01.draw();

          console.log($scope.lazyloadingMsg);

        });

    }    

  }


})();


  pluginer를 통해 "plugins/bizchart01/bizchart01.js" 에 있는 "plugin.bizchart01" 모듈을 로딩하고, $injector를 통해 "bizchart01" 서비스를 얻어와 draw()를 호출했다. 이를 확인할 수 있는 것은 크롬의 개발자 도구에서 브라우저에서 로딩 링크를 클릭했을 때 실제로 bizchart01.js 파일을 호출하는지 보면 된다. 


1) "Load Module"를 클릭한다. 



2) 크롬 개발자 도구의 Nework에서 "Load Module" 링크를 클릭시 bizchart01.js을 요청하는지 확인한다. 



이제 별도의 차트를 앵귤러 모듈로 만들어서 필요한 시점에 로딩할 수가 있다. 이외에도 ui-router를 통해 partial view가 변경될 때도 가능하고 다양한 예제는 ocLazyLoad를 참조한다. 


테스트 소스 : https://github.com/Smart-Analytics/prototyping/tree/feature/lazyloading




<참조>


  - CommonJS와 AMD 개념 비교 (NHN)

  - AMD : require.js 사용예

  - Node.js와 브라우져 사용예 (Mobicon)

  - ocLazyLoad GitHub

posted by 윤영식
2015. 1. 21. 16:15 My Projects/Jandi

2주차 진행을 하며 기존 웹 저장소를 별도로 구성해 완전히 별도의 애플리케이션을 구축할 예정이었으나 프러덕션 저장소의 업무로직의 복잡도와 지속적인 개발로 인한 신규 저장소 소스의 불일치 문제가 발생하였다. 따라서 완전히 새로운 저장소로 별도 애플리케이션을 개발하기 보다 Lagacy 코드를 리팩토링 하고 테스트 코드를 신규로 넣고 동시에 연속적인 빌드 환경을 구축하기로 한다. 



1. 모듈 리팩토링


  - AngularJS에서 하나로 묶여 있는 모듈을 여러개로 쪼개는 작업을 진행한다. 

    + 다행히 기존 애플리케이션이 generator-angular-fullstack 으로 되어 있기 때문에 별도로 작업했던 내용을 적용한다. 

    + jandiApp 모듈만 존재한다면 애플리케이션을 위한 추상화 모듈과 공통 모듈로 두가지를 생성한다. 

  - web_client 폴더는 jandi의 웹 애플리케이션 저장소 이름이다. 

    + web_client/client 폴더 밑으로 components 폴더를 만들고 그 밑으로 app와 base 폴더를 만든다. 

    + components/app는 애플리케이션과 연관이 있는 대표 모듈 폴더이다. 

    + components/base는 jandi외에도 어디서나 사용 가능한 그리고 웹에서 bower로 다운로드한 모듈의 폴더이다. 

$ cd web_client/client/

$ mkdir components && cd components

$ mkdir app

$ mkdir base


  - app와 base에 대한 대표 모듈을 만든다.

    + components/app/app.framework.js 파일 생성

    + components/base/base.farmework.js 파일 생성

    + app.framework 모듈은 base.framework 모듈을 의존한다. 

// app.framework.js 내역 

(function() {

  'use strict';


  angular

    .module('app.framework', ['base.framework']);

    

})();


// base.framework.js 내역 

(function() {

  'use strict';


  angular

    .module('base.framework', []);

    

})();


  - 자바스크립트 로딩 우선순위를 지정한다. 

    + index.html 파일의 app.js 보다 먼저 로딩 될 수 있도록 app.framework.js와 base.framework.js를 지정한다. 

    + grunt serve 명령 수행시 app.framework.js와 base.framework.js 파일이 자동 inject 되지 않도록 ingore 설정을 한다. 

    + watch와 inject에 경로를 설정하고 설정앞에 ! 주면 exclude하겠다는 의미이다. 

// index.html 일부 내역 

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

<script src="components/base/base.framework.js"></script>

<script src="components/app/app.framework.js"></script>

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

<!-- injector:js -->

<script src="assets/javascripts/angulartics-ga-custom.js"></script>



// Gruntfile.js 일부 내역

 watch: {

            injectJS: {

                files: [

                    '<%= yeoman.client %>/{app,components}/**/*.js',

                    '!<%= yeoman.client %>/{app,components}/**/*.spec.js',

                    '!<%= yeoman.client %>/{app,components}/**/*.mock.js',

                    '!<%= yeoman.client %>/components/base/base.framework.js',

                    '!<%= yeoman.client %>/components/app/app.framework.js',

                    '!<%= yeoman.client %>/app/app.js'],

                tasks: ['injector:scripts']

            }, 

... 중략 ....

injector: {

            options: {},

            // Inject application script files into index.html (doesn't include bower)

            scripts: {

                options: {

                    transform: function(filePath) {

                        filePath = filePath.replace('/client/', '');

                        filePath = filePath.replace('/.tmp/', '');

                        return '<script src="' + filePath + '"></script>';

                    },

                    starttag: '<!-- injector:js -->',

                    endtag: '<!-- endinjector -->'

                },

                files: {

                    '<%= yeoman.client %>/index.html': [

                        [

                            '{.tmp,<%= yeoman.client %>}/assets/javascripts/*.js',

                            '{.tmp,<%= yeoman.client %>}/{app,components}/**/*.js',

                            '!{.tmp,<%= yeoman.client %>}/components/base/base.framework.js',

                            '!{.tmp,<%= yeoman.client %>}/components/app/app.framework.js',

                            '!{.tmp,<%= yeoman.client %>}/app/app.js',

                            '!{.tmp,<%= yeoman.client %>}/{app,components}/**/*.spec.js',

                            '!{.tmp,<%= yeoman.client %>}/{app,components}/**/*.mock.js']

                    ]

                }

            },


  모듈 리팩토링 준비가 되었다. 리팩토링할 대상을 선정한다. generator-angular-fullstack으로 애플리케이션을 생성하면 client/app/app.js 파일이 메인 파일이 된다. 먼저 app.js안에 애플리케이션 모듈 set과 config, run에 다양한 설정이 되어 있다면 이들을 base.framework 또는 app.framework으로 분리할 수 있는지 점검한다. 일반적으로 다음과 같은 상태로 app.js를 설정하고 한다. 





  위와 같이 app.js 한 곳에 다양한 설정들이 있는 것을 리팩토링을 통해 모듈단위로 나눌것이다. 이렇게 할 수 있는 것은 메인 모듈이 의존하는 모듈을 설정하면서 의존모듈을 로딩하면서 의존모듈의 config, run을 수행해 주고 그리고 자신의 모듈을 로딩하기 때문이다. app.js안에 app.framework과 base.framework 모듈의 의존관계를 설정하고 로그를 찍어보자. 먼저 의존관계의 모듈 config와 최종 애플리케이션 모듈인 jandiApp config를 로딩한 후 run을 다시 의존 모듈부터 수행시킨다. 즉, 애플리케이션을 최초 호출할 때 config, run 은 최초 한번 수행됨을 이용해서 각 기능의 초기화를 모듈단위로 나누는 것이다. 

// app.js 의존관계 설정 

angular.module('jandiApp', [

    'base.framework',

    'app.framework',

    ... 중략 ...]);


// config, run의 수행 순서 로그 

=========> Base framework Module config

=========> App framework Module config

=========> jandiApp Module config

=========> Base framework Module run

=========> App framework Module run

=========> jandiApp Module run




2. Config 모듈 생성 


 최초 환경 설정 부분을 모듈로 만들어서 app.framework 모듈에 의존성을 설정해 보자. app.js의 run 메소드에는 api 버전과 서버주소에 대한 기본 환경 설정이 있다. 이것을 config.js라는 별도의 모듈로 빼서 환경설정에 대한 부분을 독립시킨다. 

  - config.js 모듈 파일 생성 : 최초 환경 셋업이 필요한 부분은 config 모듈의 run 메소드에 설정한다. 즉, app.js 있던것을 옮겨옴 

  - config.service.js 서비스 생성 : 해당 서비스를 통해 어디서나 주입을 받아 사용토록 한다. 

$ cd web_client/components/app

$ mkdir config && cd config


// app.js 일부내역 

app.run(function() {

$rootScope.server_address   = configuration.api_address + "inner-api/";

      $rootScope.server_uploaded  = configuration.api_address;

$rootScope.api_version      = configuration.api_version;

$rootScope.configuration    = configuration;  

});


// components/app/config/config.js 내역 

(function() {

  'use strict';


  angular

    .module('app.config', [])

    .constant('configuration', {

      name                : 'local',

      api_address         : 'http://www.jandi.com/',

      api_version         : '2',

      ga_token            : 'UA-222222-1',

      ga_token_global     : 'UA-1111111-1',

      mp_token            : 'abcdefghijklmn',

      base_url            : '.jandi.io',

      base_protocol       : 'http://',

      main_address        : 'http://www.jandi.com/main/#/',

      app_store_address   : 'xxxx',

      play_store_address  : 'yyyy'

    })

    .run(run);


  /* @ngInject */

  function run($rootScope, config) {

    // server address, server api version 

    config.init($rootScope);

  }


})();


// components/app/config/config.service.js 내역 

(function() {

  'use strict';


  angular

    .module('app.config')

    .service('config', config);

    

  /* @ngInject */

  function config(configuration) {

    var self = this;

    this.init = init; 


    function init($rootScope) {

      // TODO : maybe remove next factoring

      // compatibility for current version

      $rootScope.server_address   = configuration.api_address + "api/";

      $rootScope.server_uploaded  = configuration.api_address;

      $rootScope.api_version      = configuration.api_version;

      

      // configuration constant service       

      configuration.server_address  = configuration.api_address + "api/";

      configuration.server_uploaded = configuration.api_address;

      configuration.api_version     = configuration.api_version;


      // config service

      self.server_address  = configuration.api_address + "inner-api/";

      self.server_uploaded = configuration.api_address;

      self.api_version     = configuration.api_version; 

    }

  }

})();


  config.js 에서 app.config 모듈을 run 하면서 config.service.js 를 주입받아 최초에 init()을 하고 있다. 파라미터로 $rootScope를 전달하는데 이는 초기 app.js에서 환경값을 $rootScope에 설정해서 사용하기 때문이다. 향후에는 $rootScope를 통해 접근하는 것을 없애고 config 서비스 통해 필요한 정보를 접근토록 할 예정으로 1차로 기존 버전의 애플리케이션 수정없이 환경설정으로 뺄 수 있는 상태로 리팩토링한다. AngularJS에서 초기 설정 정보를 접근하기 위한 원칙을 다음과 같이 세우면 좋다. 


  - AngularJS의 서비스는 singleton object 이므로 초기 값을 변수로 가지고 있고 접근자 API를 노출한다. 

  - AngularJS 서비스에 $scope, $rootScope를 절대로 주입하지 않는다. 오직 controller와 module.run 에서만 주입을 받아 사용한다. 

  - 따라서 서비스는 서비스를 주입받아 공통정보에 접근해야 한다. 


원칙에 따라 하나씩 모듈로 쪼개고 app.js에는 업무적인 로직에만 집중할 수 있도록 만든다. 향후 components/app/* 모듈은 사용자가 사용하는 웹화면과 관리자가 사용하는 웹화면에 동시에 적용되는 애플리케이션과 관련된 공통 모듈이 들어갈 것이다. 현재까지 모듈의 의존관계 모습은 다음과 같이 app.js의 jandiApp 의존관계는 app.framework 밖에 없다. 하지만 서로 의존관계가 설정되어 있기 때문에 jandiApp 모듈의 AngularJS 모든 컴포넌트는 base.framework과 app.framework의 의존관계 설정 모듈의 컴포넌트를 주입받아 사용할 수 있게 된다. 

// app.js 의존관계 

angular

  .module('jandiApp', [

    'app.framework'

  ]);


// app.framework.js 의존관계

 angular

    .module('app.framework', [

        'base.framework',

        'app.config'

      ])

    .run(run)

    .config(config);


// base.framework.js 의존관계

  angular

    .module('base.framework', [

      'ui.router',

      'ui.bootstrap',

      'gettext'

      'ngCookies',

      'ngResource',

      'ngSanitize',

      'ngAnimate'

    ])


  좀 더 구조적인 관계의 모듈 설정을 통해 구조적인 애플리케이션을 개발하는 첫단추를 꽤었다. 


'My Projects > Jandi' 카테고리의 다른 글

[Jandi] 팀 커뮤니케이션 서비스  (0) 2015.01.07
posted by 윤영식
2015. 1. 7. 11:29 My Projects/Jandi

Tosslab의 잔디는 팀 그룹간에 원활한 커뮤니케이션을 위한 도구이다. 채팅 및 파일공유를 할 수 있어서 업무 커뮤케이션 툴로 사용하기에 적당하고 모바일, 웹에서 접근할 수 있다. 



잔디의 기술


  잔디는 MEAN Stack 기술을 사용하고 모바일은 네이티브로 개발한다. 업무적인 커뮤니케이션 도구는 사무실에서 웹을 통해 사용할 경우가 많아서 특히 웹에 대한 사용성이 좋아야 한다. 블로그에서는 프론트앤드 영역만을 다루도록 한다. 기존에는 AngularJS v1.2.* 기반으로 되어 있고 개발을 진행함에 따라 웹 애플리케이션의 복잡도가 증가하여서 애플리케이션을 재구성할 필요가 발생하였다. 




모듈단위 애플리케이션 구성


 애플리케이션 복잡도를 단순화 시키고 애플리케이션을 구조화하는 방안이 필요하다. 애플리케이션을 구조화 한다는 것은 거시적인 부분과 미시적인 부분의 조합으로 이루어진다. 먼저 거시적인 부분은 아키텍쳐레벨로 AngularJS의 모듈을 구성하는 것이다. 


 

  AngularJS 모듈은 컴포넌트의 그룹이다. 모듈안에는 다양한 컴포넌트를 가지고 있고 컴포넌트 성격별로 구분해 놓을 필요가 있다. 위의 예에서 가장 기본으로 있어야할 AngularJS에서 제공하는 모듈 묶음과 어느 프로젝트에서나 쓰일만한 모듈을 묶어놓은 공통 모듈이 있을 것이다. 그리고 업무적으로 추상화할 필요가 있는 업무 공통 모듈이 존재하고 업무를 처리하는 업무 모듈식으로 나눌 수 있다. 하위 모듈은 상위 모듈을 참조할 수 없고 상위 모듈만이 하위모듈안의 컴포넌트를 DI(Dependency Injection) 방식으로 사용을 할 수 있다. 




제너레이터를 통한 애플리케이션 구성


 기존의 애플리케이션을 보기하면서 다시 재구성하기 위해 무엇부터 시작을 해야할까? 


  - Yeoman Generator를 선정한다. generator-angular-fullstack을 사용할 예정이다. 

  - 자바스크립트 코딩 스타일은 구글의 자바스크립트 스타일 가이드를 따른다. 

  - AngularJS의 코딩 스타일은 존파파의 AngularJS 스타일 가이드를 따른다.


  generator-angular-fullstack의 템플릿을 존파파의 가이드에 따라 변경한 프로젝트를 만들어 본다. 불필요한 예제는 제거하고 몇가지 영역의 템플릿만을 취할 것이다. 먼저 generator-angular-fullstackgenerator-ng-component를 각각 git clone 하고 명칭을 generator-jdi, generator-jdi-component라고 바꾸고 내부의 .git폴더를 제거한 후 각 템플릿의 내용을 파파죤스 가이드에 따라서 수정을 한다. 

$ git clone https://github.com/DaftMonk/generator-angular-fullstack.git

$ git clone https://github.com/DaftMonk/generator-ng-component.git


$ mv generator-angular-fullstack generator-jdi

$ mv generator-ng-component generator-jdi-component


$ cd generator-jdi && rm -rf .git

$ cd generator-jdi-component && rm -rf .git


 파파죤스 가이드에 따라 generator-jdi-component/templates 폴더 밑의 모든 앵규러 컴포넌트를 다음과 같은 형식으로 수정한다. 

   + IIFE : Immediately Invoke Function Expression

   + 앵귤러 모듈 변수를 만들지 않음 

   + 컴포넌트의 펑션을 밖으로 뺌

   + DI 하는 부분은 ng-annotation을 이용함으로 주입받는 펑션위에 /* @ngInject */ 를 둚

(function() { 

  'use strict';


  angular

    .module('<%= scriptAppName %>')

    .controller('<%= classedName %>Ctrl', <%= classedName %>Ctrl);


  /* @ngInject */

  function <%= classedName %>Ctrl($scope) {

         

  }

})();


 수정한 내역에 대해 테스트는 다음과 같이 진행을 하면 된다.  먼저 테스트용 test 폴더를 하나 만든다. 테스트 폴더 밑에 node_modules 폴더를 만들고 밑으로 generator-jdi와 generator-jdi-component 에 대해 심볼릭 링크를 건다. 각 generator-* 폴더로 들어가 npm install을 해준다. 이제는 test밑에서 "yo jdi Jandi" 명령을 치면 애플리케이션 생성 프롬프트를 묻게 된다. 애플리케이션 템플릿은 generator-jdi/app/templates 밑에 존재하고 여기에 있는 파일도 파파죤스 스타일 가이드에 따라 수정을 해준다. 

$ mkdir test && cd test

$ mkdir node_modules && cd node_modules 


$ ln -s <fullPath>/generator-jdi  generator-jdi

$ ln -s <fullPath>/generator-jdi-component generator-jdi-component


// 둘은 같은 디렉토리에 있어야 함 

$ cd <fullPath>/generator-jdi-component && npm install

$ cd <fullPath>/generator-jdi && npm install 


$ cd test 

$ yo jdi Jandi


  각 수정한 generator-jdi, generator-jdi-component는 링크를 참조한다. 이제는 누구나 사용할 수 있도록 npm repository에 등록을 한다. npm publish 로 package.json 파일에 등록된 내역을 기반으로 배포가 된다. 따라서 clone한 generator의 버전을 따르지 않고 최초 0.0.1 버전으로 수정해 배포한다. 

$ cd generator-jdi-component 

$ npm publish

+ generator-jdi-component@0.0.1


$ cd generator-jdi

$ npm publish

+ generator-jdi@0.0.1


 이제 글로벌로 generator-jdi만을 설치해서 어디서나 "yo jdi <App명칭>" 명령을 수행해 최초 애플리케이션 골격을 만들 수 있다. 

$ npm install -g generator-jdi

$ mkdir web_tiger && cd web_tiger

$ yo jdi Jandi



'My Projects > Jandi' 카테고리의 다른 글

[Jandi] 애플리케이션 모듈 리팩토링  (0) 2015.01.21
posted by 윤영식

BroadleafThymeleaf이라는 서버 템플릿 엔진을 사용하고 있다. Broadleaf은 Tymeleaf을 상속받아 확장한 <blc:XXX> 태그를 xxxProcessor 클래스에 정의하고 있다. 따라서 템플릿에는 Tymeleaf 태그와 Broadleaf이 정의한 <blc:XXX> 태그가 함께 사용된다. 이에 대해 간략히 알아보고 Twitter Bootstrap을 템플릿에 적용해 보자 




Broadleaf에서 Thymeleaf 역할 


  - Thymeleaf은 레이아웃 템플릿 보다 텍스트 템플릿에 가깝고 객체값을 받아서 화면에 표현해 주는 제어구문들을 가지고 있다.

  - AngularJS의 Directive와 서버 템플릿의 Taglib로 사용자 정의한 태그와 백앤드/프론트앤드만 틀리고 유사하다고 하겠다. 

  - 그럼 데이터는 어떻게 주고 받아서 표현할까?

     + Thymeleaf은 HTML을 해석하고 맵핑된 객체를 통해 Value가 설정된 HTML을 최종 클라이언트에 전송한다. 

     + 이때 객체 정보는 Broadlead의 <blc:XXX> 태그에서 Broadleaf core소스에서 정의한 XXXProcessor 가 YYYService를 통해서 Thymleaf에 사용할 객체를 조회/설정/전달한다. 


  - core/broadlleaf-framework : Hibernate와 연결된 DAO와 업무 도메인 Service가 존재한다. 

  - core/broadleaf-framework-web : web이므로 프론트단에 필요한 폼, 필터, 태그처리 프로세스(Processor)등이 존재한다. 

    + DemoSite의 메인 페이지 템플릿은 /site/src/main/webapp/WEB-INF/templates/layout/home.html 해당 파일이다. 

    + home.html을 열어보면 맨위에 하기와 같이 <head>가 설정되어 있다. 

    + <head> 태그를 core/broadleaf-framework-web의 org.broadleafcommerce.core.web.processor.HeadProcessor 가 처리한다.

    + HeadProcessor는 pageTitle 값을 처리하는 클래스이다. 

    + 즉, XxxYyyZzzProcessor 가 있고 태그로 <xxx-yyy-zzz> 태그를 사용하면 XxxYyyZzzProcessor 가 동작하는 것이다. 

// home.html 일부 내역


<head th:include="/layout/partials/head (pageTitle='Broadleaf Demo - Heat Clinic')"></head>

<body>

    <div id="notification_bar"></div>

    <header th:substituteby="layout/partials/header" />

    

    <div id="content" class="width_setter group" role="main">

        <nav th:substituteby="layout/partials/nav" />

        <blc:content contentType="Homepage Banner Ad" />       

        <div id="banners" th:if="${contentItem !=null and contentItem['targetUrl'] != null and contentItem['imageUrl'] != null}">

            <a th:href="@{${contentItem['targetUrl']}}"><img blc:src="@{${contentItem['imageUrl']}}" /></a>       

        </div>

  ... 중략 ...

</body>



// HeadProcessor.java 일부 내역


import org.thymeleaf.processor.element.AbstractFragmentHandlingElementProcessor;

public class HeadProcessor extends AbstractFragmentHandlingElementProcessor {

    @Resource(name = "blHeadProcessorExtensionManager")

    protected HeadProcessorExtensionListener extensionManager;

    public static final String FRAGMENT_ATTR_NAME = StandardFragmentAttrProcessor.ATTR_NAME;

    protected String HEAD_PARTIAL_PATH = "layout/partials/head";


    public HeadProcessor() {

        super("head");

    }


    @Override

    public int getPrecedence() {

        return 10000;

    }


    @Override

    protected boolean getRemoveHostNode(final Arguments arguments, final Element element) {

        return true;

    }


    @Override

    @SuppressWarnings("unchecked")

    protected List<Node> computeFragment(final Arguments arguments, final Element element) {

        String pageTitle = element.getAttributeValue("pageTitle");

        try {

            Expression expression = (Expression) StandardExpressions.getExpressionParser(arguments.getConfiguration())

                    .parseExpression(arguments.getConfiguration(), arguments, pageTitle);

            pageTitle = (String) expression.execute(arguments.getConfiguration(), arguments);

        } catch (TemplateProcessingException e) {}


        ((Map<String, Object>) arguments.getExpressionEvaluationRoot()).put("pageTitle", pageTitle);

      ((Map<String, Object>) arguments.getExpressionEvaluationRoot()).put("additionalCss", element.getAttributeValue("additionalCss"));


        extensionManager.processAttributeValues(arguments, element);

        return new ArrayList<Node>();

    }

}


  - <blc:content> 는 특별히 admin/broadleaf-conentmanagement-modul 에서 org.broadleafcommerce.cms.web.processor.ContentProcessor 로 존재한다.

  - <blc.content> 태그는 내부적으로 서비스를 호출해서 contentItem을 값을 설정해 준다. 이때 contentType="Homepage Banner Ad"는 admin 페이지에서 설정해주는 것이다. 즉, 관리 도구를 통해 Content Mangement를 할 수 있고 이를 간단히 <blc:xxx> 태그를 사용한다.

1) admin 프로젝트에서 ant task중 jetty-demo 실행

2) http://localhost:8081/admin (id:admin, password: admin)

3) Content 메뉴의 content items 목록이 나온다. 여기서 "Content Type"으로 찾으면 됨 



  - Thymeleaf의 특정 인터페이스를 상속받으면 자신만의 태그를 쉽게 만들수 있고 Broadleaf에서는 XXXProcessor로 만들고 있는 것이다. 그리고 <blc:xxx> 태그를 만들어 xxxProcessor내부에서 Service를 통해 Hibernate를 호출해 업무적인 부분을 처리하고 있다.


  얼핏 보면 AngularJS의 Directive를 만들고 내부에서 AngularJS 서비스를 DI(Dependency Injection) 받아 호출하고 서비스내에서 서버로 AJAX 호출하는 계층을 두는 것과 유사하다. 따라서 Broadleaf에서 정의한 xxxProcessor들을 AngularJS Directive로 전환하면 AngularJS기반의 SPA(Single Page Application)으로의 전환이 가능하다. 




Twitter Bootstrap 적용하기 


  - 트위터 부트스트랩은 UI Framework으로 RWD (Responsible Web Design)을 적용할 수 있는 CSS를 제공하고 많이 사용하는 jQuery 플러그인도 함께 제공된다. 

  - 우선 트위터 부트스트랩 v3.3.1을 다운로드하고 압축을 풀면 css, fonts, js 폴더가 있다 

  - site/src/main/webapp 밑에 css, fonts, js를 복사한다. 

  - site/src/main/webapp/WEB-INF/templates/layout 폴더가 상/하/메인에 대한 레이아웃을 설정하는 템플릿 HTML이 존재한다

  - 상단 head.html에 bootstrap.css를 추가하고 하단 footer.html에 bootstrap.js를 하기와 같이 추가한다 

// layout/partials/head.html 


    <blc:bundle name="style.css" 

                mapping-prefix="/css/"

                files="bootstrap.css,

                       style.css,

                       jquery.rating.css" />



// layout/partials/footer.html


    <blc:bundle name="lib.js" 

                mapping-prefix="/js/"

                files="plugins.js,

                       libs/jquery.MetaData.js,

                       libs/jquery.rating.pack.js,

                       libs/jquery.dotdotdot-1.5.1.js,

                       bootstrap.js" />


  - 기존의 CSS는 전부 무시하고 트위터 부트스트랩을 재적용하는 단계이다. 따라서 DemoSite에 적용한 css/style.css 의 내용을 전부 제거 한다.

  - 제거한 내용에 몇가지 CSS를 하기와 같이 적용한다.

/************************************/

/* Header Customizing               */

/************************************/

.navbar .navbar-top {

height: 30px;

padding-top: 5px;

background-color: #F5F5F5;

transition: all 0.1s ease-out 0s;

-webkit-transition: all 0.1s ease-out 0s;

-moz-transition: all 0.1s ease-out 0s;

-ms-transition: all 0.1s ease-out 0s;

-o-transition: all 0.1s ease-out 0s;

font-size: 11px;

line-height: 11px;

text-transform: uppercase;

}


.navbar .container-fluid {

background-color:#525252;

}


.navbar-inverse .navbar-nav > li > a, .navbar-inverse .navbar-brand {

color: #FEFEFE;

}


html, body {

height: 100%;

}


  - site의 WEB-INF/templates/layout/home.html 의 내역에서 필요없는 부분을 주석처리 하고 partials/nav.html 를 사용한다. 

    + <blc:xxx> 로 사용하는 부분을 전부 제거했다. 나중에 필요한 부분에 넣을 것이다. 

    + partails/nav.html 에서 메뉴역할을 하도록 partails/header.html을 nav.html에 통합할 것이다. 

    + <blc:form> 을 통해 제품 검색을 수행한다. 

<head th:include="/layout/partials/head (pageTitle='Broadleaf And Twitter Bootstrap')"></head>


<body>

    <div id="notification_bar"></div>

    

    <nav th:substituteby="layout/partials/nav" />

    

    <footer th:substituteby="layout/partials/footer" />

    

</body>

</html>


  - 다음으로 메뉴를 nav.html에 적용한다. 

    + 트위터 부트스트랩에 맞게 RWD를 적용하고 카테고리(Categories)에 대해 Thymeleaf의 <li th:each> 구문을 적용한다.

    + Admin 화면 (http://localhost:8081/admin) 에서 Category에 해당하는 내용이 메뉴에 적용되는 것으로 broadleaf-framework-web 프로젝트의 org.broadleafcommerce.core.web.processor.CategoriesProcessor가 처리한다. 

<nav>   

    <div th:remove="all">

        <!--

             This template displays the navigation of the site by looking up the category named "Nav"

             and building a list of the categories sub-categories.                          

        -->

    </div>

    

    <blc:categories resultVar="navCategories" parentCategory="Primary Nav" maxResults="6" />

    <nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">

    

      <!-- TOP Header  -->

      <div class="navbar-top">

      <div class="container">

      <div class="pull-right">

   

    <div th:if="${customer.anonymous}"  th:remove="tag">

            <a th:href="@{/login}">

                <span th:text="#{home.login}">Login</span>

            </a>

            &nbsp;|&nbsp; 

            <a th:href="@{/register}">

                <span th:text="#{home.register}">Register</span>

            </a>

            &nbsp;|&nbsp; 

        </div>

        <div th:unless="${customer.anonymous}" th:remove="tag">

            <span><span th:text="#{home.welcome}">Welcome</span>, <a th:href="@{/account}" th:text="${customer.firstName}"></a></span>

            &nbsp;|&nbsp; 

            <a th:href="@{/logout}">

                <span th:text="#{home.logout}">Logout</span>

            </a>

            &nbsp;|&nbsp; 

        </div>

        <img blc:src="@{/img/icon-cart.jpg}" alt="Shopping Cart Icon" />&nbsp; 

        <a id="cartLink" th:href="@{/cart}">

            <span id="headerCartItemWordSingular_i18n" style="display: none;" th:text="#{home.item}" />

            <span id="headerCartItemWordPlural_i18n" style="display: none;" th:text="#{home.items}" />

            <span class="headerCartItemsCount" th:text="${cart.itemCount}" />

            <span class="headerCartItemsCountWord" th:text="${cart.itemCount == 1} ? #{home.item} :  #{home.items}" />

        </a>

   

    </div>   

    </div>

      </div>

      

      <!-- Responsible Menu -->

      <div class="container-fluid">

        <div class="navbar-header">

          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">

            <span class="sr-only">Toggle navigation</span>

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

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

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

          </button>

          <a class="navbar-brand" href="/">eCommerce</a>

        </div>

        

        <div id="navbar" class="navbar-collapse collapse">

          <!-- Categories -->

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

       

        <blc:form class="navbar-form navbar-right" style="margin-left:5px; margin-right:5px" th:action="@{/search}" method="GET">

            <input type="search" class="form-control" placeholder="SEARCH PROD" name="q" th:value="${originalQuery}" />

            <input type="submit" class="btn btn-default" value="go" />

    </blc:form>

   

            <li th:each="category : ${navCategories}" th:unless="${#strings.isEmpty(category.url)}">

            <a class="home" th:href="@{${category.url}}" th:class="${categoryStat.first}? 'home'" th:text="${category.name}"></a>

        </li>

          </ul>

        </div>

      </div>

      

    </nav>

    

</nav>


결과 화면은 다음과 같이 정상적인 화면과 RWD 메뉴가 적용된 화면이다. 



* 소스는 제공되지 않습니다. 관심있으신 분은 별도로 문의해 주세요. 



참조 


  - http://www.broadleafcommerce.org

  - Broadleaf core 깃헙 소스

  - http://www.thymeleaf.org

  - 트위터 부트스트랩

posted by 윤영식

BroadLeaf eCommerce 솔루션은 서버는 자바 Spring Framework와 Hibernate를 사용하고 상품에 대한 검색은 Solr를 사용한다. 그리고 클라이언트는 ThymeLeaf 템플릿 엔진을 사용해서 BroadLeaf과 강하게 결합해서 사용된다. BroadLeaf 데모 사이트를 실제 사용할 수 있는 eCommerce 사이트로 전환하는 과정을 살펴본다. 또한 Frontend 쪽에 Twitter BootstrapAngularJS를 적용해 Tymeleaf과 상호 운용될 수 있도록 설정해 본다.






Broadleaf DemoSite 설치 


  - BroadleafCommerce 는 코어 소스이고 Community 버전으로 오픈소스를 제공한다. 

  - BroadleafCommerce 코어 소스를 사용한 DemoSite를 제공한다. 

  - DemoSite를 깃헙에서 받아 사용해도 되고 eclipse workspace 파일을 별도로 이곳에서 받아도 된다. (다운로드 버전은 3.1.8 버전)

    + 만일 3.1.9 버전을 사용하고 싶다면 깃헙의 DemoSite 릴리즈에서 3.1.9-GA를 다운로드하면 된다.


  - DemoSite를 maven project로 eclipse에 import하는 과정은 BroadLeaf Starting 가이드를 참조한다.

    + eclipse는 luna를 사용하면 eclipse용 maven 플러그인이 기본 설치되어 있다. 

    + 단, Apache Maven 은 다운로드해서 설치하고 path를 잡아준다.

    + 최초 DemoSite/build.xml 을 Ant view에서 열고 "change-identifier"를 수행하고

       maven의 grouId를 com.xxxx 식으로 넣고  "y"를 선택하면 자바 패키지와 maven groupId등을 설정한 값으로 자동 변경한다. 


  - 최초에 메이븐 빌드를 한번 해준다. 먼저 DemoSite로 이동해서 하기 명령을 한번 수행한다

$ cd DemoSite

$ mvn clean install


  - Broadleaf은 Site와 Admin으로 구분되어 Site는 쇼핑사이트를 Admin은 카랄로그와 컨텐츠를 관리하고 각각 8080, 8081를 사용한다.

    + site/build.xml 과 admin/build.xml 을 ant view에서 열고 "jetty-demo"를 수행하면 된다. 

    + 데이터베이스는 기본으로 메모리DB인 HSQLDB를 사용한다.

Site

http://localhost:8080


Admin (id: admin, password: admin)

http://localhost:8081/admin/




MySQL전환하기 


  - MySQL 전환작업은 가이드를 참조한다. 

  - mysql로 전환하기 위해 데이터베이스와 계정을 broadleaf 로 지정하고 계정 암호는 abc1234 로 생성한다.

-- %> mysql --user=root -p mysql

-- Enter password: ******


-- mysql> grant all on broadleaf.* to broadleaf@localhost identified by 'abc1234' with grant option;

-- mysql> create database broadleaf;

-- mysql> flush privileges;


-- %> mysql -u broadleaf -p broadleaf

-- Enter password: ******


-- mysql> show grants for broadleaf@localhost;

-- mysql> select current_user;


  - admin과 site가 mysql을 사용하도록 DemoSite/pom.xml과 admin/pom.xml과 site/pom.xml에 다음을 설정한다 

// DemoSite/pom.xml  안에 설정 

<dependency>

    <groupId>mysql</groupId>

    <artifactId>mysql-connector-java</artifactId>

    <version>5.1.34</version>

    <type>jar</type>

    <scope>compile</scope>

</dependency> 


// admin, site/pom.xml 안에 설정 

<dependency>

    <groupId>mysql</groupId>

    <artifactId>mysql-connector-java</artifactId>

</dependency> 


  - jetty를 통해 기동될 때 데이터베이스 커넥션 유형을 3가지를 사용하는데 HSQLDB에서 MySQL로 환경값을 바꾸어 주어야 한다. 

    + /site/src/main/webapp/WEB-INF/jetty-env.xml

    + /admin/src/main/webapp/WEB-INF/jetty-env.xml

<New id="webDS" class="org.eclipse.jetty.plus.jndi.Resource">

    <Arg>jdbc/web</Arg>

    <Arg>

        <New class="org.apache.commons.dbcp.BasicDataSource">

            <Set name="driverClassName">com.mysql.jdbc.Driver</Set>

            <Set name="url">jdbc:mysql://localhost:3306/broadleaf?useUnicode=true&amp;characterEncoding=utf8</Set>

            <Set name="username">broadleaf</Set>

            <Set name="password">abc1234</Set>

        </New>

    </Arg>

</New>

<New id="webSecureDS" class="org.eclipse.jetty.plus.jndi.Resource">

    <Arg>jdbc/secure</Arg>

    <Arg>

        <New class="org.apache.commons.dbcp.BasicDataSource">

            <Set name="driverClassName">com.mysql.jdbc.Driver</Set>

            <Set name="url">jdbc:mysql://localhost:3306/broadleaf?useUnicode=true&amp;characterEncoding=utf8</Set>

            <Set name="username">broadleaf</Set>

            <Set name="password">abc1234</Set>

        </New>

    </Arg>

</New>

<New id="webStorageDS" class="org.eclipse.jetty.plus.jndi.Resource">

    <Arg>jdbc/storage</Arg>

    <Arg>

        <New class="org.apache.commons.dbcp.BasicDataSource">

            <Set name="driverClassName">com.mysql.jdbc.Driver</Set>

            <Set name="url">jdbc:mysql://localhost:3306/broadleaf?useUnicode=true&amp;characterEncoding=utf8</Set>

            <Set name="username">broadleaf</Set>

            <Set name="password">abc1234</Set>

        </New>

    </Arg>

</New>


  - 하이버네이트의 Dialect를 HSQLDB에서 MySQL로 바꾼다. 

     + /core/src/main/resources/runtime-properties/common-shared.properties  의 맨 하단에 다음을 넣는다. 

# overriding dialect 

blPU.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

blSecurePU.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

blCMSStorage.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect


  - DemoSite의 build.properties의 기본 환경값을 HSQLDB에서 MySQL로 바꾼다. 

    + /DemoSite/build.properties 의 맨 하단에 다음을 넣는다. 

# new overriding 

ant.hibernate.sql.ddl.dialect=org.hibernate.dialect.MySQL5InnoDBDialect


ant.blPU.url=jdbc:mysql://localhost:3306/broadleaf

ant.blPU.userName=broadleaf

ant.blPU.password=abc1234

ant.blPU.driverClassName=com.mysql.jdbc.Driver


ant.blSecurePU.url=jdbc:mysql://localhost:3306/broadleaf

ant.blSecurePU.userName=broadleaf

ant.blSecurePU.password=abc1234

ant.blSecurePU.driverClassName=com.mysql.jdbc.Driver


ant.blCMSStorage.url=jdbc:mysql://localhost:3306/broadleaf

ant.blCMSStorage.userName=broadleaf

ant.blCMSStorage.password=abc1234


  - 그리고 가장 중요한 설정은 site와 admin 의 common.properties에 맨 하단에 다음 사항을 필히 넣어 주어야 BLC_SKU 테이블이 생성됨

    + /site/src/main/resources/runtime-properties/common.properties

    + /admin/src/main/resources/runtime-properties/common.properties

# overriding dialect 

blPU.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

blSecurePU.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

blCMSStorage.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect


  - 전환 후에는 ant task에서 "build-app"를 해주고 "jetty-run-no-db"를 선택하면 build한 target 디렉토리의 소스가 적용된다. 


     




참조 

  

  - Broadleaf v3.1 Guide

posted by 윤영식
2014. 8. 4. 00:41 AngularJS

  AngularJS(이하, 앵귤러) 기반의 싱글 페이지 웹 애플리케이션 개발에 대한 책을 집필하고 있다. 책안에 들어갈 내용을 위해 참조한 내역을 정리해 보고자 한다. 요즘 가장 큰 화두는 다양한 프론트 앤드 프레임워크가 나옴에 따라 프론트 앤드 개발에도 아키텍팅이 필요하다는 것이다. 프론트 앤드에서는 SPA(Single Page Application 이하, 싱글 페이지 웹 애플리케이션)가 앵귤러 프레임워크 기반으로 많이 소개 되고 있다. 싱글 페이지 웹 애플리케이션 접근 방식은 JSP, PHP같은 서버 템플릿이나 스프링 프레임워크에 익숙한 개발자에게는 생소한 개념이다. 


  싱글 페이지 웹 애플리케이션(SPA)은 "싱글 페이지 웹" 글자만 빼면 그냥 애플리케이션이다. 윈도우에 설치해서 사용하는 .exe 애플리케이션 개발 방식과 하등 차이가 없고 보여지는 기반이 브라우저안이라는 것 밖에 없다. 몇년전 리치 인터넷 애플리케이션(RIA)를 이끌었던 플랙스(Flex)를 떠올려도 좋다. 하지만 지금은 모바일과 웹의 시대이니 HTML/CSS/JavaScript 만으로도 충분히 애플리케이션을 만들 수 있어야 한다. 


  앵귤러는 애플리케이션을 만드는 프레임워크이다. 스프링 MVC 패턴의 개발을 했다면 앵귤러에서도 그렇게 할 수 있다. 스프링 의존성 주입의 편리함을 누리고 있다면 앵귤러에서도 그렇게 할 수 있다. 이제 웹의 세상에 맞는 애플리케이션 개발자로 거듭나 보자. 






목차 


  1장. 싱글 페이지 애플리케이션 개발 준비

    + 1-1 개발 도구 준비

    + 1-2 싱글 페이지 애플리케이션 생성

    + 1-3 애플리케이션 컴포넌트 생성

    + 1-4 애플리케이션 테스트 및 빌드빌드



  2장. AngularJS 프레임웍 이해

    + 2-1 MV* 프레임웍

    + 2-2 양방향 데이터 바인딩

    + 2-3 의존성 주입

    + 2-4 클라이언트 템플릿

    + 2-5 지시자 (Directive)

    + 2-6 테스트 프레임웍

    


  3장. 싱글 페이지 애플리케이션 기획 및 생성

    + 3-1 애플리케이션 기획

    + 3-2 애플리케이션 제너레이터 생성

    + 3-3 싱글 페이지 애플리케이션 생성

    + 3-4 단위 업무를 위한 앵귤러 컴포넌트 조합



  4장. 애플리케이션을 위한 공통 프레임웍 개발

    + 4-1 공통 프레임웍 모듈 개발

    + 4-2 로그인 프론트앤드 개발

    + 4-3 OAuth를 이용한 인증 처리

    


  5장. 메인 페이지 개발 

    + 5-1 백앤드 API 개발

    + 5-2 프론트앤드 화면 레이아웃

    + 5-3 그룹 목록 및 정보 표현 



  6장. 그룹 페이지 개발 

    + 6-1 그룹 정보 페이지

    + 6-2 그룹 활동 페이지 

    + 6-3 설문 카드 생성

    + 6-4 설문 카드 목록에 표현

    + 6-5 설문 응답 및 결과 표현



  7장. 실시간 반응 개발

    + 7-1 Socket.IO 기반 실시간 연동

    + 7-2 카드 표현 고도화

    + 7-3 AngularJS 성능 옵션



 총 7개의 목차 중에서 1, 2장을 출판사의 양해를 얻어 공개토록 한다. 

  

posted by 윤영식
2014. 8. 3. 23:26 AngularJS/Concept

AngularJS를 배우기 위해 좌충우돌 하며 읽고, 보고, 듣고, 코딩해본 코스를 나름 정리해 보았다. 




1. 개념잡기

  - Angular's father인 미스코님의 AngularJS 소개 동영상을 본다 : 단계별로 jQuery와 잘 비교하고 있다

    

  - Art of AngularJS를 보고서 이제 배워야할 필요성을 느껴보세요. 이제 시작하세요. 

    


  - AngularJS의 중요 요소와 기본기를 다져보자 (60분 동강)

    

     + AngularJS 전체 구성 요소 : 각각에 대해 간단히 알아보자

   

      + AngularJS 흐름

   


  - 개발자 가이드에서 Bootstrap, HTML Compiler, Conceptual Overview, Directives 를 꼭 읽는다 (내 포스팅에도 정리했다)

  - AngularJS 홈페이지에서 제공하는 튜토리얼 보기

  - 요약 정리 내역을 다시 보자




2. 프로토타입핑 해보기

  - 전체 flow를 간단히 코딩해 보자

  - egghead.io 에서 모든 동강을 따라해 본다 : 100개 넘는 강좌가 있고 짧은 대신 꼭 코딩해본다 

    

  - AngularJS 팀의 Meetup 동영상들 보기 : Angular 기능별 설명을 해준다 (이건 개당 1시간씩 넘어서 틈틈히 보자)

    + Best Practices :  미스코님이 직접 설명한다.

    

  - AngularJS에서의 Deferred and Promise에 대한 이해

    

  - http://www.yearofmoo.com/ 의 AngularJS 강좌들 코딩

    + Use AngularJS to Power

    + More AngularJS Magic

  - AngularJS Design Patterns 

  - MEAN Stack 사용 예제 : MEAN is MongoDB + Express + AngluarJS + Node.js




3. Angular 생태계

  - Angular-UI : Twitter Bootstrap, Grid 등 지속적으로 AngularJS Directives 를 만들어 내고 있다 

  - angular modules는 http://ngmodules.org/ 먼저 찾아보자

    

  - angular-strap : http://mgcrea.github.io/angular-strap/

  - Learn AngluarJS : Thinkster.io

  - AngularJS-Tips




4. 데이터 연동

  - AngularJS에서 제공하는 $resource를 사용하여 RESTful 하게 개발한다

    + 동영상에서 $resource를 어떻게 사용하는지 익히자 (GitHub Demo 소스)

    

  - RestangularJS를 이용한  RESTful 서버 연동

  - Real-time을 위한 AngularJS와 Socket.io 연동하기 seed 구성




5. 개발환경 구축하기 

  - 개발 소스 코드 저장소는 DVCS인 Git + GitHub 을 사용한다 : Git 을 통하여 성공적인 브랜칭 전략을 세우자

  - Yeoman을 통하여 AngularJS의 스케폴딩 소스를 만들어 개발한다 : AngularJS 개발환경 구성하기

  - Yeoman generator Angular 와 Express.js 연동하기




6. 참여와 공유

  - AngularJS 구글 그룹스 참여

  - 공개 그룹 활동하기

    + Facebook AgularJS 오픈 그룹 

    + Google+ AngularJS 오픈 그룹 

  - JSFiddle을 통하여 예제파일 공유하기

  - StackOverflow 통하여 Q&A 하기

  - AngularJS 트위터 팔로잉

최근 -213.10.11- 으로 AngularJS에 대한 관심이 높아지고 있고, 다양한 블로그와 관련 글이 많이 올라오고 있다. 이중 AngularJS 배우기의 종결자 블로깅을 들어가서 배워보자 



7. AngularJS v1.* 에서 v2.* 으로  
  - AngularJS는 올해 v2.* 버전이 나올 예정이고 큰 변화를 예고 하고 있다. 하지만 이것은 피할 수 없는 대세로 보여진다. 이유는 간단하다. 좀 더 단순하고 좀 더 성능이 뛰어나며 인지 가능한 단순 문법으로 하는 길로 가고 있다. 
  - v1.* 에서 v2.* 로 바뀌니 어쪄냐고 호들갑 떨지 말자. v1.*도 쓰지 않으면서 v2.* 의 변화를 논하지도 말자. 지금 당장 v1.*를 써보고 좀 더 쉽게 웹 애플리케이션을 개발해 보는 경험이 중요하다. v2.*가 성숙할 때 쯤이면 우리는 더 많은 변화 앞에 놓여 있을 것이다. 
  - v2.* 간결함과 Web Components, ECMAScript 6 로의 방향의 핵심은 Componen를 사용한 Composition 패턴의 애플리케이션이라 보며 새롭지도 않으며 어렵지도 않은 방향으로 대중적으로 사용할 수 있게 만들어 가는 능력에 찬사를 보낸다. 땡큐 AngularTeam!!!
  
  



8. Directives $compile 서비스에 대한 이해 

2015 ng vegas에서 발표한 $compile에 대한 구현과정을 상세히 설명하고 있다. 예로 대시보드 빌더를 개발한다고 했을 때 대시보드 화면에 차트를 가져다 놓을 때 차트에 대한 디렉티브가 있다고 하면 동적으로 디렉티브를 컴파일하고 $scope와 링크를 맺우어주어야 한다. 이런 일련의 과정이 어떻제 진행되는지 알 수 있다. 또한 디렉티브에 대한 상호 관계로 상속(Inheritance)와 격리(Isolate)부분도 설명한다.  

- 발표 동영상 

  
  
  발표중에 나온 장표 

  




<참조>

  - 배우는 과정을 잘 요약한 블로깅 : http://blog.whydoifollow.com (강추)

  - AngularJS 내용 요약 (필독)

  - Angular v1.3 변경 사항들

  - 몇가지 참조할 만한 사이트 

    + AngularJS 포스팅 in codingsmackdown.tv

       tiny -> medium -> large project 확장하는 방법 (소스)

    + 페이지 플래그먼트의 라우팅 : http://onehungrymind.com/building-a-website-with-angularjs-routes-and-partials/

    + $apply 사용 이유http://jimhoskins.com/2012/12/17/angularjs-and-apply.html

    + $provide 사용 : http://slides.wesalvaro.com/20121113/#/

    + AngularJS에서 Dependency Injection에 대한 이해

    + 요분 유명하다 와인샐러 샘플 : http://coenraets.org/blog/2012/02/sample-application-with-angular-js

    + 전체 구성요소 설명

    + Directive 통하여 재사용가능 컴포넌트 만들기

    + Lazing Loading in AngularJS

    + Animation in AngularJS

    + AngularJS CheetSheet

  - 처음 개발할 때 8가지 유용한 개발 접근법 ($rootScope, $locationProvider 사용예)



posted by 윤영식
2014. 6. 28. 19:07 Big Data/ElasticSearch

ElasticSearch(이하 ES) 엔진에 수집된 데이터에 대하여 Kibana 도움 없이 직접 Data Visualization 하는 기술 스택을 알아보고, 실데이터를 통한 화면 1본을 만들어 본다. 




ES 시각화를 위한 다양한 방식의 시도들


사전조사-1) 직접 클라이언트 모듈 제작하여 Data Visualization 수행 

  - elasticsearch.js 또는 elatic.js 사용하지 않고 REST 호출 통해 데이터 시각화 수행 

    (Protovis 는 D3.js 기반 Chart를 사용함)

  - FullScale.co에서는 dangle 이라는 시각화 차트를 소개

    (D3.js 기반의 AngularJS Directives 차트)

  - D3.js 기반의 전문적인 차트를 사용하는 방법을 익힐 수 있다. 하지만 제대로 갖춰진 ES 클라이언트 모듈의 필요성 대두


사전조사-2) elasticsearch.js 사용하여 Data Visualization 수행

  - elasticsearch.js와 D3.js, jquery, require.js를 통해서 샘플 데이터를 시각화 수행

    (AngularJS는 사용하지 않고, 전문적인 차트 모듈사용하지 않음)

  - AngularJS 기반으로 Protovis 또는 Dangle 차트를 사용하여 표현해 본다.


사전조사-3) elastic.js 사용하여 Data Visualization 수행

  - elastic.js 홈페이지에서 API를 숙지한다.

  - DSL API를 살펴보자. DSL을 이해하기 위해서는 ES의 Search API에 대한 이해가 필요하다.

  - Query를 작성하고 Filtering 하면 group by having과 유사한 facets (지금은 aggregation 을 사용)하여 검색을 한다.

    Query -> Filter -> Aggregation에 대해 알면 DSL구성을 이해할 수 있다.

  - 자신의 Twitter 데이터를 가지고 elastic.js와 angular.js를 사용하여 트윗 내용을 표현하는 방법 (GitHub 소스)




ES Data Visualization을 위한 나만의 Tech Stack 만들기 


  - ES 클라이언트 모듈 : elastic.js 의 DSL(Domain Specific Language)를 숙지한다. 

    elastic.js는 ElasticSearch의 공식 클라이언트 모듈인 elasticsearch.js 의 DSL 화 모듈로 namespace는 ejs 이다.  

  - 시각화를 위한 D3.js 의 개념 이해 (D3.js 배우기)

  - Kibana에서 사용하고 있는 Frontend MV* Framework인 AngularJS (AngularJS 배우기)

  - AngularJS 생태계 도구인 yeoman 을 통해 개발 시작하기 (generator-fullstack,  Yeoman 사용방법)

  - 물론 Node.js는 기본이다. 


그래서 다음과 같이 정리해 본다. 


  - AngularJS 기반 Frontend 개발

    1) Node.js 기초

    2) Yeoman + generator-angular 기반 

  - D3.js 기반의 Chart 이며 AngularJS 바로 적용가능토록 Directives 화 되어 있는 차트 중 선택사용

    1) Protovis

    2) Dangle

    3) Angular nvd3 charts (추천)

    4) Angular-Charts

    5) Angular Google Charts 

  - elasticsearch.js를 DSL로 만든 elastic.js 사용


그래서 다시 그림으로 정리해본 기술 스택



ES Data Visualization 개발을 위한 구성 stack 그림






== 이제 만들어 봅시다!!! ==


 

환경설정

  - node.js 및 yeoman 설치 : npm install -g generator-angular-fullstack 설치 (generator-angular는 오류발생)

  - twitter bootstrap RWD 메뉴화면 구성 (기본 화면 구성)

  - angular-ui의 angular-bootstrap 설치 :  bower install angular-bootstrap --save

  - elasticsearch.js 설치 : bower install elasticsearch --save

  - elastic.js 설치 : bower install elastic.js --save

  - angular-nvd3-directives chart 설치 : bower install angularjs-nvd3-directives --save



angular layout 구성 

  - 애플리케이션 생성 : GitHub Repository를 만들어서 clone 한 디렉토리에서 수행하였다

$ git clone https://github.com/elasticsearch-kr/es-data-visualization-hackerton 

$ cd es-data-visualization-hackerton 

$ yo angular-fullstack esvisualization

  - main.html 과 scripts/controllers/main.js 를 주로 수정함 

// main.html 안에 angular-nvd3.directives html 태그 및 속성 설정 

    <div class="row">

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

        <div class="panel panel-default">

          <div class="panel-heading">

            <button type="button" ng-click="getImpression()" class="btn btn-default">Impression Histogram</button>

          </div>


          <div class="panel-body">


            <!-- angular-nvd3-directives : multi-bar chart -->

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

              <nvd3-multi-bar-chart

                  data="impressionData"

                  id="dataId"

                  xAxisTickFormat="xAxisTickFormatFunction()"

                  width="550"

                  height="350"

                  showXAxis="true"

                  showYAxis="true">

                    <svg></svg>

              </nvd3-multi-bar-chart>

            </div>


          </div>

        </div>

      </div>

    </div>



// main.js 안에서 elasticsearch.js를 직접 호출하여 사용함 

angular.module('esvisualizationApp')

  .controller('MainCtrl', function ($scope, $http) {


    // x축의 값을 date으로 변환하여 찍어준다 

    $scope.xAxisTickFormatFunction = function(){

      return function(d){

        return d3.time.format('%x')(new Date(d));

      }

    }


    // 화면에서 클릭을 하면 impression index 값을 ES에서 호출하여 

    // ES aggregation json 결과값을 파싱하여 차트 데이터에 맵핑한다

    $scope.getImpression = function() {


      // ES 접속을 위한 클라이언트를 생성한다 

      var client = new elasticsearch.Client({

                                              host: '54.178.125.74:9200',

                                              sniffOnStart: true,

                                              sniffInterval: 60000,

                                            });


// search 조회를 수행한다. POST 방식으로 body에 실 search query를 넣어준다 

      client.search({

          index: 'impression',

          size: 5,

          body: {

            "filter": {

                "and": [

                    {

                      "range": {

                        "time": {

                            "from": "2013-7-1", 

                            "to": "2014-6-30"

                        }

                      }

                    }

                ]

            },

            "aggs": {

              "events": {

                "terms": {

                  "field": "event"   

                },

                "aggs" : {   

                  "time_histogram" : {

                      "date_histogram" : {

                          "field" : "time",

                          "interval" : "1d",   

                          "format" : "yyyy-MM-dd" 

                      }

                  }

                }

              }

            }


            // End query.

          }

      }).then(function (resp) {

         var impressions = resp.aggregations.events.buckets[0].time_histogram.buckets;

         console.log(impressions);


         var fixData = [];

         angular.forEach(impressions, function(impression, idx) {

          fixData[idx] = [impression.key, impression.doc_count];

         });

   

   // 결과에 대해서 promise 패턴으로 받아서 angular-nvd3-directives의 데이터 구조를 만들어 준다

   // {key, values}가 하나의 series가 되고 배열을 가지면 다중 series가 된다. 

         $scope.impressionData = [

            {

              "key": "Series 1",

              "values": fixData

            }

          ];


  // apply 적용을 해주어야 차트에 데이터가 바로 반영된다.

        $scope.$apply();

      });

    }


  });




결과 화면 및 해커톤 소감

  - "Impression Histogram" 버튼을 클릭하면 차트에 표현을 한다.

  - elastic.js의 DSL을 이용하면 다양한 파라미터에 대한 핸들링을 쉽게 할 수 있을 것같다. 

  - AngularJS 생태계와 elasticsearch.js(elastic.js)를 이용하면 Kibana의 도움 없이 자신이 원하는 화면을 쉽게 만들 수 있다. 

  - 관건은 역시 ES에 어떤 데이터를 어떤 형태로 넣느냐가 가장 먼저 고민을 해야하고, 이후 분석 query를 어떻게 짤 것인가 고민이 필요!



* 헤커톤 소스 위치 : https://github.com/elasticsearch-kr/es-data-visualization-hackerton



<참조> 

  - elasticsearch.js 공식 클라이언트 모듈을 DSL로 만든 elastic.js 

  - elastic.js를 사용한 ES Data Visualization을 AngularJS기반으로 개발

  - Protovis 차트를 이용한 facet 데이터 표현

  - ElasticSearch Data Visualization 방법

  - D3.js 와 Angular.js 기반으로 Data Visualization

  - ElasticSearch의 FacetAggregation 수행 : 향후 Facet은 없어지고 Aggregation으로 대체될 것이다.

  - AngularJS Directives Chart 비교

posted by 윤영식
2014. 6. 17. 14:33 AngularJS/Concept

이제 모든 서비스는 모바일 먼저 기획을 하고 웹 서비스로 넘어가야 한다고 생각한다. 즉, Contents -> UX -> Design -> Develop -> Launch로 넘어가면서 그 기본은 모바일이 먼저이다. 기존에 Bootstrap을 사용했을 경우 RWD (Response Web Design)이 적용되어 해상도에 따른 사용자 경험을 데스크탑과 모바일에 준하게 줄 수 있었지만 네이티브 앱과 유사한 UX 경험을 제공하기에는 부족하다. HTML5 기반으로 개발을 한다면 RWD 외에 Mobile First 전략을 가지고 있는 하이브리드 웹앱 Framework을 살펴 볼 필요가 있다. 살펴볼 두가지의-ionic, onsenui - 하이브리드 앱(모바일 웹앱) 프레임워크는 PhoneGap + Angular.js 와 결합되는 형태를 취하고 있다. mobile angular ui는 PhoneGap과 연결시키는 별도의 작업을 수반한다. 




                       




Ionic 하이브리드 웹앱 프레임워크

  - Ionic 홈페이지

  - PhoneGap에 최적화 되어 프로젝트를 생성해 준다.

  - https://github.com/driftyco/ionic-starter-sidemenu 처럼 ionic-starter-xx의 sidemenu, tabs, blank 3가지 타입을 제고하고 있다

// 설치 

$ sudo npm install -g cordova 

/usr/local/bin/cordova -> /usr/local/lib/node_modules/cordova/bin/cordova

cordova@3.5.0-0.2.4 /usr/local/lib/node_modules/cordova


$ sudo npm install -g ionic

/usr/local/bin/ionic -> /usr/local/lib/node_modules/ionic/bin/ionic

ionic@1.0.14 /usr/local/lib/node_modules/ionic


// 프로젝트 생성

$ ionic start myAppSideMenu sidemenu

Running start task...

Creating Ionic app in folder /Users/prototyping/myAppSideMenu based on sidemenu project

DOWNLOADING: https://github.com/driftyco/ionic-app-base/archive/master.zip

DOWNLOADING: https://github.com/driftyco/ionic-starter-sidemenu/archive/master.zip

Initializing cordova project.

Fetching plugin "org.apache.cordova.device" via plugin registry

Fetching plugin "org.apache.cordova.console" via plugin registry

Fetching plugin "https://github.com/driftyco/ionic-plugins-keyboard" via git clone


$ cd myAppSlideMenu


  - ionic 수행

$ ionic platform add android

Running platform task...

Adding platform android

.. 생략 ..

Project successfully created.

Installing "com.ionic.keyboard" for android

Installing "org.apache.cordova.console" for android

Installing "org.apache.cordova.device" for android


$ ionic build android

Running build task...

Building platform android

Running command: /Users/prototyping/myAppSideMenu/platforms/android/cordova/build

Buildfile: /Users/prototyping/myAppSideMenu/platforms/android/build.xml

... 중략 ...

-post-build:

     [move] Moving 1 file to /Usersprototyping/myAppSideMenu/platforms/android/ant-build

     [move] Moving 1 file to /Users/prototyping/myAppSideMenu/platforms/android/CordovaLib/ant-build

debug:

BUILD SUCCESSFUL

Total time: 48 seconds


// Android의 AVD를 먼저 실행해야 한다 

$ ionic emulate android

Running emulate task...

Emulating app on platform android

Running command: /Users/nulpulum/development/studygps_company/prototyping/myAppSideMenu/platforms/android/cordova/run --emulator

... 중략 ...

BUILD SUCCESSFUL

Total time: 9 seconds

Installing app on emulator...

Using apk: /Users/prototyping/myAppSideMenu/platforms/android/ant-build/HelloCordova-debug-unaligned.apk

Launching application...

LAUNCH SUCCESS


-- AVD 통해 본 모습

  

* 안드로이드 AVD 관리하기 (참조)


 - ionic 분석

 - ionic 명령을 통하여 ionic 프레임워크와의 통합 빌드작업을 자동 수행해 준다

// 완벽하게 angular.js 기반으로 운영된다 (angular.js v1.2.12 버전 번들링)

// www/lib/ionic/js/ionic.bundle.js 


// index.html 

<!DOCTYPE html>

<html>

  <head>

    <meta charset="utf-8">

    <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">

    <title></title>


    <link href="lib/ionic/css/ionic.css" rel="stylesheet">

    <link href="css/style.css" rel="stylesheet">


    <!-- IF using Sass (run gulp sass first), then uncomment below and remove the CSS includes above

    <link href="css/ionic.app.css" rel="stylesheet">

    -->


    <!-- ionic/angularjs js -->

    <script src="lib/ionic/js/ionic.bundle.js"></script>


    <!-- cordova script (this will be a 404 during development) -->

    <script src="cordova.js"></script>


    <!-- your app's js -->

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

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

  </head>


  <body ng-app="starter">

    <ion-nav-view></ion-nav-view>

  </body>

</html>

  - ionic components



Onsen UI 프레임워크 

  - PhoneGap을 설치와 Onsen ui 설치는 별개로 진행된다. 따라서 ionic과 같이 PhoneGap에 대한 기본 지식이 필요하다  

  - Onsen ui는 angular.js directive를 제공하는데 주력한다

  - PhoneGap 환경 설정 

// PhoneGap

$ npm install -g cordova

$ cordova create MyAppOnsenui && cd MyAppOnsenui

Creating a new cordova project with name "HelloCordova" and id "io.cordova.hellocordova" at location "/Users/prototyping/MyAppOnsenui"

Downloading cordova library for www...

Download complete


$ cordova platform add android

Creating android project...

Creating Cordova project for the Android platform:

Path: platforms/android

Package: io.cordova.hellocordova

Name: HelloCordova

Android target: android-19

Copying template files...

Running: android update project --subprojects --path "platforms/android" --target android-19 --library "CordovaLib"

Resolved location of library project to: /Users/prototyping/MyAppOnsenui/platforms/android/CordovaLib

Updated and renamed default.properties to project.properties

..중략..

Project successfully created.


-- build와 emulate 전에 하기의 Onsen ui 설치와 설정을 먼저 한 후에 수행한다 


$ cordova build android

.. 중략 ..

BUILD SUCCESSFUL

Total time: 32 seconds


-- android의 AVD를 먼저 실행해야 한다 

$ cordova emulate android


  - Onsenui components : 가장 기본적인 것들을 제공함 

  - Phonegap과 Angular.js 의 결합을 개발자가 직접해야함 

  - 활발한 Community feedback이 없는 상태이다 (참조)

  - Onsenui설치

// Onsen ui

$ cd MyAppOnsenui


$ bower init  명령으로 bower.json 파일을 만들거나 기존의 generator-angular-fullstack의 것을 사용한다

bower install onsenui --save

{

  "name": "mobiconsoft-mobile-app",

  "version": "0.1.0",

  "dependencies": {

    "angular": ">=1.2.*",

    "json3": "~3.3.1",

    "es5-shim": "~3.2.0",

    "angular-resource": ">=1.2.*",

    "angular-cookies": ">=1.2.*",

    "angular-sanitize": ">=1.2.*",

    "angular-ui-router": "~0.2.10",

    "restangular": "~1.4.0",

    "jquery": "1.11.1",

    "modernizr": "~2.8.1",

    "console-shim": "*",

    "jStorage": "~0.4.8",

    "angular-gettext": "~0.2.9",

    "ladda-studygps": "~0.9.4",

    "angular-ladda-studygps": "~0.1.7",

    "onsenui": "~1.0.4"

  },

  "devDependencies": {

    "angular-mocks": ">=1.2.*",

    "angular-scenario": ">=1.2.*"

  },

  "testPath": "test/client/spec"

}


-- http://onsenui.io/OnsenUI/project_templates/sliding_menu_navigator.zip 에서 샘플 파일을 다운로드 받아서 app 폴더에 있는 것을 www 폴더로 복사한다. 그리고 index.html안에 cordova.js를 넣는다. (개발시에는 404 NOT Found 발생함)


cordova build android

cordova emulate android

-- AVD로 띄워본 결과

 


  - Sliding Menu에 있어서는 Ionic 보다는 Onsenui가 좀 더 세련되어 보임 



Mobile Angular UI

  - Slide Menu 와 기본적인 모바일 화면 구성 컴포넌트를 angular.js 기반으로 Directive화 해놓았다. 

  - 친숙한 Bootstrap 기반의 UI를 제공한다. 그러나 Twitter bootstrap의 RWD기반은 아니다



ngCordova

    

  - cordova에서 제공하는 API를 angular 에서 사용할 수 있도록 Angular Service로 만들어 놓았음



좀 더 잘 정비된 것을 쓰고 싶다면 Ionic을 기초 기술부터 체크하면 갈 것이라면 Onsenui를 추천한다. 또한 좀 더 세련되 UI는 Onsenui에 점 수를 더 주고 싶다. 그리고 서로의 아키텍쳐에 대한 부분과 커스터마이징 여부는 좀 더 들여다 보아야 할 것으로 보인다. 각각의 상황과 특성에 따라 선택하여 사용하면 될 것으로 보인다. 



<참조>

  - Ionic 시작하기 

  - onsenui 시작하기

  - onsenui sliding menu zip 

sliding_menu_navigator.zip


  - mobile angular ui

  - iSO 8 UI Framework 7

  - ngCordova

  - 용어 정의 

    

posted by 윤영식
2014. 6. 10. 16:34 My Services/PlayHub

지인이 프론트앤드 개발자를 구인하고 있어서 소개드립니다. 자세한 사항은 하기 내용 참조하세요




로앤컴퍼니와 함께 대한민국 법률시장의 대중화와 선진화를 만들어 갈 동료를 찾습니다. 함께 갑시다!


*채용안내: http://lawcompany.co.kr/jobs

*소개영상: http://youtu.be/5br8hf1ukRQ (by 무한도전 정준하)


<이런 분들과 함께 하고 싶습니다!>

- 법률과 IT의 융합을 통해 모든 국민에게 기여하는 서비스를 만들고 싶은 당신
- 미개척된 새로운 시장이 자신의 노력으로 변화하는 것을 경험하고 싶은 당신
- 서로를 자극하는 역량있는 사람들과 함께 재밌고 열정적으로 일을 하고 싶은 당신


<채용절차>

- 실력과 잠재력 중심 선발 (나이, 성별, 학력 무관)
- 이력서, 자기소개서, 포트폴리오 제출 (형태 제한 없음)
recruit@lawcompany.co.kr로 접수 후 1주일 이내에 회신해 드립니다.
- 2번의 면접을 통해 실력, 잠재력, 커뮤니케이션 능력, 팀과의 조화 여부 등을 확인합니다.


<참고사항>

- 출근은 오전 10시 (서초역 1분, 교대역 3분 거리)
- 당연하게도 월급은 섭섭지 않게, 제 때 드립니다.
- 서울에 연고가 없는 경우, 거주비 일부를 지원합니다.
- 고기를 자주 먹습니다. 야식은 더 자주 먹습니다.
- 치과 상담, 스케일링 치료 무료
- 가로수길 M모 카페, 모든 음료 하루 한잔 무료
- 변호사와 법률 상담 무료 및 법률 문제 해결 적극 지원





posted by 윤영식
2014. 6. 4. 00:20 AngularJS/Concept

Angular.js 기반 개발시 시각화를 위하여 구글 차트를 이용할 경우 Directive화 방법




구글

  - Don't invent wheel, already exist

  - 하지만 자신의 입맛에 맞게 수정은 해야 한다. 

  - 수정하려면 GitHub에서 Fork한 후에 수정을 하자

    https://github.com/mobiconsoft/angular-google-chart-resize



구글 차트 API 사용법

  - 구글 차트 사용하기 

  - 구글 Visualization 라이브러리위에 구글 Chart가 올라가는 형태이다 (참조)

  - visualization의 버전 1.0 을 명시하고 사용할 차트 패키지를 설정한다. 보통 corechart 로 하면 bar, line, pie등을 사용할 수 있다

google.load('visualization', '1.0', {'packages':[<<list-of-google-chart-libraries>>]})


  - 데이터 생성은 2차원의 google.visualization.DataTable을 이용한다 

  - https://google-developers.appspot.com/chart/interactive/docs/datatables_dataviews

  - anuglar의 controller에서 하기와 같이 json형태로 정의 가능. 또는 addColumn() 호출 설정방식중 택일

  - DataTable을 통해 .csv 파일 생성가능 

$scope.chartObject = {};

$scope.onions = [

    {v: "Onions"},

    {v: 3},

];

// 데이터 시각화를 위하여 가장 큰 값은 첫번째에, 그 다음 큰값은 맨 마지막에 오게 한다 

$scope.chartObject.data = {"cols": [

    {id: "t", label: "Topping", type: "string"},

    {id: "s", label: "Slices", type: "number"}

], "rows": [

    {c: [

        {v: "Olives"},

        {v: 31}

    ]},

    {c: $scope.onions},

    {c: [

        {v: "Zucchini"},

        {v: 1},

    ]},

    {c: [

        {v: "Pepperoni"},

        {v: 2},

    ]},

    {c: [

        {v: "머쉬롬"},

        {v: 10},

    ]},

]};


$scope.chartObject.options = {

    'title'  : 'How Much Pizza I Ate Last Night',

    'height' : 500,

    'colors' : ['#e0440e', '#e6693e', '#ec8f6e', '#f3b49f', '#f6c7b6']

}



  - 차트 커스터마이징을 위하여 옵션을 준다

  - 높이는 px(pixel) 단위이다.

  - 차트의 width + height 주는 방법은 html에 주거나, option 으로 주는 두가지 방법이 있다. (참조)

var options = {

  'legend':'left',

  'title':'My Big Pie Chart',

  'is3D':true,

  'width':400,

  'height':300

}


  - ChartWrapper를 이용하면 차트 종류, 데이터, 옵션, 그려지는 위치등의 설정을 한번에 할 수 있다 (ChartWrapp API)

var wrapper = new google.visualization.ChartWrapper({

  chartType: 'ColumnChart',

  dataTable: [['', 'Germany', 'USA', 'Brazil', 'Canada', 'France', 'RU'],

              ['', 700, 300, 400, 500, 600, 800]],

  options: {'title': 'Countries'},

  containerId: 'vis_div'

});

wrapper.draw();


  - Chart의 interaction은 ready, select, error, onmouseover/onmouseout 이 존재한다 (참조)

  - addListener를 이용하여 event handler를 등록한다 

function selectHandler() {

  var selectedItem = chart.getSelection()[0];

  if (selectedItem) {

    var topping = data.getValue(selectedItem.row, 0);

    alert('The user selected ' + topping);

  }

}


google.visualization.events.addListener(chart, 'select', selectHandler);  


  - Formatters 에는 arrow, bar, color, date, number 등이 있고, 주로 table에서 사용된다. 단, color는 차트에서도 사용됨 (참조)

  - Advanced Chart 기능을 쓰고 싶을 경우 (참조)

  - 웹툴상에서 차트의 API 테스트 하는 도구 



기존 소스 이해

  - google-chart에 대한 directive 소스에 resize 넣음 

/**

 * @description Google Chart Api Directive Module for AngularJS

 * @version 0.0.9

 * @author Nicolas Bouillon <nicolas@bouil.org>

 * @author GitHub contributors

 * @moidifier Peter Yun <nulpulum@gmail.com>

 * @license MIT

 * @year 2013

 */

(function (document, window, angular) {

    'use strict';


    angular.module('googlechart', [])


        .constant('googleChartApiConfig', {

            version: '1.0',

            optionalSettings: {

                packages: ['corechart']

            }

        })


        /**

         * 인코딩 주의)

         * index.html 안에 UTF-8 설정

         * <meta http-equiv="content-type" content="text/html; charset=UTF-8">

         *

         * 사용예)

         * <script type="text/javascript" src="https://www.google.com/jsapi"></script>

         * <script type="text/javascript">

         *   google.load('visualization', '1.0', {'packages':['corechart']});

         *   google.setOnLoadCallback(drawChart);

         *   function drawChart() { ... }

         * </script>

         *

         * @변경 : jsapi.js 파일을 로컬에서 읽어오도록 수정할 수 있다

         * googleJsapiUrlProvider.setProtocol(undiefined);

         * googleJsapiUrlProvider.setUrl('jsapi.js');

         *

         * @참조 : https://google-developers.appspot.com/chart/interactive/docs/quick_start

         */

        .provider('googleJsapiUrl', function () {

            var protocol = 'https:';

            var url = '//www.google.com/jsapi';


            this.setProtocol = function(newProtocol) {

                protocol = newProtocol;

            };


            this.setUrl = function(newUrl) {

                url = newUrl;

            };


            this.$get = function() {

                return (protocol ? protocol : '') + url;

            };

        })


        /**

         * google.load('visualization', '1.0', {'packages':['corechart']}); 수행하는 팩토리

         *

         * @param  {[type]} $rootScope     [description]

         * @param  {[type]} $q             [description]

         * @param  {[type]} apiConfig      [description]

         * @param  {[type]} googleJsapiUrl [description]

         * @return {[type]}                [description]

         */

        .factory('googleChartApiPromise', ['$rootScope', '$q', 'googleChartApiConfig', 'googleJsapiUrl', function ($rootScope, $q, apiConfig, googleJsapiUrl) {

            var apiReady = $q.defer();

            var onLoad = function () {

                // override callback function

                var settings = {

                    callback: function () {

                        var oldCb = apiConfig.optionalSettings.callback;

                        $rootScope.$apply(function () {

                            apiReady.resolve();

                        });


                        if (angular.isFunction(oldCb)) {

                            oldCb.call(this);

                        }

                    }

                };


                settings = angular.extend({}, apiConfig.optionalSettings, settings);


                window.google.load('visualization', apiConfig.version, settings);

            };

            var head = document.getElementsByTagName('head')[0];

            var script = document.createElement('script');


            script.setAttribute('type', 'text/javascript');

            script.src = googleJsapiUrl;


            if (script.addEventListener) { // Standard browsers (including IE9+)

                //console.log('>>> 1. load jsapi...');

                script.addEventListener('load', onLoad, false);

            } else { // IE8 and below

                //console.log('>>> 2. load jsapi...');

                script.onreadystatechange = function () {

                    if (script.readyState === 'loaded' || script.readyState === 'complete') {

                        script.onreadystatechange = null;

                        onLoad();

                    }

                };

            }


            head.appendChild(script);


            return apiReady.promise;

        }])


        /**

         * Element 또는 Attribute로 사용할 수 있다. Element 사용시에는 IE8+에 대한 고려가 있어야 함

         *

         * 사용예)

         * <div google-chart chart="chartObject" style="height:300px; width:100%;"></div>

         *

         * @param  {[type]} $timeout              [description]

         * @param  {[type]} $window               [description]

         * @param  {[type]} $rootScope            [description]

         * @param  {[type]} googleChartApiPromise [description]

         * @return {[type]}                       [description]

         */

        .directive('googleChart', ['$timeout', '$window', '$rootScope', 'googleChartApiPromise', function ($timeout, $window, $rootScope, googleChartApiPromise) {

            return {

                restrict: 'EA',

                scope: {

                    chartOrig: '=chart',

                    onReady: '&',

                    select: '&'

                },

                link: function ($scope, $elm, attrs) {

                    // Watches, to refresh the chart when its data, title or dimensions change

                    $scope.$watch('chartOrig', function () {

$scope.chart = angular.copy($scope.chartOrig);

                        drawAsync();

                    }, true); // true is for deep object equality checking


                    // Redraw the chart if the window is resized

                    $rootScope.$on('resizeMsg', function () {

                        $timeout(function () {

                            // Not always defined yet in IE so check

                            if($scope.chartWrapper) {

                                drawAsync();

                            }

                        });

                    });


                    // Keeps old formatter configuration to compare against

                    $scope.oldChartFormatters = {};


                    function applyFormat(formatType, formatClass, dataTable) {

                        var i, cIdx;


                        if (typeof($scope.chart.formatters[formatType]) != 'undefined') {

                            if (!angular.equals($scope.chart.formatters[formatType], $scope.oldChartFormatters[formatType])) {

                                $scope.oldChartFormatters[formatType] = $scope.chart.formatters[formatType];

                                $scope.formatters[formatType] = [];


                                if (formatType === 'color') {

                                    for (cIdx = 0; cIdx < $scope.chart.formatters[formatType].length; cIdx++) {

                                        var colorFormat = new formatClass();


                                        for (i = 0; i < $scope.chart.formatters[formatType][cIdx].formats.length; i++) {

                                            var data = $scope.chart.formatters[formatType][cIdx].formats[i];


                                            if (typeof(data.fromBgColor) != 'undefined' && typeof(data.toBgColor) != 'undefined')

                                                colorFormat.addGradientRange(data.from, data.to, data.color, data.fromBgColor, data.toBgColor);

                                            else

                                                colorFormat.addRange(data.from, data.to, data.color, data.bgcolor);

                                        }


                                        $scope.formatters[formatType].push(colorFormat)

                                    }

                                } else {


                                    for (i = 0; i < $scope.chart.formatters[formatType].length; i++) {

                                        $scope.formatters[formatType].push(new formatClass(

                                            $scope.chart.formatters[formatType][i])

                                        );

                                    }

                                }

                            }


                            //apply formats to dataTable

                            for (i = 0; i < $scope.formatters[formatType].length; i++) {

                                if ($scope.chart.formatters[formatType][i].columnNum < dataTable.getNumberOfColumns())

                                    $scope.formatters[formatType][i].format(dataTable, $scope.chart.formatters[formatType][i].columnNum);

                            }


                            //Many formatters require HTML tags to display special formatting

                            if (formatType === 'arrow' || formatType === 'bar' || formatType === 'color')

                                $scope.chart.options.allowHtml = true;

                        }

                    }


                    function draw() {

                        // 그리고 있는중에는 다시 그리기 안함

                        if (!draw.triggered && ($scope.chart != undefined)) {

                            draw.triggered = true;

                            // ref: https://docs.angularjs.org/api/ng/service/$timeout

                            $timeout(function () {

                                if (typeof($scope.formatters) === 'undefined')

                                    $scope.formatters = {};


                                var dataTable;

                                if ($scope.chart.data instanceof google.visualization.DataTable)

                                    dataTable = $scope.chart.data;

                                else if (angular.isArray($scope.chart.data))

                                    dataTable = google.visualization.arrayToDataTable($scope.chart.data);

                                else

                                    dataTable = new google.visualization.DataTable($scope.chart.data, 0.5);


                                if (typeof($scope.chart.formatters) != 'undefined') {

                                    applyFormat("number", google.visualization.NumberFormat, dataTable);

                                    applyFormat("arrow", google.visualization.ArrowFormat, dataTable);

                                    applyFormat("date", google.visualization.DateFormat, dataTable);

                                    applyFormat("bar", google.visualization.BarFormat, dataTable);

                                    applyFormat("color", google.visualization.ColorFormat, dataTable);

                                }


                                var customFormatters = $scope.chart.customFormatters;

                                if (typeof(customFormatters) != 'undefined') {

                                    for (var name in customFormatters) {

                                        applyFormat(name, customFormatters[name], dataTable);

                                    }

                                }

// resize > 0 이상이면 적용됨  

// by peter yun

 // <div style="height:100%" id="gc"

 // <div google-chart chart="chartObject" resize="#gc" style="width:100%;"></div>

 // </div>

 var height = angular.element(attrs.resize).height();

       if(height) {

$scope.chart.options.height = height;

       }


                                var chartWrapperArgs = {

                                    chartType: $scope.chart.type,

                                    dataTable: dataTable,

                                    view: $scope.chart.view,

                                    options: $scope.chart.options,

                                    containerId: $elm[0]

                                };


                                $scope.chartWrapper = new google.visualization.ChartWrapper(chartWrapperArgs);

                                google.visualization.events.addListener($scope.chartWrapper, 'ready', function () {

                                    $scope.chart.displayed = true;

                                    $scope.$apply(function (scope) {

                                        scope.onReady({chartWrapper: scope.chartWrapper});

                                    });

                                });

                                google.visualization.events.addListener($scope.chartWrapper, 'error', function (err) {

                                    console.log("Chart not displayed due to error: " + err.message + ". Full error object follows.");

                                    console.log(err);

                                });

                                google.visualization.events.addListener($scope.chartWrapper, 'select', function () {

                                    var selectedItem = $scope.chartWrapper.getChart().getSelection()[0];

                                    $scope.$apply(function () {

                                        $scope.select({selectedItem: selectedItem});

                                    });

                                });



                                $timeout(function () {

                                    $elm.empty();

                                    $scope.chartWrapper.draw();

draw.triggered = false;

                                });

                            }, 0, true);

                        }

                    }


                    /**

                     * jsapi의 load가 끝나면 draw하도록 promise를 걸어 놓은 것

                     *

                     * @return {[type]} [description]

                     */

                    function drawAsync() {

                        googleChartApiPromise.then(function () {

                            draw();

                        })

                    }

                }

            };

        }])


        /**

         * window즉 브라우져의 사이즈가 변경되면 google chart의 size를 변경토록 한다. 단 html설정에서 다음과 같이 되어야 한다

         * <div google-chart chart="chartObject" style="height:300px; width:100%;"></div>

         * height는 %가 아니라 자신이 속한 parent dive의 height를 따르도록 해주어야 한다

         *

         * @param  {[type]} $rootScope [description]

         * @param  {[type]} $window    [description]

         * @return {[type]}            [description]

         */

        .run(['$rootScope', '$window', function ($rootScope, $window) {

            angular.element($window).bind('resize', function () {

                $rootScope.$emit('resizeMsg');

            });

        }]);


})(document, window, window.angular);



<참조>

  - 구글 차트 Directive

  - 구글 차트 Directive 만들기 예제

  - 웹툴상에서 차트의 API 테스트 하는 도구 

  - Directive 수정 소스 

posted by 윤영식
2014. 6. 3. 16:25 카테고리 없음

Mobicon은 PC웹 환경을 Mobile로 옮기고, 기존 하드웨어와 Mobile을 접목한다는 Mobile + Convergence의 합성어 입니다. 3 Driven 기술을 기반으로 다양한 모바일 서비스를 런칭할 예정입니다. 개발 기술 스택은 MEAN Stack (MongoDB, Express.js, Angular.js, Node.js)를 사용하며, 데이터 분석 -사용자 분석 -을 통해 서비스 측정/개선을 수행하며 보안에 힘쏟을 것입니다. 


린 스타트업에서 MEAN Stack을 기반으로 개발하고, 3-Driven 에 대해 이야기를 함께 나누고 싶으시면 언제든 연락 주세요. 

Email : nulpulum@gmail.com





Directive Driven 

  - idea -> build -> product 영역

  - Angular.js를 기반으로 웹앱 개발 생산성 증대 

  - 모듈화를 통한 재배포 및 재사용성 증대 

  - Phonegap + SPA(Single Page Application) 의 하이브리드 웹앱 개발 생산성 증대 



Data Driven

  - measure -> data -> learn 영역

  - 초기 기술 스타트업이 간과하는 사용자 분석 기반 마련 

  - 사용자 분석을 위하여 ElasticSearchMongoDB, RedisMariaDB 등 저장소 활용



Analytics Driven

  - measure -> data -> learn 영역

  - Growth Hacking을 위한 방향을 찾고자 합니다. 

  - 개발후 분석을 통해 회사의 문제점을 찾고자 합니다. 

posted by 윤영식
2014. 6. 3. 15:23 AngularJS/Start MEAN Stack

Angular Team에서 End-To-End 테스트를 위하여 별도로 개발한 프레임워크가 Protractor 이다. protractor는 WebDriverJs기반으로 만들어 졌다. 





Protractor 설정

  - Chrome 최신 버전 사용

  - protractor 설치 및 Chrome에서 selenium standalone server로 구동하기 위한 driver도 설치한다 

$ npm install -g protractor

$ webdriver-manager update 


  - selenium server를 기동하는 방법 : 4444 port로 listen한다 ( 호출 :  http://localhost:4444/wd/hub )

$ webdriver-manager start

seleniumProcess.pid: 55938

6월 02, 2014 5:08:16 오후 org.openqa.grid.selenium.GridLauncher main

정보: Launching a standalone server

Setting system property webdriver.chrome.driver to /usr/local/lib/node_modules/protractor/selenium/chromedriver

17:08:16.913 INFO - Java: Oracle Corporation 24.51-b03

17:08:16.915 INFO - OS: Mac OS X 10.9.3 x86_64

17:08:16.935 INFO - v2.41.0, with Core v2.41.0. Built from revision 3192d8a

17:08:17.079 INFO - Default driver org.openqa.selenium.ie.InternetExplorerDriver registration is skipped: registration capabilities Capabilities [{platform=WINDOWS, ensureCleanSession=true, browserName=internet explorer, version=}] does not match with current platform: MAC

17:08:17.145 INFO - RemoteWebDriver instances should connect to: http://127.0.0.1:4444/wd/hub

17:08:17.146 INFO - Version Jetty/5.1.x

17:08:17.148 INFO - Started HttpContext[/selenium-server/driver,/selenium-server/driver]

17:08:17.149 INFO - Started HttpContext[/selenium-server,/selenium-server]

17:08:17.149 INFO - Started HttpContext[/,/]

17:08:17.510 INFO - Started org.openqa.jetty.jetty.servlet.ServletHandler@4b8e282c

17:08:17.510 INFO - Started HttpContext[/wd,/wd]

17:08:17.517 INFO - Started SocketListener on 0.0.0.0:4444

17:08:17.517 INFO - Started org.openqa.jetty.jetty.Server@3e578b06


  - 잘 동작하지 않는다면 Path에 JAVA_HOME을 설정한다. 



Protractor 환경 설정

  - 루트에 protractor-e2e.conf.js 파일 생성

  - 설정 : baseUrl은 grunt serve 시에 기동되는 port 번호

exports.config = {

  seleniumAddress: 'http://localhost:4444/wd/hub',

  capabilities: {'browserName': 'chrome'},

  specs: ['test/client/e2e/**/*.js'],

  baseUrl: 'http://localhost:9000',

  jasmineNodeOpts: {

    onComplete: null,

    isVerbose: true,

    showColors: true,

    includeStackTrace: true,

    defaultTimeoutInterval: 10000

  },

  framework: 'jasmine'

};

  - 체크

$ webdriver-manager start 

$ protractor protractor-e2e.conf.js

Using the selenium server at http://localhost:4444/wd/hub

[launcher] Running 1 instances of WebDriver


Finished in 0.001 seconds

0 tests, 0 assertions, 0 failures


[launcher] chrome passed

  - grunt를 이용하여 테스트 할 수 있도록 Gruntfile.js 에 설정. run: {..} 안의 options에 configFile 설정이 중요함 

$ npm install grunt-protractor-runner --save-dev


// grunt.initConfig 안에 설정

  protractor: {

      options: {

        keepAlive: true

      },

      run: {

        options: {

          configFile: "protractor-e2e.conf.js", // Target-specific config file

          args: {} // Target-specific arguments

        }

      }

  },


// task 설정

  grunt.registerTask('test', function(target) {

    if (target === 'server') {

      return grunt.task.run([

        'env:test',

        'mochaTest'

      ]);

    }


    else if (target === 'client') {

      return grunt.task.run([

        'concurrent:test',

        'autoprefixer',

        'karma',

        'protractor'

      ]);

    }


    else grunt.task.run([

      'test:server',

      'test:client'

    ]);

  });


// grunt test 에 첨부 

$ grunt test 또는 grunt test:client 수행 (단, 수행전에 webdriver-manager start 수행해 놓은 상태이어야함)


Running "protractor:run" (protractor) task

Using the selenium server at http://localhost:4444/wd/hub

[launcher] Running 1 instances of WebDriver


Finished in 0 seconds

0 tests, 0 assertions, 0 failures

[launcher] chrome passed

Done, without errors.


Execution Time (2014-06-02 15:43:51 UTC)

protractor:run  2.2s  ▇▇ 100%

Total 2.3s

  - 또는, grunt 설정을 하지 않았을 경우는 다음과 같이도 테스트 가능하다. 하지만 가급적 grunt를 이용하자.  

$ webdriver-manager start

$ protractor protractor-e2e.conf.js



End-to-End 테스트 코드 작성방식

  - 테스트 코드를 작성하기 위한 몇가지 개념을 익힌다. (위키)

> browser : webdriver를 감싼 객체. navigation 과 page information 을 갖음

> element : html element를 찾고 상호작용을 위한 helper method 

> by : element locator collection

> protractor : webdriver namespace를 wrapping한 protractor namespace


  - 샘플 코드 

    + browser.get : page loading 

    + element : page에서 element를 찾아줌 

    + by 종류 (소스 참조)

  • by.binding searches for elements by matching binding names, either from ng-bind or {{}} notation in the template.
  • by.model searches for elements by input ng-model.
  • by.repeater searches for ng-repeat elements. For example, by.repeater('phone in phones').row(11).column('price')returns the element in the 12th row (0-based) of the ng-repeat = "phone in phones" repeater with the binding matching{{phone.price}}.

describe('angularjs homepage', function() { it('should greet the named user', function() { // Load the AngularJS homepage. browser.get('http://www.angularjs.org'); // Find the element with ng-model matching 'yourName' - this will // find the <input type="text" ng-model="yourName"/> element - and then // type 'Julie' into it. element(by.model('yourName')).sendKeys('Julie'); // Find the element with binding matching 'yourName' - this will // find the <h1>Hello {{yourName}}!</h1> element. var greeting = element(by.binding('yourName')); // Assert that the text element has the expected value. // Protractor patches 'expect' to understand promises. expect(greeting.getText()).toEqual('Hello Julie!'); }); });


실제 End-to-End 테스트 코드

  - 로그인의 경우 e2e 코드 작성예 (필수참조)

'use strict';


var LoginPage = function() {

this.email = element(by.model('user.email'));

this.password = element(by.model('user.password'));


this.setEmail = function(email) {

this.email.sendKeys(email);

};


this.setPassword=  function(password) {

this.password.sendKeys(password);

};


this.url = function() {

browser.get('/#/signin');

};


this.login = function() {

return element(by.buttonText('button')).click();

};

};


describe('Login Page', function() {

it('should successfully login', function(){

var loginPage = new LoginPage();

loginPage.url();

loginPage.setEmail('test@test.com');

loginPage.setPassword('test');

loginPage.login().then(function(response){

console.log(response);

}, function(error){


})

});

});


  - 수행 : grunt test:client 

    + 수행 에러 발생함 : to be continue...



<참조>

  - Protractor 테스트 방법

  - Protractor Test PPT (필독)

  - Unit, E2E ToDo 예제

  - Protractor Grunt 설정하기

  - End-To-End Test 하기 예제

  - grunt-protractor-runner 

  - Angular Test Pattern

posted by 윤영식
2014. 1. 29. 23:56 AngularJS/Start MEAN Stack

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



전체 구조 이해하기 

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


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

  - 메뉴 html에 링크 설정하기 

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


// index.html 기존 설정

<body ng-app="meanstackApp">


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

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


</body>



1. 메뉴관련 html 파일 분리

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

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

<!-- menu -->

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

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


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

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

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



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

// index.html

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


// menu.html 

  <div class="container">

    <div class="navbar-header">

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

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

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

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

      </button>

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

    </div>

    <div class="collapse navbar-collapse">

      <ul class="nav navbar-nav">

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

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

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

      </ul>

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

  </div>

  

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

  - 데스크톱 해상도

  - 모바일 해상도


2. 메뉴의 링크 설정하기 

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

  

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

  - Tech 글 : MEAN Stack 관련 메뉴

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


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

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


// menu.html 변경 내용

<div class="container">

  <div class="navbar-header">

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

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

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

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

    </button>

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

  </div>

  <div class="collapse navbar-collapse">

    <ul class="nav navbar-nav">

      

      <li class="dropdown">

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

        <ul class="dropdown-menu">

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

          <li>

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

          </li>

          <li>

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

          </li>

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

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

          <li>

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

          </li>

        </ul>

      </li>


      <li class="dropdown">

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

        <ul class="dropdown-menu">

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

          <li>

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

          </li>


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

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

          <li>

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

          </li>

          <li>

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

          </li>


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

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

          <li>

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

          </li>


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

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

          <li>

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

          </li>

        </ul>

      </li>


      <li class="dropdown">

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

        <ul class="dropdown-menu">

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

          <li>

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

          </li>

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

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

          <li>

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

          </li>

        </ul>

      </li>


    </ul>

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

</div>


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



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

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

  

  - 서버에 HTML 파일 요청하기 

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

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


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

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

<div class="row">

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

    <h1>mylevel.html</h1>

  </div>

</div>


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

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

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

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

// 기존 설정

<li>

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

</li>


// 변경 설정 

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

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

</li>

  

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

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

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

'use strict';


angular.module('meanstackApp', [

  'ngCookies',

  'ngResource',

  'ngSanitize',

  'ngRoute',

  'ui.bootstrap',

  'ui.router'

])

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


    $urlRouterProvider.otherwise("/");


    $stateProvider

      .state('main', {

        url: '/',

        templateUrl: 'views/main.html',

        controller: ''

      })

      .state('mylevel', {

        url: '/mylevel',

        templateUrl: 'views/mylevel.html',

        controller: ''

      });

  }])


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

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

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

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

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


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


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

// app/index.html 기존 

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


// app/index.html 수정

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


// app/views/main.html 수정 

<div class="row">

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

    <h1>main.html</h1>

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

  </div>

</div>


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


  최종 결과 화면




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

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


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

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

// app/views/menu.html

<div class="container">


  <div class="navbar-header">

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

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

    </button>

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

  </div>

  

  <div class="collapse navbar-collapse">

    <ul class="nav navbar-nav">

      <li class="dropdown">

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

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

        </a>

        <ul class="dropdown-menu">

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

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

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

          </li>

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

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

          </li>

          

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

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

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

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

          </li>

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

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

          </li>

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

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

          </li>


        </ul>

      </li>


      <li class="dropdown">

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

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

        <ul class="dropdown-menu">

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

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

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

          </li>


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

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

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

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

          </li>

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

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

          </li>


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

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

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

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

          </li>


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

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

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

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

          </li>

        </ul>

      </li>


      <li class="dropdown">

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

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

        <ul class="dropdown-menu">

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

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

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

          </li>

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

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

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

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

          </li>

        </ul>

      </li>

    </ul>

  </div>

</div> 



// app/scripts/app.js 

angular.module('meanstackApp', [

  'ngCookies',

  'ngResource',

  'ngSanitize',

  'ngRoute',

  'ui.bootstrap',

  'ui.router'

])

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


    $urlRouterProvider.otherwise("/");


    $stateProvider

      .state('main', {

        url: '/',

        templateUrl: 'views/main.html',

        controller: ''

      })

      .state('login', {

        url: '/login',

        templateUrl: 'views/login.html',

        controller: ''

      })

      .state('mylevel', {

        url: '/mylevel',

        templateUrl: 'views/mylevel.html',

        controller: ''

      })

      .state('myqna', {

        url: '/myqna',

        templateUrl: 'views/myqna.html',

        controller: ''

      })

      .state('myfollowing', {

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

        templateUrl: 'views/myfollowing.html',

        controller: ''

      })

      .state('techarea', {

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

        templateUrl: 'views/techarea.html',

        controller: ''

      })

      .state('gururanking', {

        url: '/gururanking',

        templateUrl: 'views/gururanking.html',

        controller: ''

      })

      .state('gurumember', {

        url: '/gurumember',

        templateUrl: 'views/gurumember.html',

        controller: ''

      });


  }])


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

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


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

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


posted by 윤영식
2013. 12. 5. 13:34 AngularJS/Start MEAN Stack

이전 블로그에서 MEAN Stack에 대한 의미를 알아보았습니다. Angular.js 의 Why, How, What을 살펴보도록 하겠습니다.


1. Angular.js 가 주고자 하는 가치는 무엇인가?

  앵귤러는 모던 애플리케이션을 개발하기 위한 프레임워크입니다. 복잡하고 단순 반복적인 작업을 대폭 줄여줌으로써 신속한 개발을 가능하게 해줍니다. 이를 통해 보다 빨리 고객의 피드백을 받고 개선할 수 있는 에자일한 개발 진행이 가능해 집니다. 보다 적은 작업을 통해 보다 더 많은 행복을 주는 프레임워크입니다. 


  1989년 HTML을 시작으로 2005년 Ajax가 나오고 이후 jQuery를 통하여 DOM 을 조작을 통한 웹서비스 개발 시대를 거쳐 지속적인 브라우져 기술의 성숙과 JavaScript 해석기의 성능향상으로 이제는 자바스크립트를 통해 클라이언트에서도 서버와 같은 MVC 패턴 방식의 애플리케이션 개발이 가능해 졌습니다. 만일 jQuery만을 통해 웹앱을 개발한다고 생각하면 HTML 페이지마다 들어가는 서버코드와 자바스크립트들의 복잡한 코드에 머리를 쥐어 짤지도 모릅니다. 

  저는 13년을 넘게 자바언어로 서버만을 개발했었습니다. 가끔 클라이언트단의 jQuery 코드를 볼 때마다 저 영역으로는 절대 들어가지 말자라고 생각했습니다. 왜일까요? 그것은 서버처럼 잘 정비된 애플리케이션 프레임워크도 없으며, 서버코드와 자바스크립트를 HTML 사이사이에 끼워 넣으며 스파게티같은 코드를 짜야 하기 때문이었습니다. 

  그러나 2010년 이후부터 상황은 바뀌었습니다. Backbone.js가 이러한 복잡함을 해결하고자 초기 프레임워크로 나왔고, 이후 Ember.js 그리고 Angular.js 에서 Meteor.js 까지 모던 웹앱을 개발할 수 있는 프레임워크가 나왔습니다. 앵귤러는 어떻길래 이들 프레임워크 중 단연 선풍적인 인기를 누리고 있는 걸까요?


  - .js F/W  트랜드 



- .js 에서 dot(점)을 빼고 Backbonejs 와 Angularjs 트랜드 


  Backbonejs 와 Angularjs(노란색) 의 트랜드 변화는 급상승 중



2. Angular.js 가 어떻길래 개발자들이 열광하는가?

  앵귤러의 아버지인 미스코님(Misko Hevery)의 소개 동영상을 잠시 감상해 봅시다 


  요즘 SI시에 서버개발을 위하여 Spring Framework와 iBatis(myBatis)를 사용하는 것이 기본적인 관례처럼 되었는데요. 이들의 기능을 잠깐 생각해 볼까요. 

  - DI (Dependency Injection) 을 통하여 코드간의 결합도를 줄여주고, 테스트 코드의 작성을 쉽게 해줍니다. 

  - MVC 패턴를 통하여 기본적인 서버 개발의 틀을 가이드 해줍니다.

  - 다양한 라이브러리와 툴의 결합으로 개발 생산성을 높여줍니다.


 이를 앵귤러 입장에서 생각해 보면 정확히 위와 같은 요구사항을 충족해 주고 있습니다. 

  - DI를 지원합니다. 모듈단위의 개발로 코드를 간결하게 유지하고 테스트를 쉽게 해줍니다.

  - MV* 패턴을 통하여 역할을 나누고 아키텍쳐 Layered 개발을 가능하게 해줍니다. 당연히 유지 보수가 쉬워지겠죠

  - jQuery의 Plugins 포함한 기존의 다양한 라이브러리를 재 사용할 수 있습니다. 

  - 서버의 Maven, Ant 와 같은 관련된 라이브러리 의존성 관리 및 빌드 자동화 툴과 결합하여 개발 생산성을 높일 수 있습니다. 


  하지만 앵귤러를 시작하기 전에 하나의 선입견을 버리고 새로운 인식으로 접근을 해야 합니다. 자바스크립트는 이제 브라우져에서 화면의 단순 조작을 통한 효과에 쓰이는 언어가 아닌 진정한 엔터프라이즈급 애플리케이션부터 모바일 웹앱까지 모던한 애플리케이션을 만드는데 가장 많이 쓰이고 있는 개발언어가 되었다는 것입니다. 구글은 크롬앱을 코르도바(폰갭)를 통하여 안드로이드 및 iOS에서 구동하는 툴킷을 2014년 초에 출시할 계획입니다. 크롬앱은 자바스크립트로 개발합니다. 이는 본격적으로 네이티브 모바일 앱과 자바스크립트 모바일 웹앱이 함께 공존할 수 있는 시기가 왔다는 것을 암시합니다. 


 앵귤러(Angular.js)는 화면을 조작하는 라이브러리나 화면을 조작하는 프레임워크가 아니 모던 애플리케이션을 개발하는 프론트엔드 프레임워크입니다. 최근에는 이를 SPA(Single Page Application) 개발이라 부릅니다. Adobe의 Flex 기술을 통하여 클라이언트단에 엔터프라이즈 애플리케이션을 개발하는 RIA(Rich Internet Application) 가 선풍적인 인기를 누렸음을 잘 알것입니다. 인터넷이 되는 스마트 기기 에서 Adobe Flash의 공식 미지원 발표에 있은 후 이제 RIA의 용어는 점점 잊혀져 가고 있습니다. 하지만 사람들의 요구는 UX에 점점 더 목말라하고 있습니다. 단순한 웹서비스로는 이에 대응하기 힘들며 PC시대의 RIA를 표방한 Adobe의 Flex 프레임워크처럼 현재의 PC와 Mobile을 통합하는 진정한 RIA와 같은 자바스크립트 진영의 개발 프레임워크가 바로 앵귤러(Angular.js) 입니다. 무엇을 제공하 길래 앵귤러는 SPA 프레임워크의 큰형님이 되고 있는 걸까요?


 SPA vs RIA 비교 



3. Angular.js에는 무엇이 있는가?

  앵귤러는 클라이언트의 애플리케이션을 견고하게 만들 수 있는 방법을 제시하고 있습니다. 클라이언트의 생명은 바로 UX에 있다고 생각합니다. 훌륭한 사용자 경험을 주기 위하여 앵귤러 사용한다면 빠르게 개발하고 사용자의 피드백을 받아 개선해 갈 수 있습니다.


  견고한 싱글 페이지 웹 애플리케이션(SPA)을 만들기 위하여 앵귤러는 다음과 같은 기능을 제공합니다. 

  - 모듈 단위 개발을 통하여 글로벌 영역을 오염시키지 않고 모듈 단위 개발을 가능하게 해줍니다. 따라서 대규모의 애플리케이션 확장이 가능해 집니다. 


  - 양방향 데이터 바인딩을 통하여 View(HTML) 와 Controller(자바스크립트) 사이에 데이터에 대한 양방향 동기화를 자동으로 해줍니다. 즉, View 에서 데이터를 입력하면 Controller 단의 데이터를 업데이트 해주고, 그 반대도 가능해 집니다. 기존 jQuery에서는 데이터의 동기화를 위하여 Event와 Listener를 등록하는 코드를 모두 개발해야 했다면 앵귤러에서는 이런 코드를 찾아 볼 수 없습니다. Flex의 [Bindable]을 생각하면 됩니다. 


  - 앵귤러를 보통 MVW 프레임워크라 부릅니다. W는 Whatever의 의미로 Controller, Service등 무엇이든 올 수 있다는 것입니다. 역시 가장 중요한 것은 Model과 View의 연동을 양방향으로 함으로써 UX를 보다 쉽고 빠르게 개발토록 하는데 있습니다. 


  - Controller는 View HTML에서 발생한 이벤트에 따라 Model 변경을 제어하고, Service는 백앤드 서비스를 데이터 I/O 통신을 담당합니다. 각 필요한 기능은 DI(Dependency Injection)을 통하여 펑션의 파라미터 주입방식으로 인스턴스를 받아서 사용합니다. 


  - SPA 의 Single Page라는 의미는 최초에 index.html 을 서버로부터 받은 후 부분적인 화면의 변경을 위하여 부분 HTML(Partial HTML)만을 받아서 index.html의 특정 영역의 DOM 객체 변경을 통해 View의 일부분을 바꿔줍니다. 이후 서버로 부터 받는 것은 View의 컨텐츠 데이터인 JSON 이나 XML 입니다. 이렇게 화면의 부분 변경을 위하여 Routing 기능을 제공합니다. 


  - jQuery + PHP, jQuery + JSP 코드가 들어간 HTML 이 기존의 코딩 방식이라면 Javascript + HTML 만 존재하는 것이 앵귤러의 코딩 방식이다. 또한 반복해서 사용하는 HTML 또는 자바스크립트 코드조각이나 위젯/플러그인을 앵귤러만의 컴포넌트로 만들어서 HTML tag, attribute, class, comment 등의 방식으로 HTML에 포함시킬 수 있는 Directives(지시자) 기능을 제공합니다. Flex의 MXML에 차트관련 컴포넌트를 <Line> 태그로 표현하듯이, Angular.js의 Directive(컴포넌트)를 만들면 HTML안에 <Line> 과 같은 태그를 포함시킬 수 있습니다. 즉, Directive는 HTML 태그로 표기할 수 있는 컴포넌트 모듈을 만들 수 있고 Callback 펑션 기능을 내부로 숨기고 재사용할 수 있는 것이다. 


  - 백앤드로 Node.js를 사용한다면 Flex의 Messaging 기술과 같은 Push 방식의 구현을 Socket.io 모듈을 통하여 보다 더 쉽게 구현 할 수 있다. 


이제 자바스크립트 코딩이 가능하다면 싱글 페이지 애플리케이션(SPA) 에 도전해 봅시다.



<참조>

  - 스타트업을 위한 서비스 개발 기술 - MEAN Stack

  - 웹 애플리케이션 견고하게 만들기

  - 프론트 엔드 웹앱 프레임워크 (SlideShare)

posted by 윤영식
2013. 12. 4. 01:20 My Projects/BI Dashboard

상단의 대메뉴를 선택한 후 본문의 왼쪽에 있는 소메뉴를 클릭하면 오른쪽에 본문의 내용이 나온다. 본문을 보기까지의 라우팅(Routing)을 보면 대메뉴 클릭 -> 소메뉴 클릭 -> 본문 으로 이어지는 DOM의 변경이 일어난다. jqGrid, HigChart등의 json 환경 내역을 관리하는 화면을 만들기 위하여 Routing 방식을 고도화 해보자




1. Nest View

  - 큰메뉴 -> 작은 메뉴 -> 더 작은 메뉴 -> 본문 식의 View가 내제되어 라우팅 될 경우

  - UI-Router 를 이용한 데모, Plunker 데모

  - Route-Segment 를 이용한 데모



2. Admin 화면 구성하기

  - index.html 메뉴 추가하기

  - component에서 jqgrid, highchart와 같은 컴포넌트의 config 값 (json type)을 관리한다

// index.html 메뉴 추가하기 

<div class="collapse navbar-collapse">

  <ul class="nav navbar-nav">

    <li data-ng-class="activeWhen(path()=='/')">

      <a href="" data-ng-click="setRoute('/')">Home</a>

    </li>

    .. 중략 ..

    <li class="dropdown">

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

        <ul class="dropdown-menu">

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

          <li><a href="">User</a></li>

          <li><a href="">Customer</a></li>

          <li><a href="">Code</a></li>

          <li><a href="">Role</a></li>

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

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

          <li data-ng-class="activeWhen(path()=='/mc-component')">

      <a href="" data-ng-click="setRoute('/mc-component')">Component</a>

    </li>

          <li><a href="">Dashboard</a></li>

        </ul>

    </li>

  </ul>

</div>  



// Component partial 화면과 모듈 생성 및 등록

// 1) MCAdminCtrl.js 모듈을 만들기 

// 2) views/mc/component.html 화면 만들기

// 3) DashboardApp.js 에 MCAdminCtrl.js 모듈 명칭 등록하기 : DashboardApp.MCAdminCtrl

// 4) index.html 에 MCAdminCtrl.js 모듈 파일 <script> 태그 추가하기

  - 서버 코드로 ComponentController / Component / ComponentService / ComponentMapper(java & xml) 을 만든다 

    + compType : grid, chart, option 등 다양하게 존재할 수 있다 

// ComponentMapper.xml 에서 : 기존 소스는 typeAlias 사용하였음

<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- 

id : primary key

mysql -u user1 -p DBName

mysql> CREATE TABLE mc_component(

      id MEDIUMINT NOT NULL AUTO_INCREMENT,

      comp_type VARCHAR(5) NOT NULL,

      comp_cfg TEXT NOT NULL,

      PRIMARY KEY (id) 

    );

 -->

<mapper namespace="com.mobiconsoft.dashboard.mapper.ComponentMapper">

 

    <resultMap id="component" type="com.mobiconsoft.dashboard.domain.Component" >

        <result property="id" column="id"/>

        <result property="compType" column="comp_type"/>

        <result property="compCfg" column="comp_cfg"/>

    </resultMap>

 

    <select id="getComponents" resultType="com.mobiconsoft.dashboard.domain.Component">

        SELECT

        *

        FROM

        component

    </select>

 

    <select id="getComponent" parameterType="Integer" resultType="com.mobiconsoft.dashboard.domain.Component">

        SELECT

        *

        FROM

        component

        WHERE

        id=#{id}

    </select>

 

    <insert id="saveComponent" parameterType="com.mobiconsoft.dashboard.domain.Component"<!-- useGeneratedKeys="true" keyProperty="id"> -->

        INSERT INTO

        component(comp_type, comp_cfg)

        VALUES

        (#{compType}, #{compCfg})

    </insert>

    

    <update id="updateComponent" parameterType="com.mobiconsoft.dashboard.domain.Component"

        UPDATE 

        component

        SET

        comp_type=#{compType}, 

        comp_cfg=#{compCfg}

        WHERE

        id=#{id} 

    </update>

 

    <delete id="deleteComponent" parameterType="Integer">

        DELETE FROM

        component

        WHERE

        id=#{id}

    </delete>

 

</mapper>


// 서버 전체 파일 



3. UI-Router 사용하기

  - 관리 메뉴에서 Component를 선택하면 CRUD 할 수있는 메뉴 구조를 본문에서 가져야 한다

  - UI-Router를 이용하여 기존의 Routing 환경을 고도화한다 

// angular-route 라 반드시 설치되어 있어야 하고, 1.2.* 도 지원함 

// bower 설치 

bower install angular-ui-router --save

bower angular-ui-router#~0.2.0 install angular-ui-router#0.2.0

  - index.html에 route-segment 추가하기 

<!-- build:js scripts/angular-3.js -->

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

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

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

    .. 중략 ..

<!-- endbuild -->

  - DashboardApp.js 메인 소스에 ui-router 모듈을 추가한다 (예제 참조)

var DashboardApp = angular.module('DasbhoardApp', [

  'ngRoute', 

  'ui.router',

  .. 중략 ..

  'DasbhoardApp.RestfulSvc'

]);

  - DashboardApp.js 의 config 에서 $routeProvider를 고도화한다 (소스 참조)

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


   $urlRouterProvider.otherwise("/main");

   $stateProvider

      .state('main', {

      url: '/main',

        templateUrl: 'views/main.html'

      })

      .state('resttest', {

      url: '/resttest',

        templateUrl: 'views/restTest.html',

        controller: 'RestTestBiz.personCtrl'

      })

      .state('jqgridtest', {

      url: '/jqgridtest',

        templateUrl: 'views/jqGridTest.html',

        controller: 'JqGridBiz.salesCtrl'

      })

      .state('mc-component', {

      url: '/mc-component',

        templateUrl: 'views/mc/component.html',

        controller: 'MCAdminCtrl.componentCtrl'

      });

  }]);


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

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


      // It's very handy to add references to $state and $stateParams to the $rootScope

      // so that you can access them from any scope within your applications.For example,

      // <li ng-class="{ active: $state.includes('contacts.list') }"> will set the <li>

      // to active whenever 'contacts.list' or one of its decendents is active.

      $rootScope.$state = $state;

      $rootScope.$stateParams = $stateParams;

  }]);

  - index.html 의 링크 내역 수정 (소스 참조)

   + ui-sref 애프리뷰트 directive 사용으로 routing 되는 정보를 현재 html 페이지의 ui-view 로 연결한다 

   + ui-view 애트리뷰트 directive 사용으로 화면이 나타나는 태그 영역

   + 메뉴 active는 $state.includes('<stateName>') 을 이용하여 현재의 state 이면 active로 표현 

  -즉 한페이지에서 ui-sref와 ui-view를 통해 확장해 가는 방식

<div class="collapse navbar-collapse">

  <ul class="nav navbar-nav">

    <li data-ng-class="{ active: $state.includes('main') }"><a ui-sref="main">Home</a></li>

    <li data-ng-class="{ active: $state.includes('resttest') }"><a ui-sref="resttest">RESTTest</a></li>

    <li data-ng-class="{ active: $state.includes('jqgridtest') }"><a ui-sref="jqgridtest">jqGridTest</a></li>

    

    <!-- <li data-ng-class="activeWhen(path()=='/')">

      <a href="" data-ng-click="setRoute('/')">Home</a>

    </li>

    <li data-ng-class="activeWhen(path()=='/resttest')">

      <a href="" data-ng-click="setRoute('/resttest')">RESTTest</a>

    </li>

    <li data-ng-class="activeWhen(path()=='/jqgridtest')">

      <a href="" data-ng-click="setRoute('/jqgridtest')">jqGridTest</a>

    </li> -->

    <li class="dropdown">

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

        <ul class="dropdown-menu">

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

          <li><a href="">User</a></li>

          <li><a href="">Customer</a></li>

          <li><a href="">Code</a></li>

          <li><a href="">Role</a></li>

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

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

          <!-- <li data-ng-class="activeWhen(path()=='/mc-component')">

      <a href="" data-ng-click="setRoute('/mc-component')">Component</a>

     </li> -->

     <li data-ng-class="{ active: $state.includes('mc-component') }"><a ui-sref="mc-component">Component</a></li>

          <li><a href="">Dashboard</a></li>

        </ul>

    </li>

  </ul>

</div>


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

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

<div ui-view class="container" style="margin-top:30px"></div>



4. Component 관리를 위한 UI-Router 만들기

  - component 관리 화면 만들기

    + step-1 : component.html 으로 컴포넌트의 목록을 보여주고 컴포넌트를 선택하면 component.detail.html 이 우측에 표현된다

    + step-2 : component.read.html 로 컴포넌트 상세 정보를 읽거나 업데이트 또는 삭제한다  

    + step-3 : component.create.html 로 새로운 컴포넌트 정보를 저장한다 

    + step-4 : ComponentMod.js 개발하기 - CRUD Controller와 Service 그리고 index.html에 파일 추가하기 

    + step-5 : DashboardApp.js routing 정보 업데이트 및 ComponentMod.js 모듈 추가하기 

    

// index.html 에서 ui-view에 component.html전체가 표현됨

 <div ui-view class="container" style="margin-top:30px"></div>


// step-1,2,3) *.html 파일 만들기 

// component.html 에서 목록을 받아와서 ng-repeat 하기 

<li ng-repeat="component in components"

    ng-class="{ active: $state.includes('mc-component.detail') && $stateParams.componentId == component.id }">

  <a ui-sref=".detail({componentId:component.id})" style="padding: 2px 2px">{{component.name}}</a>

</li>

.. 중략 ..

// 여기에 component.detail.html 과 component.create.html 내용이 표현됨 

<div ui-view></div>


// component.detail.html : value에 값을 표현하기, ng-model을 통하여 controller에서 변경값 처리하기 (create html도 유사함) 

<form class="form-horizontal" role="form">

<div class="form-group">

<label class="col-sm-3 control-label"> Name</label>

<div class="col-sm-9">

<input type="text" class="form-control" placeholder="Name" ng-model="component.name" value="{{component.name}}" required>

</div>

</div>

<div class="form-group">

<label class="col-sm-3 control-label"> Type</label>

<div class="col-sm-9">

<input type="text" class="form-control" placeholder="Type" ng-model="component.type" value="{{component.type}}" required>

</div>

</div>

<div class="form-group">

<label class="col-sm-3 control-label"> Config</label>

<div class="col-sm-9">

<textarea class="form-control" id="message" name="message" ng-model="component.cfg" value="{{component.cfg}}"

placeholder="Config" rows="5" required></textarea>

</div>

</div>

<div class="form-group last">

<div class="col-sm-offset-3 col-sm-9">

<button type="submit" class="btn btn-success btn-primary" data-ng-click="update()">Update</button>

<button type="submit" class="btn btn-error btn-primary" data-ng-click="deleteComp()">Delete</button>

</div>

</div>

</form>

  - step-4 : CompoonentMod.js 개발 

'use strict';

// 모듈 정의 

var ComponentMod = angular.module('MobiConSoft.ComponentMod', []);

// ui-router의 서비스 추가 

ComponentMod.controller('ComponentMod.componentCtrl', ['$rootScope', '$scope', 'RestfulSvcApi', '$state', '$stateParams', 'componentSvc',

                                                              function ($rootScope, $scope, RestfulSvcApi, $state, $stateParams, componentSvc) {

        // 서브 화면에서 Create, Delete, Update가 일어나면 목록의 components 를 two-way binding으로 업데이트하기 위함

       $scope.$on("updateComponentList", function(event, data){

          console.log('--broadcasting data, ', data);

          $scope.components = data;

       });

var getAll = function() {

console.log('-----all: ', $scope.components);

componentSvc.getAll();

};

var getOne = function() {

console.log('-----one: ', $stateParams.componentId);

RestfulSvcApi.one({ domain: 'component', key: $stateParams.componentId },

function(response) {

console.log('componentCtrl : one data=',response);

   $scope.component = response;

},

function(response) {});

};

if($state.current.name === 'mc-component' || (!$stateParams.componentId && !$scope.components)) {

getAll();

};

if($state.current.name === 'mc-component.detail' && $stateParams.componentId) {

getOne();

};

$scope.update = function() {

console.log('componentCtrl : component=', $scope.component);

RestfulSvcApi.update({ domain: 'component'}, $scope.component,

function(response) {

console.log('---update, ', response);

getAll();

},

function(response) {});

};

$scope.deleteComp = function() {

RestfulSvcApi['delete']({ domain: 'component', key: $scope.component.id },

function(response) {

console.log('---delete, ', response);

$scope.component = {};

getAll();

},

function(response) {});

};

  

$scope.save = function() {

console.log('----> component is ', $scope.component);

RestfulSvcApi.save({ domain: 'component' }, $scope.component,

function(response) {

console.log('---save, ', response);

getAll();

},

function(response) {});

};

 }]);


// 전체 목록에 대해서만 Service로 구현함 : Broadcasting에 대한 별도 추가 테스트 필요함

ComponentMod.service('componentSvc', ['$rootScope', 'RestfulSvcApi', function ($rootScope, RestfulSvcApi) {

this.getAll = function() {

RestfulSvcApi.all({ domain: 'component' },

function(response) {

console.log('componentSvc : all data=', response);

$rootScope.$broadcast('updateComponentList', response);

},

function(response) {});

};

}]);

  - step-5 : DashboardApp.js routing status 업그레이드

'use strict';


var DashboardApp = angular.module('DasbhoardApp', [

  'ngRoute', 

  'ui.router',

  'ngAnimate',

  'ngCookies',

  'ngResource',

  'ngSanitize',

  'DasbhoardApp.CommonCtrl',

  'MobiConSoft.ComponentMod',

  'DashboardApp.JqGridDrtv',

  'DasbhoardApp.RestTestBiz',

  'DasbhoardApp.JqGridBiz',

  'DasbhoardApp.RestfulSvc'

]);


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


    $urlRouterProvider.otherwise("/main");

    $stateProvider

      .state('main', {

      url: '/main',

        templateUrl: 'views/main.html'

      })

      .state('resttest', {

      url: '/resttest',

        templateUrl: 'views/restTest.html',

        controller: 'RestTestBiz.personCtrl'

      })

      .state('jqgridtest', {

      url: '/jqgridtest',

        templateUrl: 'views/jqGridTest.html',

        controller: 'JqGridBiz.salesCtrl'

      })

      .state('mc-component', {

      url: '/mc-component',

        templateUrl: 'views/mc/component.html',

        controller: 'ComponentMod.componentCtrl'

      })

     .state('mc-component.detail', {

      url: '/detail/{componentId:[0-9]{1,4}}',

       templateUrl: 'views/mc/component.detail.html',

       controller: 'ComponentMod.componentCtrl'

     })

     .state('mc-component.create', {

      url: '/create',

       templateUrl: 'views/mc/component.create.html',

       controller: 'ComponentMod.componentCtrl'

     });

  }]);


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

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

      $rootScope.$state = $state;

      $rootScope.$stateParams = $stateParams;

  }]);

  - 결과화면 

    


* 소스 : https://github.com/ysyun/SPA_Angular_SpringFramework_Eclipse_ENV/tree/feature_admin_routing



<참조>

  - AngularJS Views and Directives

  - Angular Route Segment

  - UI-Route : 예제 소스

posted by 윤영식
2013. 11. 30. 11:58 HTML5, CSS3/jQuery

페이스북 같은 앱에서 사용하는 좌/우측 스라이딩 사이드 메뉴를 Angular.js 기반으로 만들어진 컴포넌트에 대해 알아보자. 



Angular-Snap.js

    

  - http://jtrussell.github.io/angular-snap.js/

  - 드래그하여 메뉴를 열수 있다 

  - 버튼 클릭으로 메뉴를 열수 있다 

  - IE8 지원하나 snap.js 가 IE8 미지원이므로 미지원



Angular-SlideNav

  

  - https://github.com/sthomp/angular-slidenav.js

  - 버튼 클릭으로 메뉴를 열수 있다 

  - IE8 미지원



jPanelMenu

  

  - http://jpanelmenu.com/

  - jQuery 기반 메뉴

  - IE8 ?



Sidr

  

   - http://www.berriart.com/sidr/

   - left, right 슬라이딩 메뉴 

   - IE8 지원



Nexus Style Side

  

  - http://www.jqueryscript.net/menu/Google-Nexus-Page-Like-Sidebar-Menu-with-CSS3-javascript.html

  - 작은 아이콘에서 큰 아이콘과 텍스트가 펼치는 효과 (강추)

  - IE8 미지원



Multi-Level Side Menu

  

  - http://www.jqueryscript.net/menu/Multi-Level-Slide-Push-Menu-with-CSS3-Javascript-MultiLevelPushMenu.html

  - 또 다른 멀티 레벨 메뉴 (강추) : 메뉴로 사용하기 괜찮음

  - IE8 미지원

 


Blueprint Side

  

  - http://tympanus.net/Blueprints/SlidePushMenus/

  - 왼쪽, 오른쪽, 위, 아래로 메뉴가 펼쳐진다

  - 스타일은 푸른색  

  - IE8 지원 



MMenu

  

  - http://mmenu.frebsite.nl/

  - 또 다른 jQuery MMenu (강추)

  - 모바일에 잘 맞는 슬라이딩 메뉴

  - 다른 컴포넌트도 있음 

  - IE8 미지원



Tegansnyder

  

  - https://github.com/tegansnyder/JQuery-Mobile-Slide-Menu

  - jQuery Mobile Slide Menu : 페이스북 형태 

  - 페이스북과 유사한 Slide Menu 만들기 블로그 : 이것이 좀 더 괜찮음

  - IE8 지원



Snap.js

  

  - https://github.com/jakiestfu/Snap.js

  - left, right 메뉴 제공

  - 드래그 제공

  - 페이스북 스타일 제공

  - IE8, 9 미지원 



Meny

  - http://lab.hakim.se/meny/

  - IE8 지원



<참조> 

  - 원문 : Slide Out Sidebar Navigations

  - 원문 : jQuery Side Menu 목록

  - Angular Snap.js

  - Angular SlideNav

  - Angular Datatable

  - 15개의 RWD Menu 

posted by 윤영식
prev 1 2 3 next