하이브리드 렌더링 전략 – 두 번째 이야기 (React + Backbone)

ASP.NET 2015.01.11 22:09

글은 하이브리드렌더링 전략 번째 이야기 이어지는 글이다. 


지난 포스트에선 서버 측이건 클라이언트 측이건 뷰를 렌더링하기 위해서 서버의 템플릿 엔진(Razor) 템플릿을 사용했었다.

이번 포스트에선 반대로 자바스크립트(페이스북 React) 사용해서 서버 렌더링과 클라이언트 렌더링 모두를 처리하는 방법을 정리해 보려고 한다.

 

지난해 페이스북에서 오픈 소스로 공개한 React 여타 자바스크립트 프레임웍과 구별되는 독특한 점 가지 있다. MVC에서 오직 View 기능에만 충실한 프레임웍이라는 점과 별도의 템플릿 언어(Handlebars, Mustache ) 사용하지 않고 자바스크립트만을 사용한다는 , 그리고 실행환경이 브라우저가 아니어도 상관 없다는 점이다.

번째 특성으로 인해 보통 React 여타 프레임웍들과 같이 사용될 경우가 많은데 주로 영역의 변화가 유연한 Backbone.js 자주 사용된다. Backbone.js 모델과 라우터의 역할을 하고 React 결과를 브라우저에 렌더링 하는 역할을 주로 맡는다.

번째로 별도의 템플릿 없이 자바스크립트를 그대로 DOM 표현하는 용도로 사용[각주:1]하고 렌더링 속도가 여타 클라이언트 템플릿 보다 훨씬 성능이 좋은 편인데 이유는 Virtual DOM이라는 컨셉을 차용해서 어떤 데이터가 변경되었을 업데이트 되어야 UI 모두 한번의 배치 작업으로 일괄 업데이트 하기 때문이다.

이것은 앞서 말한 번째 특성과도 연관되는데, Virtual DOM 브라우저의 DOM 직접적인 연관성이 없기 때문에 브라우저 환경이 아닌 곳에서도 React 사용될 있도록 해준다. 서버에서도 React 실행이 가능하다.

그러나 서버 환경이 Node.js 아닌 이상은 자바스크립트를 파싱할 파서는 여전히 필요하다. 따라서 서버가 닷넷이나 자바 등의 환경이라면 별도의 자바스크립트 엔진을 사용해야 한다.

고맙게도 페이스북은 React를 ASP.NET 환경에서 편리하게 사용할 수 있도록 React.NET이라는 오픈 소스 프로젝트를 직접 주도하고 있는데, ASP.NET Bundling 통해서 JSX 파일을 자바스크립트로 파싱한다든가 JSX 만들어진 뷰를 서버에서 렌더링 있도록 하는 등의 기능을 제공한다.

React.NET JSX 파일을 서버 렌더링 용으로 파싱하기 위해서 IE에서 사용하는 자바스크립트 엔진이나 크롬의 V8 엔진을 사용할 수 있도록 지원하는데, 개인적으로는 V8의 만족도가 높았다. IE 자바스크립트 엔진은(이걸 샤크라라고 불러야할지 좀 애매하다.) 파싱 과정에서 예상치 못한 에러를 발생시키는 경우가 많았고, 밑에서 다시 설명하겠지만, 자바스크립트 엔진의 Pooling을 통해 파싱 과정의 성능 향상을 시키고자 할 경우 V8은 Pooling이 가능하지만 IE엔진은 단일 쓰레드에서만 실행되는 한계로 인해 Pooling 자체가 불가능하기 때문이다.

그런데 Nuget 통해서 React.NET 설치하고 나면 디폴트로 IE 자바스크립트 엔진을 사용하도록 셋팅되어 있으므로, 별도로 V8 사용하도록 해줘야 한다. 특별한 설정은 필요 없고 Nuget 통해서 JavaScriptEngineSwitcher.V8패키지를 설치해주면 자동으로 V8 엔진을 사용한다.

 

ASP.NET React, React.NET, Backbone.js 사용한 이번 샘플은 아래의 깃허브에 업로드해 두었다. 기능은 앞선 포스트의 샘플과 동일하다. 어떤 페이지를 먼저 방문하건 최초 로딩은 서버에서 이루어지고 이후의 페이지 이동은 페이지 리프레쉬 없이 URL은 업데이트 되고 화면이 변경된다

- 소스 : https://github.com/RayKwon/HybridRendering_React_Backbone

- 데모 : http://hybrid-react.azurewebsites.net

 

<그림 - 데모 화면>


간단히 프로젝트의 중요한 부분만 설명하면, 먼저 아래의 그림은 React 이용해서 User List 보여주는 부분이다.

<그림 UsersView - app.jsx


특별히 일반적인 React 이용한 프로그래밍과 다를 없다. (이 글의 주제는 React에 대한 소개가 아니므로 React에 대한 설명은 패스~ ) 

JSX 서버측에서 바인딩 되도록 하는 방법은 가지 과정을 거친다. 일단 번째로 아래 그림처럼 Razor 파일에서 뷰를 React.NET에서 제공해주는 헬퍼 메소드, Html.React를 사용해서 바인딩한다.

<그림 Users.cshtml>

 

Html.React는 첫 번째 인자로서 React Component의 이름(App.UsersView)를 지정하고 두번째 인자로 해당 React 컴포넌트에서 사용할 모델을 넘겨준다.

 

번째로 서버 바인딩에서 사용된 JSX 파일을 ReactConfig.cs 클래스에서  아래 그림처럼 추가해 준다.

 <그림 ReactConfig>


샘플에서는 모든 JSX 코드를 app.jsx 파일에 넣어 두었으므로 app.jsx 추가했고 underscore.js 스크립트도 사용하는 부분이 있어 같이 추가해 두었다

SetUseHarmony(true)는 ECMAScript 6의 여러 기능들을 사용할 수 있도록 지원해주는데, 예를 들어 CoffeeScript 처럼 Fat Arrow를 사용한 this 컨텍스트의 보존(e.g. (result) => { ... } ) 또는, 오브젝트 리터럴 내에서 Key : Value 표현식 대신 바로 Function Declaration을 사용할 수 있는 방법등의 Syntatic Sugar를 누릴 수 있다.

SetReuseJavaScriptEngines(true) 부분은 JSX 파일을 파싱할때 사용할 자바스크립트 엔진을 매번 초기화 할것인지 아니면 Pool을 사용해서 재사용할 것인지 선택하는 옵션인데 무조건 true로 해주는 것이 좋은데 성능상에 큰 차이를 보여주기 때문이다. 아마 다음 버전의 React.NET에서는 디폴트로 true가 셋팅될 것으로 예상된다.


이 서버용 스크립트를 추가하는 부분에서  혼란스러울 수도 있는데, 여기에는 서버측에서 렌더링 사용하는 파일만을 추가해야지, 클라이언트에서 사용할 파일들 예를들어서 jQuery.js Bootstrap.js  파일들은 포함시켜선 된다. 브라우저 환경에 의존성을 가지는 라이브러리들은 포함시켜선 된다.

그리고 클라이언트에서만 작동하는 코드를 추가해야 하는 경우, 예를 들어, jQuery 플러그인들을 적용시켜야 때는 componentDidMount 메소드에서 포함시키면 된다. 메소드는 원래 React 컴포넌트가 렌더링된 후에 호출되는 일종의 Hook 인데, 서버측에서는 호출되지 않고 클라이언트에서만 실행된다. 아래 그림은 샘플의 UserView에서 Bootstrap Popover 플러그인을 적용시킨 부분이다.

 <그림 UserView - app.jsx>


서버에서 렌더링하기 위한 마지막 과정은 아래 그림처럼 React.NET에서 제공해주는 Html.ReactInitJavaScript() 함수를 모든 자바스크립트가 로드된 후에 호출해 주는 것이.

 <그림 Layout.cshtml>


이 한줄의 함수 호출은 꽤 큰 역할을 한다. 첫째로 이미 React 컴포넌트가 서버에 의해서 브라우저에 렌더링 되어 있는지 확인하고 이미 렌더링 되어 있는 경우엔 클라이언트에서는 중복하여 렌더링되지 않도록 하는 것이고, 두번째는 해당 React 컴포넌트의 클라이언트 이벤트 핸들러들을 등록해준다. 이로 인해, 렌더링은 서버에서 발생하고 동일한 React 컴포넌트의 자바스크립트 이벤트 핸들러는 클라이언트에서 실행시킬 있게 된다. (퐌타스틱~~~ !!)

 

서버에서 모든 렌더링이 종료되고 이제 사용자가 화면에서 링크를 클릭한다든가 해서 다른 페이지를 보여줘야할 경우엔 이전 번째 포스트에서 했던 것처럼 PushState 이용하면 된다. 아래는 Backbone Router 이용해서 URL 업데이트한 서버측 엔드포인트를 호출해 데이터를 받아온 React 컴포넌트를 렌더링 하는 부분이다.

 <그림 App.Router - router.js>


앞선 포스트의 Razor + PushState 이용하는 경우와 한가지 다른 점은 서버에서 덩어리를 받아오지 않고 오직 모델 데이터만 받아 오고, 뷰는 클라이언트의 React 컴포넌트를 이용한다는 점이다. 서버에서 뷰를 리턴하지 않고 모델만 리턴시키면 되므로 네트워크 부담이 조금 경감되는 장점이 있다.

 

이상으로 하이브리드 렌더링을 구현하는 두 가지 방법을 살펴보았다.

저번 포스트의 Razor 이용하는 방법과 이번 React 이용하는 방법 모두 현재 진행하고 있는 프로젝트에 적용 중인데 개인적으로는 React 이용한 방법을 선호한다.

다이나믹한 UI 필요로 하는 웹사이트의 경우엔 어차피 서버에서 보내준 템플릿 만으로는 한계가 있고 클라이언트에서 일정 부분 화면의 생성, 업데이트 등이 계속 발생할 밖에 없는데 그럴 마다 서버측을 호출해서 덩어리를 받아오는 부담스러운 작업일 밖에 없기 때문이다. 그럼에도 불구하고 이미 운영되고 있는 기존 프로젝트에 성능 향상의 방법으로는 여전히 유용한 방법임은 분명하다.

만일 처음부터 새로 시작하는 프로젝트라면 React 사용한 하이브리드 렌더링 방법을 추천하고 싶다.


p.s 이 글을 쓰고 있는 시점엔 Visual Studio에서 JSX 하이라이팅 기능을 아직 지원하지 않아 JSX 작업은 모두 서브라임텍스트를 사용했는데, 얼마전 Web Essential 개발을 리딩하고 있는 Mads Kristensen이 현재 Web Essential에서 JSX를 지원하기 위한 작업을 진행중이라는 발표가 있었다. 

 

  1. Sencha Touch 역시 별도의 템플릿 없이 자바스크립트로 UI를 생성, 업데이트 하는 등의 작업을 하지만, 개인적으로 React의 JSX 문법이 DOM 구조를 이해하는데 훨씬 수월해 보인다 [본문으로]
신고
Trackback 0 : Comments 2

하이브리드 렌더링 전략 – 첫 번째 이야기 (Razor + Backbone)

ASP.NET 2015.01.09 22:42

아마 Single Page Application 이라는 단어를 처음 접했던 2011 후반 이었던 같다.

Knockout.js 기본 라이브러리로 해서 십여 가지가 넘는 프론트엔드 개발 라이브러리와 툴들을 동원(?)해서 처음 SPA 타입의 어플리케이션을 개발했던 때가 있었다.  후로 개의 어플리케이션을 계속해서 SPA 형태로 제작 하면서 부터는 회사 내부적으로 사용할 목적으로 자바스크립트 프레임웍을 아예 자체 개발해서 쓰기도 해보고 Two-Way Binding 이건 아니다 싶어 뒤집어도 보고...

그러는 동안 CoffeeScript, Backbone.js, Ember.js, Grunt, Gulp, QUnit, Mocha, Sinon, Bower, Jam.js, Sammy, Require.js, Browserify 등등 자고 나면 새로 생겨나던 무수한 프론트엔드 툴들과 삽질하며 그전까지 백엔드와 프론트엔드를 모두 개발하던 풀스택 개발자에서 프론트엔드만~ 하는 개발자로 자의반타의반 전향하게 되었다.

그리고 그때부터는 풀스택 개발자라는 말을 신뢰하지 않는다.

풀스택 개발자는 양쪽을 잘한다는 말인듯한데 스스로를 풀스택 개발자로 부르는 개발자들 중에 최근 년간 급격히 섬세해지고 깊어진 프론트엔드 툴들과 라이브러리, 자바스크립트 프레임웍, 프론트엔드 아키텍쳐, 브라우저 렌더링, 자바스크립트 언어 자체에 대한 깊은 이해 , 프론트엔드 개발 전반에 걸친 폭넓은 경험과 이해를 가진 백엔드 개발자는 매우 드물었다.(나 역시 갈길이 멀지만 백엔드에서 손놓고 프론트엔드만 만진지 어언 몇년이 되버려서, 백엔드 개발자는 아니니 그냥 프론트엔드 개발자로...ㅜㅜ) 대부분 자바나 닷넷으로 주로 개발하면서 jQuery Angular 해본 있는 백엔드 개발자가 많았던 같다. (갑자기 Tom Dale Javascript Jabber 팟캐스트에서 했던 말이 기억난다. “You don’t know what you don’t know!” 어우 자신감 쪄는 아쟈씨... )

그리고 그럴 수밖에 없음을 충분히 이해한다. 최근 년간 프론트엔드 개발은 ~~~~~~ 변화가 많았다. 언제 그들을 쳐다보고 있으란 말이냐.. .

잡설이 너무 길었다.

 

SPA 형태로 어플리케이션을 제작하면서 계속해서 신경 쓰였던 부분이 가지가 있었다.

하나는 SEO 취약한 , 하나는 최초 페이지 로딩 타임이 전통적인 서버 렌더링 방식보다 현저히 느리다는 점인데, 얼마전까지는  가지가 크게 중요한 팩터가 아닌 어플리케이션들만을 개발했어서 그저 큰 고민 없이 SPA로 개발했었다. 

일단, 한국에서 SEO는 아무 의미 없고(구글 검색을 이용하는 사용자들의 비율도 낮을 뿐 더러, 네이버나 다음의 검색은 웹페이지 검색 이라기 보다는 자기네 DB 검색이라고 부르는게 맞지 싶다.), 내가 개발하던 웹 어플리케이션들은 대부분 제한된 사용자들만을 위한 업무용 어플리케이션 이었기 때문이다. 그런데 작년부터 일하고 있는 현재 직장에서 개발하는 사이트들은 누구나 로그인 없이 사용할 있는 공개된 사이트들이 대부분이고 이곳 호주는 한국에선 잘 신경도 안쓰던 SEO 매우 매우 중요한 고려 사항이다.

물론 SPA로 개발된 웹사이트도 PhantomJS나 Zombie 같은 툴을 동원해서 SEO 지원이 가능하기는 하지만, 검색 엔진 크롤러만을 위해 서버 리소스가 낭비되는 측면도 있고 SEO만을 위해 최적화 하기엔 쉽지 않은 부분도 있다. 그럼에도 SEO 지원이 필요한 경우엔 아래 포스트 참조~

 : 싱글 페이지 어플리케이션에서의 검색 엔진 최적화 (SEO) 

구글이 자바스크립트 어플리케이션에 대한 SEO 지원하기 시작했다고 작년 초에 발표했지만여전히 이를 신뢰하는 개발자는 많지 않은  같다예전엔 구글이 하는 거라면 철떡같이 믿고 보자는 식이었는데언젠가부터 이게 깨지기 시작하더니 Angular.js 부터는 완전 Dog실망이다그저 그런 평범한 아이가 되어버린 천재를 보는 느낌? 구글 너님,, 많이 변했어~


상황이 이러하니 당연히 SPA 형태의 개발은 전혀 고려 대상이 아니다.

그러나 그렇다고 해서 보는 눈이 현격히 높아진 최근의 사용자들에게 서버 렌더링만으로 이루어진 고리타분한 웹사이트를 들이 수도 없으니, 자바스크립트는 자바스크립트대로 여전히 많이 사용할 밖에 없다.

 

특히, 현재 개발중인 사이트는 모바일 디바이스 전용 웹사이트인데, 모바일 기기에서 메뉴를 이동할 마다 페이지 리프레쉬가 발생하는 서버 렌더링으로 처리하게 되면 느린 응답 속도 때문에 사용자 만족도가 떨어질 밖에 없다.

그래서 나온 대안이 하이브리드 렌더링, 서버 렌더링과 클라이언트 렌더링을 모두 사용하는 것인데 이를 통해 최초 페이지 로딩타임을 크게 줄일 수 있고(자바스크립트 리소스를 다운로드 받지 않아도 되니), 구글 크롤러가 방문했을시는 친히 서버 렌더링으로 맞을수 있으니 SEO 문제도 해결된다.

하이브리드 렌더링을 구현하는 방법은 다양하겠지만크게 가지 구현 방식이 있는 같다. (이제서야 본론으로,,,)

번째는, 위터처럼 최초 페이지 로딩은 서버에서 렌더링 하고 후에 사이트 내에서의 페이지 이동은 PushState 이용해서 클라이언트에서 렌더링하되 클라이언트에서 별도의 템플릿(Handlebars, Mustach 등등) 사용하지 않고 서버에서 보내준 HTML 결과의 스트링 덩어리를 그대로 렌더링하는 방식이 있고,

번째도 역시 앞서처럼 최초 로딩은 서버에서 하고 후에는 PushState AJAX 처리하되, 페이스북의 React airbnb Rendr 처럼 브라우저의 실행 환경, 즉 글로벌 window 의존적이지 않은 자바스크립트 라이브러리를 사용해서 서버 렌더링과 클라이언트 렌더링 모두 동일한 하나의 템플릿을 같이 사용하는 방법이 있다.

 

현재 회사에서 개발중인 사이트는 전자의 방식을, Gistcamp 멤버들과 개발하고 있는 모종의 사이트는 React 사용해서 후자의 방식대로 개발하고 있다.

웹사이트 모두 한창 개발 중이라 어떤 방식이 낫다고는 결론 없지만 각각의 일장일단이 있는 같다.

 

아래의 링크는 전자의 방식을 구현한 샘플 프로젝트이다

- 소스  https://github.com/RayKwon/HybridRendering_Razor_Backbone

- 데모  http://hybrid-razor.azurewebsites.net

프로젝트 소스를 실행하면 아래 그림과 같은 화면을 있다.

 

<그림 1 – 홈페이지 화면>

 

<그림 2 – User List 화면>

 

<그림3  – User Details 화면>

 

홈페이지, 사용자 목록을 보여주는 페이지가 있고 목록에서 "Details" 버튼을 누르면 "User Details" 화면으로 넘어가는데, 페이지를 처음에 방문하면 서버에서 렌더링 되지만 그 다음부터 링크를 클릭해서 다른 페이지로 이동할 경우엔 페이지 리프레쉬는 발생하지 않은채 URL은 업데이트 되고 화면이 변경된다.

 

백엔드는 ASP.NET MVC를, 프론트엔드에선 Backbone.js 사용했는데 어떤 개발환경이 되었든 기본적인 구조는 아래의 그림과 같다. 

<그림 4>

 

우선 최초에 사용자가 URL 치고 들어오면 일단 서버측에서 페이지를 렌더링한다.  아래 그림처럼 평범하게 Razor 템플릿을 사용해서 모델 데이터를 렌더링하고 브라우저에서의 이벤트 핸들링을 처리할 Backbone View 인스턴스를 생성해준다. App.UsersView가 현재 Razor 템플릿을 전담할 Backbone View 객체이다. 

 <그림 5 - User List를 렌더링하는 Razor 템플릿 - Users.cshtml>


여기까지는 기존의 전통적인 서버렌더링 방식이다. 이후부터가 달라지는데, 예를 들어 사이트 내에서 링크를 클릭한다거나 해서 다른 페이지로 이동할 때부터는 페이지 리프레쉬가 발생하는 text/html 요청이 아니고 AJAX 서버측을 호출한다. 브라우저의 URL PushState 사용해서 업데이트된다. 위의 그림에서 2번과 3번에 해당하는 부분이다. 아래의 그림은 Backbone Router 2), 3) 부분을 구현한 코드이다. 

<그림 6 - 클라이언트에서 라우팅을 담당할 Router>

 

한편, AJAX 요청을 받은 백엔드에서는 아래 그림과 같이 클라이언트의 요청이 일반적인 text/html 형태일 경우와 AJAX 형태일 경우를 구분해서 후자일 경우 JSON 형태로 리턴한다. 리턴 값에는 원래 서버측에서 렌더링하던 HTML 덩어리, 그리고 필요한 경우 모델 데이터를 하나의 JSON 각각 view, model 이라는 키값에 담아서 반환한다. 

<그림 7 – 서버측 액션 메소드들 : HomeController.cs>

 

그러면 클라이언트의 자바스크립트에서는 전달받은 뷰와 모델을 아래와 같이 적절한 곳에 렌더링하면 ~

<그림 8 – Router>

 

방식의 장점은 기존에 이미 구축된 사이트에서 서버측 코드에 변화 없이 쉽게 적용할 있다는 점이다. 클라이언트에 별도의 템플릿(Handlebars, Mustache 등등) 사용할 필요가 없고 이미 사용 중이던 서버측 템플릿(샘플의 경우엔 Razor) 그냥 사용하면 된다. 아래와 같이 Razor 뷰는 쉽게 문자열로 변환이 가능하다.

<그림 9 – Razor 결과를 문자열로 반환>

 

또한 <그림-7> 서버측 코드처럼 기존에 URL 요청을 처리하던 액션 메소드에서 AJAX 요청도 같이 처리할 있기 때문에 AJAX 요청을 처리하기 위해서 별도의 액션 메소드를 개발할 필요도 없다.  

 

SPA 스타일의 어플리케이션은 다이나믹한 UI 쉽고 유연하게 구현할 있는 장점이 있는 반면에 고려해야 사항이 생각보다 굉장히 많다. 그럴 밖에 없는 것이 기존에 백엔드의 컨트롤러와 템플릿에서 구현했던 기능들을 모조리 자바스크립트와 CSS 통해서 구현해야 되기 때문이다Ember Angular 같은 프레임웍들이 많은 부분을 도와 주기는 하지만 여전히 미진한 부분이 많고 브라우저라는 런타임 환경은 제약사항이 너무나 많다. 반면, 이 방법을 사용할 경우에 SPA에서의 골치아픈 문제들을 손쉽게 해결할 수 있다. 

간략히 샘플 소스에서의 백엔드와 프론트엔드의 역할을 정리해보면 크게 아래와 같이 나누어진다.

- Backend

  서버측 템플릿 엔진을 사용해서 UI 구현한다.

  : 클라이언트의 요청 형태에 따라 text/html 다큐먼트를 반환하던가 JSON 반환한다.

- Frontend

  : PushState 사용해서 브라우저의 URL 업데이트한다.

  : 서버측에서 반환한 뷰를 브라우저에 렌더링한다.

 

그리고 방법은 아래와 같은 경우에 선택할 있을듯하다.

이미 개발된 기존 사이트에서 소스에 큰 변화 없이 페이지 응답 속도를 향상시키길 원할 경우 (머,, 큰 변화라는 단어는 상대적이긴 하지만~ )

- 뷰 로직 또는 UI 구현을 프론트엔드 보다는 백엔드에서 최대한 처리하길 원할 경우

- 클라이언트 로직이나 자바스크립트의 사용을 최대한 줄인 가벼운 클라이언트를 선호하는 경우

- 팀내에 자바스크립트에 대한 거부감이 많은 개발자가 있는데 설득시키기 귀찮기도 하고 나보다 직급이 높을 경우 (바로 같은 경우 ,. )

 

앞서 얘기했던 것처럼 방법 말고도 서버와 클라이언트 모두 자바스크립트로 퉁치는 방법도 있다(Node.js 사용하지 않고도,,,)

방법은 현재 또다른 프로젝트에서 사용중인, 앞서 설명한 방법보다  장점이 많은 같다. 왜냐하면, 서버측 템플릿을 사용해서 UI 구현하더라도 어차피 다이나믹한 UI 사용자 경험을 향상시키기 위해선 클라이언트에서도 뷰를 생성하거나 업데이트 해야 경우가 현실적으로 많기 때문인데,,,  

그건 다음 포스트에서~

 

 

신고
tags : ASP.NET, Backbone
Trackback 0 : Comment 0

Visual Studio에서 BrowserSync 사용 설정

ASP.NET 2014.10.16 18:46

Visual Studio에서 제공하는 Browser Link의 제약점

  - 리모트에서 접근할 수 없음. 오직 개발 머신에서만 사용 가능.
  - CSS나 LESS, SASS등을 수정할때 항상 페이지가 리로딩 됨.

대안
   
효과
  - 리모트에서 접근 가능, 폰, 테블릿 등등 머신 종류 상관 없음
  - HTML, CSS, Image 가 수정되면 웹페이지를 리로딩 없이 자동 반영
  - Responsive Design 개발시에 효과 적임

Visual Studio에서 사용 방법
  1. Tools -> External Tools
  2. Add
  3.  아래 사항들 입력
      Title : BrowserSync
      Command : C:\Windows\System32\cmd.exe
      Arguments : /C browser-sync start --files "Content/css/*.css, Views/*.cshtml" --proxy localhost:54642 --logLevel debug
      Output : $(ProjectDir)
      Check "use output window"

Note : localhost:54642는 디버깅할 프로젝트 주소


신고
Trackback 0 : Comment 0

ASP.NET, RequireJS, Handlebars 함께 사용할때 설정 샘플

ASP.NET 2014.07.17 15:17

- 프로젝트 참조 

  : https://github.com/RayKwon/aspnet_requirejs_handlebars_demo


requirejs_config.js

requirejs.config({

    baseUrl: '../scripts/app',

    paths: {

        jquery: '../lib/jquery-1.10.2',

        bootstrap: '../lib/bootstrap',

        underscore: '../lib/lodash',

        backbone: '../lib/backbone',

        hbs: '../lib/require-handlebars-plugin/hbs'

    },

    shim: {

        bootstrap: {

            deps: ['jquery']

        }

    },

    hbs: { // optional

        helpers: true,            // default: true

        i18n: false,              // default: false

        templateExtension: 'hbs', // default: 'hbs'

        partialsUrl: ''           // default: ''

    }

});


build.js

({

    baseUrl: '../Scripts/app',

    name: './app',

    out: './app.min.js',

    optimize: 'none',

    paths: {

        jquery: '../lib/jquery-1.10.2',

        bootstrap: '../lib/bootstrap',

        underscore: '../lib/lodash',

        backbone: '../lib/backbone',

        hbs: '../lib/require-handlebars-plugin/hbs'

    },

    shim: {

        bootstrap: {

            deps: ['jquery']

        }

    },

    hbs: { // optional

        helpers: true,            // default: true

        i18n: false,              // default: false

        templateExtension: 'hbs', // default: 'hbs'

        partialsUrl: ''           // default: ''

    }

})



_Layout.cshtml

<!DOCTYPE html>

<html>

<head>

    <meta charset="utf-8" />

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>@ViewBag.Title - My ASP.NET Application</title>

    @Styles.Render("~/Content/css")

    @Scripts.Render("~/bundles/modernizr")

</head>

<body>

    <!-- 생략 -->

    @Scripts.Render("~/bundles/requirejs", "~/bundles/requirejs_config")

    @Scripts.Render("~/bundles/app")    

</body>

</html>



web.config

<system.webServer>

    <staticContent>

      <mimeMap fileExtension=".hbs" mimeType="text/x-handlebars" />

    </staticContent>

  </system.webServer>



BundleConfig.cs

using HandlebarsHelper;

using System.Web;

using System.Web.Optimization;


namespace aspnet_requirejs

{

    public class BundleConfig

    {

        // For more information on bundling, visit http://go.microsoft.com/fwlink/?LinkId=301862

        public static void RegisterBundles(BundleCollection bundles)

        {

            

            bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(

                        "~/Scripts/jquery.validate*"));


            bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(

                        "~/Scripts/modernizr-*"));


            bundles.Add(new ScriptBundle("~/bundles/requirejs").Include("~/Scripts/lib/require.js"));


            bundles.Add(new ScriptBundle("~/bundles/requirejs_config")

                .Include("~/Scripts/requirejs_config.js"));


#if DEBUG

            bundles.Add(new ScriptBundle("~/bundles/app")

                .Include("~/Scripts/app/app.js"));

#else

            bundles.Add(new ScriptBundle("~/bundles/app")

                .Include("~/Scripts/app.min.js"));

#endif


            bundles.Add(new StyleBundle("~/Content/css").Include(

                      "~/Content/bootstrap.css",

                      "~/Content/site.css"));


#if DEBUG

            BundleTable.EnableOptimizations = false;

#else

            BundleTable.EnableOptimizations = true;

#endif

        }

    }

}


app.js

require(['./person', 'jquery', 'underscore', 'backbone', 'hbs!../handlebars/person'],

    function (person, $, _, Backbone, personTemplate) {

        var name = person.getName();

        console.log(name);

        console.log('app.js');    

        document.body.innerHTML = personTemplate({ name: 'hyojung' });

});


Command Line

- bower install require-handlebars-plugin


신고
Trackback 0 : Comment 0

ASP.NET Bundling과 RequireJS 함께 사용시 에러 해결 방법

ASP.NET 2014.07.13 08:01

ASP.NET Bundling과 RequireJS를 함께 사용할때의 문제점과 해결방법

- 문제

- RequireJS를 사용한 모듈을 Bundling을 이용해서 로딩하려면 에러 발생

- 원인

   - RequireJS 내부적인 의존성 해결 알고리즘을 이해하지 못하니 당연함.

- 해결방법 두가지

1. 첫번째 해결방법은 자바스크립트 모듈에 모듈이름을 주고 번들링해서 사용하는 것.

- 그러나 별로 권장하는 방법 아님, 왜냐면 모듈이름을 주게 되면 RequireJS의 기능을 아주 일부만 사용하게됨. r.js를 사용해서 옵티마이제이션하는데 있어서도 제약이 생김. RequireJS 커뮤니티에서도 모듈이름을 주는건 별로 권장하지 않음.

2. 두번째 해결방법은 일반접인 SPA에서 하는 방법처럼 r.js를 사용하는것

- nuget패키지르 node.js를 사용하면 로컬머신에 node.js를 설치할 필요없음

- 아래 빌드 설정을 프로젝트 파일에 추가함

  <Target Name="BeforeBuild">

    <Exec Command=".bin\node Scripts\r.js -o Scripts\build_jsapp.js" />

  </Target>

- require_config.js

requirejs.config({

    baseUrl: '../scripts/app'

});

- build_jsapp.js

({

    baseUrl: '../Scripts',

    name: './app/app',

    out: './app/app.min.js',

    optimize: 'none'

})

- Layout.cshtml

<script type="text/javascript" src="~/Scripts/requirejs_config.js"></script>

         @Scripts.Render("~/bundles/app")

- BundleConfig.cs

bundles.Add(new ScriptBundle("~/bundles/app").Include("~/Scripts/app/app.min.js"));


신고
Trackback 0 : Comment 0

Grunt, Testem, Casper를 이용한 테스트 구성

JavaScript 2014.06.13 23:57

현재 프로젝트 설정의 장점

 - 브라우저에서 앱이 실행되는 코드와 테스트하는 환경을 완벽히 분리할 수 있음

 - 테스텀을 사용하면 Grunt만 사용해서 구성하는 것보다 훨씬 적은 코드가 사용됨

 - CI 모드에서도 사용이 용이

 - 테스텀을 사용하므로 여러가지 브라우저를 동시에 테스트해볼 수 있음

 - grunt-contrib-testem 패키지를 사용하면 글로벌하게 Testem을 NPM 설치할 필요가 없음


package.json

{

  ...

  "devDependencies": {

    "casperjs": "^1.1.0-beta3",

    "connect-livereload": "^0.4.0",

    "grunt": "^0.4.5",

    "grunt-casper": "^0.3.9",

    "grunt-contrib-connect": "^0.8.0",

    "grunt-contrib-testem": "^0.5.16",

    "grunt-contrib-watch": "^0.6.1",

    "grunt-mocha": "^0.4.11"

  }

}


bower.json

{

  ...

  "dependencies": {

    "jquery": "~2.1.1",

    "underscore": "~1.6.0",

    "backbone": "~1.1.2",

    "jquery-mockjax": "~1.5.3"

  }

}



index.html

<!DOCTYPE html>

<html lang="en">

<head>

  <meta charset="UTF-8">

  <title>Functional Test Demo</title>

</head>

<body>

  <div class="main"></div>

  <script src="bower_components/jquery/dist/jquery.js"></script>

  <script src="bower_components/underscore/underscore.js"></script>

  <script src="bower_components/backbone/backbone.js"></script>

  <script src="bower_components/jquery-mockjax/jquery.mockjax.js"></script>

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

</body>

</html>



Gruntfile.js

var lrSnippet = require('connect-livereload')({port:35729});


var folderMount = function folderMount(connect, dir) {

  return connect.static(require('path').resolve(dir));

};


module.exports = function(grunt) {


  grunt.initConfig({

    connect: {

      server: {

        options: {

          port: 8000,

          middleware: function(connect, options) {

            return [lrSnippet, folderMount(connect, '.')]

          }

        }

      }

    },


    watch: {

      options: {

        livereload: true

      },

      html: { files: ['**/*.html'] },

      js: {filse: ['app/**/*.js'] },

      test: {

        files: ['test/**/*.js'],

        task: ['casper']

      }

    },


    /* grunt-contrib-testem enables app to run testem without installing testem globally,

       run with following commands :

         grunt testem or

         grunt testem:ci:test_env_1 or

         grunt testem:run:test_env_1

      - 통합테스트는 실제 브라우저 환경에서 하는 테스트라고 가정하고

        펑셔널테스트는 가상 브라우저 환경에서 하는 테스트라고 가정했을때

        Backbone.js를 사용하는한 통합테스트와 펑셔널테스트는 어차피 테스트 케이스를 따로 작성해야함.

        왜냐하면, 펑셔널테스트를 위해서 Casper를 사용해야하는데(그렇지 않으면 특정 URL을 방문할 방법이 없음,

        Ember.js는 자체 테스트 라이브러리에서 캐스퍼와 같은 URL Navigation을 지원하므로 하나의 테스트로 통합/펑셔널 가능)

        Testem의 개발 모드 테스트에서는 Node.js를 사용할 수 있지만,

        Testem의 CI 모드 테스트에서는 실제 브라우저 환경을 사용할 것이므로 Node.js 기반으로 작성된(Casper를 활용한 테스트) 테스트는 사용할 수 없음.

        만일 통합테스트도 실제 브라우저 환경이 아니고 Casper와 Node.js를 사용한 테스트만 한다면 물론 가능할 것임.

    */

    testem: {

      test_env_1: {

        /*src: [

          'bower_components/jquery/dist/jquery.js',

          'bower_components/underscore/underscore.js',

          'bower_components/backbone/backbone.js',

          'app/app.js',

          'test/test_functional.js'

        ],*/

        options: {

          framework: 'mocha',

          parallel: 3,

          src_files:['test/**/*.js'],

          launchers: {

            Node: {

              command: "grunt casper"

            }

          },

          launch_in_dev: ['Node'],

          launch_in_ci: ['Node']

        }

      }

    },


    casper : {

      functional_test : {

        options : {

          test : true  // will use test API in casperjs

        },

        files : {

          'test/test_functional_results.xml' : ['test/test_functional.js']

        }

      }

    }

  });


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

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

  grunt.loadNpmTasks('grunt-mocha');

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

  grunt.loadNpmTasks('grunt-casper');


  grunt.registerTask('server', ['connect:server', 'watch']);

  grunt.registerTask('test:ci', ['connect:server', 'testem:ci:test_env_1']);

  grunt.registerTask('test:dev', ['connect:server', 'testem:run:test_env_1']);

};


app.js

(function(root){

  root.App = {};


  $.mockjax({

    url: '/restful/user',

    responseTime: 100,

    responseText: '[{ "id": "1", "name": "aaa" },{ "id": "2", "name": "bbb" }]'

  });


  var User = Backbone.Model.extend({});

  var Users = Backbone.Collection.extend({

    model: User,

    url: '/restful/user'

  });


  var UsersView = Backbone.View.extend({

    tagName: 'ul',

    render: function(){

      var self = this;

      if(this.collection){

        this.collection.each(function(model){

          self.addUser(model);

        });

      }

      return this;

    },

    addUser: function(model){

      var userView = new UserView({model: model});

      userView.render();

      this.$el.append(userView.el);

    }

  });


  var UserView = Backbone.View.extend({

    tagName: 'li',

    template: _.template('<span><%= name %></span>'),

    render: function(){

      this.$el.html(this.template(this.model.toJSON()));

      return this;

    }

  });


  var Router = Backbone.Router.extend({

    routes: {

      'user' : 'showUser'

    },

    showUser: function(){

      App.users = new Users;

      App.users.fetch().then(function(){

        App.usersView = new UsersView({collection:App.users});

        App.usersView.render();

        $('div.main').html(App.usersView.el);

      });

    }

  });


  new Router;

  Backbone.history.start();


})(this);


test_functional.js

/* casper에 내장된 test API를 사용할때는 casper 인스턴스를 만들면안됨. 그리고  test.done()을 반드시 호출해야함

   참조 : http://docs.casperjs.org/en/latest/testing.html#test-command-args-and-options */

// var casper = require('casper').create();


casper.test.begin('title test', function suite(test) {

  casper.start("http://localhost:8000/#user");


  casper.wait(200);  // time for loading and executing js scripts


  casper.then(function(){

    test.assertEquals(this.getTitle(), 'Functional Test Demo');

    test.assertTitle("Functional Test Demo", "title is the one expected");

  });


  casper.then(function(){

    test.assertExists('div.main ul');

    test.assertTextExists('aaa');

    test.assertTextExists('bbb');

  });


  casper.run(function() {

    test.done();

  });

});






신고
Trackback 0 : Comment 0

Single Page Application : 프론트엔드와 백엔드 개발자의 협업 방법?

ASP.NET 2014.03.24 16:16

지난 토요일, 대구 영남대에서 마이크로소프트 주관의 커뮤니티 캠프 행사에서 발표를 했다.

원래 주제는 "닷넷 기반의 오픈 소스를 활용한 모던 웹 어플리케이션 개발 전략"인데, 너무 거창했는지, 발표 내용을 오해하는 분들이 좀 계셔서, 여기 블로그에 발표 자료를 올리면서 제목을 조금 바꿔봤다.

사실 슬라이드는 별 내용이 없고 세미나의 대부분을 라이브 코딩 데모로 진행한 터라, 여기에 올릴까 말까 고민을 했는데, 요청하신 분들도 계시고해서 그냥 올린다. 슬라이드보고 너무 실망 마시길~.


Modern Web App.pptx


MS에서 이번 행사를 진행하시느라 너무나 수고가 많으신건 아는데, 지방행사는 조금 신경을 덜 쓰신듯하다.

아무런 벽보나 플래카드, 안내가 전혀 없어서 현장에 내려간 나조차 너무 당황했으니ㅜㅜ

그래도 주말에 먼곳까지 와주신 분들께 너무나 감사드린다.

신고
Trackback 0 : Comment 0

NTVS(Node.js Tools for Visual Studio) 와 Azure

JavaScript 2014.03.03 17:18

Node.js on Azure

NTVS(Node.js Tools for Visual Studio) Azure

 

이번 포스트에서는 NTVS (Node.js Tools for Visual Studio)를 사용해서 Node.js 기반의 웹 어플리케이션을 개발하고 Azure에 배포하는 과정에 대해 다룰 것이다.  

 

본 포스트에서 사용한 개발 환경은 다음과 같다.

l  Windows7

l  Visual Studio 2013

l  Node.js (nodejs.org)

l  MongoDB (http://www.mongodb.org/downloads)

 

MongoDB는 설치 후 미리 실행시켜 놓자.

> cd mongodb\bin

> mongod.exe –dbpath ..\data\db

 

 

GistCamp

지난 포스트(Azure CLI & Git을 이용한 Node.js & Ember.js 웹어플리케이션 배포)에서는 아주 간단한 샘플 어플리케이션을 사용했지만, 이번엔 좀더 상대적으로 큰 규모의 프로젝트인 GistCamp 소스 코드를 사용하겠다.

GistCamp Github의 코드 조각 저장 서비스인 Gist 서비스를 좀더 소셜하게 사용할 수 있는 웹 어플리케이션으로서 오픈 소스로 개발되고 있는 프로젝트이다. GistCamp개발에 사용되고 있는 주요 기술들은 아래와 같다.

l  Backend

: Node.js, MongoDB, Mongoose, Express.js, passport, EJS, socket.io

l  Frontend

: Backbone.js, Marionette.js, Handlebars, Require.js, Bootstrap, LESS

l  Packaging & Build Tools

: NPM, Bower, Grunt

 

실제 운영되는 URL Github 리파지토리는 아래와 같다.

l  URL : http://gistcamp.com

l  소스 : https://github.com/RayKwon/gistcamp

 

[ GistCamp 캡쳐, 즐겨찾기한 Gist를 조회하는 화면 ]

 

소스는 편리한 빌드와 Azure 배포를 위해서 필요한 부분을 수정해서 아래의 OneDrive에 올려두었다.

> https://onedrive.live.com/redir?resid=FA5A8FC921EC0704%21114

 

Node.js Tools for Visual Studio

NTVS Visual Studio를 사용해서 Node.js 코딩을 할 수 있도록 도와주는 일종의 확장팩이다. NTVS를 아래의 주소에서 다운로드 받으면된다.

l  NTVS 다운로드 : https://nodejstools.codeplex.com/

 

설치가 끝나면 Visual Studio (이하 VS)에서 gistcamp 프로젝트를 임포트 한다. VS 메뉴에서 [File] -> [New] -> [Project]를 선택하면 아래 그림과 같이 JavaScript 템플릿에 Node.js 관련 프로젝트 템플릿이 생성되어 있는데, 여기에서 [From Existing Node.js code] 템플릿을 선택하고 다운받은 gistcamp 프로젝트의 경로를 입력하고 프로젝트 이름은 “gistcamp”, 오른쪽 하단의 [Add to source control] 체크박스를 선택한다.


다음 화면에서 다시 다운받은 gistcamp 프로젝트의 경로를 입력한다

 


그리고 Next 버튼을 클릭하여 다음 화면으로 이동하면 아래와 같이 어떤 파일을 시작 파일로 선택할지 묻는 화면이 나오는데 여기서 “server.js”를 선택하도록 한다. 


프로젝트 생성이 끝나면 아래와 같이 임포트된 gistcamp 프로젝트를 솔루션탐색기를 통해서 볼 수 있다. 


이제 NPM을 사용해서 필요한 의존 모듈을 다운로드 받아야 하는데 그전에 먼저 해두어야할 작업이 두가지 있다.

l  Python 설치

l  포트번호 설정

 

먼저 Python을 설치해야 하는데 그 이유는, 명령 프롬프트에서 “node server” 를 통해서 gistcamp 프로젝트를 실행시킬 경우엔 필요 없으나 NTVS 상에서 디버깅하기 위해선 gistcamp에서 MongoDB에 연결하기 위해 사용하고 있는 mongoose 모듈의 의존성 때문에 python이 설치되어 있어야 한다.

Pythonhttp://www.python.org/downloads 에서 다운로드 받을 수 있으며 최신 python3 버전이 아닌 python2 (현재 글 쓰는 시점 기준으로는 python 2.7.6 버전) 버전을 다운로드 받아서 설치하도록 한다. 디폴트로 설치하면 C:/python27 경로에 설치될 것이고 이 경로를 환경 변수의 Path에 등록하도록 한다.

 

다음은 프로젝트를 NTVS 상에서 실행시킬때 사용할 포트 번호를 설정할 차례이다.

GistCamp는 개발 모드에서 사용할 포트 번호를 process.env.PORT 에 셋팅된 값을 사용하거나 셋팅된 값이 없으면 디폴트로 3000번을 사용한다. 사실, 개발 모드에서는 process.env.PORT를 셋팅하지 않기 때문에 보통 3000번을 사용하게 되는데 포트번호가 고정되 있어야만 추후에 GitHub API를 사용할 때 편리하다. 그렇지 않으면 GitHub 웹페이지에서 매번 사용할 포트번호를 변경해야하는 수고로움이 발생한다.

문제는 NTVS process.env.PORT 값을 매번 실행시마다 변경시킨다는 것인데 이를 고정시키려면 아래 그림과 같이 프로젝트 속성 화면에서 [Node.js port] 값을 3000 으로 설정하면 된다. 


이제 NPM을 사용해서 필요한 의존 모듈을 다운로드 받는다.

NTVS GUI 화면을 통해서 필요한 모듈을 다운로드 받을 수 있도록 지원하고 있지만, gistcamp는 이미 package.json에 필요한 모듈들을 모두 기재해 놓았으므로 명령 프롬프트를 통해서 한번에 다운로드 받도록 하자.

> cd gistcamp

> npm install

 

모든 의존 모듈이 다운로드 되었으면 솔루션 탐색기의 npm 가상 폴더에서 다운로드된 의존 모듈들을 확인할 수 있다. 각 모듈들을 클릭해서 펼쳐보면 각 모듈들이 의존성을 갖고 있는 모듈들도 확인할 수 있다. 


 

GitHub API 인증

GistCamp GitHub 에서 제공하는 API를 사용해서 Gist 서비스를 확장시킨 어플리케이션이므로 GitHub API를 사용하기 위한 설정이 필요하다.

먼저 https://github.com/settings/applications로 이동해서 새로운 어플리케이션을 추가한다. [Register new application] 버튼을 클릭하여 아래와 같이 [Application name], [Homepage URL], [Authorization callback URL]을 입력한 후 [Register application] 버튼을 클릭한다. 


그러면 아래와 같이 Client ID Client Secret 가 발생되는데 이는 GitHub API를 사용할 때 필요한 OAuth 인증용 값이다. 


이렇게 얻은 Cleint ID Client Secret 값을 infra 폴더의 config-dev.js 파일을 열어 수정해준다. Client ID 값은 “GITHUB_CLIENT_ID”의 값으로, Client Secret 값은 “GITHUB_CLIENT_SECRET” 값으로 대체해준다. 


 

Bower & Grunt

GistCamp는 프론트엔드의 의존성 모듈을 Bower를 통해서 관리하고 Grunt를 사용해서 자바스크립트, LESS 파일들을 빌드하는데 이 작업은 명령 프롬프트를 통해서 아래와 같이 수행한다.

> npm install bower –g ( 로컬에 Bower가 설치되어 있지 않을 경우 수행)

> cd gistcamp

> bower install

> npm install grunt-cli –g  ( 로컬에 Grunt가 설치되어 있지 않을 경우 수행)

> grunt

 

이제 모든 준비가 끝났다. F5 키 또는 VS 메뉴의 [Debug] -> [Start Debuging]을 사용해서 실행시키면 아래와 같은 로그인 페이지가 뜨고 [Sign in with GitHub] 버튼을 클릭하면 GitHub에 대한 OAuth 인증 절차가 끝난후 본 페이지로 이동될 것이다. 

[ GistCamp의 첫 페이지 화면 ] 

 

[ 로그인 후의 첫화면 ]


디버깅 & 인텔리젼스

VS 상에서 Node.js 코딩의 가장 큰 이점은 디버깅인 것 같다. 브레이크 포인트를 사용해서 Node.js 어플리케이션을 디버깅을 하려면 Node Inspector(https://github.com/node-inspector/node-inspector) 같은 툴을 사용할 수 있지만, 아무래도 IDE 상에서 통합되어 있는 환경이 더 편한 것 같다. 

 

Azure WebSite & MongoDB

GistCampAzure에 배포하기 위해서는 Azure 상에서 사용할 MongoDB 서버를 구축해야 한다. 가상 머신을 사용하는 클라우드 서비스일 경우엔 직접 OS 상에서 구축할 수 도 있지만, 웹사이트를 사용할 경우엔 Azure 스토어에서 MongoDB 서버를 대여해주는 플러그인을 사용할 수 있다.

나는 MongoLab 을 사용하도록 하겠다. 


플러그인이 추가되었으면 연결 문자열을 복사한다. 


복사한 연결 문자열은 infra 폴더에 config-dev.js 파일에서 “MONGO_URL” 부분에 넣어준다.

 


Publishing

Azure에 배포하기 전에 다시 한번 github api를 사용하기 위한 idsecret를 받는다. 이때는 localhost:3000이 아닌 실제로 Azure에 생성할 웹사이트의 이름을 입력한다. 참고로 Azure 웹사이트는 기본적으로 http://사이트이름.azurewebsites.net 과 같이 생성된다.


그리고 MongoDB URL 처럼 config-dev.js 파일의 GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET를 수정한다.

나머지 배포 부분은 이전 포스트와 동일하므로 자세한 배포 방법은 생략하겠다. 간략히 순서를 정리해보면

> cd gistcamp

> npm install azure-cli -g    // azure cli 툴 설치

> azure account download    // azure 계정 인증정보파일 다운로드

> azure account import "인증_파일_경로"    // 인증 정보 로드

> azure site create gistcamp-azure   // 사이트 생성. 사이트 이름은 다르게 할것.

> git add .

> git commit -m 'deploy'

> git push azure master    // 파일 업로드


단 주의할 점이 있다면, 배포 브랜치와 관련된 부분인데, 앞서 MongoDB URL, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET를 변경하였는데 보통 이렇게 개발 모드와 배포 모드에 따라 달라지는 부분은 Git 브랜치를 추가해서 관리하는게 편하다, 다만 이렇게 브랜치를 추가 생성해서 배포할때는 지난번 포스트에서 쓴것처럼 Azure 관리 포털에서 "구성 메뉴"의 "배포" 섹션의 "분기" 값을 반드시 배포하려는 브랜치이름으로 변경해 주어야 한다. 


참고로, NTVS를 사용한다면 VS 의 public 메뉴를 사용해서 바로 배포할 수도 있다. 하지만, VS를 사용한 배포 보다는 git이나 파워쉘을 이용한 배포를 더 권하고 싶다. 그 이유는 실제 프로젝트에서는 대부분 CI (Continuous Integration) 과정을 통해 테스트, 빌드 등의 모든 과정이 끝난후에 클라우드에 자동 배포하게 되므로 IDE 상에서 버튼을 클릭해서 배포할 경우가 별로 없기 때문이다.



 

신고
Trackback 0 : Comment 0

싱글 페이지 어플리케이션에서의 검색 엔진 최적화 (SEO)

JavaScript 2014.02.27 13:55

싱글 페이지 어플리케이션에서의 검색 엔진 최적화 (SEO)

 

웹어플리케이션을 제작하는 방법에 있어 싱글 페이지 어플리케이션 스타일이 유행하면서 자바스크립트의 역할이 그 어느때 보다 커진것 같다.

다이나믹한 화면을 구성할 수 있고 UI의 상태(state)를 유지하기가 수월하다는 점, 한번 화면이 로딩된 후에 화면 전환시에는 서버에서 렌더링할 때보다 속도가 빠르다는 점 등 여러가지 장점이 있다.

그런데, 이런 장접에도 불구하고 딱 한가지 걸리는 점이 있다면, SEO, 즉 검색 엔진에 잘 노출되지 못한다는 약점이 있었다.

이번 포스트에서는 어떻게 이 단점을 극복할 수 있는지 정리해보겠다.

 

주의 : 이 글은 구글과 마이크로소프트의 Bing 엔진의 검색 방식을 기준으로 하였으므로 네이버나 다음의 검색 방식과는 맞지 않을 수 있다.

 

싱글 페이지 어플리케이션(이하 SPA) 에서는 어플리케이션의 URL을 위해 해쉬(#)를 사용하거나 HTML5 History 기능을 사용한다. 어떤 방법을 사용하든 해결 방법은 매우 유사하다.

그에 앞서 구글이 SPA를 검색하는 단계를 정리해보면, 구글 크롤러는,

1.     URL에 해쉬 기호와 느낌표(!)가 들어가 있는지 체크한다. ( i.g. http://example.com/#!/posts/1 )

2.     해쉬뱅(Hashbang, #!) 부분을 “_escaped_fragment_=” 문자로 대체한다. ( i.g. http://example.com/?_escaped_fragment=/posts/1 )

3.     2번에서 대체시킨 새로운 URL로 서버에 다시 요청을 보낸다.

4.     요청에 대한 응답으로 받은 컨텐츠를 대상으로 검색 작업을 수행한다.

 

좀더 자세한 설명은 구글 페이지를 참조 : https://developers.google.com/webmasters/ajax-crawling/docs/getting-started

 

, 해쉬 기호에 느낌표(!)를 덧붙여서 URL을 구성해주면 구글 크롤러가 _escaped_fragment_= 쿼리 문자열로 치환하여 해당 서버에 다시 요청을 한다는 것이다.

그러면 내 어플리케이션의 백엔드에서는 새롭게 치환된 URL 요청에 응답해주면 끝난다.

 

해쉬(#) 기반의 URL을 위한 해결방법

l  샘플 데모 : seo-sample-hashbang.azurewebsites.net

l  샘플 소스 : https://github.com/RayKwon/seo_sample_hashbang

 

위의 샘플 데모에서 “Posts” 링크를 클릭해보면 아래와 같이 Ember.js UI를 처리한 화면, 즉 일반적인 사용자들이 보는 화면이 출력된다. 


반면에 구글 크롤러는 저 URL을 아래 그림과 같이 http://seo-sample-hashbang.azurewebsites.net/?_escaped_fragment_=/posts/1로 해석해서 서버에 요청을 다시 보내고 서버에서는 그에 상응하는 Static HTML 페이지를 보여준다. 


샘플의 서버측 코드는 Node.js를 사용했는데, 아래와 같이 요청 쿼리 문자열에 _escaped_fragment_가 있는지 검색해서 있으면 검색에 사용될 Static한 컨텐츠를 내려보내고, 없으면 프론트엔드에서 자바스크립트가 UI를 처리하도록 하였다.

var isCrawler = function(req, res, next) {

if (req.query.hasOwnProperty('_escaped_fragment_')) {

        var query = req.query._escaped_fragment_;

        var queryArray = query.split('/');

  

        if (queryArray[1] === 'posts') {

                 if (queryArray.length === 2){

                           post.renderPostList(req, res);

                 }else if (queryArray.length === 3) {

                          var options = { post_id : queryArray[2] };

                          post.renderPostDetail(req, res, options);

                 }else{

                          next();

                 }

           } else {

next();

        }

  }else{

    res.render('index');

  } 

};

 

app.get('/', isCrawler, routes.indexSpider);

 

 

HTML5 History 기반의 URL을 위한 해결 방법

구글 크롤러의 SPA 검색 방식은 위의 Hashbang 기반 URL 뿐만 아니라 History API pushState를 사용할 경우에도 그대로 사용할 수 있다.

l  샘플 데모 : http://seo-sample.azurewebsites.net

l  샘플 소스 : https://github.com/RayKwon/seo_sample

 

위의 샘플 데모를 보면 아래 그림과 같이 Hashbang 이 포함되지 않은 좀더 깔끔한 URL을 볼 수 있다. 


그런데 이 경우에는 Hashbang 기호가 없으므로 전체 URL을 서버 URL로 인식하여 _escaped_fragment_ 파라미터가 아래 그림과 같이 URL의 맨 마지막 부분에 추가된다. 


또 추가해 줘야할 작업으로, Hashbang 기호가 URL에 없으므로 구글 크롤러가 _escaped_fragment_ 파라미터를 추가해서 재요청하도록 하기 위해서 HTML head 섹션에 아래와 같이 메타 태그를 추가해 줘야 한다.

<meta name="fragment" content="!">

 

그러면 크롤러는 URL Hashbang 기호가 없더라도 _escaped_fragment_ 파라미터를 URL에 추가하여 서버에 재 요청을 보내게 된다.

 

이번 샘플 코드도 앞선 Hashbang 케이스와 마찬가지로 아래와 같이 Request _escaped_fragment_ 파라미터가 있는지 검색해서 있으면 Static한 페이지를 없으면 프론트엔드의 자바스크립트가 UI를 처리하도록 했다.

var renderIndex = function(req, res, next) {

  if (req.query.hasOwnProperty('_escaped_fragment_')) {

    next();

  }else{

    res.render('index');

  } 

};

 

app.get('/', renderIndex, routes.indexSpider);

app.get('/posts', renderIndex, post.renderPostList);

app.get('/posts/:post_id', renderIndex, post.renderPostDetail);

 

 

Zombie

위의 두가지 옵션, Hashbang을 사용하거나 pushState를 사용하거나 모두 장단점이 있다.

일단 두가지 모두 어플리케이션에서 사용할 모든 URL에 대응해서 Static HTML 페이지를 리턴해줘야하는 부담이 있고,

pushState의 경우엔 아직 몇몇 브라우저에서는 지원하지 못하는 한계가 있다. (http://caniuse.com/#search=history) 


이러한 단점을 극복하기 위해서 PhantomJS(http://phantomjs.org/) 또는 Zombie(http://zombie.labnotes.org/) 같은 가상의 브라우저를 제공해주는 툴을 사용할 수 있는데,

일일이 모든 URL에 대응하는 서버측 코드를 작성하지 말고, _escaped_fragment_ 파라미터가 요청 파라미터에 있을 경우엔

저러한 가상 브라우저 툴이 대신해서 요청 받은 URL을 처리한 후 그 결과를 크롤러에 제공해 주는 것이다.

 

샘플 코드 : https://github.com/RayKwon/seo_sample_zombie

 

샘플 코드에서는 PhantomJS 대신에 Zombie를 사용했는데, 그 이유는 PhantomJS를 사용할 수 있도록 해주는 Node.js 라이브러리들은 대부분

서버 OS PhantomJS가 설치되어 있어야 하는 의존성이 있지만 Zombie의 경우엔 그저 NPM으로 모듈을 설치만 해주면 되기 때문이다.

 

우선 NPM으로 Zombie를 설치한다.

npm install zombie --save

 

그리고 서버측에는 아래와 같이 URL 요청이 들어왔을때 zombie가 스냅샷을 찍어서 크롤러에 응답해준다.

 

/* zombie for crawler */

var browserOpts = {

  debug: true,

  waitFor: 2000,

  loadCSS: true,

  runScripts: true

};

 

var Browser = require("zombie");

 

app.get('/', function(req, res) {

  if (typeof(req.query._escaped_fragment_) !== 'undefined') {

    // this solution only works when hashbang is used

    var fullURL = req.protocol + "://" + req.get('host') + '/#!' + req.query._escaped_fragment_;

    Browser.visit(fullURL, browserOpts, function(e, browser, status){

      var html = browser.html();    

      res.send(html);

    });

  }else{

    res.render('index');

  }

});

 

일일이 URL 요청에 대응해주는 코드를 짜는것 보다 훨씬 심플하면서도 혹시 발생할 수 있는 Cloaking으로 인한 페널티 문제도 해결할 수 있다.

 

Cloaking이슈란 실제 사용자가 웹사이트를 방문했을때 보는 화면의 데이터랑 크롤러가 방문했을때의 데이터가 너무 상이할 경우 이를 부적절한 웹사이트로 인식해서 검색 결과에서 제거하게 되는 것을 말한다.

 

 

테스트

구글 웹마스터 도구는 구글 크롤러가 실제 웹사이트를 크롤링할때 어떠한 데이터를 웹서버로부터 반환받는지 미리 볼 수 있게끔하는 기능을 제공해준다.

아래는 위의 샘플에서 http://seo-sample-hashbang.azurewebsites.net/#!/posts/1 주소를 구글 크롤러가 탐색한 결과이다. 


의도한대로 검색용 데이터가 제대로 뿌려진 모습을 확인할 수 있다.

 

 

결론

위의 방법과 더불어 검색 확률을 높이기 위해선 기존의 검색 최적화 방법들, , 메타 태그의 keywords description 등에 충분한 정보를 제공해주고 Sitemap 을 제공해주는 등의 방법들도 당연히 제공해야할 것이다.

 

여기서 제시한 세가지 옵션외에도 Discourse(discourse.org) 처럼 pushState를 사용하면서 따로 검색용 페이지를 제공하지않고 <noscript> 태그 내부에 검색용 데이터를 삽입하는 방법도 있고,

이곳(http://www.ng-newsletter.com/posts/serious-angular-seo.html) 에서 제시한 방법처럼 PhantomJS Zombie를 사용해서 미리 Static HTML 페이지를 만들어놓는 방법도 있다.

 

모두다 장단점이 있기는 하지만 내 생각에는 앞서 제시한 방법중 Zombie를 사용해서(물론 PhantomJS으로도 가능) 크롤러의 요청이 왔을때 최대한 실제 사용자가 보는 데이터와 유사한 데이터를 제공해주는 방법이 최선인듯 보인다.

서버측 코딩의 부담도 줄일 수 있고 Cloaking 의 위험성도 줄이면서 오래된 브라우저에도(IE9이하 버전들) 대처할 수 있는 방법이기 때문이다.

딱 하나 맘에 안드는 점은 pushState를 사용할 때 보다 URL이 조금은 깔끔하지 못하다는 점.

 

신고
tags : JavaScript, SEO, SPA
Trackbacks 6 : Comments 2

Azure CLI & Git을 이용한 Node.js & Ember.js 기반 싱글 페이지 웹어플리케이션 배포

JavaScript 2014.02.18 10:21

[Node.js on Azure]

Azure CLI & Git을 이용한 Node.js & Ember.js 기반 싱글 페이지 웹어플리케이션 배포

 

간단한 CRUD 기능을 가진 Node.js 기반의 싱글 페이지 어플리케이션을 Windows Azure 클라우드에 배포하는 과정을 정리해 보려고 한다.


웹사이트 생성과 배포는 Azure CLI Git 사용 텐데 툴은 Node.js 기반이므로 배포 환경이 OSX이든 윈도우이든 리눅스이든 상관이 없다. 여기에서는 코딩에서부터 Azure 클라우드로의 배포까지 모든 과정을 OSX에서 수행할 것이며 주로 다루게 될 내용들은 아래와 같다.

  • Node.js와 Express.js를 이용한 간단한 RESTful 서비스 작성
  • Ember.js를 이용한 싱글 페이지 어플리케이션 작성
  • Grunt와 Bower를 이용한 프론트엔드 패키지 관리 및 빌드 자동화
  • Azure CLI (Command Line Interface)와 Git을 이용한 배포 방법 


Backend Node.js Express.js 사용해서 간단한 RESTful 서비스를 구현하고 Fronend Ember.js 사용해서 싱글 페이지 어플리케이션 형태로 구현한다그리고 Frontend 자바스크립트와 LESS 코드를 빌드하기 위해서 Grunt 사용하도록 하겠다.

 


어플리케이션의  최종 구현된 모습은 위와 같으며, 실제 실행되는 데모와 소스는 각각 아래의 링크에서 있다.

l  데모 : http://super-simple-blog.azurewebsites.net

l  소스 : Github에서 다운로드 받을 있다.


최대한 코드를 간단히 만들었지만 모든 코드를 일일이 설명하기엔 너무 길어지는거 같아서 여기 포스트에서 따라하기 스타일의 튜토리얼을 제공하지는 않을 것이다. 가능하면 위의 Github에서 소스를 다운로드 받아서 실행해볼 것을 권장한다.

 

미리 준비 해야 것들.

1.      Node.js ( nodejs.org )

2.      Grunt ( npm install grunt-cli –g )

3.      Bower ( npm install bower –g )

4.      Azure Command Line Tool ( npm install azure-cli –g )

5.      Git ( git-scm.com/downloads )

 


프로젝트 생성

먼저 Express.js에서 지원하는 스캐폴딩 기능을 사용해서 프로젝트를 생성한다

> (sudo) npm install express –g

> express –ejs super_simple_blog


 

프로젝트를 생성했으면 생성한 폴더로 이동해서 의존 패키지들을 설치하고 편의상 서버측 시작 파일 이름을 app.js에서 server.js 변경하고 서버를 실행하자.

> cd super_simple_blog

> npm install

> mv app.js server.js

> node server.js



Backend 서비스 구현 

다음은 Express.js 사용해서 간단한 CRUD 기능을 가진 RESTful 서비스를 만든다.

코드를 수정하기에 앞서 lodash 라이브러리를 추가로 설치한다.

> npm install lodash --save


server.js 코드에 CRUD용 RESTful 서비스 네가지를 아래와 같이 추가한다. 

var express = require('express');

var routes = require('./routes');

var http = require('http');

var path = require('path');

var post = require('./routes/post');

 

var app = express();

 

// all environments

app.set('port', process.env.PORT || 3000);

... 코드 생략 ...

app.use(express.static(path.join(__dirname, 'public')));

 

// development only

if ('development' == app.get('env')) {

  app.use(express.errorHandler());

}

 

app.get('/', routes.index);

app.get('/api/posts', post.getAllPosts);

app.put('/api/posts/:post_id', post.editPost);

app.post('/api/posts', post.addPost);

app.delete('/api/posts/:post_id', post.deletePost);

 

http.createServer(app).listen(app.get('port'), function(){

  console.log('Express server listening on port ' + app.get('port'));

}); 


그리고 routes 폴더에 post.js 파일을 새로 생성하고 아래와 같이 간단한 CRUD 기능을 가진 코드를 작성한다. 

var _ = require('lodash');

 

var posts = [

{

  id : 1,

  title : 'Why Discourse uses Ember.js',

  contents : ... 코드 생략 ...,

  image_url : ... 코드 생략...

},

{

  id : 2,

  title : 'Introducing node.js Tools for Visual Studio',

  contents : ... 코드 생략 ...,

  image_url : ... 코드 생략...

}

];

 

exports.getAllPosts = function(req, res){

  res.send(posts);

};

 

exports.editPost = function(req, res) {

  var post = _.find(posts, function(post){ return post.id == req.params.post_id; });

 

  post.title = req.body.title;

  post.contents = req.body.contents;

  post.image_url = req.body.image_url;

 

  res.send(post);

};

 

exports.addPost = function(req, res) {

  var post = {};

 

  post.title = req.body.title;

  post.contents = req.body.contents;

  post.image_url = req.body.image_url;

  var maxPost = _.max(posts, function(post) { return post.id; });

  post.id = maxPost.id + 1;

 

  posts.push(post);

  res.send(post);

};

 

exports.deletePost = function(req, res) {

_.remove(posts, function(post){ return post.id == req.params.post_id; }); 

   res.send(200);

}; 


굳이 데이터베이스까지 사용할 필요 없으므로 단순히 배열에 데이터를 저장하기로 한다.

(MongoDB 연결해서 사용하는 예제 Azure에서 MongoDB 사용하는 방법에 대해서는 다음 포스트에서 정리할 예정임)

 


Bower

백엔드는 의존성 모듈을 관리하기 위해서 npm 사용했지만, 프론트엔드는 Bower 사용해서 의존 모듈들을 관리한다.

Bower 사용하면 기본적으로 bower_components 라는 폴더에 모듈들을 설치하는데, 여기선 public/vendor 폴더를 사용하려고 한다. 이럴 루트 폴더에 .bowerrc 라는 이름의 폴더를 만들어서 폴더를 변경할 있다.

{

   "directory" : "public/vendor",

   "json"       : "bower.json"

}

 

그리고 아래처럼 bower.json 파일에 사용할 모듈들을 기재하고 “bower install” 명령을 통해 모듈을 설치한다. 

{

  ... 생략 ...

  "dependencies": {

    "ember": "~1.3.1",

    "jquery": "~2.0.3",

    "handlebars": "~1.3.0",

    "markdown": "~0.5.0",

    "lodash": "~2.4.1",

    "components-bootstrap": "2.3.2"

  }

}

 

 > bower install

 

싱글 페이지 어플리케이션 형태로 구현할 예정이므로 HTML 파일은 index.ejs 파일 하나로 충분하다. 그리고 Ember.js 사용해서 UI 구현할 것이므로 아래에 보이는 것처럼 HTML 파일의 코드는 매우 간단하다. 

<!DOCTYPE html>

<html>

  <head>

    <title>Super Simple Blog</title>

    <link rel="stylesheet" href="vendor/components-bootstrap/css/bootstrap.min.css" />

    <link rel='stylesheet' href='dist/app.min.css' />

  </head>

  <body>

    <script src="dist/vendor.min.js"></script>     

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

    <script src="dist/templates.js"></script>     

    <% if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { %>

      <script src="http://localhost:35729/livereload.js"></script>     

    <% } %>

  </body>

</html> 



Ember.js

인터넷에서 자바스크립트 프레임웍을 검색해보면 정말 많은 이름들이 나온다.

특히 지난 2년간은 자바스크립트 프레임웍의 홍수라 해도 과언이 아니었다.

나도 즈음에 새로 시작하는 프로젝트를 위해서 수많은 자바스크립트 프레임웍을 대상으로 POC 진행했던 경험이 있는데, 개월간에 걸친 검증 작업 후에도 어떤 프레임웍을 선정해야 할지 난감했던 기억이 있다

그러나 많던 자바스크립트 프레임웍들도 시간이 지나면서 최근 서너개 정도로 정리되는 느낌이다.

개인적인 생각으로는 Backbone.js, Angular.js, Knockout.js, Ember.js 등이 4 이루고 있는듯하다.

Backbone.js Knockout.js 프레임웍이라기 보다는 라이브러리에 가깝고 Angular.js Ember.js 흔히 지칭하는 프레임웍의 범주에 있을 같다.

중에서도 여기에서 다룰 Ember.js 추구하는 방향은 나머지 프레임웍과는 확연히 다르다.

 

Ember.js 여타 프레임웍들과 차별화되는 가장 부분은 URL 모든 기능의 중심에 있다는 점이다.

어플리케이션의 URL 현재 브라우저에서 보여지는 UI 상태를 대변해주며, 사용자가 직접 URL 타이핑하든 어플리케이션 내에서 버튼을 클릭하는 등의 이벤트를 통해서 URL 변경되든, 어떻든 간에 URL UI 매칭시켜주는 것이 Ember.js 핵심 철학이라고 있다.

사실 부분은 실제로 어플리케이션을 개발하다 보면 절실히 필요성을 느끼게 되는 부분인데, URL 해당하는 UI 상태를 관리해주면 UX적인 측면에서도 매우 플러스 요인이 된다.

위에 언급한 라이브러리를 비롯해서 대부분의 자바스크립트 라이브러리는 해쉬 또는 pushState 기반의 라우터 기능을 제공해 준다. 하지만, 그것과 URL 기반으로 UI 상태를 관리하는 것은 다른 문제이다.

 

예를 들어, 사용자가 URL 타이핑하거나 새로고침을 통해서 라우터, 컨트롤러, 순으로 접근하는 경우는 비교적 간단하게 처리할 있다. 하지만, 화면 내에서 어떠한 이벤트(버튼 클릭 ) 발생했을 UI 변경해줘야 하는 경우는 그리 단순한 문제가 아니다.

이벤트는 이벤트대로 처리해 줘야 하고, UI 맞게 브라우저의 URL 업데이트 해줘야 하는 동시에 앞서 URL 먼저 치고 들어온 경우도 염두 해두고 코딩해야 하는데 이러한 부분을 처리하기 위한 코드는 URL 레벨이 깊어지면 깊어질수록 복잡성이 심해진다.

그러나, Ember.js에서는 이러한 부분을 프레임워크에서 자동으로 처리해 주고 있다.

Ember.js 대한 본격적인 설명은 별도의 포스트를 통해서 다시 다루어볼 예정이며 지금은 우선 코드에서 가장 중요한 부분만 간략히 짚고 넘어가도록 하자.

아주 쉬운 CRUD 기능만을 구현 것이어서, 파일을 별도로 나누기 보다는 app.js 파일에 코드를 모두 몰아 넣었다. 

var App = App || Ember.Application.create();

 

App.Router.map(function(){

  this.resource('posts', function(){

    this.route('detail', { path: '/:post_id' });

    this.route('edit', { path: '/edit/:post_id' });

    this.route('new', { path: '/new'});

  });

});


... 코드 생략 ...


일단 Ember.Application.create() 통해 어플리케이션 인스턴스를 생성한 후에 필요한 URL 라우트를 정의하였다.

Ember.js Nested Route라는 컨셉을 제공하는데 코드는 아래와 같은 URL 사용하기 위함이다.

라우트 이름

URL

index

http://localhost

posts.index

http://localhost/#/posts

posts.detail

http://localhost/#/posts/12345

posts.edit

http://localhost/#/posts/edit/12345

posts.new

http://localhost/#/posts/new

 

Ember.js Convention over Configuration 이라는 컨셉에 매우 충실하다. 라우터, 컨트롤러, 템플릿의 이름은 Ember.js에서 정한 컨벤션에 맞게 생성 해야 하며, 만일 명시적으로 생성하지 않으면 Ember.js 알아서 자동 생성시킨.

예를 들어, posts 리소스의 경우에는 PostsRoute라는 라우트 핸들러, PostsController라는 컨트롤러, posts 라는 템플릿을 생성해준다물론 명시적으로 개발자가 생성해 주면 생성한 객체를 사용한다.

 

다음은 URL http://localhost/#/posts 처리할 라우트 핸들러이다.

App.PostsRoute = Ember.Route.extend({

  model: function () {

    return $.getJSON('/api/posts');

  }

}); 


Route 객체는 model이라는 프로퍼티를 가지고 있는데 녀석은 해당 라우트가 실행될 최초 한번 수행된다. 주로 백엔드에서 데이터를 가져오는 용도로 사용한다. 여기서 반환하는 객체는 추후에 템플릿에서 바인딩할 데이터로 사용된다. 

posts 라우트는 컨벤션에 따라 posts라는 Handlebars 템플릿을 찾아서 렌더링한다. 그리고 템플릿이 렌더링 되는 곳은 application 이라는 Handlebars 템플릿의 {{outlet}} 이라고 선언된 부분에 렌더링 된다.

application 템플릿은 Ember.js 기본적으로 가장 먼저 찾아서 렌더링 하는 기본 템플릿이.

마찬가지로 나머지 라우트들 ‘posts.index’, ‘posts.detail’, ‘posts.edit’, ‘posts.new’ 그에 상응하는 템플릿들은 posts 템플릿에 선언된 {{outlet}} 부분에 렌더링된다. posts 라우트가 나머지 라우트들을 포함(nesting)하고 있기 때문이다

 


Grunt

싱글 페이지 어플리케이션은 UI와 관련된 모든 기능을 프론트엔드에서 처리하므로 자바스크립트 코드와 파일의 양이 상당히 많다.

그로 인해 자바스크립트 파일의 빌드 작업(여러 파일을 하나로 합치고, 공백을 없애고 변수의 이름을 짧게 만드는 등의 작업) 자동화 해주는 툴의 필요성이 대두되었고, 현재는 Grunt 가장 많이 사용되고 있다.

 

Grunt 사용법은 Grunt 홈페이지를 참조하고 여기서는 개발 과정 동안 사용할 설정과 배포시에 사용할 설정에 대해서만 간략히 설명하겠다.

Gruntfile.js 파일의 아랫부분을 보면 아래와 같이 태스크를 등록한 부분이 있는데, 

grunt.registerTask('dev_watch', ['emberTemplates:dev', 'uglify:vendor', 'uglify:dev', 'less:dev', 'watch']);

grunt.registerTask('dist_build', ['emberTemplates:dist', 'uglify:vendor', 'uglify:dist', 'less:dist']); 


dev_watch 개발 과정동안 사용할 태스크이고, dist_build 배포시에 사용할 태스크이다.

dev_watch 태스크는 먼저 Hanldebars 템플릿과 자바스크립트 파일, LESS 파일들을 컴파일 한다. 그러고 파일들이 수정되거나 추가되는지 모니터링하고 있다가 변경이 있으면 자바스크립트와 LESS 파일들을 자동으로 컴파일 하고 브라우저를 새로 고침해준다. 소스를 고치고 일일이 브라우저에서 새로고침을 해줄 필요가 없다.

따라서 개발하는 동안에는 아래처럼 서버를 구동시킨 dev_watch 태스크를 실행시켜 주면된다. 

> node server.js

> grunt dev_watch 


샘플에서는 자바스크립트 파일이 app.js 파일 하나이지만 대부분 실제 프로젝트에서는 여러가지 파일이나 모듈별로 나누어서 개발하게 된다.

그럴때 한가지 아쉬운 점은 디버깅할때 압축된 하나의 파일을 상대로 디버깅하려면 파일 사이즈가 커서 어느 지점에서 브레이크 포인트를 걸어야할 찾느라 시간을 소비하게 된다.

그러나 샘플의 uglify 태스크에서처럼 sourceMap 옵션을 사용하면 브라우저에서 디버깅할 압축된 app.min.js 파일이 아닌 개발자가 분리한 파일을 그대로 있으므로 디버깅 시간을 단축 시킬 있다.

 

그리고 최종 배포시에는 아래와 같이 빌드하여 배포한다. 

> grunt dist_build

 

빌드된 자바스크립트와 CSS 파일들은 dist 라는 폴더에 저장된다.

 


Azure

이젠 본격적으로 프로젝트를 Azure Web Site 클라우드 서비스로 배포해보자.

먼저 azure command line tool 설치한다. 

> (sudo) npm install azure-cli –g

 

주의할 점은 Azure 사용자 계정을 가지고 있어야 하며, Azure Web Site 한번이라도 생성한 적이 있어야 하며 그렇지 않으면 커맨드라인 툴로 배포 없다.

Azure 가입은 이곳(http://www.windowsazure.com/)에서 하도록 한다.

 

Azure 커맨드라인 툴을 사용하기 위해서는 먼저 Azure Subscription 정보가 기재된 파일을 다운로드 받아야 한다. 

> azure account download

 

그러면 아래와 같이 브라우저에서 웹사이트가 열리고 자신의 계정으로 로그인하면 파일을 다운로드 받는다.

다운로드가 완료되었으면 아래와 같이 명령어를 수행하여 다운로드 받은 파일로부터 정보를 로드한다. azure import 명렁어 다음의 파라미터는 다운받은 파일의 경로이다.

이제 프로젝트를 업로드할 사이트를 Azure 생성한다.

소스가 위치한 프로젝트의 디렉토리로 이동한 , 위의 그림처럼 명령어를 수행하면 어떤 지역의 서버에 배포할 것인지를 묻는데, 나의 경우엔 East Asia 지역의 서버를 선택하였다.

super-simple-blog라는 웹사이트를 생성하였고 http://super-simple-blog.azurewebsites.net 이라는 URL 접속할 있다.

포스트를 보고 따라하고 있다면, 물론 웹사이트 이름을 다르게 설정해야 것이다.

 

위의 과정을 통해 azure 커맨드라인 명령어는 아래와 같은 몇가지 작업을 수행한다.

첫번째, Azure 웹사이트 생성

두번째, ‘git init’ 명령어 수행. 이를 통해 현재 로컬 디렉토리를 git 환경으로 인식한다.

세번째, iisnode.yml 이라는 파일 생성. Azure 배포된 Node.js 프로젝트는 IIS 서버 기반에서 실행되는데, iisnode.yml 파일에는 이와 관련된 각종 설정 정보가 들어가 있다. 예를 들어, node_env 환경 설정 정보 .

네번째, Git으로 배포할때 사용할 원격지 리파지토리인 azure 라는 이름의 원격 리파지토리 생성.

다섯번째, .gitignore 파일 생성.

 

이제 소스를 업로드할 마지막 단계만 남았다. 아래 그림과 같이 git 명령어를 사용하여 커밋할 파일을 추가하고 커밋한다.

Git 명령어에 익숙하지 않다면, Pro Git 웹사이트(http://git-scm.com/book/ko) 일독하기를 권하고 싶다.

이제 커밋한 파일을 azure 리모트 리파지토리에 업로드(push) 한다.

 

그리고 브라우저에서 http://웹사이트이름.azurewebsites.net 접속해서 제대로 배포가 되었는지 확인한다.

 


Azure 배포시 주의할점.


process.env.PORT

Azure node.js 어플리케이션을 호스팅할 사용할 포트를 80, 90, 8080 같이 셋팅하지 않는다. “process.env.PORT” 라고 지정해 주어야지만 제대로 인식한다.

 

Git Branch

앞서 git 명령어로 azure 원격 리파지토리에 배포할때 “git push azure master” 같은 명령어를 사용하였다. 이는 master 브랜치를 원격에 있는 azure 리파지토리에 배포하겠다는 의미인데. 만약 master 브랜치가 아니라 임의의 다른 브랜치의 소스를 배포하려면, 예를 들어 production 이라는 브랜치를 만들었다고 가정하면, “git push azure production” 같이 실행해 주어야 하며 그전에 Azure 관리 포털(manage.windowsazure.com) 들어가서 구성메뉴의 배포할 분기값을 “production” 이라고 반드시 변경해 주어야 한다.



정리

이렇게 Node.js, Ember.js 기반의 웹 어플리케이션을 Azure에 배포하는 과정을 간략히 정리해 보았다. 아마존이나 여타 클라우드 서비스들 대부분 여러가지 배포 방법을 지원한다. 그 중에서도 Azure는 정말 다양한 방법을 제공하는것 같다. FTP, Visual Studio, Git, 심지어 드랍박스로도 배포할 수 있다.

특히 주로 맥 OSX 를 많이 사용하는 프론트엔드 개발자에게는 다양한 OS와 배포 방법을 지원하는 Azure의 서비스 지원이 매우 반갑다.


 

신고
Trackback 0 : Comments 2

티스토리 툴바