자바스크립트/Ext JS2016. 4. 30. 16:40

ExtJS Grid CRUD 프로그래밍 강좌입니다.


http://benneykwag.github.io/lecture/2016/04/13/grid-crud.html

Posted by 베니94
자바스크립트/Ext JS2016. 4. 15. 11:42

eui프레임웍은 다국어 및 메시지를 위한 처리가 적용되어 있습니다. 일반적인 다국어는 extjs가 사용하는 컴포넌트의 기본 메시지나 레이블을 의미하며 이러한 처리의 변경은 sdk내부의 여러 국가별 로케일 파일을 적용하여 해결합니다. eui프레임웍에서의 다국어는 Ajax통신을 통해 얻어온 메시지를 또는 레이블을 컴포넌트에 쉽게 적용할 수 있는 방법을 제시합니다.


강좌 확인은 아래 링크를 클릭하세요.


http://benneykwag.github.io/lecture/2016/04/07/%EB%8B%A4%EA%B5%AD%EC%96%B4%EC%B2%98%EB%A6%AC.html

Posted by 베니94
자바스크립트/Ext JS2016. 4. 15. 10:03

Eui 프레임웍은 ExtJS 6를 기반으로 하는 프레임웍입니다. 아직 프레임웍으로서의 완전한 형태를 제공하지 못하고 있지만 한국 실정에 맞는 컴포넌트와 라이브러리를 제공하려고 합니다.

Eui 프레임웍의 모든 파일은 깃허브에 공유하고 지속적으로 업데이트 합니다.

http://benneykwag.github.io

https://github.com/benneykwag/eui

샘플사이트 : http://benneykwag.github.io/eui/samples/Eui.sample/index.html

많은 관심 부탁드립니다.

Posted by 베니94
자바스크립트/Ext JS2014. 8. 18. 01:11

ExtJS 5 멀티테마와 로케일 설정 

 

이번 강의는 애플리케이션에 내에서 테마를 변경하는 방법과 다국어를 설정하는 방법에 대해 알아볼 것이다. ExtJS5는 총4개의 테마와 모바일용 touch용 까지 세분화해 총6개의 테마를 지원한다. 일반적으로 한개의 테마만 사용하여 시스템을 구축할 것이다. 그러나 때에 따라 테마를 변경하거나 모바일에서 접근할 경우를 대비 neptune-touch, crisp-touch테마로 전환할 필요가 있다. 

또한 한국어를 포함한 영어, 일어 등 애플리케이션 내부에서 언어를 선택할 수 있도록  다국어 지원 방법을 알아보도록 하자. 


 

 

이 강좌는 KitchenSink앱을 분석하고 분석 내용을 공유한다. 더 자세한 내용을 알고 싶다면 KitchenSink앱을 자세히 분석해 보길 바란다.  

 

이번 강좌는 아래 환경에 맞춰 진행한다. 

Sencha CMD : 5.0.1.231 

ExtJS : 5.0.1 

 

테스트용 App생성 

테스트용 앱을 생성할 것이다. 이를 위해 Sencha CMD가 필요하다. 버전5에 이르러 Sencha CMD는 시작과 끝을 이어주는 필수적인 도구로 사용된다.  

 

필자는 앱이 생성될 루트에 workspace를 생성하고 진행하였다. 이렇게 하면 모든 앱마다 존재하는 ext폴더는 workspace루트에 하나만 존재하고 이하 모든 앱이 이를 공유하는 형태로 개발되게 된다. 빌드의 결과물 또한 workspace/build 폴더에 저장된다.

 sencha app watch  사용할 경우 이 workspace가 웹루트로 작동  것이다. 

  

workspace를 생성하자. 

sencha generate workspace ./workspace 

 

생성된 workspace폴더로 이동하고 App을 생성하자. 

sencha generate app -ext KitchenSink ./kitchensink 

 

 

 

 

Manifest 지정하기 

 

아래 코드는 앱루트의 index.html 파일이다. 굵은 <script.. var Ext = Ext||로 시작하는 부분이 새로 추가한 부분이다. 

Ext.beforeLoad 함수를 구현하고 있다. 이 함수는  bootstrap.js파일 내부에서 호출되며 Microloader클래스 내부에서 앱실행에 필요한 로드과정 전에 Ext.beforeLoad함수가 존재하면 실행되도록 했다. 

 

<!DOCTYPE HTML> 

<html manifest=""> 

<head> 

    <meta http-equiv="X-UA-Compatible" content="IE=edge"> 

    <meta charset="UTF-8"> 

    <title>KitchenSink</title> 

    <!-- The line below must be kept intact for Sencha Cmd to build your application --> 

    <script type="text/javascript"> 

        var Ext = Ext || {}; 

        Ext.repoDevMode = true; 

        Ext.beforeLoad = function(tags){ 

            var theme = location.href.match(/theme=([\w-]+)/), 

                    locale = location.href.match(/locale=([\w-]+)/); 

            theme  = (theme && theme[1]) || (tags.desktop ? 'crisp' : 'crisp-touch'); 

            locale = (locale && locale[1]) || 'en'; 

            Ext.manifest = theme + "-" + locale; 

            Ext.microloaderTags = tags; 

        }; 

    </script> 

    <script id="microloader" type="text/javascriptsrc="bootstrap.js"></script> 

</head> 

<body></body> 

</html> 


 
Ext.beforeLoad함수 내부에서 우리는 theme와 locale을 정하고 이를 Ext.manifest에 설정하고 있다앱에 대한 세부사항을 설정하는 app.json 파일은 Sencha CMD에 의해 런타임시 Ext.manifest에 전달하게 된다앱은 런타임시 이 manifest의 내용을 분석하여 앱실행에 관련된 모든 컨텐츠를 로딩하게 되는 것이다. 

app.json파일 수정 

이 파일은 앱에 대한 세부사항을 지정하는 파일이다. 이 파일은 Sencha CMD에 의해 생성되고 소비된다. 이 파일의 내용에 의해 Sencha CMD는 런타임에 사용할 Microloader내부의 Manifest에 전달한다. 

 

override설정 

app.json파일 내부의 override설정은 기본값이 ${app.dir}/override이다override는 app폴더내부 클래스를 재정의하여 클래스를 변경하고 적용할 수 있다다국어를 지원하기 위한 locale설정 또한 기존 클래스 변수의 내용을 재정의하는 것으로 가능한 것이다. 

 

override설정을 아래와 같이 변경하자. 

  "overrides": "${app.dir}/overrides,${app.dir}/locale/${app.locale}", 


 

아래 그림은 실제 적용 시 폴더구조의 모습이다. 

 

 

requires설정 

app.json내부 requires설정은 추가 패키지를 설정하고 다운로드 한다. 차트나 로케일 설정은 기본 패키지에 제외되어 있으므로 설정에 포함해야 한다. 

 "requires": [ 

    "ext-locale" 

], 


 

css설정 

 

기본 값은 "path":"bootstrap.css"일 것이다. 이 파일은 개발모드에서 테마파일의 위치를 알려주는 파일이다. 

 

 생성 후 최초의 모습은 아래와 같다. 

 

@import '../ext/packages/ext-theme-neptune/build/resources/ext-theme-neptune-all.css'; 

 

패스를 잘 확인해 보자. workspace 아래 ext(sdk)폴더를 가리키고 있다. 이 파일은 sencha app watch명령이나 sencha app build명령에 의해 아래처럼 바뀔 수 있다. 

 

sencha app watch 

@import '../build/development/KitchenSink/resources/KitchenSink-all.css'; 

 

watch 모드가 작동하면 bootstrap.css파일 내부는 위와 같이 변경된다watch명령은 개발을 위한 pre build와 jetty서버를 지원한다. build 폴더의 위치는 workspace\build이고 개발 모드를 위한 development폴더 아래 해당 앱이름으로 폴더가 생성되게 된다. 

 

sencha app build 명령 이후 bootstrap.css 파일은 아래처럼 변경된다 

 

sencha app build 

@import '../build/production/KitchenSink/resources/KitchenSink-all.css'; 

 

workspace/build폴더 아래 배포를 위한 production 폴더가 생성되고 해당 앱이름으로 압축과 최적화를 마친 파일들이 위치하게 된다. 

 

다국어 지원을 위해 우리는 app.json내부 css설정을 변경해야 할 것이다. 일반적인 경우 이 설정을 변경할 필요가 없을 것이다. 그러나 다국어를 가능하게 할 경우 해당 언어별로 빌드가 여러 번 이루어져 실행되는 환경이기 때문이다.  

 

"css": [ 

    { 

        "path": "${build.out.css.dir}/KitchenSink-all.css", 

        "bootstrap": true 

    } 

], 


 

테마파일의 위치를 동적으로 변경하기 위해 변수처리했다build.out.css.dir변수는 해당 앱이 빌드 된 후 테마파일이 위치할 resources폴더를 의미한다. 

 

bootstrap설정 

 

bootstrap설정은 삭제하자. 이 설정은 bootstrap.json, boostrap.css파일 등을 생성하게 되는데 우리는 이 설정을 대체하는 설정을 추가할 것이다. 

 // 아래 주석처리할 것. 

/*"bootstrap": { 

    "base": "${app.dir}", 

    "manifest": "bootstrap.json", 

    "microloader": "bootstrap.js", 

    "css": "bootstrap.css" 

},*/ 

 

"manifest": { 

    "name": "${build.id}.json", 

    "bootstrap": "${build.id}.json" 

}, 


 

manifest설정은 각 테마와 로케일 별로 조합하여 생성하게 된다. 아직 구체적인 모습이 연상되지 않을 것이므로 $build.id변수에 의해 json파일이 여러 개 생성된다는 것만 알아 두자. 

 

output설정 

 

이 설정은 Sencha CMD의 3가지 output에 대한 위치를 지정한다. output은 production, development, testing으로 각각 배포, 개발, 테스팅의 용도로 파일이 생성된다. 기본 설정은 아래와 같다. 


"output": { 

    "base": "${workspace.build.dir}/${build.environment}/${app.name}" 

}, 


 

${workspace.build.dir}은 workspace/build이거나 workspace를 생성하지 않았다면 해당 앱 이하 build폴더를 의미한다. 

${build.enviroment}는 위에서 설명한 3가지 빌드 모드를 의미하며 실행명령에 따라 다음과 같이 달라질 것이다. 

 

sencha app watch     : workspace/build/development 

sencha app build        : workspace/build/production 

sencha app build -c testing : workspace/build/testing 


 

우리 의도에 맞도록 변경해야 한다. 핵심은 ${build.id}이다.   

"output": { 

    "base": "${workspace.build.dir}/${build.environment}/${app.name}/${build.id}", 

    "page": "../index.html", 

    "manifest": "../${build.id}.json", 

    "deltas": { 

        "enable": false 

    }, 

    "cache": { 

        "enable": false 

    } 

}, 


 

locale설정 

 

필요한 언어를 설정하자. 우리는 한국어와 영어를 지원하도록 설정하자. 

 "locales": [ 

    "en", 

    "ko" 

], 


 

builds 설정 

 여러 종류의 테마를 적용할 수 있다. 이 설정 내부의 테마는 locales설정과 함께 곱하기 하여 빌드된다. 

 "builds": { 

    "classic": { 

        "theme": "ext-theme-classic" 

    }, 

    "gray": { 

        "theme": "ext-theme-gray" 

    } 

    "crisp": { 

        "theme": "ext-theme-crisp" 

    }, 

    "crisp-touch": { 

        "theme": "ext-theme-crisp-touch" 

    }, 

    "neptune": { 

        "theme": "ext-theme-neptune" 

    }, 

    "neptune-touch": { 

        "theme": "ext-theme-neptune-touch" 

    } 

}, 


 

build.id는 위의 테마와 로케일을 모두 생성해 낸다아래 예를 참고하자. 

 1. classic-en 

 2. classic-ko 

 3. gray-en 

 4. gray-ko 

 5. crisp-en 

 6. crisp-ko 

 7. crisp-touch-en 

 8. crisp-touch-ko 

 9. neptune-en 

 10.  neptune-ko 

 11. neptune-touch-en 

 12.  neptune-touch-ko 


 

테마와 언어별로 각각 빌드를 생성한다면 2개의 언어 * 6개의 테마를 곱하여 총 12개의 빌드가 각기 생성될 것이다. 

 

소스코드 

 

설정을 마쳤으니 테마변경과 로케일 설정을 확인하기 위한 소스코드를 생성하자. 

이 클래스는 Main.js내부에 추가되고 테마와 언어를 변경할 수 있게 한다. 

 Ext.define('KitchenSink.view.Header', { 

    extend: 'Ext.Container', 

    xtype: 'appHeader', 

    id: 'app-header', 

    title: 'Ext JS Kitchen Sink', 

    height: 52, 

    layout: { 

        type: 'hbox', 

        align: 'middle' 

    }, 


    initComponent: function() { 

        document.title = this.title; 


        this.items = [{ 

            xtype: 'component', 

            id: 'app-header-logo' 

        },{ 

            xtype: 'component', 

            id: 'app-header-title', 

            html: this.title, 

            flex: 1 

        }]; 


        if (!Ext.getCmp('options-toolbar')) { 

            this.items.push({ 

                xtype: 'themeSwitcher' 

            }); 

        } 

        this.callParent(); 

    } 

}); 


 
Header클래스 내부에서 테마와 로케일을 설정하는 ThemeSwitcher클래스를 생성하자. 

 Ext.define('KitchenSink.view.ThemeSwitcher', function() { 

    var theme = location.href.match(/theme=([\w-]+)/), 

        locale = location.href.match(/locale=([\w-]+)/); 

 
 

    theme = (theme && theme[1]) || (Ext.microloaderTags.desktop ? 'crisp' : 'crisp-touch'); 

    locale = locale && locale[1] || 'en'; 

 
 

    if (!Ext.themeName && !!theme) { 

        var m = theme.match(/^([\w-]+)-(?:he)$/); 

        Ext.themeName = m ? m[1] : theme; 

    } 

 
 

    return { 

        extend: 'Ext.Container', 

        xtype: 'themeSwitcher', 

        id: 'theme-switcher-btn', 

        margin: '0 10 0 0', 

        layout: 'hbox', 

 
 

        initComponent: function() { 

            function setQueryParam(name, value) { 

                var query = Ext.Object.fromQueryString(location.search); 

                query[name] = value; 

                location.search = Ext.Object.toQueryString(query); 

            } 

 
 

            function makeItem(value, text, paramName) { 

                paramName = paramName || "theme"; 

 
 

                var checked = value === (paramName == "theme" ? theme : locale); 

 
 

                return { 

                    text: text, 

                    group: (paramName == 'theme' ? 'themegroup' : 'localegroup'), 

                    checked: checked, 

                    handler: function () { 

                        if (!checked) { 

                            if(paramName == 'theme') { 

                                setQueryParam('theme', value); 

                            } else { 

                                setQueryParam('locale', value); 

                            } 

                        } 

                    } 

                }; 

            } 

 
 

            var menu = new Ext.menu.Menu({ 

                    items: [ 

                        makeItem('neptune',       'Neptune'), 

                        makeItem('neptune-touch', 'Neptune Touch'), 

                        makeItem('crisp',         'Crisp'), 

                        makeItem('crisp-touch',   'Crisp Touch'), 

                        makeItem('classic',       'Classic'), 

                        makeItem('gray',          'Gray'), 

                        '-', 

                        makeItem('en',            'English',    'locale'), 

                        makeItem('ko',            '한국어',     'locale') 

                    ] 

                }); 

 
 

            this.items = [{ 

                    xtype: 'component', 

                    id: 'theme-switcher', 

                    cls: 'ks-theme-switcher', 

                    margin: '0 5 0 0', 

                    listeners: { 

                        scope: this, 

                        click: function (e) { 

                            menu.showBy(this); 

                        }, 

                        element: 'el' 

                    } 

                }]; 

 
 

            this.callParent(); 

        } 

    }; 

}); 


 
Main 클래스 내부에서 Header클래스를 호출하도록 수정한다.  

 Ext.define('KitchenSink.view.main.Main', { 

    extend: 'Ext.panel.Panel', 

    xtype: 'app-main', 

    requires: ['KitchenSink.view.Header'], 

    controller: 'main', 

    viewModel: { 

        type: 'main' 

    }, 

    layout: { 

        type: 'border' 

    }, 

    items: [{ 

        xtype: 'appHeader', 

        region : 'north' 

    },{ 

        region: 'center', 

        xtype: 'tabpanel', 

        items:[{ 

            title: 'Tab 1', 

            html: '<h2>Content appropriate for the current navigation.</h2>' 

        }] 

    }] 

}); 


 
index.html 내부에서 기본 테마와 로케일 설정은 crisp-en일 것이다물론 desktop일 경우를 예로 한다이후 ThemeSwitcher에 의해 언어를 영어에서 한글로 변경하면 한글로 전환 되는 것을 확인하기 위해 앱루트에 locale폴더를 생성하고 Header클래스를 아래와 같이 생성할 것이다. 이 클래스는 한글을 포함하고 있다. 

한글 지원 Header클래스의 위치는 /locale/ko/view/Header.js이다. 

 Ext.define('KitchenSink.locale.view.Header', { 

    override: 'KitchenSink.view.Header', 

    title: '안녕하세요 ^^' 

}); 


 

한가지 더 테스트 하자. 위의 Header클래스는 한글을 지원하기 위해 기존 app폴더의 Header클래스를 override하고 있다. 이것은 일반적인 override와 동일하므로 일반적인 override도 같이 테스트 하자. 

 

app.json설정을 통해 앱루트 아래 override폴더에 해당 클래스를 위치 시켜보자. 

/override/view/main/Main.js 

 Ext.define('KitchenSink.override.view.main.Main', { 

    override: 'KitchenSink.view.main.Main', 

    title: '오버라이드 테스트 KitchenSink.override.view.main.Main' 

}); 


 

이 클래스는 로케일 설정과 별개로 기존 Main클래스의 title설정을 대체 할 것이다.  

 

이제 Header클래스가 실행될 경우 필요한 css를 준비하자. ExtJS는 시스템에 css를 적용하기 위해 sass를 이용한다. 앱루트의 sass폴더에 각 클래스에 적용할 css를 sass형태로 코딩하여 추가하면 빌드시 테마에 적용될 것이다. 

 

/sass/src/view/Header.scss파일을 추가하자. 이 파일은 테마별로 다른 css스타일을 적용하는 코드다. 

 $kitchensink-header-background-color: #333; 

$kitchensink-header-text-color: #fff; 

$kitchensink-header-text-shadow-color: null; 

$kitchensink-header-use-gradient: true; 

 
 

@if $theme-name == 'ext-theme-neptune' or $theme-name == 'ext-theme-neptune-touch' { 

    $kitchensink-header-background-color: #81af34; 

    $kitchensink-header-text-shadow-color: #4e691f; 

} 

 
 

@if $theme-name == 'ext-theme-classic' or $theme-name == 'ext-theme-gray' { 

    $kitchensink-header-background-color: darken($base-color, 55%); 

} 

 
 

@if $theme-name == 'ext-theme-crisp' or $theme-name == 'ext-theme-crisp-touch' { 

    $kitchensink-header-background-color: #2a3f5d; 

    $kitchensink-header-use-gradient: false; 

} 

 
 

#app-header { 

    background-color: $kitchensink-header-background-color; 

    @if $kitchensink-header-use-gradient { 

        @include background-image(linear-gradient(top, lighten($kitchensink-header-background-color, 5), darken($kitchensink-header-background-color, 5))); 

    } 

border-bottom: 1px solid darken($kitchensink-header-background-color, 15); 

 
 

    @if $theme-name == 'ext-theme-neptune' { 

        .#{$prefix}nlg & { 

            background-image: url(images/header-bg.png); 

        } 

    } 

} 

 
 

#app-header-title { 

    padding: 15px 0 10px 0; 

 
 

    color: #fff; 

    font-size: 18px; 

    font-weight: bold; 

    @if $kitchensink-header-text-shadow-color != null { 

        text-shadow: 0 1px 0 $kitchensink-header-text-shadow-color; 

    } 

} 

 
 

#app-header-logo { 

    width: 40px; 

    height: 25px; 

 
 

    background: url(images/logo.png) no-repeat center 4px; 

} 

 
 

#theme-switcher-combo-labelEl { 

    color: $kitchensink-header-text-color; 

    font-weight: bold; 

} 

 
 

.ks-theme-switcher { 

    background-image: url(images/options.png); 

    width: 22px; 

    height: 22px; 

} 


 
위의 파일에서 로딩하는 각종 이미지 파일은 kitchenSink예제의 resources/images폴더를 복사하도록 하자. 

 

최종 폴더 구조는 그림과 같다. 


 

 

최종 빌드  

 

이제 마지막 빌드과정이 남았다. 해당 앱루트로 접근하고 sencha app build 명령을 수행하자. 위에서 언급했듯이 총6개의 테마와 2개의 언어가 모두 빌드되려면 총 12번의 빌드과정을 거치게 되어 상당한 시간이 소요될 것이다. 

 

 

빌드가 완료되면 workspace/kitchensink폴더 내부를 확인하자. 그림처럼 총 12개의 json파일이 테마와 로케일 설정별로 생성된 것이 보일 것이다. 이 총12개의 파일이 app.json의 설정에 의해 생성된 manifest이고 이 파일별로 테마와 언어가 다르게 실행되는 것이다. 


 

 

그렇다면 최종 빌드파일은 어떤 모습인지 확인해야 할 것이다. sencha app build명령은 production 빌드이므로 workspace/build/production/kitchensink폴더에 빌드파일을 저장했을 것이다. 

 


 

상당히 많은 량의 파일이 생성되었음을 볼  있다. 각 테마와 로케일 별로 총12개의 개별 빌드 폴더가 생성되어 독립적으로 실행가능 될 수 있다. 

 

완료된 결과를 브라우저를 통해 확인하자. 

workspace루트로 이동하여 sencha web start 명령을 실행하자. 


 

 

오른쪽 아이콘을 클릭해 언어를 한국어로 변경하면 기존 영문이 한글로 표시될 것이다. 


 

 

 

 

 

 

 


Posted by 베니94
자바스크립트/Ext JS2014. 1. 4. 01:25

1. 모델클래스(Ext.data.Model)

Model클래스는 우리가 ExtJS에서 사용하는 데이터의 최소 단위라 할 수 있다. Model은 데이터를 표현하기 위한 틀로서 데이터베이스의 Table과 일대일로 매핑된다고 볼수 있다. 우리가 데이터베이스의 어떤 테이블을 읽어올지 관심을 갖듯 ExtJS에서 표현되는 데이터는 어떤 모델을 참조해야할지 관심을 가져야한다. 이는 하나의 엔티티로 애기할 수 있다. 모델은 필드, 검증, 관계 등을 설정할 수 있고 데이터를 읽어오거나 , 수정하거나, 삭제할수 있으며, 입력할수 있다. 하나의 데이터베이스 테이블에서 처리해야할 데이터를 모두 다룰 수 있는 클래스이다.


1-1모델의 정의

모델을 정의하기 위해 Ext.data.Model클래스를 상속 받았다. fields설정에 데이터를 표현할 name과 type을 명시하였다.

#1. name은 데이터베이스의 필드명과 동일하게 취급한다. 서버에서 전달한 데이터에 표기한 이름과 동일해야 한다.

#2. type는 해당 데이터의 형식을 말한다. int 뿐만 아니라 string, boolean, date 등이 있다.

Ext.define('Board', {

    extend : 'Ext.data.Model',

    fields : [ {                         // #1

        name : 'sequence',       // #2

        type : 'int'                    // #3

    }, {

        name : 'title',

        type : 'string'

    }, {

        name : 'userName',

        type : 'string'

    }, {

        name : 'role',

        type : 'string'

    }, {

        name : 'content',

        type : 'string'

    }, {

        name : 'createDate',

        type : 'date',

        dateFormat : 'Y.m.d'

    }, {

        name : 'updateDate',

        type : 'date',

        dateFormat : 'Y.m.d'

    }, {

        name : 'readCnt',

        type : 'int'

    }, {

        name : 'deleteYn',

        type : 'boolean',

        defaultValue: false

    } ]

});


1-2모델의 검증

Model클래스는 데이터를 담는 그릇 역할을 하므로 그릇에 담을 적절한 데이터인지 검증할 수 있도록 validations 설정을 지원한다. 기존 코드에 validations설정을 추가하자.

validations: [

    {type: 'presence',  field: 'title'},                                            // #1

    {type: 'length',    field: 'content',     min: 2, max:10},            // #2

    {type: 'inclusion', field: 'deleteYn',   list: [true, false]},             // #3

    {type: 'exclusion', field: 'role', list: ['Admin', 'Manager']},      // #4

    {type: 'format',    field: 'userName', matcher: /^[ㄱ-힣"'\\{\\}\s]+$/}   // #5

]


Model클래스가 지원하는 validations은 위의 코드에서 보듯 5가지이다.

#1. presence : 값이 채워져 있어야 한다.

#2. length : 길이를 통해 검증한다. min은 최소, max는 최대 값으로 이 사이에 있어야 한다.

#3. inclusion : 포함하는지 검증한다. list속성 내부에 포함해야 할 값을 여러 개 입력할 수 있다.

#4. exclusion : 포함하지 않는지 검증한다. list속성 내부에 포함하면 안 되는 값을 여러 개 입력할 수 있다.

#5. format : 정규식을 사용하여 한글만 입력되도록 했다.


모델을 생성하고 각 필드에 데이터를 기입한다.

var board = Ext.create('Board', {

    sequence : 1,

    title : '안녕하세요^^',              

    userName : '홍길동2',              

    content: '게시물 내용을 입력합니다.',    

    role : 'Admin',                 

    readCnt : 300,

    deleteYn: false                 

});


생성된 모델을 validata메소드를 통해 검증한다. 아래 코드는 검증결과 통과하지 못한 필드가 몇 개인지 알아보기 위해 items.length를 출력해 보았다.

console.log('검증 훌 발견된 오류수 :', board.validate().items.length);


검증에 통과하지 못한 내용을 확인하는 코드이다.

Ext.each(board.validate().items, function(item){

    console.log(item)

});


코드를 실행하면 아래와 같이 검증 결과를 확인할 수 있다.

검증 결과 3개의 오류가 발견되었고 각각의 오류를 Ext.each문으로 확인하였다.


발견된 3개의 오류를 수정해 보자. Model클래스는 내용을 확인할 때 get메소드, 수정 할때 set메소드를 사용한다. 이를 이용하여 board 모델 객체의 내용을 수정하여 오류를 정정해 보도록 하자.

board.set('content', '게시물내용 입력'); // #1

board.set('role', 'User'); // #2

board.set('userName', '홍길동'); // #3


console.log('---- 수정 된 내용 확인 ----')

console.log('content : ', board.get('content'));

console.log('role : ', board.get('role'));

console.log('userName : ', board.get('userName'));


var errors = board.validate();

console.log('검증 후 발견된 오류수 :', errors.length, '검증 통과여부 : ', errors.isValid());


Ext.each(errors.items, function(item){

    console.log(item)

});


#1. content필드는 길이 검증에 실패하였으므로 최소2, 최대10 안에 들도록 길이를 줄였다.

#2. role필드는 포함되지 않는 조건검증에 실패하였다. ‘Admin’,’Manager’에 포함되지 않도록 ‘User’로 변경하였다.

#3. userName필드는 오직 한글만 입력 받도록 하는 조건 검증에 실패하였으므로 이전 이름에 숫자를 제거하였다.


실행 결과 검증을 모두 통과하고 validations에 아무것도 검색되지 않는 것을 확인했다.



1-3. 프록시를 이용한 입력, 수정하기

모델은 데이터의 최전방에 있는 클래스로 자기 스스로 데이터를 관리할 수 있도록 서버와 통신 할 수 있는 프록시 설정을 가지고 있다. 모델은 데이터베이스의 테이블 정보와 매핑되므로 하나의 모델이 데이터베이스 내부의 하나의 테이블의 데이터를 입력, 수정, 삭제, 읽기 등의 행위를 할 수 있도록 구현된 것이다.


기존 Board모델에 프록시 설정을 추가하자. 프록시는 Ext.data.proxy.Proxy클래스로 데이터 입출력을 담당하며 각종 통신 (Ajax, Rest, JsonP, Direct)을 위한 클래스 이다.

proxy : {

    type : 'ajax',

    actionMethods : {       // #1

        read : 'GET',       // #2

        create : 'POST',    // #3

        update : 'POST',   // #4

        destroy : 'POST'   // #5

    },

    api : {                 // #6

        read : 'boards.json?read',      // #7

        create : 'boards.json?create',    // #8

        update : 'boards.json?update',    // #9

        destroy : 'boards.json?destroy'    // #10

    },

    reader : {             // #11

        type : 'json',

        root : 'entitys'

    }

}


#1. actionMethods는 CRUD(create, read, update, destroy) 처리 시 통신방식을 설정한다. 통신방식은 GET, POST, PUT, DELETE가 있고 이러한 방식은 주로 RESTFUL통신시 각 액션에 맞도록 설정할 수 있도록 한다.

#2. 데이터를 로딩할 때는 GET방식을 사용한다.

#3,4. 데이터를 입력,수정 처리 할 때는 POST방식을 사용한다.

#5. 데이터를 삭제할 때는 DELETE를 사용한다.

#6. api는 CRUD 처리 시 필요한 서버주소를 각기 명시한다.

#7~10. 데이터를 처리시 필요한 서버의 처리주소다. 주소와 ?뒤의 파라메터는 개발자가 임의로 지정한 것이다.

#11. reader는 api의 read액션 처리 시 필요한 설정으로 읽어오는 데이터의 type과 읽어온 데이터가 복수 개 일 경우 root설정을 추가할 수 있다.


api에 의해 CRUD를 처리할 서버페이지를 작성한다. 아래 코드는 서버가 요청을 처리한 이후 클라이언트에서 응답할 메시지를 출력할 뿐이다.(서버의 처리 로직은 생략한다.)


// boards.json

{entitys: [

    {

        "sequence": 33,

        "title": "ExtJS에 대한 문의",

        "content": "ExtJS Model클래스의 Proxy설정에 대해 알아봅니다.",

        "userName": "홍길동",

        "role": 'User',

        "createDate": '2013-12-03',

        "updateDate": '2013-12-04',

        "readCnt": 230,

        "deleteYn": false

    }],

    success: false

}


이제 모델객체를 생성하고 해당 모델을 저장해 보도록 하자 이때의 저장은 생성 된 모델객체의 데이터를 서버에 전달하는 과정을 의미한다.

var board = Ext.create('Board', {

    sequence : 1,

    title : '안녕하세요^^',              

    userName : '홍길동',              

    content: '게시물 내용을 입력합니다.',  

    role : 'User',

    deleteYn: false                

});


저장을 위해 모델클래스의 save()메소드를 호출한다.

board.save({

    success: function (record, operation) { // #1

        console.log('읽어온 데이터 레코드는 : ', record.data)

    },

    failure  : function(record,options){ // #2

        console.log('저장실패');

    },

    callback: function(){ // #3

        console.log('callback 처리 ');

    }

});


#1. 서버와 통신이 성공할 경우 호출된다. 첫 번째 인자는 입력된 모델객체를 리턴하는데 서버에서 응답해준 데이터가 있다면(boards.json) 응답한 데이터를 모델객체로 리턴하고 서버에서 전달해준 데이터가 없다면(boards.json내용을 모두 삭제) ExtJS생성한 모델객체를 그대로 리턴한다.

#2. 서버와 통신이 실패한 경우 또는 저장처리 시 사용자에 의해 인위적으로 실패된 경우이다. 인위적인 실패에 경우 boards.json파일 내부의 success: true를 false로 변경할 경우 이 함수가 호출된다.

#3. callback함수의 구현은 ajax통신이 비동기 통신이므로 순차적인 실행이 어려우므로 callback함수를 구현한다. 저장 작업 이후 처리 로직을 구현한다.


아래 그림은 개발자 도구의 Network탭을 통해 서버와 통신내역을 확인 한 것이다. 



save메소들 실행한 이후 서버와 통신한 결과를 확인하자. api설정의 4가지(CRUD) 중 create를 실행하였고(boards.json?create) 이후 서버에 모델데이터를 전달하였다. 그렇다면 save메소드는 api 중 create에 해당하는 것일까? 꼭 그렇지만은 않다 save()메소드는 update api에서도 사용한다. 

이 때 필요한 속성이 idProperty이다. 이 속성은 데이터를 구분하는 유일한 필드를 설정하는 것으로 데이터베이스에 테이블의 Primary Key와 동일한 개념이다. save()메소드는 idProperty속성이 존재하지 않거나 존재하더라도 모델에 해당 필드의 값이 비어 있을 경우 api의 create를 실행하게 된다. 이와 반대로 idProperty필드에 데이터가 포함되어 있을 경우 update api를 실행시키게 된다.

Board 모델의 유일성을 보장하는 필드는 sequence이다. idProperty속성으로 아래와 같이 지정하자.

Ext.define('Board', {

    extend : 'Ext.data.Model',

    idProperty: 'sequence',


이제 코드를 실행하자. 아래 그림과 같이 서버에 요청한 url은 boards.json?update..로 api 중 update가 실행 된 것을 확인 할 수 있다. idProperty 설정에 따라 입력과 수정이 결정 됨을 명심하자.



참고 : update 실행 시에는 서버에서 응답한 결과를 create와 달리 반환하지 않음을 알아두자.


1-4. 프록시를 이용한 데이터 읽기, 삭제하기

입력과 수정을 알아보았으니 데이터를 읽고 삭제하는 기능을 구현해 보자.

api에 설정된 read 액션으로 아래 코드와 같이 Ext.ModelMgr클래스의 getModel()메소드로 모델 객체를 생성한다.

var board = Ext.ModelMgr.getModel('Board');

load() 메소드로 idProperty인 sequence값을 전달한다.

board.load(33, {

    success: function (record, operation) { // #1

       console.log('읽어온 데이터 레코드는 : ', record.data);  // #2

    }

});


#1. 서버의 요청이 성공하면 success 구문이 실행된다.

#2. 서버에서 응답한(boards.json) 데이터를 출력한다.



읽어오기(load)가 성공하면 success메소드가 실행되고 서버에서 전달해준 데이터를 인자로 전달한다. 전달되는 객체는 모델 객체로 이 객체를 이용하여 다시 서버에 update요청을 할 수 있고 destroy작업을 요청할 수도 있다. 읽기 작업 이후 서버에서 전달받은 모델객체를 통해 삭제작업(destroy)을 진행 해 보자.

success: function (record, operation) {

   console.log('읽어온 데이터 레코드는 : ', record.data)

    record.destroy({

        success: function (record, operation) {

            console.log('삭제 후 서버에서 전달한 결과는 : ', record.data)

        }

    });

}


삭제가 성공하면 읽기와 동일하게 success메소드가 호출되고 서버의 응답이 있다면 모델객체로 반환한다.


이렇듯 하나의 데이터를 읽어오고 삭제하는 작업이 간단히 처리되었다. 모델을 통해 서버에 데이터를 입력처리하고 입력된 데이터를 읽어와 수정하고 삭제 처리가 가능하다.


1-5. 모델간의 관계 설정을 통한 손쉬운 데이터로딩

모델클래스는 데이터베이스 테이블에 비유하였다. RDBMS는 각 테이블 간의 관계를 설정하여 데이터를 표현하고 관리한다. 클라이언트에서 다루는 데이터 또한 데이터베이스의 데이터와 동일하므로 이러한 데이터베이스의 관계설정을 클라이언트에서도 활용할 수 있도록 ExtJS에서도 관계의 설정을 지원하고 있다.

아래 그림과 같이 데이터베이스가 구성되었다고 가정하자. 이러한 데이터베이스 관계를 

ExtJS 모델을 통해 구현하도록 한다. 



우선 아래 데이터베이스 구조에 대한 간략한 관계정보를 정리해 보자.

 테이블명

설명 

 Primary Key

 Foreign Key

 

 MemberMaster

 회원정보

 email

 

 

 MemberDetail

 회원상세정보

 email                >> MemberMaster.email

 

 BoardMaster

 게시판 관리

 boardId

 

 

 BoardContent

 게시물 저장

 contentId

 boardId             >> BoardMaster.boardId

createEmail        >> MemeberMaster.email

 

 BoardReply

 댓글저장

 replyId

 contentId          >> BoardContent.contentId

createEmail        >> MemeberMaster.email

 



ExtJS에서는 모델간의 관계 설정을 위해 아래와 같은 3가지 Type을 지원한다.

  •  일 대 다 (Ext.data.HasManyAssociation)
  •   (Ext.data.BelongsToAssociation)
  •   (Ext.data.association.HasOne)

아래 표는 데이터베이스 테이블간의 관계를 모델의 관계로 풀어서 표현한 것이다. 각 테이블간의 관계는 테이블의 컬럼 정보를 Primary Key와 Foreign Key로 설정하였고 이러한 컬럼 정보는 모델클래스의 field로 전이 되어 각 모델간의 관계를 hasMany, belongsTo, HasOne 등의 3가지로 표현 할 수 있게 된다.

모델관계

MemberMaster

MemberDetail

BoardMaster

BoardContent

BoardReply

hasMany

email



createEmail


hasMany

email




createEmail

hasMany

belongsTo



boardId

boardId


hasMany

belongsTo




contentId

contentId

hasOne

email

email





예제를 통해 3가지 모델관계에 대해 알아보자.

첫 번째는 일대다(Ext.data.HasManyAssociation)관계이다. BoardMaster모델과 BoardContent모델을 구현해 보고 이 두 모델을 일대다(hasMany)관계로 설정하는 코드를 작성한다.


BoardMaster모델 클래스를 작성하자. 위의 ER Diagram을 보고 필드를 추가한다.

Ext.define('MyApp.model.BoardMaster', {

    extend: 'Ext.data.Model',

    fields: [

        {

            name: 'boardId',

            type: 'int'

        },

        {

            name: 'boardName',

            type: 'string'

        },

        {

            name: 'openYn',

            type: 'string'

        },

        {

            name: 'createDate',

            type: 'date',

            dateFormat: 'Y.m.d'

        }

    ]

});


BoardMaster테이블 프라이머리키를 idProperty설정을 통해 설정한다.

Ext.define('MyApp.model.BoardMaster', {

    extend: 'Ext.data.Model',

    idProperty : ‘boardId’

    fields: [


모델 스스로 CRUD가 가능하도록 프록시를 설정한다.

    proxy: {

        type: 'ajax',

        actionMethods: {

            read: 'GET',

            create: 'POST',

            update: 'POST',

            destroy: 'POST'

        },

        api: {

            read: 'data/boardMaster.json?read',

            create: 'data/boardMaster.json?create',

            update: 'data/boardMaster.json?update',

            destroy: 'data/boardMaster.json?destroy'

        },

        reader: {

            type: 'json',

            root: 'data'

        }

    }


BoardMaster모델이 스스로 데이터를 읽어올 때 데이터를 전달할 boardMaster.json파일을 아래와 같이 코딩한다. 이 파일에는 BoartMaster테이블과 일대다로 연결되어 있는 BoardContent테이블의 데이터도 함께 제공 해주고 있다.

{

    success: true,

    data: [

        {

            "boardId": 100,

            "boardName": "ExtJS Q&A",

            "openYn": "Y",

            "createDate": '2013-12-03',

            "contents": [

                {

                    "contentId": 1,

                    "subject": "ExtJS에 대한 1번 질문입니다.",

                    "content":"Model에 대한 궁금증.",

                    "createEmail":"extjs1@mail.com",

                    "createDate": '2013-12-03'

                },

                {

                    "contentId": 2,

                    "subject": "ExtJS에 대한 2번 질문입니다.",

                    "content":"Model에 대한 궁금증.",

                    "createEmail":"extjs1@mail.com",

                    "createDate": '2013-12-13'

                },

                {

                    "contentId": 3,

                    "subject": "ExtJS에 대한 3번 질문입니다.",

                    "content":"Model에 대한 궁금증.",

                    "createEmail":"extjs1@mail.com",

                    "createDate": '2013-12-02'

                }

            ]

        }

    ]

}


BoardMaster모델을 통해 위의 json파일을 호출해 보도록 하자. 아래 코드는 load메소드를 통해 boardId가 11번에 해당하는 BoardMaster테이블의 데이터를 요청하는 코드이다.

MyApp.model.BoardMaster.load(11, {

    success: function (record, operation) {

        console.log(record.data)

    }

});


위의 코드를 실행하고 결과를 확인하자. boardMaster.json파일 내부의 정보를 읽어와 보여주고 있다. 그러나 하위 테이블인 BoardContent(게시물)데이터는 표시하지 않고 있다. 이는 BoardMaster모델에 boardMaster.json파일 내부의 하위 데이터 필드 contents를 모델의 필드로 갖고 있지 않아서 이다.


BoardMaster모델의 필드에 아래와 같이 contents필드를 추가하고 결과를 확인하자.

Ext.define('MyApp.model.BoardMaster', {

   

        {

            name : 'contents'

        }

    ],


boardMaster.json내부의 하위 게시물 정보까지 출력되는 것을 확인했다.


위의 경우는 단순 데이터로서 처리한 것으로 모델의 관계설정과는 관련이 없다는 것을 알 수 있다. 이 경우 처럼 하위 데이터를 한번에 상위 데이터와 묶어서 전달해주면 쉽겠지만 각기 다른 데이터베이스 테이블의 데이터를 묶어서 전달하는 일은 서버의 부하와 구현상의 문제로 쉽게 생각할 부분은 아닌 것이다. 해서 각 모델은 독립적으로 데이터를 핸드링 할 수 있고 서로 관계를 설정하여 상호 작용할 수 있어야 하겠다.


이제 BoardContent(게시물)모델을 작성하고 BoardMaster모델과 관계를 설정하도록 한다.

ER Diagram을 보고 BoardContent모델을 정의하자.

Ext.define('MyApp.model.BoardContent', {

    extend: 'Ext.data.Model',

    idProperty: 'contentId',

    fields: [

        {

            name: 'boardId',

            type: 'int'

        },

        {

            name: 'contentId',

            type: 'int'

        },

        {

            name: 'subject',

            type: 'string'

        },

        {

            name: 'createEmail',

            type: 'string'

        },

        {

            name: 'content',

            type: 'string'

        },

        {

            name: 'createDate',

            type: 'date',

            dateFormat: 'Y.m.d'

        },

        {

            name: 'updateDate',

            type: 'date',

            dateFormat: 'Y.m.d'

        },

        {

            name: 'removeDate',

            type: 'date',

            dateFormat: 'Y.m.d'

        },

        {

            name: 'readCount',

            type: 'int'

        },

        {

            name: 'deleteYn',

            type: 'boolean',

            defaultValue: false

        }

    ]

});


BoardMaster모델에 아래와 같이 일대다(hasMany)관계설정을 추가한다.

    associations: [

        {

            type: 'hasMany',                     #1

            model: 'MyApp.model.BoardContent',  #2  

            name : 'contents'                    #3

        }

    ],


 #1. 일대다 관계를 의미한다.

 #2. associations설정을 가진 BoardMaster모델(일)과 관계(다)를 맺을 모델클래스

 #3. name에 설정된 contents라는 이름으로 store객체를 리턴하게 된다. BoardContent모델은 1개 이상의 데이터를 갖고 있어야 하므로 contents는 store를 의미한다.


참고 : Store에 대해서는 이후에 자세히 다루도록 하자. 여기서는 여러 개의 모델 데이터를 표현하기 위해서 모델집합을 갖는 Store클래스를 사용한다는 점만 이해하자.


위의 설정을 통해 모델간의 관계가 설정되어지고 아래 코드와 같이 모델을 통한 관계정보에 접근 할 수 있다. 기존 실행 코드를 아래와 같이 수정하자.

MyApp.model.BoardMaster.load(11, {

    success: function (record, operation) {

        // record >> MyApp.model.Board모델 객체.(단건의 데이터)

        console.log(' BoardMaster >>');

        console.log('   boardId : ', record.get('boardId'));

        console.log('   boardName : ', record.get('boardName'));

        console.log('   BoardContent >>');

        // Case1

        var associationData = record.getAssociatedData(), idx=0;    // #1

        Ext.each(associationData.contents, function (content) {      // #2

            idx++;

            console.log('   No(',idx,') >>');

            console.log('       contentId : ', content.contentId);    // #3

            console.log('       subject : ', content.subject);        // #4

            console.log('       createEmail : ', content.createEmail);


        });

        // Case2

        var associationStore = record.contents(), idx=0;               // #5

        associationStore.each(function (content) {                    // #6

            idx++;

            console.log('   No(',idx,') >>');

            console.log('       contentId : ', content.get('contentId'));  // #7

            console.log('       subject : ', content.get('subject'));

            console.log('       createEmail : ', content.get('createEmail'));

        });

    }

});


 #1. BoardMaster모델을 로드하고 이를 통해 관계설정 정보를 변수에 전달한다.

 #2. 관계설정 시 사용한 ‘name’으로 접근하면 json파일에서 전달한 하위 데이터에 접근할 수 있다. associationData.content는 배열이다.

 #3,4. 하위 데이터 정보를 출력한다.

 #5. record.content()메소드는 Store객체를 반환한다. 이는 관계설정시 ‘contents’라는 이름을 사용하였고 ExtJs에서는 이 이름으로 접근할 경우 복수의 모델정보를 담고 있는 Store정보를 리턴하도록 해준다.

 #6. Store내부 모델 정보를 each문으로 하나씩 접근하고 #7과 같이 get메소드를 통해 내용을 확인한다.

Case1과 Case2의 결과는 동일하다. getAssociatedData메소드는 다수의 관계설정 정보에 대한 Array데이터 집합을 가진다.


아래 그림을 통해 실행결과를 확인하면 boardMaster.json에서 전달한 두 개의 테이블 정보를 모두 출력하는 것을 볼 수 있다. 이는 모델간의 관계설정으로 인해 두 개의 모델이 연결되었다는 증거이다.


load메소드를 실행 할 경우 아래 그림과 같이 Ajax호출이 이루어지고 전달한 11이라는 id값이 전달되는 것을 볼 수 있다. 이 id값은 모델클래스에 설정된 idProperty:’boardId’를 의미하나 요청 전달 시 boardId가 아니라 id로 전달되는 것을 명심하자.


참고 : 필자는 위와 같은 경우 id라는 이름의 파라메터를 사용하지 않고 idProperty값인 boardId가 전달되도록 하기 위해 override코드를 사용한다. 이 책 실무 코딩 부분에서 코드를 설명하기로 하자.


이번에는 Store를 이용하는 방법을 알아보자. 아직 Store에 대해 배우진 않았지만 Store는 복수개의 모델객체를 담을 수 있는 클래스로 Grid, Tree, View등에서 여러 개의 반복적인 데이터를 표현할 클래스에서 Store를 이용해 데이터를 전달받는다.


BoardMaster모델클래스를 이용하는 Store를 생성한다.

var store = Ext.create('Ext.data.Store', {      // #1

    model: 'MyApp.model.BoardMaster',    // #2

    autoLoad: true,                       // #3

    proxy: {

        type: 'ajax',

        url: 'data/boardMaster.json',       // #4

        reader: {

            type: 'json',

            root: 'data'

        }

    }

});


 #1. Store의 클래스명은 Ext.data.Store이다.

 #2. 적재할 모델을 명시한다.

 #3. autoLoad:true는 생성과 함께 서버에 #4에 해당하는 주소로 요청을 보낸다.

 #4. 복수개의 BoardMaster정보를 리턴한다.


autoLoad설정이 true로 생성과 함께 요청을 보내게 하였다. 이러한 과정을 Store가 load한다고 한다. 이 때 load이벤트가 발생하고 이를 리스닝하여 서버에서 전달받은 결과를 해석하고 처리할 수 있다. 아래 코드는 load이벤트를 리스너에 추가하고 반환된 결과를 이전에 배웠던 코드와 동일하게 코딩하였다.

store.on('load', function () {                 // #1

    store.each(function (record) {           // #2

        console.log(' BoardMaster >>');

        console.log('   boardId : ', record.get('boardId'));

        console.log('   boardName : ', record.get('boardName'));

        console.log('   BoardContent >>');

        var associationData = record.getAssociatedData(), idx=0;       // Array로 리턴.

        Ext.each(associationData.contents, function (content) {

            idx++;

            console.log('   No(',idx,') >>');

            console.log('       contentId : ', content.contentId);

            console.log('       subject : ', content.subject);

            console.log('       createEmail : ', content.createEmail);


        });



        var associationStore = record.contents(), idx=0;               // Author Store

        associationStore.each(function (content) {

            idx++;

            console.log('   No(',idx,') >>');

            console.log('       contentId : ', content.get('contentId'));

            console.log('       subject : ', content.get('subject'));

            console.log('       createEmail : ', content.get('createEmail'));

        });

    });

});


 #1. load이벤트를 리스닝 한다.

 #2. boardMaster.json파일에서 반환한 다수의 결과를 BoardMaster모델로 반환하고 이를 each문으로 풀어낸다. 

코드를 실행하면 이전에 보았던 탐색결과와 동일한 결과를 얻을 수 있다. 만약 boardMaster.json파일 내부에 BoardMaster테이블의 정보가 하나만 존재하지만 이를 복사해 2개 이상으로 늘릴 경우 탐색결과 또한 반복적으로 늘어날 것이다. 


여기까지는 boardMaster.json파일에서 BoardMaster와 BoardContent 두개의 테이블에 데이터를 한꺼번에 제공해주는 경우를 예로 들었다. 모든 일대다의 관계에 있는 테이블 정보를 한꺼번에 반환하는 것은 서버의 부하나 구현상의 어려움이 있으므로 실무에서는 각 모델이 독립적으로 Proxy를 구현하도록 하여 필요에 따라 상위 모델과 하위모델의 관계설정에 의해 그때 그때 데이터를 로드 할 수 있도록 하는 것이 좋겠다.


아래와 같이 BoardContent(하위 모델)모델이 독립적으로 데이터를 로드 할 수 있도록 프록시를 설정하자.

Ext.define('MyApp.model.BoardContent', {

   

    proxy: {

        type: 'ajax',

        actionMethods: {

            read: 'GET',

            create: 'POST',

            update: 'POST',

            destroy: 'POST'

        },

        api: {

            read: 'data/boardContent.json?read',

            create: 'data/boardContent.json?create',

            update: 'data/boardContent.json?update',

            destroy: 'data/boardContent.json?destroy'

        },

        reader: {

            type: 'json',

            root: 'data'

        }

    }

});


게시물 데이터를 제공 할 boartContent.json파일을 준비하자. 

{

    success: true,

    data: [

        {

            "boardId": 11,

            "contentId": 100,

            "subject": "ExtJS에 대한 1번 질문입니다.",

            "content": "Model에 대한 궁금증.",

            "createEmail": "extjs1@mail.com",

            "createDate": '2013-12-03'

        },

        {

            "boardId": 11,

            "contentId": 200,

            "subject": "ExtJS에 대한 2번 질문입니다.",

            "content": "Model에 대한 궁금증.",

            "createEmail": "extjs1@mail.com",

            "createDate": '2013-12-13'

        },

        {

            "boardId": 11,

            "contentId": 300,

            "subject": "ExtJS에 대한 3번 질문입니다.",

            "content": "Model에 대한 궁금증.",

            "createEmail": "extjs1@mail.com",

            "createDate": '2013-12-02'

        }

    ]

}


이제 BoardContent모델도 스스로 데이터를 로드 할 수 있게 되었다. BoardMaster모델에 설정된 관계를 통해 하위 모델에 접근하는 코드를 만들어 보자.

MyApp.model.BoardMaster.load(11, {         // #1

    success: function (boardMasterRecord, operation) {          // #2

        var boardContent = boardMasterRecord.contents(),      // #3

            boardId = boardMasterRecord.get('boardId');        // #4

        console.log(‘상위모델:’, boardMasterRecord.data);

        boardContent.load({                                  // #5

            callback: function (boardContentRecord, operation) {// #6

                console.log(‘하위모델:’, boardContentRecord.data);             // #7

            },

            failure: function () {                              // #8

                console.log('실패..');

            }

        });

    }

});


 #1. BoardMaster모델을 통해 게시판 마스터 정보를 로드한다.

 #2. 로드가 성공하는 경우 서버에서 전달받은 데이터를 모델객체로 변환 전달 받는다.

 #3. 전달받은 모델객체를 통해 관계설정 중 content라는 이름을 찾고 이를 contents()메소드로 접근하고 있다. 이는 Store객체를 반환하게 된다. 일대다의 관계는 한 개의 모델에 복수개의 모델객체를 담고 있는 Store를 통해 표현 된다. 즉 boardContent변수는 Store객체로 복수개의 모델을 탑재하고 있는 것이다.

 #4. 로드 된 모델 객체에서 boardId값을 변수에 전달한다.

 #5. boardContent.load는 Store.load()와 동일하다. 즉 Store를 통해 서버에 데이터를 요청하고 있다. 이는 모델에 설정된 프록시 정보를 이용하여 서버와 통신한다.

 #6. 데이터로드가 끝나면 필수로 실행된다.

 #7. 데이터로드가 실패 시 실행된다.


코드를 실행하고 개발자 도구의 Network탭을 열고 하단 필터를 XHR로 설정하면 2개의 Ajax콜이 이루어진 것을 확인할 수 있다. 첫 번째는 BoardMaster모델에서 한 개의 모델객체를 로드한 것이고 두 번째는 BoardMaster에 설정된 BoardContent모델간의 관계 설정에 의해 Store를 통해 로드한 결과 이다.



개발자 도구의 Console탭을 열고 실행코드에서 원하는 값을 출력하고 있는지 확인하자.


실행 결과를 확인한 결과 이상한 점이 있다. 분명 Ajax전송은 2번에 걸쳐 BoardMaster와 BoardContent 데이터를 가져오는 것으로 확인했다. 그러나 실제 코드에서는 BoardMaster모델이 로드한 결과만 출력 할 뿐 Store를 통해 로드한 boardContent.load()에 대한 callback이 실행되지 않은 것이다. 해답은 BoardMaster모델의 관계설정에 있다. 아래와 같이 BoardMaster모델의 관계설정에 primaryKey를 추가하자. 여기서 추가한 primaryKey는 일대다 관계에서 상위모델의 idProperty 필드를 설정한다.

    associations: [

        {

            type: 'hasMany',

            model: 'MyApp.model.BoardContent',

            name : 'contents',

            // primaryKey는 contents를 통해 ajax load할 경우 필수다.

            primaryKey: 'boardId' 

        }

    ],


코드를 다시 실행하면 아래 그림과 같이 상위, 하위 데이터를 모두 출력하는 것을 볼 수 있다.


다시 개발자 도구의 Network탭을 확인하자. 2번의 Ajax전송에 차이가 있다. 첫 번째 호출은 id값을 전달하지만 두 번째 호출은 id값을 전달하지 않고 있다. 테스트를 위해 정해진 json파일을 이용하므로 데이터를 반환 받게 되지만 실제 서버로직을 구현하고 DBMS로 하위 테이블(BoardContent)에 대한 쿼리실행에 boardId의 값이 있어야 적절한 데이터를 전달 받을 수 있게 된다. 


boardContent데이터를 로드하는 코드를 아래와 같이 수정하자. Store는 load메소드 내부에서 params속성을 통해 서버에 특정 파라메터를 전달 할 수 있도록 한다.

boardContent.load({

    params : {

        boardId : boardId

    },


다시 실행하면 그림과 같이 params에 설정 한 boardId가 서버에 전달된 것을 확인할 수 있다.


이렇듯 일대다 모델간의 관계를 이용하여 상위 모델을 통해 하위모델데이터에 접근할 수 있게되었다.


모델의 관계설정 두 번째로 다대일(Ext.data.BelongsToAssociation) 관계에 대해 알아보자. 

BoardMaster와 BoardContent는 일대다 관계를 맺고 있다. 즉 BoardMaster기준으로 다수의 BoardContent데이터에 접근하는 것이다. 이와는 반대로 다수의 BoardContent데이터 기준에서 한 개의 BoardMaster데이터에 접근 해야 하는 경우가 발생한다. 이 경우 모델의 관계설정이 belongsTo 이다. 이 설정은 BoardContent모델에 필요하다.

아래 코드와 같이 모델내부에 관계설정을 정의하자.

Ext.define('MyApp.model.BoardContent', {

    ….

    associations: [

        {

            type: 'belongsTo',                     // #1

            model: 'MyApp.model.BoardMaster',   // #2

            foreignKey: 'boardId',                 // #3

            getterName: 'getBoardMaster',        // #4

            setterName: 'setBoardMaster'         // #5

        }

    ]

});


 #1. 관계설정 타입을 belongsTo로 설정한다.

 #2. 다대일 관계에서 ‘일’에 해당하는 모델의 클래스명을 설정한다.

 #3. foreignKey는 관계를 설정 할 모델의 idProperty필드로 BoardMaster모델의 boardId를 설정한다.

 #4. getterName은 관계로 설정된 모델에 접근할 때 사용한다. 즉 BoardMaster데이터에 접근할 때 이 이름으로 접근하고 데이터를 얻어온다.

 #5. setterName은 BoardContent모델이 가지는 BoardMaster의 idProperty값을 변경할 때 사용한다. 이 부분은 실행코드에서 자세히 설명하기로 하자.


모델에 관계설정이 왼료되었다. 이제 실행코드를 작성하여 모델관계가 잘 작동하는지 확인하도록 하자.

MyApp.model.BoardContent.load(1, {                  // #1

    success: function (record, operation) {             // #2

        // 실제 ajax콜이 이루어진다.

        // book_id를 99로 변경해 서버에 요청한다.

        record.getBoardMaster({                     // #3

            callback: function (record, operation) {     // #4

                console.log('callback:', record.data);

            },

            success: function (record, operation) {     // #5

                console.log('success:', record.data)

            },

            failure: function (record, operation) {      // #6

                console.log('failure:', record)

            }

        });

    }

});


 #1. BoardContent 모델객체를 로드하자. id값으로 1을 전달한다.

 #2. 로드가 성공하는 경우다.

 #3. ExtJS는 관계설정 시 사용한 getterName인 getBoardMaster라는 이름으로 메소드를 제공한다. 이 메소드를 호출하여 BoardMaster객체를 반환하게 된다.

 #4. callback메소드는 데이터 로드가 완료되면 무조건 실행된다.

 #5. success메소드는 데이터 로드가 성공하면 실행된다.

 #6. failure메소드는 데이터 로드에 실패할 경우 실행된다.


코드를 실행하면 callback메소드와 success메소드가 실행될 것이다.

아래 그림을 통해 실행결과를 확인하자.


두 개의 모델이 관계설정으로 인해 Ajax호출은 어떻게 되는지 개발자 도구의 Network탭을 확인하도록 하자. 


BoardContent모델의 프락시 설정에 의해 boardContent.json파일이 데이터를 제공했다. 이 때 제공한 데이터에 boardId값은 11이다. 이 값은 관계 설정에 의해 BoardMaster모델이 데이터 로드 시 id값으로 제공되게 된다. 즉 관계설정에 의해 2개의 모델이 각각 연관성 있는 데이터를 호출하도록 지원하고 있다.

 

모델 관계설정의 마지막인 일대일(Ext.data.association.HasOne )에 대해 알아보자. hasOne은 하나의 모델데이터와 다른 하나의 모델데이터가 관계를 맺게 된다.

MemberMaster(회원 마스터)와 MemberDetail(회원 상세)의 관계를 생각하자. 회원의 기본정보를 MemberMaster테이블에 저장하고 아주 상세한 내용의 데이터를 MemberDetail테이블에 저장한다면 이해가 쉽겠다. 두개의 테이블은 email필드를 Primary Key로 사용하므로 동일한 email은 존재하지 않는다.


두개의 테이블에 대응하는 모델클래스를 구현하자.

Ext.define('MyApp.model.MemberMaster', {

    extend: 'Ext.data.Model',

    idProperty: 'email',        // #1

    fields: [                   // #2

        {

            name: 'email',

            type: 'string'

        },

        {

            name: 'name',

            type: 'string'

        },

        {

            name: 'gender',

            type: 'string'

        },

        {

            name: 'createDate',

            type: 'date',

            dateFormat: 'Y.m.d'

        }

    ],

    // 관계를 설정

    associations: [         // #3

        {

            type: 'hasOne', // #4

            model: 'MyApp.model.MemberDetail',  // #5

            foreignKey: 'email',  // #6. 없다면 id값이 넘어가지 않음.

            getterName : 'getMemberDetail'      // #6

        }

    ],


    // CRUD를 설정한다.

    proxy: {        // #7

        type: 'ajax',

        url: 'data/memberMaster.json?read',

        reader: {

            type: 'json'

        }

    }

});


위의 코드는 MemberMaster모델 클래스이다. 

 #1. idProperty로 사용할 필드는 유일한 키인 email필드이다.

 #2. fields내부에 필드를 정의한다.

 #3. 관계를 설정한다. 

 #4. hasOne은 일대일 관계로 하나의 모델객체에 다른 하나의 모델객체가 관계를 맺는다.

 #5. hasOne관계를 맺을 모델클래스를 명시한다.

 #6. 외래키를 설정한다. 이 외래키는 MemberDetail모델에서 주키로 사용할 MemberMaster모델 필드를 의미하므로 명시 된 email필드는 MemberMaster모델의 필드다.

 #7. 프록시 정보를 설정한다.


MemberMaster모델이 데이터를 로드 할 때 필요한 memberMaster.json 파일을 작성하자.

{

    "success": true,

    "email":'master@extjs.com',

    "name":"홍길동",

    "gender":"M",

    "createDate": '2013.01.10'

}


이제 MemberDetail모델클래스를 정의하자. 관계설정(hasOne)으로 MemberMaster를 설정하여 MemberDetail모델객체를 통해 MemberMaster모델을 로드 할 수 있도록 한다.

Ext.define('MyApp.model.MemberDetail', {

    extend: 'Ext.data.Model',

    idProperty: 'email',    // #1

    fields: [               // #2

        {

            name: 'email',

            type: 'string'

        },

        {

            name: 'address',

            type: 'string'

        },

        {

            name: 'age',

            type: 'int'

        },

        {

            name: 'job',

            type: 'string'

        }

    ],

    // 관계를 설정

    associations: [         // #3

        {

            type: 'hasOne', // #4

            model: 'MyApp.model.MemberMaster',  // #5

            foreignKey: 'email',  // #6

            getterName : 'getMemberMaster'      // #7

        }

    ],

    // CRUD를 설정한다.

    proxy: {

        type: 'ajax',

        url: 'data/memberDetail.json?read',

        reader: {

            type: 'json'

        }

    }

});


 #1. MemberDetail 모델도 MemberMaster모델과 동일한 email필드를 idProperty로 사용한다.

 #2. field를 정의한다.

 #3. 관계를 설정한다.

 #4. 일대일(hasOne)으로 관계타입을 정한다.

 #5. 일대일 관계를 맺을 클래스로 MyApp.model.MemberMaster모델을 설정한다.

 #6. MemberMaster모델이 참조 할 MemberDetail모델의 필드를 설정한다.

 #7. MemberMaster모델에 접근 할 getterName을 설정한다.


MemberDetail모델이 데이터를 로드 할 때 필요한 memberDetail.json 파일을 작성하자.

{

    "success": true,

    "email":'master@extjs.com',

    "address":"서울시 강남구 역상동",

    "age":20,

    "job":"프로그래머"

}


이제 두 모델 클래스가 완성되었고 서로 일대일(hasOne)관계가 교차되어 맺어졌다. 실행 코드를 작성하여 모델간의 관계설정이 정상적인지 확인하자.

 

모델 클래스의 load메소드를 실행하여 한 개의 MemberMaster객체를 로드하자.

MyApp.model.MemberMaster.load('master@extjs.com', {

    success: function(master){

        console.log('master::', master.data);

    }

});


success메소드는 로드가 완료된 후 MemberMaster모델객체를 반환한다. 반환된 모델객체를 통해 관계설정의 getterName으로 MemberDetail모델객체를 로드하는 코드를 추가한다.

MyApp.model.MemberMaster.load('master@extjs.com', {

    success: function(master){

        console.log('master::', master.data)

        master.getMemberDetail(function (detail, operation) {

            console.log('detail ::', detail.data);

        });

    }

});


코드를 실행하면 MemberMaster모델 데이터가 로드되고 이후 MemberDetail데이터가 로드 된 것을 확인할 수 있다.


개발자 도구의 Network탭을 열면 두 번의 Ajax호출이 이뤄진 것을 알 수 있다. 각각의 통신은 get방식으로 id파라메터에 master@extjs.com이라는 값을 전달하고 있다. 서버에서는 이 값을 이용해 DBMS를 검색하고 결과를 반환하면 되는 것이다.


이번에는 MemberDetail모델을 통해 MemberMaster모델에 접근해 보도록 하자. MemberMaster의 관계설정 처럼 MemberDetail모델에도 MemberMaster에 대한 관계설정 코드를 구현한 것을 기억 할 것이다.

MyApp.model.MemberDetail.load('master@extjs.com', {     // #1

    success: function(detail){                             // #2

        console.log('detail::', detail.data)                  // #3

        detail.getMemberMaster(function (master, operation) { // #4

            console.log('master ::', master.data);              // #5

        });

    }

});


 #1. load메소드를 통해 MemberDetail모델 데이터를 로드한다.

 #2. 데이터 로드가 성공하면 success메소드가 호출된다.

 #3. 로드 된 데이터의 내용을 출력한다.

 #4. #2에서 반환된 MemberDetail모델 객체를 통해 일대일 관계의 MemberMaster모델 객체를 로드한다.

 #5. #4번의 결과 MemberMaster데이터가 로드되었고 데이터 내용을 출력하고 있다.


아래 그림들은 실행결과를 보여준다. 이전 MemberMaster를 통해 MemberDetail모델에 접근한 것과 반대로 실행되는 것을 확인할 수 있겠다.



참고 : 모델을 통해 데이터를 로드 할 때 복수개의 데이터를 전달하더라도 첫 번째 데이터만 인식하고 반환한다. 편의상 json파일이 복수개의 데이터가 제공하더라도 이를 알고 있도록 하자. 

관계설정 시 hasMany는 Store객체를 , belongsTo, hasOne는 모델객체를 반환한다. 모델객체는 단일 데이터만 표현하고 복수개의 데이터를 표현 할 경우에는 Store를 사용해야 하기 때문이다.


2. 스토어(Ext.data.Store)

...

Posted by 베니94
자바스크립트/Ext JS2014. 1. 4. 00:35


이번 강좌에서는 게시판 애플리케이션의 주요 로직을 구현해 보도록 하자. 게시판의 로직을 구현하기 위해 Store와 모델에 의해 데이터를 로드하고 입력, 수정, 삭제가 가능하도록 코딩한다.


연재순서

1회 | 2013.09 | ExtJS4 클래스 시스템의 이해

2회 | 2013.10 | ExtJS4 MVC 아키텍처의 이해

3회 | 2013.11 | 시스템 뼈대 구현하기

4회 | 2013.12 | ExtJS4 실전코딩 : 멀티 게시판 애플리케이션 구현(1)

5회 | 2014.01 | ExtJS4 실전코딩 : 멀티 게시판 애플리케이션 구현(2)


이전 시간에 우리는 메뉴를 통해 동일한 형태의 게시판이지만 관리아이디가 다른 게시판 애플리케이션을 중복으로 실행하여 멀티게시판의 형태를 구성하였다. 이제 게시판의 구체적인 기능을 구현하도록 한다.


게시판 리스트가 데이터를 불러올 수 있도록 BoardList클래스를 <리스트 1>과 같이 render된 시점에 Store를 로드하도록 수정하자.


<리스트 1> BoardList클래스 렌더시 데이터 로드

me.on('render', function () {

        me.store.proxy.extraParams = {

            brd_category_cd: this.brd_category_cd

        }

        me.store.load();

});



Store클래스인 MyMvc.store.Boards를 통해 리스트를 제공받는다. 프록시의 read에 해당하는 url인 board.do?boards에 의해 전달받는다. 이는 아래의 json결과를 반환하고 BoardList클래스에 표현된다.


<리스트2> board.do?board에 제공되는 json데이터

{

    success: true,

    totalCount: "2",

    entitys: [

        {

            brd_title: "테스트..",

            brd_seq: 11,

            brd_input_user_nm: "홍길동",

            brd_read_cnt: 10,

            brd_input_date: "2013-09-04 07:31:30",

            brd_content: "test"

        },

        ..

    ]

}


<그림1>은 그리드에 출력된 데이터를 보여주고 있다.

<그림 1> 게시물 리스트


다음은 그리드를 클릭 할 경우와 “글쓰기” 버튼을 클릭 할 경우를 구현하자. <리스트 3>와 Board 컨트롤러 코드를 수정하자.


<리스트 3> MyMvc.controller.Board.js

Ext.define('MyMvc.controller.Board', {

    init : function(app) {

        this.control({

            'boardlist button[action=board.create]' : {             // #1

                click: this.newBoard                                          // #2

            },

'boardlist' : {                                                         // #3

                itemclick : this.onBoardSelect                              // #4

            }

        });

    },


    onBoardSelect : function(grid, record, index) {

        var fp = grid.up('boardmain').down('boardview');        // #5

        if (fp.collapsed) {                                                          // #6

            fp.expand();                                                            // #7

        }

        fp.fireEvent('setFormData', record);                            // #8

    },


    newBoard: function(button){

        var fp = button.up('boardmain').down('boardview');  // #9

        if (fp.collapsed) {

            fp.expand();

        }

        fp.fireEvent('setFormData', Ext.create('MyMvc.model.Board'));   // #10

    }

});


코드를 상세히 설명하기로 하자.

 #1. boardlist는 게시물 리스트를 보여주는 그리드패널이다. 이 패널 내부의 버튼 중 action속성이 create인 버튼이 클릭(#2)이벤트를 발생 시킬 경우를 리스닝한다.

 #2. 클릭 이벤트가 발생하면 컨트롤러 내부의 newBoard메소드를 실행시킨다.

 #3. 게시물 리스트

 #4. 게시물 리스트에서 itemclick이벤트가 발생하면 onBoardSelect메소드를 실행 시킨다.

 #5. 게시물 상세내역을 보여주는 패널을 변수에 담는다.

 #6. 패널이 접혀 있다면 #7과 같이 펼쳐지도록 한다.

 #8. 게시물 상세내역 패널에 이벤트를 발생시키고 인자로 그리드의 클릭 된 모델객체를 전달한다.

 #9. 새로운 게시물을 등록하는 메소드로 게시물 입력 패널(상세내역패널과 동일)을 변수에 담는다.

 #10. setFormData이벤트를 발생시키고 비어있는 Board모델 객체를 전달한다.



이제 게시물 정보를 입력하고 보여주는 BoardView클래스에 setFormData이벤트를 추가하고 저장기능과, 삭제 기능을 처리할 메소드를 추가하도록 하자.


<리스트 4> BoardView.js

Ext.define('MyMvc.view.board.BoardView', {

    ..

    initComponent: function () {

        var me = this;

        Ext.apply(this, {

            items: [

                {

                    xtype: 'form',

                    bodyPadding: 10,

                    itemId: 'board.form',

                    layout: 'anchor',

                    defaults: {

                        anchor: '100%'

                    },

                    buttons: [

                        {

                            xtype: 'statusbar',

                            defaultText: '&nbsp;',

                            flex: 1

                        },

                        {

                            action: 'board.save',

                            text: 'Save',

                            scope: me,

                            handler: function () {

                                this.onSave();    // #17

                            }

                        },

                        {

                            action: 'board.remove',

                            text: 'Del ',

                            scope: me,

                            handler: function () {

                                this.onRemove();  // #18

                            }

                        } ,

                        ..

            ],


        .. 중략 ..

            }]

        });

        this.callParent(arguments);

        this.on('setFormData', function (rec) {                     // #1

            me.down('form[itemId=board.form]').getForm().reset();   // #2

            me.down('form').loadRecord(rec);                        // #3

        });

    },

    onSave: function () {                                           // #4

        var me = this;

        var record = this.down('form').getForm().getRecord();       // #5

        this.down('form').getForm().updateRecord(record);           // #6

        record.save({                                               // #7

            success: function (a, b) {                             // #8

                var pers = Ext.ModelMgr.getModel('MyMvc.model.Board');// #9

                pers.load(record.get('brd_seq'), {                      // #10

                    success: function (record, operation) {             // #11

                        me.down('form').loadRecord(record);             // #12

                        me.up('boardmain').down('boardlist').insertRecord(record.data);// #13

                    }

                });

            }

        });

    },

    onRemove: function () {                                     // #14

        var me = this, form = this.down('form').getForm(),

            record = form.getRecord();

        form.updateRecord(record);

        me.collapse();                                          // #15

        record.destroy();                                       // #16

    }

});


코드를 자세히 설명하도록 하자.

 #1. setFormData 이벤트가 이 클래스에서 발생할 경우 리스닝하도록 했다.

 #2. 내부 폼패널을 사용하기 전에 reset한다.

 #3. setFormData 이벤트는 인자로 데이터가 담긴 모델객체를 전달받는다. 이 전달받은 모델을 loadRecord메소드를 통해 폼에 세팅한다.

 #4. 폼패널을 저장하는 메소드이다.

 #5. 폼패널에 입력된 값을 갖는 모델객체를 변수에 담는다.

 #6. updateRecord메소드에 입력된 값을 갖는 모델객체를 전달하여 폼패널에 세팅하고 #5의 모델데이터의 수정을 완료한다.(클라이언트에서의 완료를 의미함) updateRecord메소드를 꼭 호출해야함.

 #7. 모델의 프록시 설정을 통해 서버에 저장을 요청한다.

 #8. 저장 완료 후 호출된다.

 #9. 저장한 데이터를 다시 불러오기 위해 모델 객체를 생성한다.

 #10. 변경 된 조회수와, 수정일 등을 알아오기 위해 모델의 load메소드에 현재 게시물의 아이디를 전달해 최종 데이터를 불러온다.

 #11. 모델의 load메소드의 호출이 성공하면 호출된다.

 #12. loadRecord메소드를 통해 폼패널에 데이터를 다시 전달한다.

 #13. 게시물리스트 그리드 패널에도 수정, 입력된 데이터를 반영하도록 한다.

 #14. 게시물 삭제 처리용 메소드이다.

 #15. 게시물 상세보기화면을 접도록 한다.

 #16. 모델객체에 접근하여 destroy메소드를 호출 서버에서 삭제처리 한다.

 #17. 저장 버튼을 클릭 할 경우 onSave메소드를 호출한다.

 #18. 삭제 버튼을 클릭 할 경우 onRemove메소드를 호출한다.


게시물 리스트 클래스인 BoardList에 insertRecord메소드를 구현하자. 이 메소드는 게시물의 저장버튼에 반응하여 수정, 신규저장 된 폼패널의 내용을 좌측 그리드패널에 반영되도록 하는 역할을 한다.

insertRecord: function (record) {

    index = this.store.findExact('brd_seq', record.brd_seq);    // #1

    if (index != -1) {                                          // #2

        var rs = this.store.getAt(index);                       // #3

        rs.set(record);                                         // #4

        return;                                                 // #5

    }

    this.store.insert(0, record);                               // #6

}


 #1. findExact메소드는 Store내부에서 원하는 필드에 값을 가진 데이터가 존재하는지 확인하는 메소드이다. 이 메소드에 인자로 전달받은 데이터의 brd_seq값을 넘겨 index변수에 저장한다.

 #2. 검색된 결과가 존재하지 않으면 -1을 반환하므로 존재한다는 조건식은 if(index != -1)이고 조건식에 맞는 결과가 있다면 index변수에 결과에 맞는 모델객체는 Store내부에 몇 번째인지 index변수에 전달한다.

 #3. index변수는 검색결과에 매칭되는 데이터의 순번을 가지므로 해당 순번으로 Store내부의 모델객체를 rs변수에 전달한다.

 #4. 외부에서 전달한 데이터를 Store내부 모델객체에 세팅한다. 이렇게 하여 외부의 새로운 값을 Store에 반영하는 것이다.

 #5. 여기까지는 수정에 대한 처리이다. Store내부에 외부에서 전달한 데이터의 brd_seq값이 존재하였으므로 수정행위로 보고 이후에 this.store.insert 로직이 실행되지 않도록 한다.

 #6. index변수에 -1이 전달될 경우 즉 외부에서 전달한 데이터의 brd_set값이 Store내부에 존재하지 않으면 Store의 첫 번째로 신규 입력한다.


이제 코드를 실행하고 테스트를 진행하자.

게시물 리스트를 클릭하면 Board컨트롤러가 itemclick이벤트를 감지하여 onBoardSelect메소드를 호출하고 이 메소드는 BoardView클래스에 클릭한 모델데이터를 전달하여 폼패널에 데이터를 보여지도록 하였다.



<그림 2> 게시물 선택


“글쓰기” 버튼을 클릭하자. 글쓰기 버튼이 클릭되면 setFormData이벤트를 호출하고 인자로 비어 있는 모델객체(Ext.create('MyMvc.model.Board'))를 전달한다. 폼패널에 이 빈 모델 객체가 loadRecord메소드에 세팅되면 폼은 빈 상태로 사용자의 입력을 기다리게 된다.


데이터를 모두 채우고 ‘Save’버튼을 클릭하여 게시물을 저장하자. ‘Save’버튼이 클릭 되면 onSave메소드가 호출되고 이 메소드는 폼패널에서 모델객체를 추출하여 record.save() 메소드를 실행한다. 여기서 폼패널의 데이터가 입력이든 수정이든 record.save()메소드가 실행되는 것은 동일하다. 그렇다면 모델의 api설정 중 craete와 update 중 하나를 실행하여 입력과 수정작업이 이루어지는데 무엇으로 판단하는 것일까? 모델의 save메소드는 모델내부의 idProperty의 필드 값의 존재 유무에 따라, 즉 brd_seq값이 있다면 update이고 없다면 craete로 실행되는 것이다.


좌측 게시물을 선택하여 해당 모델객체를 우측 패널에 로드 할 경우 당연히 brd_seq값이 존재할 것이고 “글쓰기”버튼을 클릭하여 빈 모델객체를 우측 패널에 로드 할 경우 brd_seq값은 비어 있게 되므로 이러한 특성을 이용하여 입력과 수정을 한가지 로직으로 처리하게 된다.


<그림 3>은 데이터를 수정할 경우 Ajax호출 모습이다. record.save()메소드는 수정로직을 처리한 이후 수정한 데이터를 다시 로드 한 뒤 폼패널에 세팅하는 과정을 거쳐 서버에서 최종 업데이트 된 내용을 클라이언트에서 재확인 시켜준다.


<그림 3> 게시물 수정작업 결과


이번에는 입력처리에 대한 결과를 확인하자. 입력처리도 수정과 마찬가지 과정을 거친다. record.save()메소드를 실행하면 ExtJS는 모델객체 내부에 brd_seq값이 존재하지 않으므로 api의 create가 실행되고 입력작업을 서버에 요청한다. 이후 요청이 성공하면  입력된 데이터를 다시 로드하여 폼패널에 세팅하고 게시판리스트에 신규 데이터를 추가하도록 하였다.


<그림4> 게시물 입력작업 결과


마지막으로 삭제 기능에 대해 설명하자. 삭제 기능 또한 모델클래스의 destroy메소드를 이용하여 처리하였다. destroy메소드는 아래 <그림 5>와 같이 모델 api설정 중 destroy가 실행되고 모델객체의 데이터가 서버에 전송되므로 서버에서는 brd_seq값으로 해당 테이블의 데이터를 삭제 처리하면 되겠다. destory메소드가 호출되면 데이터베이스에 실제 데이터가 정상적으로 삭제 처리되면 이후 좌측 게시물리스트 그리드에서도 해당 데이터가 삭제되는 것을 확인 할 수 있다.


<그림 5> 삭제 처리 시 Ajax전송 처리 결과


정리하며

지금까지 총 5회에 걸쳐 ExtJS를 이용하여 멀티 게시판을 구현해 보았다. 강좌에서 핵심적으로 봐야 할 부분은 UI를 확장한 어떤 클래스도 메뉴를 통해 중앙영역에 추가할 수 있는 구조와, 동일한 애플리케이션 클래스를 여러 개 실행하여 각기 다른 데이터를 다루도록 하는 것이다.

우리는 이러한 모든 것을 클래스화 하였기에 가능하다는 것을 알게 되었다. 클래스의 개념은 ExtJS가 가지는 가장 중요한 개념이고 복잡하고 큰 애플리케이션을 자바스크립트로 개발할 수 있게되었다. 



Posted by 베니94
자바스크립트/Ext JS2013. 11. 20. 15:23

이번 시간에는 게시판 애플리케이션의 UI를 구성하고 데이터를 채울 준비를 해본다. 간략히 게시판 애플리케이션에 대해 설명하면, 좌우로 나눠진 게시판 애플리케이션에서 좌측에 게시판 리스트를 그리드로 구성하고 해당 그리드에서 게시물을 클릭하면 우측 패널에 해당 글의 내용이 보여지는 구조다.

 

---------------------------------------

연재순서

1회 | 2013. 9 | ExtJS4 클래스 시스템의 이해

2회 | 2013. 10 | ExtJS4 MVC 아키텍처의 이해

3회 | 2013. 11 | 시스템 뼈대 구현하기

4회 | 2013. 12 | ExtJS4 실전코딩 : 멀티 게시판 애플리케이션 구현 (1)

5회 | 2014. 1 | ExtJS4 실전코딩 : 멀티 게시판 애플리케이션 구현 (2)


이전시간에 우리는 애플리케이션의 뼈대를 구성하고 UI를 가지고 있는 클래스라면 어떤 형태로도 보여지는 구조를 구현해 보았다. 이런 구조의 뼈대는 게시판과 같이 동일한 구조를 반복적으로 표현하는 애플리케이션뿐만 아니라 전혀 다른 여러가지 형태의 애플리케이션 클래스를 실행할 수 있는 좋은 구조다.

이제 게시판 애플리케이션의 UI를 구성해 보자. <표 1>은 게시판을 구성할 클래스들에 대한 설명이다.

 타입

 클래스명

 기능

비고 

 모델

 MyMvc.model.Board

 게시판 데이터 모델 클래스

 

 뷰

MyMvc.view.BoardMain 

 한 개의 게시판 대표 클래스

 

MyMvc.view.BoardList 

 게시판 리스트를 표현할 그리드 클래스

 

 MyMvc.view.BoardView

 한 개의 게시물의 내용을 보여줄 클래스

 

 컨트롤러

 MyMvc.controller.Board

 게시판 관리 컨트롤러 클래스

 

 스토어

MyMvc.store.Boards 

 게시판 데이터 집합

 

<표 1> 클래스 정의

 

UI 클래스를 구현하기 앞서 <리스트 1>과 같이 실행할 프로그램의 클래스명을 수정하도록 하자. 게시판 애플리케이션의 메인 클래스는 MyMvc.view.board.BoardMain이다. 최초 애플리케이션이 실행되면 첫번째 데이터가 선택되고 BoardMain 클래스가 실행되도록 한다는 내용이다.

 

<리스트 1> /json/programlist.json

{"entitys": [
    {
        "pgm_syscd": "F001",
        "pgm_class": "MyMvc.view.board.BoardMain",
        "pgm_icon": "grid",
        "pgm_nm": "ExtJS",
        "brd_number": "001"
    },
    {
        "pgm_syscd": "F001",
        "pgm_class": "MyMvc.view.board.BoardMain",
        "pgm_icon": "grid",
        "pgm_nm": "Sencha Touch",
        "brd_number": "002"
    },
    {
        "pgm_syscd": "F002",
        "pgm_class": "MyMvc.view.board.BoardMain",
        "pgm_icon": "grid",
        "pgm_nm": "구인_구직",
        "brd_number": "003"
    }
],
    "errMsg": "", "errTitle": "검색결과", "message": "", "success": true, "totalCount": "2"}

 

app/view 폴더 아래에 board 폴더를 생성하고 <리스트 2>와 같이 BoardMain.js 클래스를 구현하자. 이 클래스는 board 레이아웃을 사용해 좌측에 그리드와 우측에 패널을 가지고 있고, 게시판 아이디로 사용할 brd_number를 config 정보로 가지고 있다.

 

<리스트 2> BoardMain 클래스

Ext.define('MyMvc.view.board.BoardMain', {
    extend: 'Ext.container.Container',
    alias: 'widget.boardmain',
    layout : 'border',

    config: {
      brd_number : ''
    },
    initComponent: function () {
        var me = this;
        me.initConfig();

        Ext.apply(me, {
            items: [
                {
                    region: 'center',
                    xtype: 'boardlist',
                    brd_category_cd : me.getBrd_number()
                },
                {
                    xtype: 'boardview',
                    title: '게시물',
                    width : 500,
                    region: 'east',
                    collapsible: true,
                    collapsed : true,
                    split: true
                }
            ]
        });
        me.callParent(arguments);
    }
});

좌측 그리드 클래스를 작성하자. 클래스명은 BoardList이고 Grid 클래스를 상속받으며 boarlist이라는 위젯명을 사용한다. <리스트 3>에서 스토어는 Ext.create를 사용해 매번 생성하도록 했다. 컨트롤러에 스토어를 등록하고 store: 'Board'로 사용할 수도 있겠으나, 이렇게 할 경우 모든 게시판이 하나의 스토어를 공유해 사용하게 되므로 각 게시판별 게시물을 읽어오기 어려워진다.

 

<리스트 3> BoardList 클래스 코드

Ext.define('MyMvc.view.board.BoardList', {
    extend: 'Ext.grid.Panel',
    alias: 'widget.boardlist',
    columnLines: true,
    initComponent: function () {
        var me = this;
        this.store = Ext.create('MyMvc.store.Boards');
        Ext.apply(this, {
            tbar: [
                {
                    xtype: 'button',
                    text: '글쓰기 ',
                    action: 'board.create'
                }
            ],
            columns: [
                {
                    text: '게시글번호 ',
                    width: 80,
                    dataIndex: 'brd_seq',
                    field: {
                        allowBlank: false
                    }
                },
                {
                    text: '제목 ',
                    flex: 1,
                    dataIndex: 'brd_title',
                    field: {
                        allowBlank: false
                    }
                },
                {
                    text: '입력자 ',
                    width: 70,
                    dataIndex: 'brd_input_user_nm',
                    field: {
                        allowBlank: false
                    }
                },
                {
                    text: 'Input Date ',
                    width: 150,
                    align: 'center',
                    dataIndex: 'brd_input_date',
                    field: {
                        allowBlank: false
                    }
                },
                {
                    text: 'Read Cnt ',
                    width: 70,
                    dataIndex: 'brd_read_cnt',
                    field: {
                        allowBlank: false
                    }
                }
            ],
            dockedItems: [
                {
                    xtype: 'pagingtoolbar',
                    store: this.store,
                    dock: 'bottom',
                    displayInfo: true
                }
            ]
        });

        me.callParent(arguments);
    }
});

 

우측 패널 클래스를 작성한다. 우측 패널 클래스는 내부에 게시물 내용을 확인하는 폼 패널과 함께 조작을 위한 버튼을 가지고 있다.

 

<리스트 4> BoardView 클래스 코드

Ext.define('MyMvc.view.board.BoardView', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.boardview',
    bodyPadding: 5,
    autoScroll: true,
    collapsed: true,

    initComponent: function () {
        var me = this;
        Ext.apply(this, {
            items: [
                {
                    xtype: 'form',
                    bodyPadding: 10,
                    itemId: 'board.form',
                    layout: 'anchor',
                    defaults: {
                        anchor: '100%'
                    },
                    buttons: [
                        {
                            xtype: 'statusbar',
                            defaultText: ' ',
                            flex: 1
                        },
                        {
                            action: 'board.save',
                            text: 'Save',
                            scope: me,
                            handler: function () {
                                this.onSave();
                            }
                        },
                        {
                            action: 'board.remove',
                            text: 'Del ',
                            listeners: {
                            }
                        } ,
                        {
                            action: 'board.close',
                            text: 'Close ',
                            listeners: {
                            }
                        }
                    ],

                    items: [
                        {    xtype: 'hiddenfield', name: 'brd_seq'    },
                        {    xtype: 'hiddenfield', name: 'brd_category_cd'    },
                        {
                            xtype: 'textfield',
                            name: 'brd_title',
                            allowBlank: false,
                            fieldLabel: '제 목',
                            emptyText: 'Title'
                        },
                        {
                            xtype: 'textareafield',
                            grow: true,
                            growMin: 150,
                            growMax: 300,
                            allowBlank: false,
                            name: 'brd_content',
                            fieldLabel: '내 용 '
                        },
                        {
                            xtype: 'textfield',
                            name: 'brd_input_user_nm',
                            allowBlank: false,
                            fieldLabel: '등 록 자'
                        },
                        {
                            xtype: 'displayfield',
                            fieldLabel: '입 력 일',
                            name: 'brd_input_date'
                        },
                        {
                            xtype: 'displayfield',
                            fieldLabel: '수 정 일',
                            name: 'brd_update_date'
                        }
                    ]
                }
            ]
        });
        this.callParent(arguments);
    }
});

 

좌측 그리드에서 사용할 Store 클래스를 작성하자. Store 클래스는 CRUD가 모두 가능하도록 플록시를 설정하자. actionMethod의 경우 서버쪽 코드에 따라 변경될 수 있으므로 필요 시 수정하도록 한다.

 

<리스트 5> Boards Store 클래스 코드

Ext.define('MyMvc.store.Boards', {
    extend: 'Ext.data.Store',
    autoLoad: false,
    model: 'MyMvc.model.Board',
    proxy: {
        type: 'ajax',
        actionMethods: {
            read: 'GET',
            create: 'POST',
            update: 'PUT',
            destroy: 'DELETE'
        },
        api: {
            read: '/board.do?boards',
            create: '/board.do?create',
            update: '/board.do?update',
            destroy: '/board.do?remove'
        },
        reader: {
            type: 'json',
            root: 'entitys',
            totalProperty: 'totalCount',
            messageProperty: 'message'
        },
        listeners: {
            exception: function (proxy, response, operation) {

            }
        }
    }
});

 

<리스트 5>의 Store 클래스에서 사용하는 모델 클래스를 작성하자. 이 클래스 또한 자체적으로 CRUD가 모두 가능하도록 <리스트 5>의 플록시 설정을 그대로 사용하도록 하자. 일반적으로 스토어에 설정된 플록시 정보는 게시물의 리스트를 저장하는 용도로 사용하고, 모델에 설정된 플록시 정보는 폼 정보와 연관지어 입력, 수정, 삭제 등을 수행하는 데 사용한다.

 

<리스트 6> Board 모델 클래스 코드

Ext.define('MyMvc.model.Board', {
    extend: 'Ext.data.Model',
    idProperty: 'brd_seq',
    fields: [
        {name: 'brd_seq', type: 'int'},
        'brd_title',
        'brd_content',
        'brd_category_cd',
        'brd_input_user_nm',
        'brd_input_date',
        {name: 'brd_read_cnt', type: 'int'},
        'brd_update_date'
    ],
    proxy: {
        type: 'ajax',
        actionMethods: {
            read: 'GET',
            create: 'POST',
            update: 'PUT',
            destroy: 'DELETE'
        },
        api: {
            read: '/board.do?boards',
            create: '/board.do?create',
            update: '/board.do?update',
            destroy: '/board.do?remove'
        },
        reader: {
            type: 'json',
            root: 'entitys'
        }
    }
});

게시판 클래스들이 모두 동적으로 로딩될 수 있도록 컨트롤러 클래스를 만들고 앞서 작성한 모든 클래스를 등록하자. 이를 구체적으로 구현하는 방법에 대해서는 다음 시간에 설명하기로 한다.

 

<리스트 7> Board 컨트롤러 클래스 코드

Ext.define('MyMvc.controller.Board', {
    extend : 'Ext.app.Controller',

    views : [ 'board.BoardMain', 'board.BoardList', 'board.BoardView' ],
    models: ['Board'],
    stores : ['Boards'],

    init : function(app) {
        this.control({
        });
    }
});

 

이제 Board 컨트롤러 클래스를 application.js 클래스에 등록해 전체 애플리케이션이 Board컨트롤러 클래스를 인식하도록 변경해준다.

 

<리스트 8> application.js 내부 컨트롤러 추가

Ext.define('MyMvc.Application', {
	name : 'MyMvc',

	extend : 'Ext.app.Application',

	views : [
	// TODO: add views here
	],

	controllers : [ 'Frame' , 'Board'],

	stores : [
	// TODO: add stores here
	]
});

 

애플리케이션을 재실행하면 <그림 1>과 같이 게시판 리스트와 게시물 보기 화면을 확인할 수 있다.

<그림 1> 게시판 애플리케이션 UI 구성

 

좌측 메뉴에서 선택된 게시판은 BoardMain 클래스에 의해 우측 중앙 영역에 보여지게 된다. 표면적으로는 각 메뉴에서 선택된 게시판은 동일하나 각각의 게시판 구분은 /json/programlist.json의 brd_number에 의해 달라지게 된다.

이는 <리스트 9>와 같이 BoardMain 클래스를 수정하면, 좌측 게시판 리스트를 클릭할 때 마다 게시판 아이디가 다르다는 것을 확인할 수 있다. 이 게시판 아이디에 의해 데이터베이스의 게시물을 가져오거나 입력할 수 있다.

 

<리스트 9> BoardMain 클래스 수정

Ext.define('MyMvc.view.board.BoardMain', {
    extend: 'Ext.container.Container',
    alias: 'widget.boardmain',
    layout : 'border',

    config: {
      brd_number : ''
    },
    initComponent: function () {
        var me = this;
        me.initConfig();

        .. 중략 ..
        me.callParent(arguments);
        me.on('render', function(){
           console.log("현재 게시판의 게시판 아이디는 " + me.getBrd_number());
        });
    }
})

 

<그림 2> 게시판 아이디 확인 결과

 

정리하며

이번 시간에는 게시판 애플리케이션의 UI를 구현해 봤다. 동일한 게시판 프로그램을 개발하고 해당 프로그램이 각기 다른 데이터를 표현하도록 하는 것이 핵심이다. 다음 시간에는 게시판의 CRUD 기능을 구현해보겠다. 지면 관계상 생략된 내용은 필자의 블로그를 통해 확인하자.

Posted by 베니94
자바스크립트/Ext JS2013. 10. 13. 15:09

시스템 뼈대 구현하기

 

이번 강좌부터 3회에 걸쳐 ExtJS MVC를 통해 게시판을 구현해보기로 하자. 금번 강좌에서는 게시판이 들어갈 애플리케이션의 뼈대를 만들고 이를 통해 MVC를 적용하는 방법을 같이 배우게 된다. 굳이 게시판을 만드는 이유는 웹애플리케이션에서 게시판이 가지고 있는 의미가 CRUD성격을 모두 지니고 있고 모든 웹개발자들이 만들어봐야 하는 기본 애플리케이션으로 주제에 대한 공통적인 이슈가 모두 공유되어 있다고 믿기 때문이다.

이전 시간에 우리는 Sencha CMD를 통해 애플리케이션을 생성하고 빌드하는 방법을 배웠다. 또한 내장된 웹서버를 이용 개발된 애플리케이션을 실행해보았다. 우리가 만들 게시판은 서버코드가 필요하고 데이터베이스가 필요하므로 개발툴을 에디터에서 이클립스로 바꿀 필요가 있다. 이번 강좌에서는 이전시간에 만들었던 MyMvc애플리케이션을 이클립스의 Dynamic Web Project와 함께 묶어 차후에 있을 서버코드 구현에 대비하도록 하자.

우선 그림1과 같이 이클립스 Dynamic Web Project를 생성하고 프로젝트 설정에서 Text file encoding을 EUC_KR에서 UTF-8로 변경하자.

<그림1> 이클립스 프로젝트 설정

 

필자는 MasoExtJSLecture라는 이름의 프로젝트를 생성하였다. 이제 Sencha CMD를 통해 Extjs 애플리케이션을 생성하자. 이클립스 프로젝트와 맞추려면 WebContent폴더에 ExtJS애플리케이션을 생성하도록 하자. 애플리케이션 생성 코드는 아래와 같다.

sencha -sdk C:\Sencha\ext-4.2.1.883 generate app MyMvc C:\이클립스프로젝트\WebContent

 

정상적으로 애플리케이션이 생성되면 이클립스에서 해당 프로젝트를 refresh하고 이클립스 톰캣플러그인으로 웹서버를 실행한다. 브라우저를 통해 실행하면 이전 강의에서 보았듯이 기본 어플리케이션이 그림2와 같이 실행되는 것을 볼 수 있다.


<그림2> Sencha CMD를 통해 생성된 기본 애플리케이션

 

게시판 애플리케이션은 <그림3>과 같이 상단영역과 좌측 메뉴영역 우측 중앙의 애플리케이션영역으로 나누어 진다.


<그림3> 애플리케이션인 MockUp

 

표1은 뼈대를 이룰 클래스들에 대한 설명이다.

 

클래스명

기능

Model

MyMvc.model.Program

게시판 또는 우측패널에 표시될 애플리케이션의 속성

View

MyMvc.view.WestMenuPanel

좌측에 표시할 게시판그룹

MyMvc.view.WestMenuDataViewPanel

게시판그룹에 해당되는 게시판리스트

MyMvc.view.Header

애플리케이션 상단영역

Controller

MyMvc.controller.Frame

뼈대를 제어할 Controller 클래스

Store

MyMvc.store.Programs

좌측 메뉴에 표시 할 Store

<표1> 클래스 정의

 

MyMvc.view.Viewport.js를 구현하자. Viewport에서는 애플리케이션의 기본 구조를 구성한다.

<리스트1> MyMvc.view.Viewport.js

Ext.define('MyMvc.view.Viewport', {

extend : 'Ext.container.Viewport',

requires : [],

layout : 'border',

items : [ {

    region : 'north',

    xtype : 'container',

    html : '상단영역',

    height : 100,

    style : {

     borderColor : '#000000',

     borderStyle : 'solid',

     borderWidth : '1px'

    }

}, {

    region : 'center',

    xtype : 'tabpanel',

    title : '게시판 프로그램 영역',

    style : {

        borderColor : '#000000',

        borderStyle : 'solid',

        borderWidth : '1px'

    }

}, {

    region : 'west',

    xtype : 'container',

    html : '메뉴 영역',

    width : 200,

    style : {

        borderColor : '#000000',

        borderStyle : 'solid',

        borderWidth : '1px'

    }

} ]

});

 

브라우저를 통해 실행하면 좌측, 상단, 우측으로 영역이 나눠지는 것을 확인 할 것이다.

이제 상단 영역을 구현하자. 상단영역에 해당되는 클래스를 <리스트2>와 같이 코딩하고

Viewport.js의 상단영역을 <리스트2>의 Header클래스로 대체하자.

 

<리스트2> 상단 Header 클래스 및 Viewport클래스 수정

Ext.define('MyMvc.view.Header', {

extend: 'Ext.container.Container',

alias: 'widget.mymvcheader',

id: 'app-header',

height: 52,

layout: {

type: 'hbox',

align: 'middle'

},

initComponent: function() {

this.items = [{

xtype: 'component',

id: 'app-header-title',

html: 'ExtJS MVC를 활용한 게시판 구현하기',

flex: 1

}];

this.callParent();

}

});

// Viewport클래스 기존 상단영역 수정

Ext.define('MyMvc.view.Viewport', {

    extend : 'Ext.container.Viewport',

    requires : [ 'Ext.layout.container.Border', 'MyMvc.view.Header',

            'Ext.tab.Panel' ],

    layout : 'border',

    items : [ {

        region : 'north',

        xtype : 'mymvcheader' // 기존 container

 

상단영역에서 필요한 css를 index.html에 추가하자.

 

<리스트3> 상단영역에서 사용할 index.html 내부 css

<link rel="stylesheet" href="bootstrap.css">

<style type="text/css">

#app-header {

background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #8fc33a), color-stop(100%, #739b2e));

background-image: -webkit-linear-gradient(top, #8fc33a, #739b2e);

background-image: -moz-linear-gradient(top, #8fc33a, #739b2e);             background-image: -o-linear-gradient(top, #8fc33a, #739b2e);

background-image: linear-gradient(top, #8fc33a, #739b2e);

border-bottom: 1px solid #567422;

}

#app-header-title {

padding: 15px 10px 10px 31px;

color: white;

font-size: 18px;

font-weight: bold;

text-shadow: 0 1px 0 #4e691f;

}

</style>

<script src="ext/ext-dev.js"></script>

 

실행하면 <그림4>와 같은 결과를 확인할 수 있다.

 

<그림4> 상단영역을 구현한 결과

 

좌측 메뉴영역을 구현하자. 좌측 메뉴는 2depth로 이루어진다. 게시판그룹을 accordion레이아웃으로 노출하고 클릭 시 세부게시판 리스트를 출력하도록 한다.

게시판그룹을 나타낼 WestMenuPanel클래스를 <리스트4>와 같이 정의하자.

 

<리스트4> 게시판 그룹 클래스

Ext.define('MyMvc.view.WestMenuPanel',{

extend : 'Ext.panel.Panel',

alias : 'widget.westmenupanel',

layout:'accordion',    // ① 레이아웃 설정

collapsible: true,

split:true,

title : 'Forum List',

initComponent: function() {

var me = this;

this.callParent(arguments);

this.on('render', this.setSubMenu, this); // ② render이벤트 발생시 함수 호출

},

// ③ 스토어 호출 후 게시판 그룹을 추가.

setSubMenu: function(){

    var me = this;

    var store = Ext.create('MyMvc.store.Systems');

    store.load(function(record, b, c){                

     store.each(function(rec){

        me.add({

          xtype:'panel',

          title:rec.get('pgm_sysnm'),

          pgm_syscd:rec.get('pgm_syscd'),

          iconCls:rec.get('pgm_sysicon')

         });

     });

    });

}

});

 

주요코드를 설명하자. ①은 게시판그룹을 추가할 때 accordion형태로 추가되도록 레이아웃을 설정한다. ②는 현 클래스에서 render이벤트가 발생시 게시판그룹을 추가할 메소드를 호출한다. ③함수는 게시판그룹 store를 호출하고 자식으로 추가한다.

 

게시판 그룹에 사용할 Model과 Store를 정의하자.

 

<리스트5> MyMvc.model.Program.js

Ext.define('MyMvc.model.Program', {

extend : 'Ext.data.Model',

fields : [ 'pgm_id', 'pgm_nm', 'pgm_syscd', 'pgm_sysnm', 'pgm_class',

    'pgm_icon', 'pgm_sysicon', 'group_id', 'group_nm',

    'group_status_cd', 'group_status_nm', 'group_pgm_status_nm',

    'title', 'brd_number']

});

 

<리스트6> MyMvc.store.Systems.js

Ext.define('MyMvc.store.Systems', {

extend: 'Ext.data.Store',    // 당연히 store상속

autoLoad : false,        // 자동 로드는 꺼놓자.

model: 'MyMvc.model.Program',    // 모델 세팅

proxy: {

type: 'ajax',

// json폴더를 루트에 만들고 systemlist.json이라는 파일를 통해

// 시스템 리스트를 받을 수 있다.

url: '/json/systemlist.json',    

reader: {

    type: 'json',

    root: 'entitys',

    totalProperty: 'totalCount',

    messageProperty: 'message'

},

listeners: {

exception: function(proxy, response, operation){}

    }

}

});

 

<리스트7>/json/systemlist.json

{

    "entitys":[{

        "pgm_syscd":"F001",

        "pgm_sysicon":"grid",

        "pgm_sysnm":"Sencha",

        "title":"Sencha"

    }, {

        "pgm_syscd":"F002",

        "pgm_sysicon":"grid",

        "pgm_sysnm":"사는애기 ",

        "title":"사는애기"

    }, {

        "pgm_syscd":"F003",

        "pgm_sysicon":"grid",

        "pgm_sysnm":"Front End",

        "title":"Front End"

    }],

    "errMsg":"",

    "errTitle":"검색결과",

    "message":"",

    "success":true,

    "totalCount":""

}

 

Viewport.js를 수정하여 <리스트5~7> 코드가 적용되도록 하자.

<리스트8>Viewport.js수정

…중략..

requires : [ 'Ext.layout.container.Border', 'MyMvc.view.Header',

            'Ext.tab.Panel', 'MyMvc.view.WestMenuPanel' ],

..중략..

}, {

    region : 'west',

    xtype : 'westmenupanel',

    width : 200

} ]

결과를 실행하면 <그림5>와 같이 게시판그룹이 출력된다.

<그림5> 좌측 게시판 그룹 구현

 

<그림5>는 accordion레이아웃을 이용하여 panel에 panel을 배치한 것이다. 이제 각 게시판그룹 이하에 중앙패널에 표시할 게시판리스트가 나오도록 구현해 보자.

 

<리스트9> Controller를 Application클래스에 등록

Ext.define('MyMvc.Application', {

    ..중략..

    controllers : [ 'Frame' ],

    ..중략..

});

 

<리스트10> MyMvc.controller.Frame.js 생성

Ext.define('MyMvc.controller.Frame', {

extend: 'Ext.app.Controller',

views: ['MyMvc.view.WestMenuDataViewPanel'],

stores : ['Programs'],

init : function(app) {

    this.control({

    });

}

});

 

<리스트11> MyMvc.store.Programs.js 생성

Ext.define('MyMvc.store.Programs', {

extend : 'Ext.data.Store',

autoLoad : true,

model : 'MyMvc.model.Program',

proxy : {

    type : 'ajax',

    url : '/json/programlist.json',

    reader : {

     type : 'json',

     root : 'entitys',

     totalProperty : 'totalCount',

     messageProperty : 'message'

    },

    listeners : {

     exception : function(proxy, response, operation) {

     }

    }

},

filterUsersByDepartment : function(pgm_syscd) {

    this.clearFilter();

    this.filter([ {

        property : 'pgm_syscd',

        value : pgm_syscd

    } ]);

},

refresh : function() {

    this.clearFilter();

}

});

 

<리스트12> /json/programlist.json

{"entitys":[

{    "pgm_syscd":"F001","pgm_class":"Ext.panel.Panel","pgm_icon":"grid","pgm_nm":"ExtJS","brd_number":"001" },

{    "pgm_syscd":"F001","pgm_class":"Ext.panel.Panel","pgm_icon":"grid","pgm_nm":"Sencha Touch","brd_number":"002" },

{    "pgm_syscd":"F002","pgm_class":"Ext.panel.Panel","pgm_icon":"grid","pgm_nm":"구인_구직","brd_number":"003" },

{    "pgm_syscd":"F002","pgm_class":"Ext.panel.Panel","pgm_icon":"grid","pgm_nm":"요즘","brd_number":"004" },

{    "pgm_syscd":"F003","pgm_class":"Ext.panel.Panel","pgm_icon":"grid","pgm_nm":"jQuery","brd_number":"005" },

{    "pgm_syscd":"F003","pgm_class":"Ext.panel.Panel","pgm_icon":"grid","pgm_nm":"Javascript","brd_number":"006" }

],

"errMsg":"","errTitle":"검색결과","message":"","success":true,"totalCount":"2"}

 

<리스트13> MyMvc.view.WestMenuDataViewPanel.js

Ext.define('MyMvc.view.WestMenuDataViewPanel', {

extend : 'Ext.panel.Panel',

alias : 'widget.menuboard',

animCollapse : true,

collapsible : true,

collapsed : true,

useArrows : true,

rootVisible : false,

multiSelect : false,

header : {

    toolFirst : true

},

initComponent : function() {

    var me = this;

    this.initConfig(); // 초기화 실행

    Ext.apply(this, {

     items: [{

        xtype: 'dataview',

        store : 'Programs',

        trackOver: true,

        cls: 'feed-list',

        itemSelector: '.feed-list-item',

        overItemCls: 'feed-list-item-hover',

        tpl: '<tpl for="."><div class="feed-list-item {pgm_icon}">{pgm_nm}</div></tpl><p>'

     }]

    });

    this.callParent(arguments);

}

});

 

<리스트13>이 호출 될 수 있도록 게시판그룹을 표현한 WestMenuPanel의 setSubMenu 메소드를 아래와 같이 수정하자.

변경전 : xtype: 'panel' -> 변경후 xtype : 'menuboard'

 

실행 결과를 확인하면 <그림6>와 같이 좌측 게시판 그룹에 게시판 리스트가 추가된 것을 볼 수 있다.

 

<그림6> 좌측 메뉴에 게시판 리스트 추가.

 

각각의 게시판그룹을 클릭해보자. 우리가 구현하고자 하는 것은 게시판 그룹을 클릭 후 해당 게시판 그룹코드(pgm_syscd)와 동일한 코드를 가지고 있는 게시판만 출력되도록 하는 것이다. 현재는 모든 게시판이 보여지고 있으므로 해당그룹에 맞는 게시판을 필터링 하도록 구현해보자.

<리스트14>와 같이 Frame Controller를 수정하여 게시판그룹 패널과 내부 게시판리스트 패널의 이벤트를 리스닝 할 수 있도록 하자.

 

<리스트14> Frame Controller수정

Ext.define('MyMvc.controller.Frame', {

extend : 'Ext.app.Controller',

views : [ 'MyMvc.view.WestMenuDataViewPanel', 'MyMvc.view.WestMenuPanel' ],

stores : [ 'Programs' ],

init : function(app) {

    this.control({

     'westmenupanel > menuboard' : {    // ① 이벤트 리스닝

        afterrender : function(panel){    // ② 최초 발생

         panel.firstSelectDataView();

        },

        expand : function(panel){

         panel.onItemClicked();    // ③ 패널이 열릴때 발생

        }

     }

    });

}

});

 

<리스트14>를 자세히 설명하자. ①은 Controller의 주요 역할인 View의 이벤트를 리스닝하고 있다. westmenupanel(게시판그룹패널) 하위에 존재하는 menuboard(게시판리스트패널)에서 발생하는 이벤트를 리스닝한다.(westmenupane과 menuboard는 위젯명임을 명심하자.)

②는 render가 끝난 후 호출된다. 인자로 menuboard패널이 넘어온다. afterrender이벤트가 발생하면 firstSelectDataView()함수를 호출한다. ③은 패널이 expand(열릴 때) 될 때 onItemClicked()함수를 호출한다.

②, ③에 해당되는 WestMenuDataViewPanel클래스의 메소드를 <리스트15>와 같이 구현하자.

 

<리스트15> WestMenuDataViewPanel의 로직구현

Ext.define('MyMvc.view.WestMenuDataViewPanel', {

.. 중략 ..

// dataview상에 출력된 리스트 중에 맨처음 프로그램이

//          선택되어지도록 한다.

firstSelectDataView : function() {

    var me = this;

    if (me.collapsed)

     return;

 

    var store = this.onItemClicked();

    if (store) {

     var task = new Ext.util.DelayedTask(function() {

        me.down('dataview').getSelectionModel().select(store.getAt(0));

     });

     task.delay(1000);

    }

},

// 시스템 패널을 클릭 할 때 마다 expand이벤트가 호출 된다.

// 이 때 클릭되어 expand된 패널 위에 하위 프로그램을 출력하면 된다.

onItemClicked : function() {

    var me = this;

    if (me.collapsed)

     return // 패널이 접히지 않은 패널을 찾는다.

            

    me.down('dataview').store

    .filterUsersByDepartment(this.pgm_syscd);

    return me.down('dataview').store;

}

});

 

수정된 코드를 실행하고 게시판 그룹을 클릭하면 해당 그룹에 맞는 게시판 리스트가 필터링 되어 보여지는 것을 볼 수 있다. 이는 WestMenuDataViewPanel에 구현된 두 개의 메소드에 의해 작동된다.

firstSelectDataView() 메소드는 게시판리스트를 담고 있는 store의 첫번째 데이터가 select되어지도록 하는 코드이다. onItemClicked() 메소드는 게시판 리스트를 출력하는 dataview가 가지는 store를 통해 해당 게시판 그룹에 맞는 게시판을 필터링 하는 역할을 한다.

메뉴에 들어갈 css를 index.html에 <리스트16>과 같이 추가하고 실행하자.

 

<리스트16> 좌측 메뉴용 css

.feed-list {

padding: 0 3px 0 3px;

}

.feed-list-item {

margin-top: 3px;

font-size: 11px;

line-height: 20px;

cursor: pointer;

border: 1px solid #fff;

}

.feed-list .x-item-selected {

font-weight: bold;

color: #15428B;

background-color: #DFE8F6;

border: 1px dotted #A3BAE9;

}

 

<그림7은> 게시판 그룹별 게시판리스트를 필터링하여 보여주고 있다.

 

<그림7> 게시판 그룹별 필터링 적용 후

 

이번 강좌의 마지막 작업이자 가장 핵심적인 작업이 남아있다. 좌측 메뉴를 구현했고 최초 로딩 시 첫번째 게시판 그룹의 첫번째 게시판이 자동으로 선택되어지게 하였다. 이때 선택되어진 또는 사용자가 선택할 경우 우측에 해당 게시판 애플리케이션을 출력되도록 해야 한다.

 

<리스트17>과 같이 Frame Controller에 코드를 추가하자.

Ext.define('MyMvc.controller.Frame', {

..중략..

Init : function(app){

this.control({

.. 중략 ..

     'menuboard > dataview' : { // 게시판 리스트의 dataview에서 발생한 이벤트

     select : this.onProgramSelect

     }

});

},

onProgramSelect : function(dataview, record) {

    var pgm_class = record.get('pgm_class'), pgm_nm = record.get('pgm_nm');

    var centerpanel = Ext.ComponentQuery

                .query('viewport container[region="center"]')[0];

    var tab = centerpanel.down('[tabuniqid=' + pgm_class + pgm_nm + ']');

    if (!tab) {

     tab = Ext.create(pgm_class, {

        brd_number : record.get('brd_number')

     });

     tab.title = pgm_nm+ "("+record.get('brd_number')+")";

     tab.tabuniqid = pgm_class + pgm_nm;

     centerpanel.add(tab);

    }

    centerpanel.setActiveTab(tab);

}

});

 

<리스트17>에 대해 자세히 설명하겠다. Frame Controller에 새로운 이벤트를 리스닝하도록 코드를 추가하였다. 이 코드는 게시판리스트 패널(WestMenuDataViewPanel) 내부의 dataview에서 select이벤트가 발생하면 onProgramSelect()메소드를 호출하도록 했다. onProgramSelect()메소드는 select이벤트 발생시 전달받은 게시판정보를 이용해 애플리케이션 중앙의 탭패널에 자식으로 게시판 클래스 인스턴스를 생성하여 추가하는 로직이다.

programlist.json파일을 상기시켜보자 아래와 같이 pgm_class에 일괄적으로 Ext.panel.Panel클래스가 들어 가있다. 이것은 아직 구현되지 않은 게시판 클래스 대신 Panel클래스를 넣어둔 것이다. 이러한 형태는 이후 게시판 뿐만 아니라 새로 개발된 어떤 클래스도 중앙패널에 추가할 수 있다는 것을 의미한다.

"pgm_syscd":"F001","pgm_class":"Ext.panel.Panel","pgm_icon":"grid","pgm_nm":"ExtJS","brd_number":"001" },

 

최종 실행하면 <그림8>과 같은 결과를 볼 수 있을 것이다.

<그림8> 최종 구현된 뼈대의 실행결과

 

정리하며

이번 시간에는 ExtJS의 MVC를 적용하여 애플리케이션의 뼈대를 구현해보았다. 이러한 뼈대는 향후 게시판 뿐만 아니라 어떤 어플리케이션도 배치할 수 있는 좋은 구조라 할 수 있겠다.

Posted by 베니94
자바스크립트/Ext JS2013. 10. 13. 15:08

오늘날 웹은 비약적인 발전을 이뤄, 이전에는 할 수 없었던 많은 일들이 웹상에서 가능하게 됐다. 웹의 패러다임이 변한만큼 기술도 서버에서 클라이언트로 중심이 이동했다. 이 정에서 자바스크립트 UI 프레임워크가 대세로 자리 잡은 게 현실이지만, 코드는 복잡해지고 유지보수와 확장성의 한계를 드러내게 됐다. 그런 점에서 개발 단계에만 초점이 맞춰진 여타 프레임워크에 비해 ExtJS는 유지보수와 향후 확장이 용이한 MVC 아키텍처를 제공하고 있다.

 

이번 시간에는 ExtJS의 MVC 패턴을 구현해 보고, MVC 패턴을 실무에서 어떻게 활용할 수 있는지 알아보자. 우선 ExtJS에서 사용하는 MVC 패턴을 살펴보겠다.

 

M : 모델(Model)을 지칭한다. ExtJS에서는 데이터를 표현하는 단위가 모델이다. 스토어(Store)가 필요로 하는 Grid, DataView, Form 등의 컴포넌트와 연결될 수 있다.

V : 뷰(View)는 데이터를 표현할 수 있는 모든 컴포넌트를 지칭한다. 모델을 통해 뷰는 데이터를 전달받아 이를 표현한다.

C : 컨트롤러(Controller) 뷰 사이에서의 통신을 처리한다. 컨트롤러는 뷰와 스토어를 참조할 수 있고, 자신이 관장하는 모든 뷰에서 발생하는 이벤트를 감시하며, 각 뷰에게 특정 처리에 대한 명령을 내리는 핵심적인 역할을 한다.

 

우리는 한 가지 내용을 더 알아야 한다. 바로 스토어다. 스토어는 모델들의 집합체로, 서버에서 통신 데이터를 가져오고, 이 데이터를 모델단위로 탑재해 Grid, DataView, Tree 등과 같은 컴포넌트에 전달한다.

이제 코드를 통해 MVC 패턴을 구현해 보자. ExtJS는 Sencha Cmd에서 애플리케이션을 생성하고 빌드할 수 있도록 지원한다.

 

개발환경 구축

개발환경 구축을 위해 Sencha Cmd와 ExtJS를 다운로드 받아 설치하고 환경을 세팅해보도록 하겠다.

아래 <그림 0-0>과 같이 Sencha사이트에 접속하여 좌측의 "Download"를 클릭하여 ExtJS를 다운받고 우측 하단의 "Sencha Cmd"를 클릭하면 <그림 0-1>과 같이 Sencha Cmd를 다운받는다.

<그림 0-0> extjs와 Sencha Cmd 다운로드 페이지

 

<그림 0-1> Sencha Cmd다운로드 페이지

 

이제 다운로드 받은 프로그램을 설치 하도록 하자. 참고로 자바가 먼저 설치되어 있어야 한다.

첫번째는 Sencha Cmd 설치파일을 실행하고 아래 <그림 0-2>에서 "Next"버튼을 클릭한다.

<그림 0-2> Sencha Cmd설치 화면1

 

아래 <그림 0-3>에서 "I accept the agreement"에 체크하고 "Next"버튼을 클릭하자.

<그림 0-3> Sencha Cmd설치 화면2

 

아래 <그림 0-4>에서 설치 폴더를 설정하고 "Next"버튼을 클릭한다.

<그림 0-4> Sencha Cmd설치 화면3

 

이후 계속 "Next"버튼을 클릭하면 설치과정이 진행되고 최종 "Finish"버튼을 클릭하면 Sencha Cmd 설치가 완료된다.

설치가 정상적인지 확인하기 위해 명령프롬프트 창을 열고 "sencha"명령을 실행해서 아래<그림 0-5>와 같이 나오면 설치가 잘 된것이다.

<그림 0-5> Sencha Cmd설치 확인화면

 

이 과정을 거쳤다면 C드라이브에 Sencha라는 폴더가 생성되었고 안으로 들어가면 Cmd폴더에 Sencha Cmd가 설치 되었을 것이다. 이제 다운로드한 Extjs4 라이브러리를 압축을 해제하고 C:\Sencha폴더 아래에 옮기도록 한다.

<그림 0-6> 설치 이후 폴더구조

이제 테마컴파일을 위해 루비를 설치하자. 아래 사이트에 접속하여 루비 Ruby 1.9.3-p448를 내려받고 설치하자.

http://rubyinstaller.org/downloads/

Sencha Cmd에서는 1.8또는 1.9버전만 지원한다. 꼭 1.9버전을 설치하자.

다음으로는 <그림 0-7>과 같이 빌드 과정에서 테마를 위한 sass컴파일용 compass를 설치하자.

 

<그림 0-7> compass 설치과정

  혹여 윈도우 7에 경우 정상적으로 설치 되지 않는 경우가 있는데 이때는 아래 사이트를 참고하여 수동으로 compass를 설치할 수 있다. 

http://awordpress.net/install-sass-compass-manually-windows/


애플리케이션 생성

아래 명령을 실행하여 MyMvc라는 애플리케이션을 생성하자.

sencha -sdk C:\Sencha\ext-4.2.1.883 generate app MyMvc c:\Sencha\MyMvc

 

<그림 1> Sencha Cmd로 생성한 애플리케이션 폴더 구조

 

Sencha Cmd을 통해 생성된 애플리케이션의 주요 코드를 살펴보자. <리스트 1>은 app.js와 application.js의 코드다. 이 두 개의 파일에는 애플리케이션의 주요 설정이 들어있다. ①은 만들려는 애플리케이션 이름이다. 보통 패키지 명의 최상위 이름을 사용한다. ②에서는 application.js의 클래스를 상속했다. ③은 Viewport 객체를 자동으로 생성할 것인지를 설정하는 부분이다. 이때 값이 true이면 view 폴더의 Viewport 클래스를 자동으로 호출할 수 있다. ⑥은 ③이 false일 경우 이 함수 내부에서 Viewport를 별도로 생성하면 사용할 수 있다.

 

<리스트 1> app.js와 application.js 내부 코드

// app.js

Ext.application({

    name: 'MvcApp', // ① 애플리케이션 명

    extend: 'MvcApp.Application', // ② application.js를 상속했다.

    autoCreateViewport: true    // ③ view.Viewport의 인스턴스 생성

});

// application.js

Ext.define('MvcApp.Application', {

    name: 'MvcApp',        

    extend: 'Ext.app.Application',    // ④ 애플리케이션 실행 이후 처리

    controllers: [

        'FrameController'        // ⑤ 사용할 컨트롤러

    ],

    launch:function(){        // ⑥ 애플리케이션 실행 이후 처리

    }

});


<리스트 1>에서 autoCreateViewport를 true로 설정해 view 폴더에 Viewport.js 파일을 만들어 구현해야 한다. <리스트 2>와 같이 border layout을 사용해 서쪽, 북쪽, 중간에 container를 배치한다.

 

<리스트 2> Viewport.js 파일 코드

Ext.define('MvcApp.view.Viewport', {

    extend: 'Ext.container.Viewport',

    layout : 'border',

    items: [{

        region: 'north',

        xtype: 'container',

        height : 100,

        style : { borderColor : '#000000', borderStyle :'solid', borderWidth : '1px' }

    }, {

        region: 'center',

        xtype: 'container',

        style : { borderColor : '#000000', borderStyle :'solid', borderWidth : '1px' }

    }, {

        region: 'west',

        xtype : 'container',

        style : { borderColor : '#000000', borderStyle :'solid', borderWidth : '1px' },

        width: 200

    }]

});


이제 애플리케이션을 실행해 보자. <리스트 3>과 같이 Sencha Cmd에서 제공되는 웹서버를 작동시킨다.

 

<리스트 3> 애플리케이션 실행 코드

C:\Sencha>sencha fs web -port 8000 start -map c:\Sencha\MvcApp

Sencha Cmd v4.0.0.126

[INF] Starting shutdown listener socket

[INF] Listening for stop requests on: 1916

[INF] Mapping http://localhost:8080/ to c:\Sencha\MvcApp...

[INF] Starting http://localhost:8080

[INF] jetty-8.1.7.v20120910

[INF] NO JSP Support for /, did not find org.apache.jasper.servlet.JspServlet

[INF] started o.e.j.w.WebAppContext{/,file:/C:/Sencha/MvcApp/}

[INF] started o.e.j.w.WebAppContext{/,file:/C:/Sencha/MvcApp/}

[INF] Started SelectChannelConnector@0.0.0.0:8080

[INF] Started http://localhost:8080


브라우저에서 실행하면 <그림 2>와 같은 결과를 볼 수 있다.

<그림 2> index.html 실행 결과

 

그 다음 컨트롤러를 구현하자. controller 폴더에 FrameController.js 파일을 만들고 <리스트 4>와 같이 작성한다.

 

<리스트 4> FrameController.js 코드

Ext.define(' MvcApp.controller.FrameController', {

    extend : 'Ext.app.Controller',

    views : [],            // ① 참조할 뷰

    stores : [],        // ② 참조할 스토어

    refs : [],            // ③ 참조할 뷰 getter 설정

 

    init : function(app) {    // ④ 초기화

        this.control({        // ⑤ 현재 컨트롤러 영역

            'button' : {    // ⑥ 이벤트를 일으킨 컴포넌트

                click : function(){    // ⑦ 이벤트

                    console.log(arguments);

                }

            }

        });

    }

});


<리스트 4>를 자세히 살펴보자. ①에서는 컨트롤러가 참조할 View 클래스를 명시한다. 여기에 명시되면 각 뷰에서 발생하는 이벤트를 감지할 수 있다. ②에서는 애플리케이션에서 공유할 스토어를 명시했다. 단, 공유하지 않고 독립적으로 사용하는 스토어는 기입하지 않으며 각각의 View 클래스별로 따로 생성해야 문제가 없다.

③에는 참조된 View 클래스를 어떤 이름으로 사용할지 기입한다. 이 부분은 뒤에서 설명하겠다. ④는 컨트롤러의 핵심으로, 참조하는 모든 View 클래스에서 발생하는 이벤트를 감지하는 영역이다. ⑥과 ⑦은 Viewport의 Button에서 Click 이벤트가 발생하면 console.log를 실행하는 코드다.

이제 <리스트 4>가 작동되도록 <리스트 1>코드에 있는 ③ 'FrameController' 주석을 제거하고 <리스트 2>에서의 north 영역 코드를 <리스트 5>와 같이 수정하자.

 

<리스트 5> Viewport.js의 north 영역 코드

Ext.define(' MvcApp.view.Viewport', {

..

items: [{

region: 'north',

     xtype: 'button',

     text : 'Button',

     height : 100,

     style : { borderColor : '#000000', borderStyle :'solid', borderWidth : '1px' }

    },

    ..


코드를 실행하고 north 영역의 버튼을 선택하면 <그림 3>과 같은 결과를 얻을 수 있다. <그림 3>과 같이 선택된 버튼 이벤트를 컨트롤러가 감지해야 원하는 명령을 내릴 수 있다.

 

<그림 3> 버튼 추가 후 실행화면

 

이제 좀더 복잡한 코드를 만들어 보자. <그림 4>에서 빈 칸으로 남겨둔 views, stores, ref를 구현하겠다. 아직 비어있는 Viewport의 중간 패널에 간단한 그리드 패널을 추가하고 Click 이벤트를 가지고 특정 함수를 호출한다. <리스트 6>, <리스트 7>, <리스트 8>, <리스트 9>와 같이 클래스를 생성하고 각 폴더에 추가하자.

<리스트 6> Program Model 클래스

// app/model/Program.js

Ext.define(' MvcApp.model.Program', {

    extend: 'Ext.data.Model',

fields: [

        'pgm_nm',

        'pgm_class'

]

});


<리스트 7> Program Store 클래스

// app/store/Programs.js

Ext.define(' MvcApp.store.Programs', {

    extend : 'Ext.data.Store',

    autoLoad : false,

    model : ' MvcApp.model.Program',

    proxy : {

        type : 'ajax',

        url : '/json/programlist.json',

        reader : {

            type : 'json',

            root : 'entitys',

            totalProperty : 'totalCount',

            messageProperty : 'message'

        }

    }

});


<리스트 8> 스토어에 제공할 데이터 파일

// 웹루트에 json 폴더를 만들고 programlist.json 파일을 생성한다.

{"entitys":[{    "pgm_class":"benney.board.Board","pgm_nm":"게시판"},

            {    "pgm_class":"benney.login.Login","pgm_nm":"로그인"}],

"errMsg":"","errTitle":"검색결과","message":"","success":true,"totalCount":"2"

}


<리스트 9> 새로 추가할 그리드 클래스

Ext.define(' MvcApp.view.MyGrid',{

    extend : 'Ext.grid.Panel',

    alias : 'widget.mygrid',

    

    initComponent : function(){

        var me = this;

        Ext.apply(this, {

            store : 'Programs',

            columns : [{

                text : '클래스 이름 ',

                flex : 1,

                dataIndex : 'pgm_class'

            },{

                text : '프로그램명 ',

                flex : 1,

                dataIndex : 'pgm_nm'

            }]

        });

        me.callParent(arguments);

    }

});


이때 컨트롤러에서 <리스트 9>의 MyGrid 클래스와 Programs Store 클래스를 참조할 수 있게 코드를 수정한다.

 

<리스트 10> 변경된 컨트롤러 클래스

Ext.define(' MvcApp.controller.FrameController', {

    ..중략..

    views : ['MyGrid'],            // ① 참조할 뷰

    stores : ['Programs'],        // ② 참조할 스토어

    refs : [{

        ref : 'myGrid',

        selector : 'mygrid'

    }],            // ③ 참조할 뷰의 getter 설정

..

});

그리고 Viewport 클래스의 center 영역에 MyGrid 클래스의 widget명을 추가한다(<리스트 11> 참조).

<리스트 11> Viewport 클래스 center 영역 수정

{

    region: 'center',

    xtype: 'mygrid',

    style : { borderColor : '#000000', borderStyle :'solid', borderWidth : '1px' }

},


이제 브라우저를 재실행해 결과를 확인한다(<그림 4> 참조).

<그림 4> MyGrid 클래스를 추가한 결과

 

컨트롤러에 있는 views:[] 부분에 MyGrid 클래스를 등록해 Viewport 클래스에 있는 widget명인 'mygrid'로 사용할 수 있다. 만약 컨트롤러에 등록된 코드를 지우면 프로그램은 정상으로 작동하지 않는다.

그 다음 Viewport 상단 버튼을 선택해 하단 그리드에 있는 데이터가 표시되게 하고, 표시된 그리드를 선택하면 그리드 패널의 타이틀이 세팅되도록 <리스트 12>와 같이 수정하자.

 

<리스트 12> 수정된 FrameController 컨트롤러

Ext.define(' MvcApp.controller.FrameController', {

    ..        

    init : function(app) {    // ④ 초기화

        this.control({        // ⑤ 현재 컨트롤러 영역

            'button' : {    // ⑥ 이벤트를 일으킨 컴포넌트

                click : function(){    // ⑦ 이벤트

                    this.getStore('Programs').load();// ⑧

                }

            },

            'mygrid' : {    // ⑨

                itemclick : this.myGridClick    // ⑩

            }

        });

    },

    myGridClick : function(){

        var grid = this.getMyGrid();    // ⑪

        grid.setTitle('그리드 클릭');

    }

});

<리스트 12>를 자세히 설명하겠다. ⑥에 있는 이벤트를 발생시킨 컴포넌트로 인해 버튼에서 ⑦에서는 Click 이벤트가 발생했고, 그 결과 ⑧을 실행시킨다. ⑧에 있는 this.getStore() 메소드는 컨트롤러 상단 stores:[]에 등록된 스토어를 호출하게 해주고, 스토어의 load() 메소드가 호출됨으로써 그리드에 데이터가 출력된다.

⑨와 ⑩에서 그리드에 표시된 데이터 로우를 클릭하면 myGridClick 함수가 호출된다. 그리고 ⑪의 this.getMyGrid() 함수는 컨트롤러 상단의 ref 참조란에 표기된 ref:'myGrid'의 getter 메소드다. 또 this.getMyGrid() 함수를 통해 MyGrid 클래스의 인스턴스에 접근해 setTitle 메소드로 그리드 패널의 title 속성을 변경한 것이다.

변경된 코드를 실행하면 <그림 5>와 같은 결과를 얻을 수 있다.


<그림 5> 수정된 컨트롤러의 실행결과

 

이렇게 해서 하나의 MVC 애플리케이션을 완성했다. 이제 Sencha Cmd를 통해 배포과정을 진행해 보자. 애플리케이션 폴더로 이동해 <리스트 13>과 같이 빌드 과정을 진행한다.

 

<리스트 13> 애플리케이션 배포 코드

C:\Sencha\MvcApp>sencha app build

Sencha Cmd v4.0.0.126

[INF]

[INF] init-plugin:

[INF]

[INF] cmd-root-plugin.init-properties:

[INF]

[INF] init-properties:

… 중략…

[INF] -after-build:

[INF]

[INF] build:

[INF]

[INF] app-build:


빌드가 완료됐으니 최종 배포할 애플리케이션과 개발 단계의 애플리케이션이 어떻게 다른지 알아보자. <그림 6>은 빌드 전 캐시를 모두 지운 상태에서 최초로 실행한 결과이고, <그림 7>은 캐시를 이용 두 번째 실행한 결과를 개발자 도구에 있는 Network 탭에서 확인한 결과다.

 

<그림 6> 개발 버전의 최초 실행결과

 

<그림 7> 개발 버전의 두 번째 실행결과

 

이제 빌드를 완료한 배포판 애플리케이션을 실행하고 <그림 6>과 <그림 7>과 동일하게 <그림 8>과 <그림 9>처럼 확인하자. Sencha Cmd는 웹루트/build/MvcApp 이하에 애플리케이션을 빌드하므로 이를 브라우저에서 실행하면 된다.

 

<그림 8> 배포판의 최초 실행결과        

 

<그림 9> 배포판의 두 번째 실행결과

 

<그림 6>부터 <그림 9>까지의 결과를 정리한 것이 <표 1>이다.

 

 개발 버전

 배포 버전

 캐시 Ⅹ

 캐시 ○

 캐시 Ⅹ

 캐시 ○

 요청 파일 수

 13개

 13개

 4개

 3개

 전송용량

 6.8MB

 4.5KB

 2.2MB

 542Byte

 실행시간

 894ms

 503ms

 626ms

 68ms

<표 1> 개발 버전과 배포 버전의 네트워크 사용량 비교

 

<표 1>에서 캐시를 사용한 결과를 비교하자. Sencha Cmd는 빌드 시 사용되는 모든 클래스를 하나의 파일 합치고 공백과 변수 등을 1Byte로 줄여 전반적인 배포 용량을 줄였다. 이를 통해 실행시간 또한 대폭 줄어 든 것을 확인 수 있다. 이는 애플리케이션 규모가 커질수록 이런 빌드 과정이 절대적으로 필요함을 느끼게 해주는 부분이다.

 

정리하며

지금까지 ExtJS의 MVC 애플리케이션을 구현하고 Sencha Cmd를 통해 애플리케이션을 배포하는 과정을 알아봤다. 이후 진행될 연재에서는 지금까지 살펴본 것들을 가지고 게시판을 개발하면서 좀더 구체적인 ExtJS의 코드를 알아보겠다.

Posted by 베니94
자바스크립트/Ext JS2013. 10. 13. 15:08

ExtJS 4 클래스 시스템


ExtJS는 300여개 이상의 클래스와 이를 기반한 아키텍쳐를 가졌다. 객체지향 언어를 표방해 상속, 다중상속, 이벤트를 통한 접근 등 고급언어에서 볼 수 있는 거의 모든 개념을 탑재해 다양하고 복잡한 어플리케이션 구현이 가능하다.


자바스크립트는 유연한 언어다. 그러나 이러한 유연함이 많은 코딩스타일과 기법 등을 낳게 됐고 코드를 예측하거나 유지하는 데 많은 비용을 지불하게 됐다. ExtJS 클래스 시스템은 캡슐화를 지원하고, 표준코딩 컨벤션을 사용 할 수 있어 작성된 코드의 예측, 확장이 용이하다. ExtJs 클래스시스템을 통해 시간이 지날 수록 어플리케이션의 가치는 증가하고 안정적인 유지보수가 가능해 진다.

 

이장에서는 아래 기능을 통해 ExtJs의 새로운 클래스시스템을 맛보고 배우게 된다. ExtJS를 이해하는 가장 기초적이며 가장 중요한 장이라 하겠다.

  • 클래스 선언과 생성
  • Config 기반 구성요소
  • 상속(클래스 확장)
  • 믹스인(Mixins)
  • 스태틱
  • 동적 클래스 로딩
  • Alias(xtype)

클래스 선언과 생성

아래 코드는 전형적인 ExtJS 클래스 정의 및 생성 방법이다.

Ext.define('MyClass', {         // Step 1

    extend : 'Ext.panel.Panel',     // Step 2

    title : '안녕하세요 환영합니다.^^',

    initComponent : function() {     // Step 3

        var me = this;

        this.callParent(arguments);    // Step 4

    }

});

 

var myClass = Ext.create('MyClass', {     // Step 5

    renderTo : Ext.getBody()

});

 

Step1 : Ext.define함수를 통해 클래스를 정의했다. 클래스명에는 문자와 숫자가 포함될 수 있고 밑줄은 권장하지 않는다. 패키지를 표현하기 위해 '.'으로 구분하며 아래 예시처럼 최상위이름과 최종이름은 카멜케이스를 쓰고 그 외에는 소문자로 표현하는 것을 권장한다.


    MyClass.form.action.AutoLoad

 

Step2 : 상속받아 확장하려는 클래스를 명시한다. ExtJS는 300여개 이상의 클래스가 존재하고 이중 하나를 골라 적절히 수정해 원하는 기능을 구현한다.

    

Step3 : 확장할 클래스 기능을 재정의 할 때 initComponent함수가 사용된다.

이 함수는 확장할 클래스 구성요소를 초기화하고 원하는 대로 변경 할 수 있게한다.

 

Step4 : "this.callParent(arguments)"는 확장할 클래스의 initComponent함수를 호출한다. 이는 재정의한 내용을 확장할 클래스에 전달해서 확장할 클래스가 작동하기 위함이다.

 

Step5 : 정의된 클래스를 사용하기 위한 클래스 생성코드다. ExtJs는 정의된 클래스를 생성시 Ext.create함수를 사용해 생성하고 위 코드처럼 특정 변수에 인스턴스를 저장한다.

 

아래 그림은 위의 코드를 실행한 결과다.

Config 기반 구성요소

ExtJS 클래스 시스템은 config설정 기능을 제공한다. 클래스에 config옵션을 정의하면 클래스 시스템은 자동으로 4개 메소드(getter, setter, reset, apply)를 생성한다. 이를 통해 클래스에 매개변수를 전달해 값을 설정하고 수정할 수 있으며 코드라인이 줄어들어 API간 일관성을 유지할 수 있는 장점을 제공한다.

 

새로운 클래스를 만들고 config를 설정 해보자.

Ext.define('Benney.ClassRoom', {

    config : {                // Step1

        grade : '미정',

        className : '미정',

        teacher : '미정'

    },

    constructor : function(config) {    

        this.initConfig(config);    // Step2

    },

    getString: function(){    // Step3

        return     '- 교실정보 -\n학년 : '

                + this.getGrade()

                + '\n반명 : ' + this.getClassName()

                + '\n담임 : ' + this.getTeacher();

    }

},

function() {

    console.log('Benney.ClassRoom class가 정의됐음!');

});

 

var classroom = Ext.create('Benney.ClassRoom');    // Step4

classroom.setGrade('2');                // Step5

classroom.setClassName('꾸러기')

classroom.setTeacher('홍길동');

console.log(classroom.getString());            // Step6


아래 그림은 위의 코드를 실행한 결과다.

Step1 : config정보를 정의한다. 위의 코드에서는 grade, className, teacher 등 3개 config변수를 선언했다.

 

Step2 : 생성자 함수로 여기서는 'this.initConfig(config)'함수를 호출해 config변수를 초기화 했다. 예제는 기존 ExtJS클래스를 확장하지 않아 생성자에 'this.initConfig(config)'를 호출하지만 확장하면 initComponent함수 내부에서 실행되게 코딩 해야한다.

 

Step3 : getString()함수는 config로 설정된 내부 변수값을 확인하기 위해 각 config 변수의 getter메소드를 호출한다.

 

Step4 : 정의한 클래스를 생성한다.

Step5 : 외부에서 새로운 값을 세팅한다.

Step6 : 새로 세팅된 값이 적용됐는지 getString메소드를 통해 확인한다.

 

위에서 getter, setter메소드를 알아봤다. 이제 소스를 수정해 apply메소드를 알아보자.

apply메소드는 setter메소드 호출 시 자동으로 실행된다..

아래 코드는 setter메소드 이후에 apply메소드가 호출되는지 알아보기 위해 각config변수에 맞는 apply메소드를 재정의 하였다. 기존 getString메소드의 텍스트 문구는 각 apply메소드로 이전했다.

    applyGrade : function(grade){

        this.grade = '학년 : ' + grade

    },

    

    applyClassName : function(className){

        this.className = '반명 : '+ className;

    },

    

    applyTeacher : function(teacher){

        this.teacher = '담임 : '+ teacher;

    },

    getString: function(){    // Step3

        return     '- 교실정보 -\n'

                + this.getGrade()

                + '\n' + this.getClassName()

                + '\n' + this.getTeacher();

    }


위의 코드를 실행하면 이전과 동일한 내용의 결과가 확인된다. 이처럼 apply메소드는 setter메소드와는 달리 config값을 포함한 그 밖의 내용을 설정하기 위해 사용한다.

  

상속(클래스 확장)

ExtJS는 extend함수를 통해 상속을 지원한다. 상속을 통해 이미 존재하는 클래스 기능을 그대로 사용하면서 원하는 기능만 일부 수정한다. 상속을 통해 UI가 존재한다면 UI를, 기능이 존재한다면 함수를 재활용 한다.

아래 코드는 'ClassRoom'클래스를 정의하고 이를 확장해 'Student'클래스를 만든다.


  •  ‘RiaApp.ClassRoom’이라는 이름의 클래스를 정의한다.

Ext.define('RiaApp.ClassRoom', {

    config : {

        grade : '미정',

        className : '미정',

        teacher : '미정'

    }

});


  • config정보를 재정의할 apply메소드를 정의한다.

applyGrade : function(grade) {

this.grade = '학년 : ' + grade

},

      applyClassName : function(className) {

this.className = '반명 : ' + className;

},

applyTeacher : function(teacher) {

this.teacher = '담임 : ' + teacher;

},


  • 생성자를 정의하고 config정보를 초기화 한다.

constructor : function(grade, className, teacher) {

this.initConfig(); // 초기화 실행

if (grade) // setter메소드로 인자로 받아온 값을 세팅한다.

this.setGrade(grade);

if (className)

this.setClassName(className);

if (teacher)

this.setTeacher(teacher);

},


  • 전체 config정보를 출력 할 메소드를 정의한다.

getString : function() {

  console.log('RiaApp.ClassRoom called')

        return '\n- 교실정보 -\n' + this.getGrade() + '\n' +  this.getClassName()

                + '\n' + this.getTeacher();

}


  • 클래스를 실행할 코드를 만든다. 클래스명 뒤에 학년, 교실명, 담임명을 인자로 전달하면 생성자에 의해 
    config정보가 세팅된다.

var classroom = Ext.create('RiaApp.ClassRoom', '2', 'ExtJS배워보기', '홍길동');

console.log(classroom.getString());

    

  • 프로그램을 실행하고 콘솔을 통해 확인한다.

  • ClassRoom클래스를 확장해 Student클래스를 만들어 보자. 이 Student클래스는 studentName,
    studentAddress라는 두개 config변수를 가진다.

Ext.define('RiaApp.Student',{

extend : 'RiaApp.ClassRoom',    // 확장클래스명

      config : {                // config

          studentName : '',

          studentAddress : ''

}

});


  • 생성자를 구현한다. 생성자는 외부로부터 인자를 받아 현클래스 config정보를 세팅하고 확장한 클래스 
    생성자도 호출해 config정보를 세팅했다.

constructor : function(grade, className, teacher,

      studentName, studentAddress) {    // Step1

            

          this.initConfig();    // Step2

          if (studentName)     // Step3

              this.setStudentName(studentName);

          if (studentAddress)

              this.setStudentAddress(studentAddress);

            

          // Step4

          this.callParent([ grade, className, teacher ]);

},

Step1 : 클래스 생성시 외부에서 받아올 인자로 확장할 클래스에서 사용할 인자도 포함한다.


Step2 : config정보 초기화를 위해 this.initConfig()를 호출한다.


Step3 : 인자로 받아온 값이 정상적이라면 setter메소드를 통해 config를 설정한다.


Step4 : this.callParent함수는 확장한 슈퍼클래스의 함수를 호출 한다. 이 코드를 호출한 함수와 동일한 이름의 함수를 슈퍼클래스에서 호출한다. 여기서는 생성자에서 호출돼 슈퍼클래스 생성자를 호출한다. this.callParent호출 시 3개(grade, className, teacher) 인자를 넘긴다. 슈퍼클래스 생성자 또한 이 3개 인자를 받아 구현됐다.


  • getStudent함수는 getString()함수와 함께 학생정보와 교실정보를 한꺼번에 표시한다..

getStudent : function(){

    return '- 학생정보 -'

    + '\n이름 : '+ this.getStudentName()

    + '\n주소 : '+ this.getStudentAddress() +

    this.getString();

},


  • getString()함수를 보자. 아래 코드는 return this.callParent()만 존재한다. 이는 슈퍼클래스 getString()함수를 호출하고 그 값을 반환하라는 의미다.

getString : function() {

    console.log('RiaApp.Student called')

    return this.callParent();

}


  • 여기까지 ClassRoom클래스를 확장해 Student클래스를 작성했다. 작성된 클래스를 생성하고 getStudent()함수를 호출한다.

var student = Ext.create('RiaApp.Student','2','꾸러기', '홍길동','김철수', '경기도 고양시 일산동');

console.log(student.getStudent());


  • 아래 그림은 위의 코드를 실행한 결과다.

클래스 확장은 가장 강력한 기능이고 모든 클래스에서 광범위하게 사용된다. 위에서는 쉽게 설명하기 위해 UI가 없는 클래스를 작성했지만 대부분 UI콤포넌트를 확장해 프로그래밍을 한다.

 

믹스인(Mixins)

ExtJs는 단일 클래스만 상속할 수 있어 다중 상속을 지원하기 위해 믹스인(Mixins)을 사용한다. 여러개 클래스를 믹스인을 통해 참조하고 참조된 클래스 기능을 사용할 수 있게 했다. 상속(extend)과 override와 유사하지만 이미 존재하는 함수를 교체하지 않는다.

앞서 설명한 'ClassRoom'클래스를 Mixins에 포함시키고 Ext.panel.Panel클래스를 확장해 UI를 갖는 변형된 Student클래스를 만든다. 대부분 경우 UI 클래스로 이와 함께 config를 포함하면서 Mixins를 같이 사용하는 경우를 위한 예제다.


  • Ext.define을 사용 클래스를 정의한다. 이전 'Student'클래스와 달리 Ex.panel.Panel클래스를 상속받고 mixins내부에 classroom변수에 'RiaApp.ClassRoom'클래스를 문자열로 참조했다.

Ext.define('RiaApp.Student',{

    extend : 'Ext.panel.Panel',

        config : {

            studentName : '',

            studentAddress : ''

        },

        mixins : {

            classroom : 'RiaApp.ClassRoom'

        },


  • initComponent는 컴포넌트 초기화를 위한 함수다. 아래처럼 코드구현이 끝나면 this.callParent()를 실행, 상속한 클래스의 initComponent함수를 호출하고 동일한 초기화를 진행한다. this.mixins.classroom.constructor은 mixins에 설정된 변수에 접근하기 위한 코드다. 'ClassRoom'클래스에 접근해 생성자를 호출하고 3개 인자를 전달한다.

      initComponent: function(){

          // mixins의 classroom변수에 접근 생성자 호출

          this.mixins.classroom.constructor

              (this.grade, this.className, this.teacher );

      

          this.callParent(arguments);

},


  • 학생정보를 출력하는 함수다. Mixins을 참조해 'ClassRoom'클래스 getString()함수를 호출한다.

getStudent : function(){

            return '- 학생정보 -'

            + '\n이름 : '+ this.getStudentName()

            + '\n주소 : '+ this.getStudentAddress() +

            this.mixins.classroom.getString();

      }


  • 실행코드를 작성한다. 이 클래스는 Ext.panel.Panel클래스를 상속받아 이전과 달리 config정보를 인자로 전달하지 않고 아래처럼 '{}'내부에 Panel에서 사용하는 기타 정보와 같이 전달했다.

    var student = Ext.create('RiaApp.Student',{

        title : 'Hello Student !!',

        html : '안녕하세요 여러분',

        grade : '2',

        className : '꾸러기',

        teacher : '홍길동',

        studentName : '김철수',

        studentAddress : '경기도 고양시 일산',

        renderTo : document.body

    });

 

  • 위의 코드를 실행하면 아래처럼 이전과 동일한 내용이지만 Ext.panel.Panel클래스로 인해 UI가 존재하는 클래스가 됐다.

 

스태틱

모든 클래스는 스태틱(statics)메소드를 정의할 수 있다. static은 인스턴스를 생성하지 않고 접근이 가능하다. 즉 create로 생성하지 않고 '클래스명.변수/함수'로 접근한다.

아래 코드를 살펴보자. (좀더 보강…)

Ext.define('Student', {

config : {

          studentName : null

      },

statics : {


studentCount : 0,

            student : function(studentName) {

                return new this({    // 강제로 생성자 호출

                    studentName : studentName

                });

}

},

      // 생성자

constructor : function(config) {

this.initConfig(config);

     // this.statics().studentCount++;과 동일

this.self.studentCount++;

return this;

}

});

var student1 = Student.student('홍길동');

var student2 = Student.student('김철수');

console.log(student2.getStudentName());

console.log(Student.studentCount);

 

아래 그림은 위의 코드를 실행한 결과다.

동적클래스 로딩

동적 클래스 로딩은 ExtJS4의 새로운 기능으로 빠른 페이지 로딩보다는 유연성이 중요한 개발환경에서 사용된다. 자바스크립트를 사용하기 위해 HTML파일 내부에 아래처럼 자바스크립트 파일을 포함시켜야 한다.

<script type="text/javascript" src="app.js"></script>

운영환경은 모든 자바스크립트 파일을 병합, 압축해 HTML파일에 포함시키지만 개발환경에서는 병합, 압축하지 않은 독립된 파일로 존재하는 클래스를 HTML파일에 포함시켜야 하는 경우가 발생한다. 즉 Student클래스가 ClassRoom클래스를 상속했다면 절대로 ClassRoom클래스 파일이 HTML 파일에 포함돼 있어야 한다.

 

  • 예제를 통해 확인해 보자. 아래 그림은 예제 폴더구조다. 앞서 살펴본 ClassRoom, Student클래스를 활용 동적 클래스로딩이 어떻게 작동되는지 알아보자.

 

 

예제 루트에 src폴더를 만든다. 일반적인 형식이니 따르기로 하자. src폴더 밑에 우리가 만들 어플리케이션 이름으로 폴더를 하나 더 생성한다. 이전 믹스인과 상속을 설명한 예제에서 ClassRoom, Student 두개 코드만 추출해 각기 클래스명으로 js파일을 생성했다.

 

  • inxdex.html파일을 작성하자. 두 개 define된 클래스와 이를 생성하고 실행시키는 app.js 등 총3개 js파일을 포함시켰다.

 

ClassRoom 클래스와 Student클래스는 앞서 설명한 내용과 동일하다. 단 이 2개 클래스를 생성하여 실행하는 방법을 설명하려 한다.

아래 코드는 동적로딩을 사용하지 않고 html파일에 필요한 js파일들이 포함돼 있어 정상적으로 실행된다.

// 이하 app.js

Ext.onReady(function() {

    

    var student = Ext.create('RiaApp.Student',{

        title : 'Hello Student !!',

        html : '안녕하세요 여러분',

        grade : '2',

        className : '꾸러기',

        teacher : '홍길동',

        studentName : '김철수',

        studentAddress : '경기도 고양시 일산',

        renderTo : document.body

    });

    console.log(student.getStudent()); // Step6

});

 

  • 아래 그림은 위의 코드를 실행한 결과다.

  • 이제 동적클래스 로딩을 테스트 하기 위해 index.html파일 내부 ClassRoom.js와 Student.js파일을 주석 처리하고 app.js내 Ext.onReady전에 아래처럼 Ext.Loader를 설정한다.

// Configure the Ext.Loader class

Ext.Loader.setConfig({

    enabled : true,     // Step1

    paths : {        // Step2

        'RiaApp' : 'src/RiaApp'

    }

});

Ext.require('RiaApp.Student'); // Step3

Ext.onReady(function() {

..

 

Step1 : Ext.Loader를 사용할 수 있게 활성화 한다.

Step2 : src폴더를 RiaApp라는 이름으로 경로를 잡아준다. src폴더에는 'RiaApp.'으로 시작하는 클래스가 위치한다.

Step3 : Ext.require함수에 동적로딩 할 클래스를 명시한다.

 

  • 브라우저를 통해 실행한다. 이전과 동일하게 실행된다. Index.html파일 내부에 두 개 클래스 파일이 주석으로 제거된 상태에도 이전과 같이 정상적인 결과를 보여준다. 위의 코드로 인해 내부적으로 어떤 일이 생겼는지 확인해 보자. 아래 그림은 인스펙터 Element탭에서 최종 실행 된 html 결과다. 

그림을 자세히 보면 ClassRoom.js와 Student.js는 주석처리 됐고 app.js하단에 다시 ClassRoom.js와 Student.js가 링크된 것이 보인다. 왜 일까? 주석으로 막힌 두개의 js파일은 앞서 우리가 주석 처리한 코드이고 app.js하단 두개의 js파일은 Ext.Loader 설정에 의해 자동으로 생성됐다. 즉 Ext.Loader를 설정하고 Ext.require('RiaApp.Student')를 통해 동적로딩할 클래스를 명시했기 때문에 자동으로 Student.js파일 링크가 생성됐다. 그런데 우리가 필요하다고 설정한 클래스는 RiaApp.Student클래스 하나로 ClassRoom.js은 왜 있는 것일까?

 

이는 ExtJS 클래스 시스템이 RiaApp.Student클래스를 실행하기 위해 필수적으로 필요한 클래스가 무엇인지 파악한 후 필요한 클래스도 같이 로딩하기 때문이다.

 

이처럼 동적클래스 로딩은 특정 폴더를 경로로 설정해 폴더내부 클래스 파일을 Ext.require를 통해 로딩해줘 html파일에 수작업으로 js파일을 링크하지 않는다.

이는 개발단계에서 특정 라이브러리를 포함하거나 그외 어플리케이션 클래스를 사용할 경우 유용하다.

 

  • 좀더 알아보자. 아래 처럼 Ext.require를 주석처리하고 실행한다.

//Ext.require('RiaApp.Student');

이전과 달리 콘솔창 상단에 경고메시지가 출력된다. 메시지 내용은 Ext.onReady상단에 Ext.require('RiaApp.Student')를 추가하라는 내용이다. 경고를 보였을 뿐 정상적으로 프로그램은 실행됐다. 다시 인스펙터 Element탭을 보자. 이전과 달라진 점이 무엇인지 아래 그림을 통해 확인하자.

 

이전과 달리 app.js하단에 ClassRoom.js와 Student.js 파일 링크가 없어졌다. 이는 Ext.require를 주석 처리해 링크가 사라진 것이다.

그렇다면 왜 프로그램은 정상적으로 실행됐을까?

 

  • 이유는 실행 코드에 있다. 우리는 Ext.Loader를 통해 RiaApp이하 클래스 폴더가 어디 있는지 설정해 줬다.

Ext.Loader.setConfig({

    enabled : true,

    paths : {

        'RiaApp' : 'src/RiaApp'

    }

});


  • 이후 이 예제를 실행하기 위해 Ext.create를 사용했다. 즉 아래처럼 클래스 풀네임을 쓰고 생성할 경우 이미 RiaApp클래스 폴더 경로(src/RiaApp)가 존재해 RiaApp.Student로 생성해도 Student클래스에 직접 접근 가능하다.

var student = Ext.create('RiaApp.Student',{ // src/RiaApp폴더에 접근 가능

    title : 'Hello Student !!',

    …

    renderTo : document.body

});

 

Alias (xtype)

이렇게 실행에 문제가 없다면 왜 경고를 보여주는 것일까? Ext.create는 단일 클래스를 생성하는 코드로는 적합하나 여러 개 클래스를 하나의 클래스 자식으로 포함 할 경우에는 부적합하다. 이 경우 변수를 통해서 자식을 추가하기 보다는 클래스에 alias를 설정해 부모가 될 클래스 xtype속성으로 사용하는 것을 권장한다.

 

  • Student클래스에 alias를 'widget.student'로 지정하자. 'widget.'이후 student가 위젯명으로 사용된다.

Ext.define('RiaApp.Student', {

extend : 'Ext.panel.Panel',

      alias : 'widget.student',

 

  • Ext.create를 통한 실행코드를 위의 alias로 변경해 본다. 아래 코드는 Ext.Viewport를 생성하고 Student클래스를 alias명으로 Ext.Viewport item으로 추가하는 코드다.

    // Step1

    Ext.create('Ext.Viewport',{

        items : [{

            // xtype 을 이용해 컴포넌트를 배치한다.

            // 여기서는 정확히 require가 돼 있어야 한다.

            // xtype으로는 상단경로와 함께 클래스를 찾지 못한다.

            xtype : 'student',        // Step2

            title : 'Hello Student !!',

            html : '안녕하세요 여러분',

            grade : '2',

            className : '꾸러기',

            teacher : '홍길동',

            studentName : '김철수',

            studentAddress : '경기도 고양시 일산'

        }]

    });

 

Step1 : Ext.Viewport는 브라우저에 유일하게 하나만 존재하는 최상위 객체다.

Step2 : Ext.Viewport 자식으로 추가한다. xtype속성에 Student클래스 alias명을 넣는다.

기존 Ext.create 내부 코드를 Ext.Viewport 내부 items 코드안으로 그대로 가져왔다.

 

  • 아래 그림은 위의 코드를 실행한 결과다.

실행 결과 'widget.student'를 인식하지 못한다. 이는 alias로 사용한 위젯명이 클래스명 처럼 '.'을 통해 폴더명을 포함하지 않고 'student'로 함축적으로 사용했기 때문에 widget.student인 Student클래스를 못찾는다. 이때 필요한 것이 Ext.require('RiaApp.Student') 코드다. 이전에 주석처리 했으므로 주석을 없애고 다시 실행하면 정상적으로 동작한다.

이처럼 widget형태로 간략히 쓰기 위해서는 Ext.require를 통해 필요한 클래스가 미리 로딩돼야 한다..


section2.zip


 

Posted by 베니94