Ember.js – Summary of core concept and usage

현재 개발하고 있는 Single Page WebApp의 기반 프레임워크로 Ember.js를 사용중이다. 한번 작업을 할 때는 대강 구조를 알겠다 싶었지만 몇년을 손에 익힌 언어와 프레임웍이 아니다보니 자꾸 헷갈리게 된다.  다시 한번 머리속의 내용을 정리하는 차원에서 기록해둔다.

대부분의 핵심 내용은 Ember.js 사이트에서 자료를 구했다.  여기에서는 업무를 진행하기 위해 알고 있어야 하는 부분들만 추려봤다. 좀 더 상세한 자료는 아래 사이트에서 찾아보면 상세한 자료를 얻을 수 있다.

Ember.js: https://guides.emberjs.com/v2.4.0/getting-started/core-concepts/

Ember.js Workflow

Ember의 간단한 흐름은 아래와 같다.  시나리오는 사용자가 http://localhost:4200/rentals 라는 URL로 접근하는 경우이다.

  • 라우터로 “rentals”를 등록한다.  이때 묵시적으로 “/” 라우터도 등록된다.
  • /rentals URI로 접근을 위한 로직 처리를 Ember.Router 객체가 제공한는 Method를 Override하는 방식으로 처리한다. 여기에서는 템플릿에서 참조하는 데이터 모델을 제공하기 위해 model() 메소드를 Overriding했다.
  • 실제 UI를 HBS(Handlebars)를 이용해서 그린다.
  • 세부 영역의 UI 핸들링을 위해서 Component를 활용한다.

이제 각각에 대한 세부적인 부분들을 살펴보도록 하자.

App Directory Structure

Ember를 사용하는 앱 관점에서 다음과 같은 디렉토리 구조를 가지고 있다.

ember-directory

  • src/hbs – 라우터 정의에 따른 HBS 파일 경로. 파일명은 라우터의 이름과 동일해야 함.
  • src/js – Ember의 주 실행 코드 영역
    • adapter
    • components
    • controller
    • helpers
    • models
    • routers
    • app.js

이제 세부적인 각 항목의 역할을 아는 만큼 정리해본다.

Core concepts

Router

URI의 각 Path 항목들에 Mapping된 Route를 정의한다.  예를 들어 다음과 같은 URI를 살펴보자.

/family/father/job

이 부분은 다음과 같은 영역으로 구분된다.

  1. / – FamilyIndexRoute
  2. /family – FamilyRoute
  3. /family/ – FatherIndexRoute
  4. /family/father – FatherRoute

각 IndexRoute는 명시적으로 선언하지 않으면 암묵적으로 선언되며 또한 실행된다.  위 구조를 적절히 라우터로 선언하면 아래와 같은 JS 코드로 표현할 수 있다.

App.Router.map(function() {
  this.resource.route(index, { path: '/' });
  this.resource.route('family', { path: '/family' }, function() {
    this.resource.route('father');
});

평범한 거 말고 아래처럼 와일드 카드 정의도 가능하다.

Router.map(function() {
  this.route('catchall', { path:'/*wildcard' });
});

Template

Handlebars(hbs) 템플릿 언어로 기술되고 UI를 그린다.  그릴 때 필요한 데이터는 Model  혹은 Controller를 통해 정의된 속성으로 얻을 수 있다.  HBS 자체도 제어 구조를 제공하며, 이를 활용하면 간단한 반복문이나 예외 케이스등을 처리할 수 있다.

기본적으로는 HTML을 거의 동일하게 사용할 수 있으며, HBS 형식을 이용해 모델 혹은 속성을 참조할 수 있따.

  • Expression – {{firstname}} – 모델의 필드 이름을 적는다.
  • Outlets – {{outlet}} – 다른 템플릿(hbs) 파일의 내용을 outlet 이라는 공간에 들어와서 재활용 가능하도록 한다.  아래의 Index Routing에서 만약 별도의 hbs  파일을 두지 않는다면 outlet이 기본 hbs로 제공된다.

HBS 파일은 별도의 확장자를 가지기 때문에 브라우저를 통해 열어볼 수 없다.  이 부분은 디자인팀과의 협업 관점에서 마이너스 요인이 된다.

Component

Component는 HBS와 맞물려서 UI를 그릴 때 사용한다.

Model

Persistent data model, HBS에서 모델을 이용해서 화면에 내용을 출력한다.  Ember에서 손쉽게 Async ajax 호출을 위한 방안을 제공한다.

  • @each – Object 배열의 특정 element 하나를 지칭하고 싶을 때 사용한다.
  • Ember.observer – 객체내의 특정 변수의 값이 변경된 경우, 이에 대한 비동기 작업을 수행한다.

모델은 Adapter와 연계해서 Ember를 통해 동적 Ajax Request를 보낼 수 있도록 지원한다.

Controller

HBS에서 사용할 속성을 정의한다.  속성은 데이터가 될 수도 있고, 함수가 될 수도 있다.

Route

Route는 URL 접근에 따라 Controller, HBS 실행을 전/후에 실행되어야 할 JS action을 정의한다.  실행은 Route 객체의 이벤트 메소드를 Overriding하여 이뤄진다. 각 메소드의 구현체에서 model 데이터를 얻기 위해 어떤 일을 해야하는지 혹은 다른 URI 위치로 이동해야한다면 이동을 한다든지의 동작을 수행한다.

App.TicketsIndexRoute = Ember.Route.extend({
  model: function(params) {
    var slug = 'tickets';
    var activeTab = params.tab_id || null;
    var blockPromise = this.store.query('slug', {contentType: slug}).then(function(list) {
      return App.DefaultLocaleHelper.getFirstMatchingLocale(list);
    });

    return Ember.RSVP.hash({
      block: blockPromise,
      activeTab: activeTab
    });
  },
  afterRender: function() {
    $(document).foundation();
  }
});

The Application

Ember 앱이 만들어지게 되면 다음과 같이 기본 세가지 파일이 만들어진다.

{{appName}} - {{model.title}}

HBS에서의 변수 참조는 아래와 같이 처리된다.

  • Controller 속성(변수/함수) – 정의된 이름을 직접 참조
  • Model 속성 – “model.속성명”

Controller는 모델 이외에 추가로 정의된 속성을 정의한다.

 
App.ApplicationController = Ember.Controller.extend({
  appName: 'My First Example' 
}); 

라우트에서는 Model을 요청하는 메소드를 후킹해서 title 이라는 신규 속성이 내려가도록 한다.

App.ApplicationRoute = Ember.Route.extend({
  model() {
    return { title: "Hello World" };
  }
});

만약 다른 동작이 실행되길 원한다면 아래와 같이 화면 렌더링이 되기 전에 다른 곳으로 이동되도록 처리할 수도 있다.

App.LeagueIndexRoute = Ember.Route.extend({
  beforeModel: function(transition) {
    var league = this.modelFor('league');
    this.transitionTo('tournament', league.get('mainTournament.title'));
  }
});

 

Dynamic Segments – Path Variable

Path Variable을 사용해야 하는 경우에 이를 처리 하는 방법은 아래와 같다.

var Router = Ember.Router.extend();

Router.map(function(){
  this.route('post', { path: '/posts/:post_id' });
});

위와 같이 :post_id 와 같은 Path Variable을 입력으로 받아들인다면 해당 값은 아래와 같이 참조가 가능하다.

import ajax from 'ic-ajax';

export default Ember.Route.extend({
  model(params) {
    return ajax('/my-service/posts/' + params.post_id);
  }
});

 

Index Routing

만약 /favorite 같이 URI를 잡으면 Ember 내부적으로는 / 에 대한 라우팅과 /favorite에 대한 라우팅 두 개가 모두 선언된 것 같과 같다.  따라서 다음 두개의 코드는 서로 같은 의미를 가진다.

Router.map(function(){
  this.route('favorites');
});
Router.map(function(){
  this.route('index', { path: '/' });
  this.route('favorites');
});

재미있는건 /favorite으로 이동하라는 요청을 받았을 때 Ember가 내부적으로 동작은 아래의 순서를 따른다.

  • 먼저 / 에 대응하는 model, controller, hbs 를 실행한다.
  • 만약 인덱스에 대응하는 hbs가 없으면 outlet으로만 채워진 hbs가 기본으로 제공된다.
  • 그 다음에 /favorite에 대응하는 model, controller, hbs가 실행된다.

 

Advanced Features

Route class member methods for overriding

  • model()
  • setupController(controller) – controller.set(‘model’, model)를 사용해서 Controller 내부에서 참조할 값들을 셋팅할 수 있다.
  • setupController(controller, model) – this.controllerFor(‘otherController’)를 사용해서 다른 Controller의 초기화도 가능하다.
  • renderTemplate() – this.render(‘template_name’)을 활용하거나 확장해서 다른 Controller를 지정하거나 다중 Outlet을 사용할 수 있다.
  • beforeModel()
  • afterModel(posts, transition)
  • activate()

 

Component

To be summarized

Adapter

Ember에서 Adapter는 Model과 꿍짝을 이뤄 API 서버에서 데이터를 받아와 이를 Model Object 만드는 역할을 수행한다.   Ember 환경에서 API 시스템을 통한 데이터 처리는 RESTful API 규격을 따라 GET/POST/PUT/DELETE 처리에 의해 데이터의 가공이 이뤄진다.  조회 요청을 위한 파라미터의 처리는 API의 Path Variable을 통해 처리되는 것을 기본으로 한다.

 

어플리케이션 전체 수준에서 동일한 API를 사용하는 경우에는 src/js/adapter/application.js 파일에 이를 정의함으로써 처리할 수 있다.

export default DS.RESTAdapter.extend({
  namespace: 'api/1',
  host: 'https://api.example.com'
});
  • namespace – URI의 prefix에 해당 문자열을 추가한다.
  • host – API Host를 지정한다.

만약 개별 모델에 따라 이를 정의하고자 한다면 각 모델 이름 뒤에 Suffix로 Adapter를 부여한 객체를 별도로 만들면 된다.  예를 들어 League라는 모델의 경우 아래와 같이 LeagueAdapter를 만들어주면 된다.

(function (App, Ember, DS) {
  App.NavItemAdapter = DS.RESTAdapter.extend({
    namespace: 'api/v1',
    host: Riot.services.api
  });
}(App, Ember, DS));

 

Conclusion

결론은 이러하다.

EmberFlowSummary