반응형
블로그 이미지
개발자로서 현장에서 일하면서 새로 접하는 기술들이나 알게된 정보 등을 정리하기 위한 블로그입니다. 운 좋게 미국에서 큰 회사들의 프로젝트에서 컬설턴트로 일하고 있어서 새로운 기술들을 접할 기회가 많이 있습니다. 미국의 IT 프로젝트에서 사용되는 툴들에 대해 많은 분들과 정보를 공유하고 싶습니다.
솔웅

최근에 받은 트랙백

글 보관함

Sencha Touch 2 Tutorial - Controllers -

2012. 3. 5. 19:19 | Posted by 솔웅


Controllers

Controller는 어떤 event가 일어 났을 때 다른 어떤 동작이 일어날 수 있도록 Control 하는 역할을 합니다. 만약 앱에 로그아웃 버튼이 있다면 유저는 이 버튼을 tap 할테고 Controller는 이 버튼에 대한 tap event를 listening 하고 있다가 이벤트가 발생하면 로그아웃 시키는 동작을 하도록 합니다. 이러한 기능은 View 클래스가 보여 주는 데이터를 변경하도록 할 수 있고 또 Model 클래스가 데이터를 로딩,저장 등의 동작을 할 수 있도록 해 줍니다. Controller는 이 사이에서 그런 동작들이 자연스럽게 이뤄질 수 있도록 Control 해 줍니다.

Relation to Ext.app.Application

Controller는 어플리케이션의 context 안에 존재합니다. 하나의 어플리케이션은 대개 어떤 특정한 부분을 handle 하는 여러 Controller들로 구성 돼 있습니다. 예를 들어 온라이 쇼핑 싸이트에서 주문을 하는 앱을 생각해 보면 이 앱에는 주문과 고객 그리고 상품들에 대한 Controller들이 있을 겁니다.

모든 컨트롤러는 그 어플리케이션의 Ext.app.Application.controller config에서 정해 주시면 됩니다. 이 어플리케이션은 각 컨트롤러를 자동적으로 instantiate 시켜주고 계속 참조하게 됩니다. 그래서 컨트롤러를 직접 instantiate 시키는 상황은 특별한 경우에만 한합니다. convention에 의해 컨트롤러들은 명명되는데 대개 Model 작업 수행 이전에 복수개의 컨트롤러들에 대해 이 작업이 이뤄집니다. 예를 들어 MyApp이라는 앱이 있고 여기에 컨트롤러가 Product를 관리한다면 convention은 app/controller/Products.js라는 파일에 MyApp.controller.Products 라는 클래스를 생성합니다.

Launching

어플리케이션이 launch 될 때 4가지의 주요 단계를 거치게 됩니다. 2가지는 컨트롤러 안에서 행해집니다. 첫번째로 각각의 컨트롤러는 init 함수를 정의할 수 있게 됩니다. 이것은 Application launch 함수 이전에 call 됩니다. 두번째는 Application과 Profile launch 함수가 call 된 이후인데요, 프로세스의 마지막 단계로 컨트롤러의 launch 함수가 call 됩니다.

    Controller#init functions called
    Profile#launch function called
    Application#launch function called
    Controller#launch functions called

대개 Controller-specific launch 로직은 Controller의 launch 함수 안에 있어야 합니다. 왜냐하면 이것은 Application과 Profile launch 함수 이후에 call 되기 때문입니다. 바로 이 시점에 initial UI가 있게 됩니다. 만약 app launch 이전에 Controller-specific processing이 필요하다면 Controller init 함수를 implement 할 수 있습니다.

Refs and Control

컨트롤러에서 중요한 두가지는 refscontrol configuration입니다. 이 둘은 앱의 Component들에 쉽게 reference들을 얻을 수 있게 해주고 어떤 이벤트가 발생하면 이에 대해 어떤 동작이 일어날 수 있도록 해 줍니다. refs 먼저 보겠습니다.

Refs

Refs는 아주 강력한 ComponentQuery 신택스에 영향을 주는것으로 각 페이지에 쉽게 Comopnent들을 위치시킬 수 있도록 합니다. 각 콘트롤러에 대해 원하는 만큼의 refs를 정의할 수 있습니다. 예를 들어 아래 예제에서는 maniNav라는 아이디라는 Component를 찾는 nav라 불리는 ref를 정의합니다. 그 다음에 그 아래에 addLogoutButton 안의 ref를 사용합니다.

Ext.define('MyApp.controller.Main', {
    extend: 'Ext.app.Controller',

    config: {
        refs: {
            nav: '#mainNav'
        }
    },

    addLogoutButton: function() {
        this.getNav().add({
            text: 'Logout'
        });
    }
});

대개 ref는 key/value 조합입니다. 키(위의 경우 nav)는 reference의 이름으로 ref를 생성하기 위해 사용됩니다. 그리고 값(위의 경우 #mainNav)는 ComponentQuery selector로 Componet를 찾을 때 사용 됩니다.

그 아래에 addLogoutButton이라는 간단한 함수를 call 했습니다. 이것은 getNav 함수를 발생시키면서  이 ref를 사용하게 될 겁니다. 이런 getter 함수들은 당신이 정의한 refs를 바탕으로 발생됩니다. 이 함수의 이름은 get 다음에 ref의 대문자(NAV)를 붙여서 이름을 만드는 규칙을 따릅니다. 이 경우엔 nav reference가 툴바이고 함수가 호출 돼었을 때 이 툴바에 Logout 버튼을 추가하도록 했습니다. 이 ref는 아래와 같이 툴바를 인식할 겁니다.

Ext.create('Ext.Toolbar', {
    id: 'mainNav',

    items: [
        {
            text: 'Some Button'
        }
    ]
});

우리가 addLogoutButton 함수를 run 할 때 이미 툴바가 생성돼 있었다고 가정합시다. (이 과정이 어떻게 진행되는지는 나중에 볼 겁니다.) 이 경우에는 두번째 버튼이 생길 겁니다.

Advanced Refs

Refs는 name과 selector 이후에 몇개의 추가 옵션을 설정할 수 있습니다. autoCreate, xtype 등이 그것인데 대부분 같이 사용 됩니다.

Ext.define('MyApp.controller.Main', {
    extend: 'Ext.app.Controller',

    config: {
        refs: {
            nav: '#mainNav',

            infoPanel: {
                selector: 'tabpanel panel[name=fish] infopanel',
                xtype: 'infopanel',
                autoCreate: true
            }
        }
    }
});

이제 우리의 Controller에 두번째 ref를 add 했습니다. 마찬가지로 이름이 key 입니다. 이경우에는 infoPanel이 되겠죠. 여기서는 value(값) 대신 object를 pass 하고 있습니다. 조금 더 복잡한 selector query도 있죠? 한번 상상해 보세요. 앱이 tab panel을 가지고 있고 그 tab 판넬의 아이템 중 하나가 이름이 fish라구요. 위 예제의 selector는 tab panel 아이템 안에 infopanel이라는 xtype을 가진 Component를 match 할 겁니다.

이 예제에서 좀 다른 점은 infopanel이 fish panel 안에 존재하지 않는다면 Controller안에서 this.getInforPanel을 call 할 때 자동적으로 생성하게 될 겁니다. 그 이유는 이 컨트롤러의 selector가 아무것도 return 하지 않고 event 시 instantiate 하도록 xtype을 제공하기 때문입니다.

Control

refs config와 짝을 이루는 것이 바로 control입니다. 콘트롤은 콤포넌트에 의해 일어나는 이벤트나 Controller의 어떠한 react를 listening 하는 수단입니다. Control은 자신의 key로 Component ComponentQuery selectors 와 refs를 받습니다. 아래 예제가 있습니다.

Ext.define('MyApp.controller.Main', {
    extend: 'Ext.app.Controller',

    config: {
        control: {
            loginButton: {
                tap: 'doLogin'
            },
            'button[action=logout]': {
                tap: 'doLogout'
            }
        },

        refs: {
            loginButton: 'button[action=login]'
        }
    },

    doLogin: function() {
        // called whenever the Login button is tapped
    },

    doLogout: function() {
        // called whenever any Button with action=logout is tapped
    }
});

이 예제에서 우리는 두개의 control을 정의했습니다. 하나는 loginButton ref를 위한 것이고 다른 하나는 logout 기능을 할 한 버튼을 위한 겁니다. 이 각각의 정의에 하나의 이벤트 핸들러를 넣었습니다. 바로 tap 이벤트에 대해 listening 하도록 했습니다. 이 버튼들에 tap 이벤트가 발생하면 어떤 동작이 일어날 겁니다. tap 다음에 doLogin과 doLogout이 있죠? 이건 함수 이름입니다. 어딘가에 이 함수가 있고 그 안에 필요한 기능들이 코딩 돼 있을 겁니다. 중요한 부분이죠.

각각의 control 정의할 때 원하는 만큼의 이벤트 리스너를 달 수 있습니다. 그리고 키로서 key로서 ComponentQuery selector와 refs를 섞어서 매치할 수 도 있습니다.

Routes

Sencha Touch 2에서 Controller는 route를 direct하게 명시할 수 있습니다. 이것은 앱 안에서 history support 제공을 가능하게 하며 앱 내에 어떤 페이지든지 직접 링크를 걸어서 가도록 할 수 있습니다. 이것이 route를 제공하기 때문에 가능한 일들 입니다.

예를 들어 로그인과 유저 프로파일을 보여주는 일을 담당하는 Controller가 있다고 합시다. 그리고 이 화면이 url로 접근 할 수 있도록 하고 싶다고 합시다. 아래와 같이 하시면 될 겁니다.

Ext.define('MyApp.controller.Users', {
    extend: 'Ext.app.Controller',

    config: {
        routes: {
            'login': 'showLogin',
            'user/:id': 'showUserById'
        },

        refs: {
            main: '#mainTabPanel'
        }
    },

    // uses our 'main' ref above to add a loginpanel to our main TabPanel (note that
    // 'loginpanel' is a custom xtype created for this application)
    showLogin: function() {
        this.getMain().add({
            xtype: 'loginpanel'
        });
    },

    // Loads the User then adds a 'userprofile' view to the main TabPanel
    showUserById: function(id) {
        MyApp.model.User.load(id, {
            scope: this,
            success: function(user) {
                this.getMain().add({
                    xtype: 'userprofile',
                    user: user
                });
            }
        });
    }
});

위에서 명시한 routes는 간단하게 브라우저 address bar를 Controller 함수로 매핑 시킵니다. 이 routes는 login route 같이 http://myapp.com/#login에 매치 되는 간단한 텍스트가 될 수도 있습니다. 또는 http://myapp.com/#user/123 같이 url에 매치되는 user/:id route 같이 wildcard를 가지고 있을 수도 있습니다. 주소가 바뀔 때마사 콘트롤러는 명시된 함수를 자동적으로 call 합니다.

showUserById 함수에서 유저 인스턴스를 첫번째로 로드하는 부분을 주의해서 보세요. route를 사용할 때마다 이 함수는 그것과 관련한 데이터나 상태 저장에 대해 완료하는 책임을 갖는 route에 의해 불려지게 됩니다. 유저가 이 url을 다른 사람에게 전달할 수도 있고 아니면 단순하게 그 페이지를 refresh 할 수도 있기 때문입니다. 그래서 지금 로드 했던 것들을 cache에서 지워버릴 필요가 있기 때문입니다. route와 관련해서 restoring state에 대한 좀 더 자세한 설명은 application architecture guides에서 보실 수 있습니다.

Before Filters

마지막으로 Routing의 context내에서 Controller가 제공하는 것은  routes를 작성하기 이전에 작성되는 filter 함수 정의 before 입니다.

Ext.define('MyApp.controller.Products', {
    config: {
        before: {
            editProduct: 'authenticate'
        },

        routes: {
            'product/edit/:id': 'editProduct'
        }
    },

    // this is not directly because our before filter is called first
    editProduct: function() {
        //... performs the product editing logic
    },

    // this is run before editProduct
    authenticate: function(action) {
        MyApp.authenticate({
            success: function() {
                action.resume();
            },
            failure: function() {
                Ext.Msg.alert('Not Logged In', "You can't do that, you're not logged in");
            }
        });
    }
});

유저가 http://myapp.com/#product/edit/123같이 url로 navigate할 때마다 컨트롤러의 authenticate 함수가 불려질겁니다. 그리고 만약 before 필터가 존재하지 않을 경우 Ext.app.Action을 pass할 겁니다. Action은 단순하게 Controller, function(이 예제의 경우 editProduct)를 표현합니다.  rmflrh url에 있는 ID 같은 다른 데이터도 표현합니다.

이제 필터는 동기적으로나 비동기적으로 필요한 일을 할 수 있습니다. 이 예제의 경우에는 어플리케이션의 유저가 제대로 로그인 정보를 주었는지에 대한 authenticate 함수를 실행합니다. 이 동작은 서버에 있는 정보와의 비교가 필요하기 때문에 AJAX request를 사용할 것이고 비동기적으로 작동될 겁니다. 인증이 성공하면 action.resume() 함수를 call 해서 다음 동작을 이어 가게 됩니다. 만약 인증되지 않으면 다시 로그인 하도록 해야 합니다.

before 필터는 어떤 특정 action이 행해지기 이전에 추가적인 클래스들을 load하기 위해 사용 될 수 있습니다. 예를 들어 어떤 동작은 드물게 실행 되어서 필요한 상황이 될 때까지 로딩을 하지 않고 싶은 경우가 있을 수 있습니다. 그러면 애플리케이션이 시작할 때 좀 더 빠르게 시작되게 할 수 있겠죠. 이를 위해 간단히 필요할 때 로드될 수 있도록 Ext.Loader를 사용하면 됩니다.

각 action에 대해 원하는 만큼의 before 필터가 명시 될 수 있습니다. 1개 이상의 필터를 사용하려면 배열을 pass 해 주면 됩니다.

Ext.define('MyApp.controller.Products', {
    config: {
        before: {
            editProduct: ['authenticate', 'ensureLoaded']
        },

        routes: {
            'product/edit/:id': 'editProduct'
        }
    },

    // this is not directly because our before filter is called first
    editProduct: function() {
        //... performs the product editing logic
    },

    // this is the first filter that is called
    authenticate: function(action) {
        MyApp.authenticate({
            success: function() {
                action.resume();
            },
            failure: function() {
                Ext.Msg.alert('Not Logged In', "You can't do that, you're not logged in");
            }
        });
    },

    // this is the second filter that is called
    ensureLoaded: function(action) {
        Ext.require(['MyApp.custom.Class', 'MyApp.another.Class'], function() {
            action.resume();
        });
    }
});

이 필터들은 순서대로 call 됩니다. 그리고 다음 단계로 가기 위해서 반드시 각각 action.resume()을 call 해야 합니다.


Profile-specific Controllers

Superclass, shared stuff:

Ext.define('MyApp.controller.Users', {
    extend: 'Ext.app.Controller',

    config: {
        routes: {
            'login': 'showLogin'
        },

        refs: {
            loginPanel: {
                selector: 'loginpanel',
                xtype: 'loginpanel',
                autoCreate: true
            }
        },

        control: {
            'logoutbutton': {
                tap: 'logout'
            }
        }
    },

    logout: function() {
        // code to close the user's session
    }
});

Phone Controller:

Ext.define('MyApp.controller.phone.Users', {
    extend: 'MypApp.controller.Users',

    config: {
        refs: {
            nav: '#mainNav'
        }
    },

    showLogin: function() {
        this.getNav().setActiveItem(this.getLoginPanel());
    }
});

Tablet Controller:

Ext.define('MyApp.controller.tablet.Users', {
    extend: 'MyApp.controller.Users',

    showLogin: function() {
        this.getLoginPanel().show();
    }
});

===== o ===== o ===== o ===== o ===== o =====

오늘도 개념적인 부분을 많이 다뤘습니다. 다음시간에는 View에 대해서 다룰 겁니다.
제목에서 알 수 있듯이 다음 시간에는 코딩하고 결과를 눈으로 확인하면서 할 수 있을 겁니다.
어쨌든 오늘 다룬 Controller도 아주 중요한 부분이니까 어느정도 이해 될 때까지 읽어 보고 다음으로 넘어가야겠습니다.


반응형

Comment

  1. nabiko 2012.07.29 19:39

    input box에 쓰여있는 글의 길이에 따라 이벤트를 동작시키려고 하는데 아무리 검색해도 tap 이벤트 밖에 이벤트가 나와있질 않아서 질문드립니다.저런 경우라면 어떤 이벤트를 key로 설정해주는게 좋을 까요?

    • 솔웅 2012.09.04 13:32 신고

      자바스크립트를 보시면 input box에 글이 입력될 때 이것을 catch 하는 이벤트가 있을 겁니다.
      그 이벤트를 걸어서 input box 안에 있는 value를 체크해서 그 길이를 체크하고 원하는 동작을 하도록 하시면 될 것 같습니다.
      저도 센차터치는 많이 해보지를 않아서 센차터치에 관련한 이벤트가 따로 있는지는 모르겠는데요.
      자바스크립트에는 그런 작업을 많이 해 봤습니다.

      관련 소스를 찾아보시면 금방 찾으실 수 있을 거예요.

      감사합니다.