GNOME3ApplicationDevelopmentBeginnersGuide:Chapter 08

From 흡혈양파의 번역工房
Jump to navigation Jump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.
제 8 장 데이터 다루기

데이터 다루기

데이터에 대해 이야기할 때는 데이터베이스뿐만 아니라 다른 소스로부터의 데이터까지를 지칭한다. 이는 데이터로 접근하고 조작하며 사용자에게 표시하는 것까지 포함한다. 데이터로 접근성이 좋다는 것은 더 나은 통합을 의미하는데, GNOME 은 이를 잘 구현한다. GNOME 은 해당 작업을 수행하도록 많은 API를 제공하는데, 이번 장에서는 이러한 API를 논하도록 하겠다.


본 장에서는 여러 소스로부터 데이터를 얻는 방법과 이것을 화면으로 표시하는 방법을 집중해서 살펴보고자 한다. 화면에 데이터를 표시하기 위해 GTK+ TreeView 위젯을 사용할 것이다. 또한 주소록으로부터 데이터를 수집할 수 있는 "Evolution Data Server(에볼루션 데이터 서버)" 라이브러리를 소개하고자 한다. 본 장의 내용을 간단하게 유지하기 위해 Seed와 Glade만 사용할 것이다.


본 장에서 학습하게 될 내용은 다음과 같다.

  • TreeView를 이용해 데이터 표현하기
  • 에볼루션 데이터 서버 아키텍처
  • 에볼루션 데이터 서버로 주소록 접근하기


이제 데이터를 꺼내보자!


TreeView를 이용해 데이터 표현하기

GTK+ TreeView는 데이터의 트리 타입과 리스트 타입을 모두 표시하는 데 사용되는 위젯이다. 위젯의 디자인은 데이터 모델의 구현, 데이터를 어떻게 표현하는지, 데이터로 어떻게 접근하고 조작하는지를 논리적으로 구분하기 위해 모델-뷰-컨트롤러(MVC) 디자인을 사용한다.

GNOME3 Chapter08 01.png


위의 그림은 디자인 패턴을 시각적으로 설명한다. 그림을 설명하기 위해 Web에 있는 검색 엔진을 상상해보자. 최종 사용자든 코드의 조각이든 상관없이 사용자는 컨트롤러에서 무언가를 트리거한다. 실제로는 최종 사용자가 검색 쿼리를 Web으로 제출하는 코드, Search 버튼, 또는 컨트롤러가 무언가를 실행하도록 직접 트리거하는 무언가를 누를 때가 이 단계에 해당하겠다. 컨트롤러는 트리거에 따라 모델을 수정한다.


우리 검색 엔진 예제에서는 서버상의 검색 엔진에 해당하겠다. 검색 엔진은 쿼리를 취하여 매개변수로서 형성한 후 모델에 입력으로 전달한다. 그러면 모델은 새로운 데이터를 생성한다. 새로운 데이터는 뷰 객체의 표현을 대체하고 새로고침한다. 이후 사용자는 이 새로운 표현을 확인할 수 있다. 구체적으로 말해, 표현은 현재 검색 결과를 표시하는 화면이 될 것이다.


이러한 접근법을 이용하면 동일한 모델과 컨트롤러의 구현을 가지면서 여러 유형의 표현을 가질 수 있다. 웹 브라우저에서 검색 엔진은 세부적인 결과를 표시하는 반면 스마트폰에서는 결과를 간단한 형태로 표시할 수 있는 것이다.


GTK+에서 모델은 TreeModel 인터페이스에서 정의되는데, 이는 모델 공급자(provider)에 의해 구현되어야 한다. 하지만 GTK+는 즉시 사용이 가능한 간단한 모델을 두 개 제공하므로, 별도로 TreeModel 인터페이스를 구현하지 않아도 된다. 제공되는 모델들은 바로 ListStore와 TreeStore다. ListStore는 간단한 리스트 데이터 구조체에 사용되는 반면 TreeStore는 트리 데이터 구조체를 제공하는 데에 사용된다. 모델에서 우리는 유지하고자 하는 데이터의 열을 정의한다.


모델로 접근하고 싶다면 모델을 반복하기 위해 Iter 객체로부터 도움이 필요하다. Iter 객체는 데이터 모델에서 특정 레코드를 가리킨다. 데이터를 이동하면서 이 객체를 이동시킬 수 있다.


실행하기 - TreeView 사용하기

테이블에 데이터를 열거할 수 있는 간단한 애플리케이션을 만들길 원한다고 가정해보자. 테이블에서 데이터를 제거하기도 하고 새로운 데이터를 추가할 수도 있다면 좋겠다. 이는 아래 단계를 통해 구현할 수 있다.

  1. treeview.ui 라는 새로운 Glade UI 파일을 생성하라.
  2. 파일에 창을 삽입하라.
  3. 창에 항목이 두 개인 Box를 삽입하고 수직 박스로 만들어라.
  4. ScrollableWindow 창을 UI의 상단 부분에 위치시키고, 확장 가능하게 만들어라.
  5. 두 개의 항목을 포함할 수 있는 또 다른 Box를 하단 부분에 위치시키되, 이번에는 수평 박스로 만들어야 한다.
  6. 두 개의 버튼을 수직 박스의 각 열에 추가하고, 각각 RemoveAdd 라벨로 된 btnRemove 와 btnAdd로 명명하라.
  7. TreeView 위젯을 (Control and Display에서 이용 가능) ScrollableWindow 창으로 추가하라.
  8. 모델을 요청하는 대화상자가 나타날 것인데, 타원형 버튼을 누르고 New 를 클릭하라.
  9. 모델 대화상자는 이제 자동으로 liststore1로 채워진다.
  10. Create 버튼을 클릭하라.
  11. 위젯 리스트에서 liststore1 을 선택하라.
  12. General 탭에서 선택된 리스트스토어를 store로 재명명하라.
  13. 그 아래 Columns 데이터에서 <define a new column> 을 클릭하면 gchararray 타입으로 된 텍스트 엔트리로 변환될 것이다.
  14. 이를 한 번 더 실행하여 gchararray 타입으로 된 엔트리가 총 두 개가 되도록 하라.
  15. treeview1 객체를 view로 재명명하라.
  16. view에서 treeselection1 객체를 찾아 selection으로 재명명하라.
  17. 여기까지 완료되면 우리의 Glade 파일은 아래 스크린샷과 같은 모습이 될 것이다.
    GNOME3 Chapter08 02.png
  18. treeview.js 로 명명된 새로운 Seed 스크립트 파일을 생성하라.
  19. 파일을 아래 코드로 채워라.
    #!/usr/bin/env seed
    
    Gtk = imports.gi.Gtk;
    GObject = imports.gi.GObject;
    
    Main = new GType({
        parent: GObject.Object.type,
        name: "Main",
        init: function(self) {      
            var columns = {
                NAME: 0,
                ADDRESS: 1,
            }
    
        var ui = new Gtk.Builder()
            this.ui = ui;
            ui.add_from_file("treeview.ui");
            var window = ui.get_object("window1");
            window.resize(300, 400);
            window.show_all();
            window.signal.destroy.connect(Gtk.main_quit);
    
            this.clients = {};
            var view = ui.get_object("view");
            var selection = ui.get_object("selection");
            selection.signal.changed.connect(function(s) {
                var btnRemove = ui.get_object("btnRemove");
                btnRemove.sensitive = true;
            });
    
            var btnRemove = ui.get_object("btnRemove");
            btnRemove.signal.clicked.connect(function() {
                var selection = view.get_selection();
                if (selection) {
                    var selected = {};
                    var valid = selection.get_selected(selected);
                    if (valid && selected.iter) {
                        var model = view.get_model();
                        model.remove(selected.iter);
                    }
                }
            });
    
            var btnAdd = ui.get_object("btnAdd");
            btnAdd.signal.clicked.connect(function() {
                var selection = view.get_selection();
                if (selection) {
                    var selected = {};
                    var valid = selection.get_selected(selected);
                    if (valid && selected.iter) {
                        var model = view.get_model();
                        model.insert(selected.iter, 1);
                    }
                }
            });
    
            column = new Gtk.TreeViewColumn({title:'Name'});
            cell = new Gtk.CellRendererText();
            cell.editable = true;
            column.pack_start(cell);
            column.add_attribute(cell, 'text', columns.NAME);
            cell.signal.edited.connect(function(obj, path, text) {
                var store = view.get_model();
                var path = new Gtk.TreePath.from_string(path);
                var iter = {};
                store.get_iter(iter, path);
                store.set_value(iter.iter, columns.NAME, text);     
            });
            view.append_column(column);
    
            column = new Gtk.TreeViewColumn({title:'Address'});
            cell = new Gtk.CellRendererText();
            cell.editable = true;
            column.pack_start(cell);
            column.add_attribute(cell, 'text', columns.ADDRESS);
            cell.signal.edited.connect(function(obj, path, text) {
                var store = view.get_model();
                var path = new Gtk.TreePath.from_string(path);
                var iter = {};
                store.get_iter(iter, path);
                store.set_value(iter.iter, columns.ADDRESS, text);  
            });
            view.append_column(column);
    
            var store = view.get_model();
            var iter = {};
            store.append(iter);
    
            store.set_value(iter.iter, columns.NAME, "Robert");     
            store.set_value(iter.iter, columns.ADDRESS, "North Pole");
        }
    });
    
    Gtk.init(Seed.argv);
    var main = new Main();
    Gtk.main();
    
  20. 애플리케이션을 실행하라. 필드를 클릭하면 데이터를 편집할 수 있다. 하나의 행을 선택하고 Add 를 클릭하면 새로운 행을 하나씩 추가할 수 있다. 선택된 행을 제거할 수도 있다. 출력된 내용은 아래의 스크린샷과 같다.
GNOME3 Chapter08 03.png


무슨 일이 일어났는가?

이번 연습문제에서는 TreeView가 어떻게 작동하는지를 학습하였다. TreeView를 뷰로 갖게 되었고, 두 개의 CellRendererText 위젯과 그에 연관된 TreeView 열도 생겼다. 이 모델에서는 ListStore를 사용한다.


먼저 유지하길 원하는 데이터를 정의해야 한다. 이는 두 개의 열로 된 데이터로, 각 열은 타입 문자열을 갖는다. 열 번호는 상수를 이용해 참조한다. 다음과 같이 열 번호 0에는 NAME을, 열 번호 1에는 ADDRESS를 사용한다.

var columns = {
    NAME: 0,
    ADDRESS: 1,
}


빠르게 접근할 수 있도록 TreeView 참조는 view 변수에 보관한다.

var view = ui.get_object("view");


선택된 내용의 변경된 시그널을 구독한다. 행이 선택될 때마다 이 코드가 호출될 것이다. 현재로선 유용한 내용이 포함되어 있지 않지만 실제 프로젝트에서는 특정 버튼을 활성화시키거나 알림을 표시하는 등의 일을 수행할 수 있다. 아래는 선택내용(selection) 코드에 해당한다.

var selection = ui.get_object("selection");
    selection.signal.changed.connect(function(s) {
    var btnRemove = ui.get_object("btnRemove");
    btnRemove.sensitive = true;
});


아래 코드에서 레코드의 제거를 Remove 버튼의 클릭된 시그널로 연관시킨다.

var btnRemove = ui.get_object("btnRemove");
btnRemove.signal.clicked.connect(function() {


먼저 아래 코드를 실행함으로써 현재 selection 객체를 얻는다.

var selection = view.get_selection();
if (selection) {
    var selected = {};
    var valid = selection.get_selected(selected);


selection 객체가 유효해지면 선택된 행 객체를 얻는다. 레코드가 있다면 선택된 객체 안에 iter 객체가 얻어질 것이다.


iter 객체를 얻을 때는 아래 코드를 이용해 레코드를 삭제할 수 있다.

var model = view.get_model();
model.remove(selected.iter);


이후 Add 버튼에도 동일한 과정을 수행한다. 단, 이번에는 레코드를 제거하는 대신 새로운 레코드를 삽입한다.

var btnAdd = ui.get_object("btnAdd");
btnAdd.signal.clicked.connect(function() {
    var selection = view.get_selection();
    if (selection) {
        var selected = {};
        var valid = selection.get_selected(selected);
        if (valid && selected.iter) {
            var model = view.get_model();
            model.insert(selected.iter, 1);
        }
    }
});


이제 열을 준비해보자. 각 열은 TreeViewColumn 위젯으로 표현된다. 각 TreeViewColumn 위젯은 CellRenderer 클래스를 이용해 연관시킨다. 우리 데이터는 일반 문자열이기 때문에 우리가 사용하는 렌더러는 CellRendererText가 된다.

column = new Gtk.TreeViewColumn({title:'Name'});
cell = new Gtk.CellRendererText();


다음으로 데이터를 열로 패킹한 다음 데이터를 편집할 수 있도록 셀을 편집 가능하게 만든다.

cell.editable = true;
column.pack_start(cell);


add_attribute 함수를 이용해 ListStore에서 열, 셀, 데이터를 연관시킨다. 아래 코드는 열 번호 0(columns.NAME의 값)에서 얻은 데이터를 이용해 cell 객체의 text 프로퍼티를 수정할 것이다. 모든 것이 설정되고 나면 열을 view 변수 뒤에 추가한다.

column.add_attribute(cell, 'text', columns.NAME);
view.append_column(column);


Anjuta에서 liststore1 위젯에 두 개의 gchararray 항목을 포함할 수 있도록 두 개의 열을 생성했던 일을 기억하는가? 이는 두 열 모두 gchararray (문자열에 대한 또 다른 이름) 타입임을 나타내기 위한 행동이었다. 첫 번째 열은 column.NAME 값을, 두 번째 열은 column.ADDRESS 값을 보유하기 위함이다.


다음으로 셀의 편집된 시그널을 연결한다. 핸들러에서 해야 할 일은 단순히 새로운 편집된(edited) 텍스트를 얻어 다시 우리 모델로 넣는 일이다.

cell.signal.edited.connect(function(obj, path, text) {
    var store = view.get_model();


먼저 인자에서 path 변수가 표시한 경로를 얻는다. ListStore가 해석할 수 있는 경로 표현을 얻는다.

var path = new Gtk.TreePath.from_string(path);
var iter = {};


데이터를 설정하기 위해서는 현재 편집된 객체의 iter 객체를 얻을 필요가 있다. path 변수를 이용해 iter 객체로 변환한다. iter 객체를 얻은 후에는 set_value 함수를 이용해 모델을 설정한다. set_value에서는 편집하고자 하는 열을 명시하는 일도 해야 한다.

store.get_iter(iter, path);
store.set_value(iter.iter, columns.NAME, text);


주소에 대한 다른 열을 추가하는데, 이에 해당하는 코드는 이름 열에 사용된 코드와 동일하다.


초기 데이터를 모델로 추가한다. 그리고 하나의 행만 추가한다. 데이터를 추가하기 위해서는 먼저 모델을 얻을 필요가 있다.

var store = view.get_model();


다음으로 iter 객체를 모델 뒤에 추가한다.

var iter = {};
store.append(iter);


이후 iter 객체를 이용해 값을 넣는다.

store.set_value(iter.iter, columns.NAME, "Robert");     
store.set_value(iter.iter, columns.ADDRESS, "North Pole");


데이터를 얻고 조작하는 과정은 꽤 간단하게 보인다. 먼저 모델을 얻고 Iter 객체를 얻은 다음 상호작용하길 원하는 열을 명시함으로써 모델로부터 새 값을 얻거나 새 값을 모델로 추가하면 된다.


그 다음 가짜(fake) 데이터를 사용하는 대신 에볼루션 데이터 서버를 활용하여 라이브 데이터를 사용한다.


에볼루션 데이터 서버[EDS] 아키텍처

EDS는 주소록, 달력, 작업(task)으로의 접근과 조작을 추상화한다. 여기에는 개발자가 특정 주소록, 달력 또는 작업 서비스로 접근하기 위한 플러그인을 작성할 수 있도록 해주는 플러그인 아키텍처가 있다. EDS 사용자는 추상화된 API를 사용하고, 문제가 있는 서비스의 세부 내용에 대해 알 필요가 없다. 이 접근법을 이용하면 GNOME은 이론적으로 어떤 유형의 주소록, 달력 또는 작업 서비스든 지원이 가능하다. 아래는 EDS 아키텍처의 구조를 간략하게 표시한 그림이다.

GNOME3 Chapter08 04.png


EDS는 EDS의 사용자를 위해 메모리에서 유지되는 데몬을 제공하는데, 사용자는 보통 이메일 클라이언트, 인스턴스 메시징 애플리케이션, 또는 데이터로 접근해야 하는 애플리케이션에 해당한다. 서비스가 인증(authentication) 또는 승인(authorization)을 필요로 하는 경우 GNOME은 대화상자를 팝업시켜 최종 사용자가 가령 비밀번호를 채우는 일이나 접근을 허용하는 일 등을 실행할 수 있도록 한다.


EDS에서 주소록은 로컬 또는 원격 데이터 소스처럼 데이터의 소스를 나타내는 데이터 소스 그룹의 개념을 갖고 있다. 이것은 EBox.SourceGroup 객체에서 논리적으로 표현된다. 각 SourceGroup 그룹은 실제 데이터 소스를 표현하는 소스를 많이 포함할 수 있다. 이는 EBook.Source 객체에서 설명된다.


시도해보기 - 주소록과 달력 데이터 소스 준비하기

주소록과 달력 데이터로 접근하기 전에 먼저 데이터 소스를 준비할 필요가 있다. 이제 GNOME에서 Google 계정을 어떻게 설정하는지 논해보자. 시작하기 전에 활성화된 Google 계정을 갖고 있는지 확인하라. 이제 어떻게 계정을 준비시키는지 살펴보자.

  1. GNOME System Settings 를 열어라.
  2. Online Accounts 를 열어라.
  3. 창의 하단 좌측 모서리에 + 버튼을 클릭하라.
  4. GNOME과 사용하길 원하는 계정 유형을 선택하라. 이번 연습문제에는 Google services 를 선택한다.
  5. 서비스 로그인 페이지가 나타날 것이다.
  6. 서비스에 성공적으로 로그인하도록 하라.
  7. GNOME으로 접근을 허용하고, 해당 서비스를 사용하길 원함을 알려라.
  8. 모든 온라인 단계가 완료되면 컴퓨터에서 사용하길 원하는 서비스를 활성화하라. 특히 아래 스크린샷과 같이 Contacts 서비스를 활성화하라.
GNOME3 Chapter08 05.png


무슨 일이 일어났는가?

온라인 Google 계정의 연결이 끝났다. 즉, 이제 Google 계정에 GNOME APIs로 사용할 수 있는 데이터가 상주하고 있다는 말이다. 이것이 우리 다음 목표다.


실행하기 - 주소록 접근하기

간단한 주소록 프로그램을 생성하길 원한다고 가정하자. 데이터는 로컬 컴퓨터나 Google에서 원격으로 가져올 수 있다. 아래 단계를 실행하라.

  1. address-book.ui 라고 불리는 새로운 Glade .ui 파일을 생성하라.
  2. 두 개의 항목을 포함하는 수평 박스를 추가하라.
  3. 박스의 좌측면에 TreeView 위젯을 위치시켜라. bookView로 재명명하라. 모델을 요청하면 새로운 ListStore 모델을 생성하고 books로 재명명하라.
  4. TreeView 위젯에 자동으로 생성되는 TreeSelection 객체를 selection으로 재명명하라.
  5. ScrollableWindow 창을 박스의 우측면에 놓아라.
  6. 이후 다른 TreeView를 ScrollableWindow 안에 넣어라. 이를 위한 또 다른 ListStore 모델을 생성하고 contacts로 재명명하라. TreeView를 contactView로 재명명하라.
  7. books로 명명된 ListStore 모델을 편집하라. 해당 모델 내부에 gchararray 타입으로 된 열을 두 개 추가하라.
  8. contact로 명명된 ListStore 모델을 편집하라. 다시 gchararracy 타입으로 된 두 개의 열을 모델 안에 추가하라.
  9. 이제 UI 디자인은 아래 스크린샷과 비슷한 모양이 될 것이다.
    GNOME3 Chapter08 06.png
  10. address-book.js 라는 새로운 Seed 스크립트를 생성하라.
  11. 아래는 스크립트의 실행에 매우 중요한 역할을 하는 코드 블록이다.
    Main = new GType({
        parent: GObject.Object.type,
        name: "Main",
        init: function(self) {
    
            var bookColumn = {
                UID: 0,
                NAME: 1,
            }
    
            var contactColumn = {
                NAME: 0,
                EMAIL: 1,
            }
    
            this.listContacts = function(e) {
                var c = {};
                var q = EBook.BookQuery.any_field_contains("");
                var r = e.get_contacts_sync(q.to_string(), c, null);
                if (r && c && c.contacts && c.contacts.length > 0) {
                    var store = self.contact_view.get_model();
                    c.contacts.forEach(function(contact) {
                        var iter = {};
                        store.append(iter);
    
                        var name = contact.full_name;
                        if (!name) {
                            name = contact.nickname;
                        }
                        store.set_value(iter.iter, contactColumn.NAME, name);
                        store.set_value(iter.iter, contactColumn.EMAIL, contact.email_1);
                    });
                }
            }
    
            this.clients = {};
    
            var book_view = ui.get_object("bookView");
            var selection = ui.get_object("selection");
            selection.signal.changed.connect(function(s) {
                var selected = {}
                s.get_selected(selected);
                var book = selected.model.get_value(selected.iter, bookColumn.UID);
    
                var uid = book.value.get_string();
                if (uid == "") {
                    return;
                }
                source = self.sources.peek_source_by_uid(uid);
                var e = null;
                if (typeof(self.clients[uid]) !== "undefined") {
                    e = self.clients[uid];
                    if (e) {
                        self.clients[uid] = e;
                        self.listContacts(e);
                    }
                } else {
                    var e = new EBook.BookClient.c_new(source);
                    var r = e.open(false, null, function() {
                        if (e) {
                            self.clients[uid] = e;
                            self.listContacts(e);
                        }
                    });
                }
            });
    
            var cell = new Gtk.CellRendererText();
            var column = new Gtk.TreeViewColumn({title:'Book'});
            column.pack_start(cell);
    
            column.add_attribute(cell, 'markup', bookColumn.NAME);
            book_view.append_column(column);
    
            var contact_view = ui.get_object("contactView");
            this.contact_view = contact_view;
            cell = new Gtk.CellRendererText();
            column = new Gtk.TreeViewColumn({title:'Name'});
            column.pack_start(cell);
            column.add_attribute(cell, 'text', contactColumn.NAME);
            contact_view.append_column(column);
    
            cell = new Gtk.CellRendererText();
            column = new Gtk.TreeViewColumn({title:'E-mail'});
            column.pack_start(cell);
            column.add_attribute(cell, 'text', contactColumn.EMAIL);
            contact_view.append_column(column);
    
            var s = {};
            var e = EBook.BookClient.get_sources(s);
            this.sources = s.sources;
    
            var groups = this.sources.peek_groups();
            if (groups && groups.length > 0) {
                var store = book_view.get_model();
                groups.forEach(function(item) {
                    var iter = {};
                    store.append(iter);
    
                    store.set_value(iter.iter, bookColumn.UID, "");
                    store.set_value(iter.iter, bookColumn.NAME, "<b><i>"+item.peek_name()+ "</i></b>");     
    
                    var sources = item.peek_sources();
                    if (sources && sources.length > 0) {
                        sources.forEach(function(source) {
                            store.append(iter);
                            store.set_value(iter.iter, bookColumn.UID, source.peek_uid());
                            store.set_value(iter.iter, bookColumn.NAME, source.peek_name());        
                        });
                    }
                });
            }
        }
    });
    
  12. 코드를 실행하라. 애플리케이션이 실행되면 아래 스크린샷과 같은 창이 표시된다.
GNOME3 Chapter08 07.png


무슨 일이 일어났는가?

앞의 연습문제 결과는 설정에 따라 모습이 다를 수도 있다. (텍스트를 흐리게 만든 것은 이메일 주소이기 때문에 개인 정보를 보호하기 위함이니 양해 바란다.) 위의 스크린샷에서는 EDS 가 On This Computer, On LDAP Servers, WebDAV, Google 이라는 네 개의 주소록 소스를 리턴한다. 이 소스들 중 실제 데이터를 포함하는 건 두 개, On This ComputerGoogle 밖에 없다. 스크린샷에 열거된 주소록의 이름은 PersonalContacts 다.


Contacts 를 클릭하면 모든 연락처의 리스트가 창의 우측면에 표시된다. 여기서는 이름과 이메일 주소, 두 개의 열만 표시한다.


소스 코드를 자세히 살펴보자.


먼저 열에 대한 상수를 정의한다. 여기서 두 개의 모델을 갖고 있는데, 하나는 주소록 컬렉션(books라고 부른다)이고 하나는 연락처 컬렉션(contacts라고 부른다)용이다. 각 책에는 유일한 식별자의 이름이 있다. 따라서 열에 대해서는 아래의 데이터를 사용한다.

var bookColumn = {
    UID: 0,
    NAME: 1,
}


연락처의 경우, 이름과 이메일 주소만 표시하므로 두 개의 열만 있으면 된다.

var contactColumn = {
    NAME: 0,
    EMAIL: 1,
}


bookView 변수의 참조는 .ui 파일에 보관하고 아래와 같은 코드 행에서는 book_view 변수 안에 저장한다.

var book_view = ui.get_object("bookView");


bookView 변수의 selection 객체도 얻어 selection 변수에 넣는다.

var selection = ui.get_object("selection");


선택내용으로부터 책을 선택하면 해당하는 책의 내용을 얻길 원한다. 이를 구현하기 위해서는 아래와 같이 변경된 시그널을 함수로 접속(hook)해야 한다.

selection.signal.changed.connect(function(s) {


핸들러에서는 먼저 선택내용으로부터 selected 객체를 얻는 일을 해야 한다.

var selected = {}
s.get_selected(selected);


selected 객체에서는 iter member에 보관되는 Iter 객체를 얻는다. 우리는 즉시 열 번호 0의 값을 얻게 된다 (이 값은 bookColumn.UID를 이용해 기호화된다). 값은 타입 문자열에 있으므로 get_string() 함수를 이용한다. 우리는 uid 값이 비어 있을 때마다 행이 특정 책을 가리키지 않음을 의미하도록 행위를 설정하는 것이다. 이것은 주소록의 소스를 표시하기 위해 프로그램에 의해 사용된다.

var book = selected.model.get_value(selected.iter, bookColumn.UID);
var uid = book.value.get_string();
if (uid == "") {
    return;
}


uid가 어떤 값을 갖고 있다면 우리는 EDS에게 EBook.Source를 얻도록 직접 요청하는데, 이는 uid 값으로 식별된다. 이에 따라 e 변수에 저장된 EBook.Source 타입을 리턴할 것이다.


얻은 소스는 클라이언트 캐시에 보관하므로 책을 클릭할 때마다 매번 소스를 다시 열지 않아도 된다. 캐시가 uid 값으로 정의된 소스를 갖고 있다면 그냥 listContacts 함수를 호출하면 된다. 그러한 소스가 없다면 우선 열어야 한다. 그리고 나서 캐시에 보관할 것이다. 이후 아래 코드 조각과 같이 listContacts를 이용해 내용을 열거한다.

    source = self.sources.peek_source_by_uid(uid);
    var e = null;
    if (typeof(self.clients[uid]) !== "undefined") {
        e = self.clients[uid];
        if (e) {
            self.clients[uid] = e;
            self.listContacts(e);
        }
    } else {
        var e = new EBook.BookClient.c_new(source);
        var r = e.open(false, null, function() {

            if (e) {
                self.clients[uid] = e;
                self.listContacts(e);
            }
        });
    }
});


콜백을 함수의 인자로 제공함으로써 소스를 비동기식으로 연다는 점을 주목하라. 이 접근법을 이용하면 애플리케이션을 방해하지 않고 사용자에게 비응답식(unresponsive)으로 만들어 소스를 열기 위한 시간을 제공할 수 있다. 소스가 만일 승인을 받아야 하고 우리의 주의를 끌어야 할 경우 이 시점에서 대화상자가 나타날 것이다.


여기서는 bookView를 위한 열을 정의한다. 시각적으로는 하나의 열만 표시하는데, 책의 제목이나 책의 소스 그룹이 해당한다.

var cell = new Gtk.CellRendererText();
var column = new Gtk.TreeViewColumn({title:'Book'});
column.pack_start(cell);


다음 코드 부분에 설명되어 있듯이 Pango 마크업을 사용한다. 그래야만 CellRendererText의 text 프로퍼티를 사용하는 대신 마크업을 이용해 열 1로 매핑할 수 있다 (bookColumn.NAME으로 표기).

column.add_attribute(cell, 'markup', bookColumn.NAME);
book_view.append_column(column);


아래 코드 조각에는 연락처에 해당하는 열을 정의한다. 눈에 보이는 열은 두 개가 있으며, 이들을contactColumn.NAME과 contactColumn.EMAIL 이름으로 각각 할당할 것이다.

var contact_view = ui.get_object("contactView");
this.contact_view = contact_view;
cell = new Gtk.CellRendererText();
column = new Gtk.TreeViewColumn({title:'Name'});
column.pack_start(cell);
column.add_attribute(cell, 'text', contactColumn.NAME);
contact_view.append_column(column);

cell = new Gtk.CellRendererText();
column = new Gtk.TreeViewColumn({title:'E-mail'});
column.pack_start(cell);
column.add_attribute(cell, 'text', contactColumn.EMAIL);
contact_view.append_column(column);


초기화 중에는 get_sources 함수를 이용해 주소록의 소스를 얻는다. 이후 시스템에서 이용 가능한 그룹은 peek_groups 함수를 이용해 찾는다.

var s = {};
var e = EBook.BookClient.get_sources(s);
this.sources = s.sources;

var groups = this.sources.peek_groups();
if (groups && groups.length > 0) {
    var store = book_view.get_model();


각 그룹에서 우리는 책 ID와 책 이름을 테이블로 추가하지만 uid 값은 채우지 않을 것인데, 실제 주소록 소스의 uid 값만 보관하길 원하기 때문이다. 그룹명은 아래 코드에서 볼 수 있듯이 볼드체와 이탤릭체 스타일의 마크업을 이용해 닫는다.

groups.forEach(function(item) {
    var iter = {};
    store.append(iter);

    store.set_value(iter.iter, bookColumn.UID, "");
    store.set_value(iter.iter, bookColumn.NAME, "<b><i>" +item.peek_name()+ "</i></b>");


앞의 코드 조각에서 HTML 마크업과 비슷한 것을 사용하였음을 주목하라. 사실 그들은 Pango 마크업으로, GNOME 프레임워크에서 사용되는 텍스트 렌더링 엔진인데 HTML과 비슷하지만 기능의 수는 비교적 적다. 여기에 마크업을 넣은 이유는 위젯을 표현 계층에서 디자인하길 원하기 때문이다. 따라서 여기서 데이터를 건들거나 스타일을 추가해서는 안 되며, 위젯이 표시되면 스타일을 디자인하기만 한다. 하지만 앞의 접근법은 올바르지 않은데, 데이터를 저장공간에 넣기 전에 수정하기 때문이다. 데이터 내부를 검색하면 데이터를 찾을 수 없을 것인데, 이미 마크업을 이용해 채워졌기 때문이다. 사실상 렌더링 위젯 안에서 디자인하는 것이 올바른 구현일 것이다. 올바르게 구현하면 우리는 더 이상 Gtk.CellRendererWidget을 사용할 수 없게 되지만 대신 데이터를 표시하기 전에 디자인하는 커스텀 렌더러 위젯을 사용할 것이다.


peek_sources 함수를 이용하면 각 그룹에 대한 실제 주소록 소스를 얻고, 이용 가능한 각 소스를 테이블로 넣을 것이다. 이제 열 0에 uid를 위치시키는데 (bookColumn.UID) 이는 아래 코드 조각에 표시하겠다.

var sources = item.peek_sources();
if (sources && sources.length > 0) {
    sources.forEach(function(source) {
        store.append(iter);
        store.set_value(iter.iter, bookColumn.UID, source.peek_uid());
        store.set_value(iter.iter, bookColumn.NAME, source.peek_name());
    });


열 0은 테이블에서 표시되지 않는데, 그곳에 TreeViewColumn을 추가하지 않은 까닭이다.


listContacts 함수가 먼저 하는 일은 주소록으로 쿼리를 준비하는 일이다. EDS는 쿼리를 EDS로 전송하기 위해 EBook.BookQuery 객체를 제공한다. 아래 코드 조각을 통해 우리는 any_field_contains("") 함수를 이용해 쿼리를 생성함으로서 모든 데이터를 얻도록 EDS에게 요청할 것이다.

this.listContacts = function(e) {
    var c = {};
    var q = EBook.BookQuery.any_field_contains("");
    var r = e.get_contacts_sync(q.to_string(), c, null);
    if (r && c && c.contacts && c.contacts.length > 0) {


get_contacts_sync로 전달한 객체의 contacts member는 주소록의 연락처로 채워질 것이다. 우리가 얻은 연락처마다(EBook.Contact의 형태) 흥미로운 프로퍼티를 얻어 (full_name, 닉네임, email_1 프로퍼티만 원한다) 모델로 추가한다.

        var store = self.contact_view.get_model();
        c.contacts.forEach(function(contact) {
            var iter = {};
            store.append(iter);

            var name = contact.full_name;
            if (!name) {
                name = contact.nickname;
            }
            store.set_value(iter.iter, contactColumn.NAME, name);
            store.set_value(iter.iter, contactColumn.EMAIL, contact.email_1);
        });
    }
}


시도해보기 - 데이터를 주소록으로 저장하기

앞의 연습문제는 주소록의 내용만 표시하도록 구현하였다. 그렇다면 조금 더 개선하여 edit 버튼을 추가해보는 건 어떨까?


이를 실행하기 위해 편집된 시그널을 얻을 때마다 새 텍스트가 EBook.Contact 구조체로 다시 저장되도록 확보하라. e.modify_contact_sync 함수를 호출할 수 있다면 그 뼈대(skeleton)는 다음과 같은 모습일 것이다.

modifiedContact.full_name = newName;
// and/or
modifiedContact.email_1 = newEmail;
// then save it with
e.modify_contact_sync(modifiedContact, null);


소스를 다시 읽음으로써 모델을 새로고침해야 한다.


요약

GNOME에서 데이터 프로그래밍의 중점은 Treeview 위젯 패밀리에 있다. 애플리케이션의 디자인은 TreeView 위젯의 MVC 디자인 패턴도 따라야 한다. 모델을 최신으로 유지하는 것이 중요한데, 그래야만 사용자가 이미 무용지물인 렌더링된 데이터 대신 실제 데이터를 항상 눈으로 확인할 수 있다. 뷰는 주로 TreeViewColumn 객체와 CellRenderer 클래스로 표현된다. GTK+에서 사용할 준비가 된 CellRenderer 클래스는 많지만 하나의 커스텀 위젯이 필요하다면 언제든 인터페이스의 구현을 통해 새 것을 만들 수 있다.


이번 장에서는 EDS라는 데이터 소스를 하나 살펴보았다. 이는 주소록뿐 아니라 달력, 작업, 이메일도 포함한다. 이를 위해 에볼루션 프로그램이라는 중요한 애플리케이션을 생성하기도 했다.


다음으로는 HTML5 애플리케이션을 GNOME3와 통합이 가능하도록 만들고자 한다. 이를 어떻게 구현할 것인지는 다음 장에서 살펴보도록 하자.


Notes