GNOME3ApplicationDevelopmentBeginnersGuide:Chapter 13

From 흡혈양파의 번역工房
Revision as of 19:12, 9 June 2014 by Onionmixer (talk | contribs) (GNOME3 제 13 장 흥미로운 프로젝트 페이지 추가)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search
제 13 장 흥미로운 프로젝트

흥미로운 프로젝트

별로 유용하지 않은 애플리케이션을 개발하는 방법을 학습하는 데에 많은 시간을 소요했으니 이제 다음 단계로 넘어갈 차례다. 이제 웹 브라우저와 Twitter 클라이언트를 개발해보도록 하겠다. 우리가 만들고자 하는 애플리케이션은 간단하면서도 유용하다. 고급 버전을 만들기로 결정했다면 기본으로 사용해도 좋을 내용이다.


이번 장에서는 지금까지 배운 대부분의 주제들을 다시 살펴보되 약간 복잡하게 만들어 좀 더 깊이 알아보고자 한다. 구체적으로 익히게 될 지식은 다음과 같다.

  • WebKitGTK를 이용해 웹 브라우저 구현하기
  • Configure 스크립트를 이용해 경로 하드코딩하기
  • 다중 스크립트 Seed 애플리케이션 개발하기


첫 프로젝트부터 시작해보자.


제 1부 - 웹 브라우저

제일 먼저 개발할 애플리케이션은 웹 브라우저다. 개발이 단순한 기본 브라우저를 목표로 하겠다. 브라우저에는 탐색 버튼의 세트와 인터넷으로부터 페이지를 여는 기능이 포함되어 있다.


전체적인 개발이 수월해지도록 브라우저의 목업 버전을 만든다. 목업은 웹 브라우저의 모습이 어떨지를 보여주는 그림에 불과하다. 이러한 목업을 바탕으로 Anjuta/Glade 툴에서 UI 레이아웃을 도출할 것이다.

GNOME3 Chapter13 01.png


먼저 브라우저의 UI 구조에 익숙해지자. 메인 창은 두 가지 부분으로 나뉜다. 웹 페이지를 표시하는 영역이 창의 대부분 영역을 차지한다. 최상위 부분은 탐색 버튼과 URL 엔트리가 사용한다.


탐색 버튼은 독립된 버튼으로서 각각 액션을 뒤로 이동(back), 앞으로 이동(forward), 중지하기(stop)/재로딩하기(reload)를 나타낸다. URL 엔트리 옆에는 go 버튼이 추가로 숨겨져 있다. 버튼은 URL 엔트리를 채우면 표시된다.


이게 전부다. 정말 간단하다. 그건 그렇고 탐색 버튼과 URL 엔트리를 관리(host)하는 UI는 GUI 디자인 용어로 "chrome"(크롬)이라 부른다.


실행하기 - UI 디자인하기

이제 Anjuta를 작동시켜 UI를 디자인해보자. Glade 디자이너는 팔레트에 GtkWebUI를 갖고 있지 않기 때문에 다음과 같은 작업을 실행해야 한다.

  1. 새로운 Vala 프로젝트를 생성하고 web-browser라고 불러라. UI에 GtkBuilder를 사용하라.
  2. Project 도크에 파일을 더블 클릭하여 Glade 디자이너로 web_browser.ui를 열어라.
  3. 두 개의 항목이 있는 창에 수직 Box 객체를 넣어라.
  4. 이번에는 수평 Box 박스를 수직 Box 박스의 상단에 놓고, 하단 부분은 빈 채로 두어라. 수평 Box 객체에서 Number of elements 옵션을 5로 하여 5개 요소로 나누어라.
  5. 세 번째 요소를 제외하고 비어 있는 박스 요소 각각에 button 을 넣어라. 각 버튼마다 Stock button 옵션을 활성화하고, 각 button 에서 gtk-go-back, gtk-go-forward, gtk-refresh, gtk-ok stock으로 채워라.
  6. 버튼에 btn_back, btn_forward, btn_refresh, btn_go를 이용해 이름을 제공하라. 이름은 버튼의 각 Name 옵션에 넣어라.
  7. 세 번째 빈 box 에 Entry 객체를 놓고 url_entry로 이름을 붙여라. Packing 탭으로 가서 Expand 옵션이 Yes 로 설정되도록 하라.
  8. Scrolled Window 객체(Containers 섹션에 위치)를 하단의 빈 박스로 넣어라. Expand 옵션이 Yes 가 되도록 하라.
  9. Horizontal Scrollbar PolicyVertical Scrollbar Policy 값을 Never 로 설정하라.
  10. UI 파일이 아래와 같은 모습으로 준비되었을 것이다.
GNOME3 Chapter13 02.png


무슨 일이 일어났는가?

지금까지는 웹 브라우저를 위한 UI 레이아웃을 생성하였다. 볼 수 있듯이 목업으로부터 간단한 해석에 해당한다. 한 가지 빠진 내용이 있는데, 웹 페이지 영역이 여전히 비어 있다는 점이다. 하지만 나중에는 WebView 객체에 대한 컨테이너로 ScrolledWindow를 갖게 된다. ScrolledWindow 컨테이너를 사용하는 목적은 창의 크기가 내용에 따라 조정되지 않도록 확보하는 것인데, 크기 조정은 이미 ScrolledWindow로 넘어갔기 때문이다. WebView는 스스로 스크롤바를 이미 처리하므로 수직축과 수평축에 있는 스크롤바는 비활성화한다.


브라우저 상호작용

이제 브라우저의 상호작용 디자인을 검사해보자. 초기 단계에서는 idle 브라우저가 있어서 모든 탐색 버튼이 비활성화되어 있고 URL 엔트리가 비어 있다. 이 때 웹 뷰는 Welcome 텍스트만 포함한다 (만일 다른 언어로 지역화되었다면 Welcome에 대한 번역어만 포함한다). 사용자가 그 다음으로 실행할 수 있는 것은 URL 엔트리를 채우는 일이다.

GNOME3 Chapter13 03.png


사용자가 무언가를 입력하기 시작하면 go 버튼이 나타난다. 아직 표시할 내용이 없기 때문에 모든 탐색 버튼은 비활성화되어 있다. 따라서 사용자가 입력을 계속하거나 go 버튼을 클릭하여 프로세스를 완료할 것을 결정한다. 둘 중 하나라도 발생하면 웹 뷰는 요청된 URL을 로딩하기 시작한다.


페이지가 로딩되는 동안 refresh 버튼은 스스로를 stop 버튼으로 변경한다. 사용자가 이 버튼을 누르면 로딩이 즉시 중단되고 모든 것을 취소한다. stop 버튼은 refresh 버튼으로 다시 변경된다. 어떤 상태든 브라우저는 유휴(idle) 상태로 유지된다.


사용자가 로딩된 웹 페이지와 상호작용을 계속할 때마다 탐색 버튼의 상태는 그에 따라 업데이트된다. 브라우저가 뒤로 돌아갈 수 있다면 back 버튼이 활성화되었음을 의미한다. forward 버튼도 마찬가지다.

GNOME3 Chapter13 04.png


앞의 설명을 바탕으로 하면 네 개의 상태가 브라우저로 적용되어 있다. 사용자가 상호작용을 시작하길 기다리는 동안에는 "idle"(유휴) 상태다. 이후 사용자가 무언가를 입력하기 시작하면 "typing url"(url 입력) 상태다. 아니면 사용자가 링크나 페이지를 클릭할 때마다 "Clicking link"(링크 클릭) 상태로 들어갈 가능성도 있다. 이후 어떤 페이지가 로딩되고 있다면 상태는 "loading"(로딩) 상태가 된다. 마지막에는 다시 유휴 상태가 될 것이다.


이제 이러한 디자인을 사용해보았으니 코드를 구현할 준비가 된 셈이다.


실행하기 - 빌드 기본구조 준비하기

이제 앞서 생성한 프로젝트를 수정하여 i18n(국제화) 으로 개선한 후 빌드 기본구조를 테스트해보자.

  1. configure.ac 파일을 수정하여 아래와 같은 모습이 되도록 하라.
    AC_INIT(web_browser, 0.1)
    AC_CONFIG_HEADERS([config.h])
    AM_INIT_AUTOMAKE([1.11])
    AM_SILENT_RULES([yes])
    AC_PROG_CC
    
    LT_INIT
    IT_PROG_INTLTOOL()
    AH_TEMPLATE([GETTEXT_PACKAGE], [Package name for gettext])
    GETTEXT_PACKAGE=web-browser
    
    AC_DEFINE_UNQUOTED([GETTEXT_PACKAGE], ["$GETTEXT_PACKAGE"],
                [The domain to use with gettext])
    AC_SUBST(GETTEXT_PACKAGE)
    AM_GLIB_GNU_GETTEXT
    
    dnl Check for vala
    AM_PROG_VALAC([0.10.0])
    
    dnl Development mode
    AC_ARG_ENABLE(development,
        AS_HELP_STRING([--enable-development],[enable development mode]),
            enable_development="$enableval",
                enable_development=no)
    if test "x$enable_development" = "xyes"; then
        DEVELOPMENT_MODE="yes"
        PACKAGE_LOCALE_DIR=[${PWD}/locale]
        PACKAGE_UI_DIR=[${PWD}/src]
    else
        PACKAGE_LOCALE_DIR=[${datadir}/locale]
        PACKAGE_UI_DIR=[${datadir}/web-browser]
    fi
    
    AC_SUBST(PACKAGE_LOCALE_DIR)
    AC_SUBST(PACKAGE_UI_DIR)
    AH_TEMPLATE([PACKAGE_UI_DIR], [Location of the .ui file])
    AC_DEFINE_UNQUOTED([PACKAGE_UI_DIR], ["$PACKAGE_UI_DIR"],
                [Location of the .ui file])
    
    AC_SUBST(DEVELOPMENT_MODE)
    AH_TEMPLATE([DEVELOPMENT_MODE], [Whether in development mode or not])
    AC_DEFINE_UNQUOTED([DEVELOPMENT_MODE], ["$DEVELOPMENT_MODE"],
                [Development mode])
    
    PKG_CHECK_MODULES(WEB_BROWSER, [gtk+-3.0 webkitgtk-3.0])
    PKG_CHECK_MODULES(TEST_WEB_BROWSER, [gtk+-3.0 ])
    
    AC_OUTPUT([
    Makefile
    src/Makefile
    tests/Makefile
    po/Makefile.in
    ])
    
  2. Makefile.am 파일을 수정하여 아래의 코드로 채워라.
    SUBDIRS = src tests po
    
    web_browserdocdir = ${prefix}/doc/web_browser
    web_browserdoc_DATA = \
        README\
        COPYING\
        AUTHORS\
        ChangeLog\
        INSTALL\
        NEWS
    
    EXTRA_DIST = \
        $(web_browserdoc_DATA)
        intltool-extract.in \
        intltool-merge.in \
        intltool-update.in
    
    DISTCLEANFILES = \
        intltool-extract \
        intltool-merge \
        intltool-update \
        po/.intltool-merge-cache \
        $(NULL)
    
    # Remove doc directory on uninstall
    uninstall-local:
        -rm -r $(web_browserdocdir)
    
  3. src/Makefile.am 파일을 수정하고 아래의 전체 파일을 사용하라.
    uidir = $(PACKAGE_UI_DIR)
    ui_DATA = web_browser.ui
    
    AM_CPPFLAGS = \
        -DPACKAGE_LOCALE_DIR=\""$(localedir)"\" \
        -DPACKAGE_SRC_DIR=\""$(srcdir)"\" \
        -DPACKAGE_DATA_DIR=\""$(pkgdatadir)"\" \
        $(WEB_BROWSER_CFLAGS)
    
    AM_CFLAGS =\
        -Wall\
        -g
    
    bin_PROGRAMS = web_browser
    web_browser_SOURCES = \
        main.vala web_browser.vala config.vapi
    
    web_browser_VALAFLAGS = \
        --vapidir . \
        --pkg gtk+-3.0 \
        --pkg webkit-1.0 \
        --pkg libsoup-2.4 \
        --Xcc='--include config.h'
    
    web_browser_LDFLAGS = \
        -Wl,--export-dynamic
    
    web_browser_LDADD = $(WEB_BROWSER_LIBS)
    
    EXTRA_DIST = $(ui_DATA)
    
    # Remove ui directory on uninstall
    uninstall-local:
        -rm -r $(uidir)
        -rm -r $(pkgdatadir)
    
  4. po 디렉터리를 생성하라.
  5. 아래의 내용으로 된 po/POTFILES.in을 추가하라.
    [type: gettext/glade]src/web_browser.ui
    src/web_browser.vala
    
  6. 아래의 내용으로 된 po/POTFILES.skip을 추가하라.
    src/web_browser.c
    tests/web_browser.c
    
  7. po/LINGUAS 파일을 추가하고 지원하고자 하는 언어 코드로 채워라. 가령 인도네시아어를 지원하기 위해서는 아래의 내용을 넣어라.
    id
    
  8. po 디렉터리 안에서 아래의 명령을 실행하여 번역 템플릿을 채워라.
    intltool-update -pot
    
  9. 결과가 되는 web-browser.pot을 id.po(또는 LINGUAS 파일의 내용에 따라 다른 파일로)로 복사하고 제 11장, 애플리케이션 국제화하기에서 po 파일을 어떻게 처리할 것인지를 참조하라.
  10. tests 디렉터리를 생성하라.
  11. 아래의 내용을 이용해 tests/Makefile.am을 추가하라.
    AM_CPPFLAGS = \
        -DPACKAGE_LOCALE_DIR=\""$(localedir)"\" \
        -DPACKAGE_SRC_DIR=\""$(srcdir)"\" \
        -DPACKAGE_DATA_DIR=\""$(pkgdatadir)"\" \
        -I. \
        $(WEB_BROWSER_CFLAGS)
    
    AM_CFLAGS =\
        -Wall\
        -g
    
    TESTS=test_web_browser
    check_PROGRAMS = test_web_browser
    
    test_web_browser_SOURCES = \
        ../src/config.vapi webkit.vala ../src/web_browser.vala test_web_
    browser.vala
    
    test_web_browser_VALAFLAGS = \
        --pkg gtk+-3.0 \
        --Xcc='--include config.h'
    
    test_web_browser_LDFLAGS = \
        -Wl,--export-dynamic
    
    test_web_browser_LDADD = $(TEST_WEB_BROWSER_LIBS)
    
  12. tests/test_web_browser.vala를 추가하고 아래의 코드로 채워라.
    using Gtk;
    
    public class TestWebBrowser {
    
        static void process_events()
        {
            while (Gtk.events_pending ()) {
                Gtk.main_iteration_do(true);
            }
        }
        static int main (string[] args)
        {
            Gtk.test_init (ref args);
    
            Idle.add (() => {
                Test.run ();
                Gtk.main_quit ();
                return true;
            });
    
            Gtk.main ();
    
            return 0;
        }
    }
    
  13. tests/webkit.vala를 추가하고 아래의 내용을 사용하라.
    using Gtk;
    
    [CCode (lower_case_cprefix = "webkit_")]
    namespace WebKit {
    
        [CCode (cheader_filename="webkit/webkit.h")]
        public class WebFrame : Object {
        }
    
        [CCode (cheader_filename="webkit/webkit.h")]
        public class WebView : Viewport {
    
            public WebView() {
            }
    
            [CCode (cname="webkit_web_view_load_string")]
            public void load_string (string content, string mime, string encoding, string base_uri) {
            }
        }
    }
    
  14. 본 서적에 함께 따라오는 webkit-1.0.vapi 파일을 src 디렉터리로 넣어라.
  15. src/web_browser.vala 파일에 아래 코드를 사용하라.
    using GLib;
    using Gtk;
    using WebKit;
    
    public class WebBrowser : Object
    {
        internal Builder builder = null;
        WebView view = null;
        const string UI_FILE = Config.PACKAGE_UI_DIR + "/" + "web_browser.ui";
    
        public WebBrowser ()
        {
            Gtk.Settings.get_default ().gtk_button_images = true;
            try
            {
                builder = new Builder ();
                builder.add_from_file (UI_FILE);
    
                view = new WebView();
                view.load_string("<h1>" + _("Welcome") + "</h1>", "text/ html", "UTF-8", "/");
                var box = builder.get_object ("webhost") as Container;
                box.add(view);
    
                var window = builder.get_object ("window") as Window;
                window.show_all ();
    
                window.destroy.connect(() => {
                    Gtk.main_quit();
                });
            }
            catch (Error e) {
                stderr.printf (_("Could not load UI: %s\n"), e.message);
            }
        }
    }
    
  16. src/main.vala를 생성하고 아래의 내용을 사용하라.
    using GLib;
    using Gtk;
    
    public clas Main : Object
    {
        static int main (string[] args)
        {
            Gtk.init (ref args);
            var app = new WebBrowser ();
            Gtk.main ();
            return 0;
        }
    }
    
  17. src/config.vapi를 아래와 같이 수정하라.
    [CCode (cprefix = "", lower_case_cprefix = "", cheader_filename = "config.h")]
    namespace Config {
        public const string DEVELOPMENT_MODE;
        public const string GETTEXT_PACKAGE;
        public const string SPRITE_DIR; 
        public const string BACKGROUND_DIR;
        public const string PACKAGE_DATA_DIR; 
        public const string PACKAGE_UI_DIR; 
        public const string PACKAGE_LOCALE_DIR; 
        public const string PACKAGE_NAME; 
        public const string PACKAGE_VERSION; 
        public const string VERSION; 
    }
    
  18. 아래를 실행함으로써 --enable-development 옵션의 사용을 빌드하라.
    ./autogen.sh --enable-development
    
  19. 아니면 옵션을 Build 메뉴의 Configure Project... 하위메뉴에서 Configure Options 필드에 넣어라.
  20. 콘솔로 가서 아래 명령을 입력하라.
    make check
    
  21. 단위 테스트의 통과 여부를 확인하기 위해서는 이 출력을 확인해야 한다.
  22. 애플리케이션을 실행할 수 있어야 하며, 닫는 기능을 제외하고는 어떠한 상호작용도 없는 UI가 표시되어야 한다.
GNOME3 Chapter13 05.png


무슨 일이 일어났는가?

우아! 꽤 많은 액션이 실행되었다. 그렇지 않은가?


이제 i18n과 단위 테스팅을 포함하는 빌드 기본구조를 준비하였다. 기능은 구현하지 않은 채로 두었다. 구현 또한 빌드 과정이 성공할 정도로만 최소한으로 유지하였다. 이제 좀 더 자세히 살펴보자.


제일 먼저 configure.ac 파일을 구성하였다. 이는 앞의 여러 장에서 실행한 것과 꽤 비슷하지만 몇 가지 흥미로운 점이 있다. 첫 번째는 다음과 같다.

dnl Development mode
AC_ARG_ENABLE(development,
    AS_HELP_STRING([--enable-development],[enable development mode]),
        enable_development="$enableval",
            enable_development=no)
if test "x$enable_development" = "xyes"; then
    DEVELOPMENT_MODE="yes"
    PACKAGE_LOCALE_DIR=[${PWD}/locale]
    PACKAGE_UI_DIR=[${PWD}/src]
else
    PACKAGE_LOCALE_DIR=[${datadir}/locale]
    PACKAGE_UI_DIR=[${datadir}/web-browser]
fi


위는 configure 스크립트에 --enable-development 옵션의 추가를 보여준다. PACKAGE_LOCALE_DIR와 PACKAGE_UI_DIR의 정의를 추가한 것만 제외하면 꽤 익숙한 모습이다. 이러한 변수들의 값은 configure 스크립트의 매개변수로부터 --enable-development 옵션을 빼는지 추가하는지에 따라 달려있다. PACKAGE_LOCALE_DIR는 제 11장, 애플리케이션 국제화하기에서 논했듯이 LOCALE 디렉터리의 위치를 보유한다. PACKAGE_UI_DIR는 .ui 파일의 위치를 보유한다.


이는 .ui 와 번역 파일을 어디서 찾아야 하는지 코드에게 일일이 알려주는 기능을 보였던 이전 접근법에 비하면 상당히 개선된 모습이다. 주요 개선점으로는 개발 모드에 있는지 혹은 소스 코드에 있지 않은지를 검사할 필요가 없다는 점이다. 위치는 configure 스크립트로 --enable-development 옵션을 제공하는지 여부에 따라 자동으로 하드코딩된다.


옵션을 제공했다면 PACKAGE_UI_DIR 는 src 디렉터리를 가리키고, 제공하지 않았다면 PACKAGE_UI_DIR는 /usr/share/web-browser 디렉터리를 가리킨다. 이는 컴파일 시 이루어져야 하므로 배포 버전(deployment version)을 빌드하기 전에 --enable-development 옵션을 생략할 것을 잊지 말라.


코드로부터 접근 가능한 PACKAGE_UI_DIR의 값을 얻기 위해서는 값을 config.h 파일로 삽입한다. 이는 configure 스크립트에서 다음과 같이 이루어진다.

AC_SUBST(PACKAGE_UI_DIR)
AH_TEMPLATE([PACKAGE_UI_DIR], [Location of the .ui file])
AC_DEFINE_UNQUOTED([PACKAGE_UI_DIR], ["$PACKAGE_UI_DIR"], [Location of the .ui file])


이후 이를 src/config.vapi 파일로 추가함으로써 Vala 코드로부터 접근 가능하게 만들 필요가 있다.

public const string PACKAGE_UI_DIR;


이후 변수는 Config.PACKAGE_UI_DIR로서 사용된다.


다음으로 아래의 행이 흥미롭다.

PKG_CHECK_MODULES(WEB_BROWSER, [gtk+-3.0 webkitgtk-3.0])
PKG_CHECK_MODULES(TEST_WEB_BROWSER, [gtk+-3.0 ])


첫 행은 WEB_BROWSER_CFLAGS와 WEB_BROWSER_LIBS의 새 변수를 자동으로 내보내고(export), 두 번째 행은 TEST_WEB_BROWSER_CFLAGS와 TEST_WEB_BROWSER_LIBS 변수를 내보낼 것이다. 애플리케이션을 컴파일하는 데에는 아래와 같이 src/Makefile.am의 첫 번째 변수와,

SER, [gtk+-3.0 ])

AM_CPPFLAGS = \
    -DPACKAGE_LOCALE_DIR=\""$(localedir)"\" \
    -DPACKAGE_SRC_DIR=\""$(srcdir)"\" \
    -DPACKAGE_DATA_DIR=\""$(pkgdatadir)"\" \
    $(WEB_BROWSER_CFLAGS)


아래를 사용한다.

web_browser_LDADD = $(WEB_BROWSER_LIBS)


이는 빌드 시스템이 컴파일에 필요한 플래그와 헤더 파일뿐만 아니라 애플리케이션의 연계에 필요한 라이브러리명까지 얻는다는 뜻이다. 이 모든 것은 스텁 컴파일을 성공적으로 만드는 데에 필요하다.


단위 테스팅 실행을 수월하게 만들기 위해 WebKit을 스터빙하는 webkit.vala 코드가 있다. tests/Makefile.am에서 볼 수 있듯이 TEST_WEB_BROWSER_LIBS를 이용해 테스트 파일을 컴파일하면 단위 테스트로 연결된 원본 WebKit 라이브러리를 제외시킬 수 있다. 그러면 src에 포함된 소스 코드는 WebKit로 컴파일될 것이고, tests 디렉터리 내의 소스 코드는 스텁으로 컴파일될 것이다.


하지만 단위 테스트에서는 여전히 TEST_WEB_BROWSER_CFLAGS 대신 WEB_BROWSER_CFLAGS를 이용해 원본 WebKit 헤더 파일을 사용한다. 원본 헤더는 생성된 C 소스 코드를 컴파일하는 데 필요하다.


이제 i18n 부분으로 넘어가보자.


우리의 Vala 코드, 즉 src/web_browser.vala와 .ui 파일을 po/POTFILES.in에 넣음으로써 번역 가능한 파일로 표현해보자. 모든 파일을 추가할 필요는 없으며, 번역 가능한 텍스트가 포함된 파일을 신중하게 선택한다. 그 다음, 모든 생성된 C 코드를 po/POTFILES.skip으로 추가하여 번역 불가한 파일로 표시한다. 이는 이중 번역을 피하기 위함이다.


나머지는 꽤 간단하므로 바로 살펴보겠다.


실행하기 - 끝마치기

  1. tests/test_web_browser.vala를 아래의 코드로 채워라.
    using Gtk;
    
    public class TestWebBrowser {
    
        static void process_events()
        {
            while (Gtk.events_pending ()) {
                Gtk.main_iteration_do(true);
            }
        }
    
        static void test_initial_state ()
        {
            var web = new WebBrowser();
            process_events();
            assert (web.state == WebBrowser.State.IDLE);
        }
    
        static void test_typing_url ()
        {
            var web = new WebBrowser();
            var window = web.builder.get_object ("window") as Window;
            window.show_now ();
            web.url_entry.show_now ();
    
            assert (web.btn_go.visible == false);
    
            var entry_w = web.url_entry.get_window ();
            web.url_entry.focus(0);
            assert (web.state == WebBrowser.State.IDLE);
            Gdk.test_simulate_key (entry_w, 5, 5, Gdk.Key.a, 0, Gdk.EventType.KEY_PRESS);
            Gdk.test_simulate_key (entry_w, 5, 5, Gdk.Key.a, 0, Gdk.EventType.KEY_RELEASE);
    
            process_events ();
            assert (web.state == WebBrowser.State.TYPING_URL);
            assert (web.btn_go.visible == true);
    
            var btn_w = web.btn_go.get_window ();
            web.btn_go.focus(0);
            Gdk.test_simulate_key (btn_w, 5, 5, Gdk.Key.Return, 0, Gdk.EventType.KEY_PRESS);
            Gdk.test_simulate_key (btn_w, 5, 5, Gdk.Key.Return, 0, Gdk.EventType.KEY_RELEASE);
    
            process_events ();
            assert (web.state == WebBrowser.State.LOADING);
        }
        static int main (string[] args)
        {
            Gtk.test_init (ref args);
    
            Test.add_func ("/test-initial-state", test_initial_state);
            Test.add_func ("/test-typing-url", test_typing_url);
    
            Idle.add (() => {
                Test.run ();
                Gtk.main_quit ();
                return true;
            });
    
            Gtk.main ();
    
            return 0;
        }
    }
    
  2. tests/webkit.vala 스텁 파일을 열고 아래 코드를 사용하라.
    using Gtk;
    
    [CCode (lower_case_cprefix = "webkit_")]
    namespace WebKit {
    
        [CCode (cheader_filename="webkit/webkit.h")]
        public class WebFrame : Object {
        }
    
        [CCode (cheader_filename="webkit/webkit.h")]
        public class WebView : Viewport {
            public bool _can_go_back;
            public bool _can_go_forward;
            public signal void load_started (WebFrame frame);
            public signal void load_finished (WebFrame frame);
            WebFrame frame;
    
            public WebView() {
                frame = new WebFrame ();
            }
    
            [CCode (cname="webkit_web_view_can_go_back")]
            public bool can_go_back() {
                return _can_go_back;
            }
    
            [CCode (cname="webkit_web_view_can_go_forward")]
            public bool can_go_forward() {
                return _can_go_forward;
            }
    
            [CCode (cname="webkit_web_view_go_back")]
            public void go_back () {
            }
    
            [CCode (cname="webkit_web_view_go_forward")]
            public void go_forward () {
            }
    
            [CCode (cname="webkit_web_view_load_uri")]
            public void load_uri (string uri) {
                load_started (frame);
            }
    
            [CCode (cname="webkit_web_view_load_string")]
            public void load_string (string content, string mime, string encoding, string base_uri) {
            }
    
            [CCode (cname="webkit_web_view_stop_loading")]
            public void stop_loading () {
            }
    
            [CCode (cname="webkit_web_view_reload")]
            public void reload () {
            }
        }
    }
    
  3. 애플리케이션을 빌드하여 실행하라.
GNOME3 Chapter13 06.png


무슨 일이 일어났는가?

기본구조를 구성해 놓았으니 이제 프로젝트의 진짜 목적에 집중할 수 있겠다. WebBrowser 객체로 시작해보자.

const string UI_FILE = Config.PACKAGE_UI_DIR + "/" + "web_browser.ui";


여기서는 파일명 뒤에 붙은 Config.PACKAGE_UI_DIR 값을 얻음으로써 .ui 파일의 정확한 위치를 얻는다. 위치는 이미 빌드 시스템에서 하드코딩되기 때문에 제 11장, 애플리케이션 국제화하기에서처럼 개발 모드인지 아닌지 확인하기 위해 추가로 if branch 코드를 넣을 필요가 없다.

internal Button btn_back = null;
internal Button btn_forward = null;
internal Button btn_go = null;
internal Button btn_refresh = null;
internal Entry url_entry = null;


이는 탐색 버튼과 URL 엔트리 위젯이다. 단위 테스트로부터 접근이 가능하도록 internal(내부적)로 표시한다.

internal enum State {
    IDLE,
    TYPING_URL,
    LOADING
}


이는 앞서 논한 상태들이다. 하지만 링크 클릭(clicking link) 상태가 빠져 있다. 클릭은 WebKit 안에서 직접 처리되기 때문에 enum 루프에서 상태를 제거하였고, 그 외에는 우리가 처리하는 상태들이다. 따라서 클러터링(cluttering)을 피하기 위해서는 사용하지 않을 enum 값은 넣지 않을 것이다.


하지만 값을 그냥 제거하는 것은 올바른 실습이 아니다. 제거의 이유를 설명하는 주석을 코드에 추가시키는 편이 낫다.


먼저 초기 상태를 유휴 상태로 초기화한다.

internal State state = State.IDLE;


이후 상태가 변경될 때마다 호출될 함수가 생긴다. 이 함수에서는 영향을 받는 구성요소의 모양이나 다른 프로퍼티를 업데이트할 것이다.

void update_state()


아래 코드는 builder 객체가 존재하도록 확보하는 데에 사용된다.

if (builder == null) {
    return;
}


유휴 상태에서는 go 버튼을 숨기고, refresh 버튼의 아이콘과 라벨을 사용한다.

switch (state) {
    case State.IDLE:
        btn_go.hide ();
        btn_refresh.label = "gtk-refresh";
        break;


URL을 입력하면 go 버튼을 표시한다.

case State.TYPING_URL:
    btn_go.show ();
    break;


로딩 상태에서는 단순히 go 버튼을 다시 숨기고 refresh 버튼을 stop 버튼으로 변경한다.

case State.LOADING:
    btn_go.hide ();
    btn_refresh.label = "gtk-stop";
    break;


진행할 것인지 여부에 따라 forward 버튼을 활성화/비활성화로 설정한다.

if (view.can_go_forward ()) {
    btn_forward.sensitive = true;
} else {
    btn_forward.sensitive = false;
}


이와 비슷하게 진행 여부에 따라 backward 버튼을 활성화/비활성화로 설정한다.

if (view.can_go_back ()) {
    btn_back.sensitive = true;
} else {
    btn_back.sensitive = false;
}


이제 구성요소의 전체적인 상호작용을 준비하게 될 생성자를 살펴보자.

public WebBrowser ()


여기서 텍스트만 사용하는 대신 버튼에 대한 이미지를 사용한다.

Gtk.Settings.get_default ().gtk_button_images = true;


아래 코드에서는 단순히 .ui 파일을 열어 builder 객체로 삽입한다.

builder = new Builder ();
builder.add_from_file (UI_FILE);


아래 코드 조각에서는 웹 뷰를 인스턴스화하고 Welcome 텍스트를 표시한다. 텍스트는 gettext의 밑줄 함수로 닫아서(enclose) 번역 가능하게 만든다. HTML 헤딩 마크업의 번역은 무관하기 때문에 건너뛴다. 우리 관심사는 포맷팅 자체보다는 실제로 표시되는 텍스트에 있다.

view = new WebView();
view.load_string("<h1>" + _("Welcome") + "</h1>", "text/html", "UTF-8", "/");


다음으로 뷰의 load_started 시그널을 연결한다. 이는 링크를 클릭 시 호출되는 시그널로, 링크 연결 상태로 매핑된다. 하지만 이 시그널이 호출되고 나면 상태는 곧바로 로딩 상태로 이동한다. 이후 update_state 함수를 호출함으로써 UI의 모양을 업데이트한다.

view.load_started.connect(() => {
    state = State.LOADING;
    update_state();
});


하지만 이 함수는 Internet에서 이미지, 스크립트, 또는 다른 파일을 포함해 다른 소스를 로딩할 때마다 호출된다는 사실을 주목해야 한다.


load_finished 시그널은 로딩이 완료될 때마다 호출된다. 이런 경우 유휴 상태로 돌아간다.

view.load_finished.connect(() => {
    state = State.IDLE;
    update_state();
});


여기서는 URL 엔트리에서 키 누름 이벤트를 처리한다. 앞서 디자인한 바와 같이 무언가를 입력할 때마다 url 입력(typing url) 상태로 업데이트할 것이다. 따라서 이를 실행할 장소는 아래와 같다.

url_entry = builder.get_object ("url_entry") as Entry;
url_entry.key_press_event.connect(() => {
    state = State.TYPING_URL;
    update_state();
    return false;
});


아래 코드는 .ui 파일에 준비한 컨테이너로 뷰를 삽입하는 데에 그친다.

var box = builder.get_object ("webhost") as Container;
box.add(view);


다음으로 backward 버튼의 클릭 시그널을 처리한다. 버튼이 클릭되면 뷰에게 한 페이지씩 뒤로 갈 것을 요청한다.

btn_back = builder.get_object ("btn_back") as Button;
btn_back.clicked.connect(() => {
    view.go_back ();
});


Forward 버튼도 마찬가지다.

btn_forward = builder.get_object ("btn_forward") as Button;
btn_forward.clicked.connect(() => {
    view.go_forward ();
});


Refresh 버튼에는 이중 기능이 있으므로 먼저 새로고침 역할을 하는지, 아니면 중지 역할을 하는지 확인할 필요가 있다. 새로고침 역할을 한다면 뷰에서 reload를 호출하고, 중지 역할을 한다면 stop_loading을 호출한다.

btn_refresh = builder.get_object ("btn_refresh") as Button;
btn_refresh.clicked.connect(() => {
    if (btn_refresh.label == "gtk-refresh") {
        view.reload ();
    } else {
        view.stop_loading ();
    }
});


Go 버튼은 클릭 또는 활성화되어야 한다(Enter 누름). 두 액션 모두 URL 엔트리에 입력된 주소가 로딩되는 결과를 야기한다.

btn_go = builder.get_object ("btn_go") as Button;
btn_go.activate.connect(() => {
    view.load_uri (url_entry.text);
});

btn_go.clicked.connect(() => {
    view.load_uri (url_entry.text);
});


이는 단순히 창을 표시한다.

var window = builder.get_object ("window") as Window;
window.show_all ();


그리고 언제나처럼 창이 닫힐 때마다 애플리케이션을 종료할 것이다.

window.destroy.connect(() => {
    Gtk.main_quit();
});


상태를 초기화하기 위해 update_state를 호출할 것이다.

update_state();


꽤 간단한 코드다. 그렇지 않은가? 다음으로 할 일은 단위 테스트에서 update_state 함수를 테스트하고, 뷰의 스텁을 준비시키는 일이다. 테스트를 먼저 살펴보자. 여기서 모든 테스트는 TestWebBrowser 클래스에서 선언한다.

public class TestWebBrowser {


첫 번째 함수는 process_events 함수다. 이 함수는 제 12장, 품질이 좋으면 모든 게 쉬워진다에서 살펴본 함수와 정확히 동일하다. 큐에 대기 중인 이벤트를 모두 처리하고, 처리가 끝날 때까지 명시적으로 기다린다.

static void process_events()
{
    while (Gtk.events_pending ()) {
        Gtk.main_iteration_do(true);
    }
}


다음으로 이 테스트 함수에서 브라우저의 초기 상태를 테스트한다. WebBrowser 객체를 생성하고, 상태가 실제로 유휴 상태인지 확인한다. 그 다음 url 입력 상태를 테스트한다.

static void test_initial_state ()
{
    var web = new WebBrowser();
    process_events();
    assert (web.state == WebBrowser.State.IDLE);
}


이 함수에서는 WebBrowser 객체를 생성하고, show_all을 이용해 URL 엔트리와 창을 즉시 표시한다. 이는 위젯을 곧바로 사용할 수 있도록 확보하기 위함이다.

static void test_typing_url ()
{
    var web = new WebBrowser();
    var window = web.builder.get_object ("window") as Window;
    window.show_now ();
    web.url_entry.show_now ();


다음으로 go 버튼이 표시되는지 여부를 확인한다. 우리가 디자인한 바와 같이 숨겨져야 한다.

assert (web.btn_go.visible == false);


여기서는 마우스 포커스를 URL 엔트리로 두어 타이핑이 가능하도록 한다.

var entry_w = web.url_entry.get_window ();
web.url_entry.focus(0);


뭔가를 확인하기 전에는 유휴 상태에 있도록 해야 한다.

assert (web.state == WebBrowser.State.IDLE);


그리고 키 누름(과 해제를)을 시뮬레이트한다.

Gdk.test_simulate_key (entry_w, 5, 5, Gdk.Key.a, 0, Gdk.EventType.KEY_PRESS);
Gdk.test_simulate_key (entry_w, 5, 5, Gdk.Key.a, 0, Gdk.EventType.KEY_RELEASE);


이벤트가 실제로 위젯으로 전달되고 핸들러를 적절하게 호출되도록 하려면 초기에 호출해야 한다는 사실을 주목한다.

process_events ();


여기서는 url 입력 상태에 있는지 확인한다.

assert (web.state == WebBrowser.State.TYPING_URL);


이제 go 버튼이 표시되었는지 확인한다.

assert (web.btn_go.visible == true);


이후, 입력을 마치고 나면 포커스를 go 버튼으로 이동한다.

var btn_w = web.btn_go.get_window ();
web.btn_go.focus(0);


그리고 버튼에서 Enter 키를 누른다.

Gdk.test_simulate_key (btn_w, 5, 5, Gdk.Key.Return, 0, Gdk.EventType.KEY_PRESS);
Gdk.test_simulate_key (btn_w, 5, 5, Gdk.Key.Return, 0, Gdk.EventType.KEY_RELEASE);

process_events ();


웹 뷰가 페이지를 로딩하므로 상태는 로딩 상태로 변경되어야 한다.

assert (web.state == WebBrowser.State.LOADING);


두 개의 테스트를 갖게 되었다. 이제 main 함수를 살펴볼 차례다. 여기서 테스트 도구(test suite) 프레임워크를 초기화한다.

static int main (string[] args)
{
    Gtk.test_init (ref args);


앞서 정의한 함수로 매핑하는 두 개의 테스트를 등록한다.

Test.add_func ("/test-initial-state", test_initial_state);
Test.add_func ("/test-typing-url", test_typing_url);


이후 아이들 핸들러(idle handler)를 구성하여 Gtk.main을 호출한 직후 테스트가 실행될 수 있도록 한다. 모든 테스트가 실행되고 나면 애플리케이션을 즉시 중단한다.

Idle.add (() => {
    Test.run ();
    Gtk.main_quit ();
    return true;
});


여기서 이벤트 루프를 시작하고 GTK로 전달한다.

Gtk.main ();


테스트를 실행하기 위해 네트워크 연결을 사용하지 않음을 주목하라. 앞서 논했듯이 WebKit을 사용조차 하지 않는다. 대신 테스트 도중에 WebKit을 대신할 고유의 스텁을 사용한다.


우리 스텁에서는 스터빙하고자 하는 객체와 정확히 동일한 이름을 사용해야 한다. 또 WebKit 네임스페이스를 선언하여 using WebKit 절을 코드에서 사용할 수 있어야 한다. webkit_ 접두사를 이용해 C 코드를 생성할 것을 Vala에게 요청한다. 이러한 작업을 하지 않으면 Vala는 web_kit_ 접두사를 생성할 것이다(단어 사이에 밑줄을 주목).

[CCode (lower_case_cprefix = "webkit_")]
namespace WebKit {


이는 load_started와 load_finished 시그널에서 사용되는 헬퍼(helper) 클래스다.

[CCode (cheader_filename="webkit/webkit.h")]
public class WebFrame : Object {
}


그리고 변수와 시그널을 선언한다.

[CCode (cheader_filename="webkit/webkit.h")]
public class WebView : Viewport {
    public bool _can_go_back;
    public bool _can_go_forward;
    public signal void load_started (WebFrame frame);
    public signal void load_finished (WebFrame frame);
    WebFrame frame;


여기서는 단순히 frame 객체를 초기화한다.

public WebView() {
    frame = new WebFrame ();
}


나머지는 꽤 간단할 뿐더러 제 12장, 품질이 좋으면 모든 게 쉬워진다편에서 이미 논했기 때문에 자세히 논하지 않겠다. 이 함수들은 빈 함수들로, 스텁에서는 구현 코드에서 실제로 사용되는 함수들만 정의한다.


시도해보기 - 더 많은 테스트 생성하기

이미 눈치 챘겠지만 우리에겐 두 가지 기능밖에 없다. 따라서 가능한 상태를 모두 테스트하지 못했다. 이제 가서 테스트를 더 만들어보자!


제 2부 - Twitter 클라이언트

이제 다음 주제인 트위터 클라이언트로 넘어가보자. 애플리케이션에는 우리가 검색하는 텍스트와 함께 트위트(tweet)의 스트림을 표시한다는 특징이 있다. 이는 웹 브라우저보다 더 간단하므로 Seed를 이용해 개발하도록 하겠다.

GNOME3 Chapter13 07.png


다시 말하지만 UI는 복잡하지 않다. 기본적으로 화면에 두 가지 부분이 있는데, 내용 영역(Content area)과 검색 영역(Search area)이다. 검색 영역 또한 두 가지 부분, 검색 박스(Search box)와 검색 버튼(Search button)으로 나뉜다. 버튼을 클릭할 때마다 검색 텍스트와 함께 트위트의 스트림으로 내용 영역이 업데이트된다.


실행하기 - Twitter 클라이언트 구현하기

세 개의 Seed 스크립트가 생길 것인데 이들을 디렉터리에 넣을 필요가 있다. 애플리케이션을 실행하기 위해서는 인터넷 연결이 필요함을 잊지 말라.

  1. tweet-feed.js라는 새로운 스크립트를 생성하고 아래의 코드로 채워라.
    #!/usr/bin/env seed
    
    Gtk = imports.gi.Gtk;
    Gdk = imports.gi.Gdk;
    GObject = imports.gi.GObject;
    
    f = imports.feedEntry;
    t = imports.twitter;
    
    var twitter = new t.Twitter();
    
    Gtk.init(Seed.argv);
    var window = new Gtk.Window();
    window.resize(400,400);
    window.title = "Tweet Feed";
    var box = new Gtk.Box({orientation: Gtk.Orientation.VERTICAL});
    window.add(box);
    
    var scroll = new Gtk.ScrolledWindow();
    var viewport = new Gtk.Viewport();
    box.pack_start(scroll, true, true);
    
    var search_box = new Gtk.Box({orientation: Gtk.Orientation.HORIZONTAL});
    var entry = new Gtk.Entry();
    entry.placeholder_text = "Enter your search topic";
    
    search_box.pack_start(entry, true, true);
    var search_button = new Gtk.Button({label: "gtk-find"});
    search_button.use_stock = true;
    search_button.signal.clicked.connect(function() {
        twitter.search(entry.text);
    });
    search_box.pack_start(search_button, false, false);
    
    box.pack_start(search_box, false, false);
    
    var entries = new Gtk.Box({orientation: Gtk.Orientation.VERTICAL});
    scroll.add(viewport);
    viewport.add(entries);
    window.show_all();
    
    twitter.signal.connect("data-available", function(object) {
        entries.foreach(function(content) {
            entries.remove(content);
        });
        for (var i = 0; i < twitter.data.results.length; i ++) {
            var entry = new f.FeedEntry(twitter.data.results[i]);
            entries.pack_start(entry, false, false);
        }
    });
    
    Gtk.main();
    
  2. feedEntry.js라는 새로운 파일을 추가하고 아래의 코드를 사용하라.
    Gtk = imports.gi.Gtk;
    Gdk = imports.gi.Gdk;
    GObject = imports.gi.GObject;
    
    FeedEntry = new GType({
        parent: Gtk.TextView.type,
        name: "FeedEntry",
        properties: [
            {
                name: 'from_user_name',
                type: GObject.TYPE_STRING,
                default_value: "",
                flags: (GObject.ParamFlags.CONSTRUCT
                    | GObject.ParamFlags.READABLE
                    | GObject.ParamFlags.WRITABLE),
            }
            , {
                name: 'from_user',
                type: GObject.TYPE_STRING,
                default_value: "",
                flags: (GObject.ParamFlags.CONSTRUCT
                    | GObject.ParamFlags.READABLE
                    | GObject.ParamFlags.WRITABLE),
            }
            , {
                name: 'text',
                type: GObject.TYPE_STRING,
                default_value: "",
                flags: (GObject.ParamFlags.CONSTRUCT
                    | GObject.ParamFlags.READABLE
                    | GObject.ParamFlags.WRITABLE),
            }
        ],
    
        class_init: function(klass, prototype) {
            prototype.update_data = function() {
                var writer_tag = new Gtk.TextTag({
                    name: "writer",
                    size_points: 12,
                    weight: 700,
                    style: 3,
                    foreground:"#000000"});
                this.buffer.tag_table.add(writer_tag);
    
                var user_tag = new Gtk.TextTag({
                    name: user",
                    style: 0,
                    foreground:"#888888"});
                this.buffer.tag_table.add(user_tag);
    
                var content_tag = new Gtk.TextTag({
                    name: „content",
                    size_points: 12,
                    style: 0,
                    weight: 400,
                    foreground: „#000000"
                    });
                this.buffer.tag_table.add(content_tag);
    
                this.buffer.insert_at_cursor (this.text + „\n", -1);
                var start_iter = this.buffer.get_start_iter ();
                var end_iter = this.buffer.get_end_iter ();
                this.buffer.apply_tag_by_name (content", start_iter.iter, end_iter.iter);
    
                var cursor_pos = this.buffer.cursor_position;
                this.buffer.insert_at_cursor (this.from_user_name + " ", -1);
                var start_iter = this.buffer.get_iter_at_offset(cursor_pos);
                var end_iter = this.buffer.get_end_iter ();
                this.buffer.apply_tag_by_name ("writer", start_iter.iter, end_iter.iter);
                var cursor_pos = this.buffer.cursor_position;
                this.buffer.insert_at_cursor ("@" + this.from_user + "\n", -1);
                var start_iter = this.buffer.get_iter_at_offset(cursor_pos);
                var end_iter = this.buffer.get_end_iter ();
                this.buffer.apply_tag_by_name ("user", start_iter.iter, end_ iter.iter);
            }
        },
        init: function(self) {
            self.wrap_mode = Gtk.WrapMode.WORD;
            self.editable = false;
            self.update_data();
            self.show_all();
        }
    });
    
  3. twitter.js라는 새로운 파일을 또 추가하고 아래 코드로 채워라.
    Gio = imports.gi.Gio;
    GObject = imports.gi.GObject;
    
    Twitter = new GType({
        parent: GObject.Object.type,
        name: "Twitter",
        signals: [
            {
                name: "data-available",
                parameters: []
            }
        ],
        class_init: function(klass, prototype) {
            prototype.search = function(keyword) {
                var url = "http://search.twitter.com/search.json?q=" + keyword;
                var data_source = Gio.file_new_for_uri(url);
                var self = this;
                data_source.read_async(0, null,
                    function(source, result) {
                        var input = source.read_finish(result);
                        var stream = new Gio.DataInputStream.c_new(input);
                        self.data = JSON.parse(stream.read_until("", 0));
                        self.signal["data-available"].emit();
                    }
                );
            }
        },
    
        init: function(self) {
            this.data = {};
        }
    });
    
  4. 셸에 아래와 같은 명령을 입력함으로써 tweet-feed.js 파일에 실행 가능 권한을 부여하라.
    chmod +x tweet-feed.js
    
  5. 이를 실행하고 아무 검색 텍스트든 입력한 후 Find 버튼을 눌러라.
GNOME3 Chapter13 08.png


무슨 일이 일어났는가?

본문에서는 기존의 위젯을 확장하는 기술 뿐 아니라 네트워크로부터 자원을 읽는 지식도 이용하였다. 뿐만 아니라 개발을 모듈화하고 간소화하기 위해 다중 스크립트를 이용해 Seed 애플리케이션을 개발하는 방법도 학습하였다.


애플리케이션은 세 개의 모듈로 나누었다. 첫 번째는 메인 애플리케이션용이고, 두 번째는 트위터 데이터의 읽기용이며, 마지막은 트윗을 표시하기 위한 새로 파생된 위젯용이다.


마지막 위젯, 즉 FeedEntry 위젯부터 시작해보자. 먼저 TextView 위젯으로부터 새 위젯을 파생한다.

FeedEntry = new GType({
    parent: Gtk.TextView.type,
    name: "FeedEntry",


이를 파생하는 이유는 TextView에서 이용 가능한 텍스트 래핑 기능을 원하기 때문이다. 여기서 Twitter API로부터 비롯된 데이터를 정확하게 따르는 이름으로 몇 가지 프로퍼티를 정의한다.

properties: [
    {
        name: 'from_user_name',
        type: GObject.TYPE_STRING,
        default_value: "",
        flags: (GObject.ParamFlags.CONSTRUCT
            | GObject.ParamFlags.READABLE
            | GObject.ParamFlags.WRITABLE),
    }


API에는 "tweep"(트윕; 트윗을 게시한 사람)의 성명, 사용자명, 트윗 자체를 각각 포함하는 from_user_name, from_user, text가 있다. 따라서 Twitter API와 프로퍼티 간 직접적 맵을 새로운 위젯에서 이용하면 된다.


다음으로 프로토타입에 update_data 함수를 정의한다. 이는 함수를 클래스에서 이용할 수 있다는 뜻이다.

class_init: function(klass, prototype) {
    prototype.update_data = function() {


그리고 몇 가지 태그를 정의한다. 태그는 텍스트 버퍼에 입력하는 텍스트를 디자인하는 데에 사용된다.

var writer_tag = new Gtk.TextTag({
    name: "writer",
    size_points: 12,
    weight: 700,
    style: 3,
    foreground:"#000000"});
this.buffer.tag_table.add(writer_tag);


트윗에는 일반 스타일, 트윕의 이름에는 볼드체 스타일, 트위터 사용자 ID에는 연한(lighter) 글씨체를 사용하고자 한다. 하지만 태그를 사용하기 전에 먼저 태그를 태그 테이블로 추가한 후 모든 태그를 대상으로 실행한다. 이후 트윗을 먼저 삽입한 다음 새 행을 추가한다.

this.buffer.insert_at_cursor (this.text + "\n", -1);


여기서 Iter 객체를 얻음으로써 텍스트의 시작 위치를 기억한다. Iter 객체는 텍스트 버퍼 내 특정 위치를 가리키는 데 사용된다. 이것은 텍스트 선택내용(selection)에 비유하여 생각할 수 있겠다.

var start_iter = this.buffer.get_start_iter ();


그리고 이곳에서 텍스트 선택내용의 끝을 표시한다.

var end_iter = this.buffer.get_end_iter ();


이후 명시된 이름으로 된 태그를 시작과 끝 iters로 덮인 텍스트에 적용한다.

this.buffer.apply_tag_by_name ("content", start_iter.iter, end_iter.iter);


선택내용의 비유로 돌아가면 이 부분은 텍스트 문서에서 선택된 텍스트로 포맷팅 스타일을 제공하는 것으로 생각할 수 있겠다.


여기서는 Iter 객체의 시작을 얻기 위해 더 이상 get_start_iter 함수를 사용할 수 없다.

var cursor_pos = this.buffer.cursor_position;


대신 현재 커서 위치를 얻어 아래에 삽입될 텍스트의 시작으로 표시한다.


다음으로 앞에서 기록한 커서 위치를 가리킴으로써 start iter를 얻는다.

this.buffer.insert_at_cursor (this.from_user_name + " ", -1);
var start_iter = this.buffer.get_iter_at_offset(cursor_pos);


이는 앞의 것과 동일하므로 그냥 텍스트의 끝을 이용해 end iter를 얻으면 된다.

var end_iter = this.buffer.get_end_iter ();


다음으로 iter 매개변수들 사이의 텍스트로 태그를 적용한다.

this.buffer.apply_tag_by_name ("writer", start_iter.iter, end_iter.iter);


자연적으로 TextView는 텍스트 편집용이지만 여기서는 객체의 인스턴스 중 편집을 비활성화한다.

init: function(self) {
    self.wrap_mode = Gtk.WrapMode.WORD;
    self.editable = false;


초기화 중에 즉시 데이터를 업데이트하면 데이터가 렌더링된다.

self.update_data();
self.show_all();


이게 전부다. 이제 데이터 소스의 기능을 하는 twitter.js 파일을 살펴보자.


여기서 Twitter 객체를 간단한 GObject 객체로 정의한다.

Twitter = new GType({
    parent: GObject.Object.type,
    name: "Twitter",


data-available이라는 시그널이 있으며, 이는 어떠한 매개변수도 없이 호출된다.

signals: [
    {
        name: "data-available",
        parameters: []
    }
],


시그널은 우리가 Twitter API로부터 JSON 포맷의 새로운 데이터가 들어올 때마다 호출된다. 이는 클래스에서 이용 가능하게 만드는 search 함수다. 이 함수는 언급된 URL로부터 스트림을 얻어 뒤에 키워드를 추가하는 일을 한다. 스트림은 Gio를 이용해 연다.

class_init: function(klass, prototype) {
    prototype.search = function(keyword) {
        var url = "http://search.twitter.com/search.json?q=" + keyword;
        var data_source = Gio.file_new_for_uri(url);
        var self = this;


이후 비동기식으로 스트림을 읽어 인터넷으로부터 데이터를 로딩하는 동안 UI를 방해하지 않도록 한다. 데이터가 준비되면 read_async 함수 뒤에 클로져(closure)를 넣어 함수를 입력한다. 여기서 스트림은 DataInputStream 인스턴스로 변환되고, 우리는 데이터를 JavaScript 객체로 파싱한다. 모든 것이 준비되면 data-available 시그널을 발생시킨다.

data_source.read_async(0, null,
    function(source, result) {
        var input = source.read_finish(result);
        var stream = new Gio.DataInputStream.c_new(input);
        self.data = JSON.parse(stream.read_until("", 0));
        self.signal["data-available"].emit();
    }
);


우리 초기화 함수는 이토록 간단하다. 그저 data member를 빈 객체로 초기화할 뿐이다.

init: function(self) {
    this.data = {};
}


다음으로 tweet-feed.js에서 메인 애플리케이션 코드를 살펴보자. 여기서는 다른 두 개의 스크립트를 가져와 f와 t 변수로부터 접근 가능하게 만든다.

f = imports.feedEntry;
t = imports.twitter;


코드 베이스가 커지면 이는 바람직하지 못한 예제가 된다. feedEntryScript 또는 twitterScript와 같은 이름을 사용하는 편이 더 낫다.


아래 행에서는 Twitter 객체를 인스턴스화한다.

var twitter = new t.Twitter();


그리고 Gtk를 초기화하여 창을 생성한다. 처음 크기를 설정하고 제목을 제공한다.

Gtk.init(Seed.argv);
var window = new Gtk.Window();
window.resize(400,400);
window.title = "Tweet Feed";


레이아웃으로 사용될 Box 객체가 있으며, 이는 창으로 넣는다.

var box = new Gtk.Box({orientation: Gtk.Orientation.VERTICAL});
window.add(box);


이제 창에 하나 이상의 위젯을 추가할 수 있다.


여기에는 viewport와 함께 생성된 scroll이라 불리는 새로운 ScrolledWindow 객체가 있다. 창의 크기를 넘어갈 때 트윗을 스크롤하려면 이를 꼭 실행해야 한다.

var scroll = new Gtk.ScrolledWindow();
var viewport = new Gtk.Viewport();
box.pack_start(scroll, true, true);


이후 새로운 수평적 박스를 화면의 하단에 추가한다. 텍스트 엔트리와 검색 버튼의 위치일 것이다.

var search_box = new Gtk.Box({orientation: Gtk.Orientation.HORIZONTAL});


여기서는 사용자가 검색 텍스트를 입력하도록 텍스트 엔트리를 생성한다. 사용자가 해야 할 일을 말해주는 힌트로 플레이스 홀더 텍스트를 사용한다. 그리고 수평적 박스로 엔트리를 패킹한다.

var entry = new Gtk.Entry();
entry.placeholder_text = "Enter your search topic";
search_box.pack_start(entry, true, true);


여기서는 검색 버튼을 생성한다. 스톡 항목이 있는 간단한 버튼이므로 라벨을 생성할 필요가 없다. 다음으로 twitter.search 함수를 호출하고 스트림 다운로드를 시작함으로써 클릭된 시그널을 연결한다.

var search_button = new Gtk.Button({label: "gtk-find"});
search_button.use_stock = true;
search_button.signal.clicked.connect(function() {
    twitter.search(entry.text);
});
search_box.pack_start(search_button, false, false);


게다가 들어오는 트윗을 모두 관리하기 위한 수직 박스를 생성한다. 박스는 스크롤이 있는 창 안에 위치한 뷰포트에 놓인다.

var entries = new Gtk.Box({orientation: Gtk.Orientation.VERTICAL});
scroll.add(viewport);
viewport.add(entries);


이것은 data-available 시그널 핸들러다. 앞서 언급했듯이 시그널은 우리가 전체 트위터 스트림을 수신할 때 발생된다.

twitter.signal.connect("data-available", function(object) {
    entries.foreach(function(content) {
        entries.remove(content);
    });
    for (var i = 0; i < twitter.data.results.length; i ++) {
        var entry = new f.FeedEntry(twitter.data.results[i]);
        entries.pack_start(entry, false, false);
    }
});


따라서 우리는 기존의 엔트리가 있다면 이를 먼저 제거해야 한다. 그리고 twitter.data.results 배열에 있는 트윗을 모두 반복한다. 각 트윗에는 단순히 FeedEntry 객체를 생성한다. 엔트리 구조체는 직접 전달해야 하는데, 구조체의 member가 이미 FeedEntry 클래스에 매핑되어 있기 때문이다. 이후 entries 배열에 엔트리 객체를 추가한다.


요약

이제 두 개의 실제 애플리케이션이 있다. 하지만 여전히 이곳저곳에 놓친 부분들이 있다. 그리고 이 부분들은 당신이 수정해주길 기다리고 있다.


첫 번째 프로젝트에서는 configure 스크립트로부터 경로를 직접 하드코딩함으로써 지금 개발 모드인지 아닌지를 확인하는 코드의 생성을 피하는 새로운 트릭을 학습하였다. 또 웹 페이지를 로딩하는 방법과 크롬의 나머지 부분과 상호작용하는 방법도 논했다.


후에 두 번째 프로젝트에서는 위젯과 다중 스크립트 애플리케이션을 확장하는 방법을 학습하였다. 이는 알아두면 중요한 내용인 것이, 실제 세계에서 애플리케이션이 이 상황과 비슷할 수 있기 때문이다.


두 프로젝트에서 우리는 UI가 아닌(non-UI) 코드를 전용 클래스에 넣음으로써 논리와 UI 빌드를 분리하는 것을 논했다. 이를 통해 UI 코드로 혼합되었기 때문에 좀 더 복잡한 비지니스 프로세스를 쉽게 추가하면서도 혼동하지 않을 수 있다.


축하한다. 이제 책의 마지막에 도달했다! 이제 다음으로 할 일은 무엇일까?


책을 마쳤다 하더라도 지금까지 읽은 표면 지식의 세부내용을 파고 들어갈 필요가 있다. 하지만 필수 내용은 이미 이해했으니 세부내용을 학습하기가 수월할 것이다. 한 가지 기억해야 할 중요한 사항은 GNOME 플랫폼이 오픈 소스 프로젝트란 사실이다. 여느 다른 프로젝트와 마찬가지로 전 세계 해커들이 기여한 덕분이다. 이제 당신도 그 이상의 일을 할 수 있다!


Notes