GNOME3ApplicationDevelopmentBeginnersGuide:Chapter 09

From 흡혈양파의 번역工房
Jump to: navigation, search
제 9 장 GNOME을 통해 HTML5 애플리케이션 활용하기

GNOME을 통해 HTML5 애플리케이션 활용하기

모바일 세계에서는 아직도 HTML5를 개발할 것인지 네이티브 애플리케이션을 개발할 것인지에 대한 논쟁이 진행되고 있다. HTML5를 선호하는 사람들은 HTML5를 이용해 애플리케이션을 개발하여 네이티브 코드로 쓰인 UI 래퍼를 이용해 애플리케이션을 실행하는데, 애플리케이션을 완전히 네이티브 코드로 작성하는 상반된 접근법도 있다. 하지만 데스크톱 세계에선 어떨까? 이번 장을 통해 HTML5 접근법을 살펴보고 논쟁에서 자신만의 의견을 가져보도록 하자!


HTML5 애플리케이션을 실행한다는 개념은 가장 기본적인 웹 브라우저에서 애플리케이션을 래퍼(wrapper)로 실행하는 것과 같다. HTML5 애플리케이션에 대한 UI 래퍼는 Vala로 쓸 것이며, WebKitGTK+ 라고 불리는 유명한 WebKit 레이아웃 엔진의 GTK+ 특성을 이용한다. 이번 장에서는 UI 래퍼에서 HTML5 애플리케이션을 설치하는 방법뿐 아니라 GNOME 플랫폼을 미들웨어로 사용하는 방법도 학습할 것이다. 구체적으로 이번 장에서 다룰 주제는 다음과 같다.

  • GTK+ 애플리케이션에 WebKit 포함시키기
  • JavaScriptCore 소개하기
  • JavaScriptCore와 상호작용하기


이제 시작해보자!


시작하기 전에

이번 장에 실린 논의의 대부분은 HTML5, JSON, 일반적인 클라이언트측 JavaScript 프로그래밍에 대해 어느 정도의 지식수준을 요한다. 특히 한 가지 연습문제에서는 실제 HTML5 애플리케이션이 어떻게 구현될 것인지를 보이기 위해 JQuery와 JQuery Mobile을 사용한다.


WebKit 포함시키기

먼저 학습해야 할 것은 우리의 GTK+ 애플리케이션 안에 어떻게 WebKit 레이아웃 엔진을 포함시킬 것인지에 관해서다. WebKit을 포함시킨다는 것은 이제 HTML과 CSS를 GTK+ 또는 Clutter 대신 사용자 인터페이스로 사용할 수 있게 된다는 뜻이다.


실행하기 - WebKit 포함시키기

WebKitGTK+를 이용하면 아래와 같이 매우 간단한 작업이 된다.

  1. GtkBuilder와 라이센스가 모두 없는 빈 Vala 프로젝트를 생성하라. 프로젝트를 hello-webkit로 명명하라.
  2. WebKitGTK+를 프로젝트로 포함시키도록 configure.ac를 수정하라. 파일에서 아래와 같은 코드 행을 찾아라.
    PKG_CHECK_MODULES(HELLO_WEBKIT, [gtk+-3.0])
    
  3. 위의 행을 제거하고 아래로 대체하라.
    PKG_CHECK_MODULES(HELLO_WEBKIT, [gtk+-3.0 webkitgtk-3.0])
    
  4. src 폴더에서 Makefile.am을 찾아 WebKitGTK를 Vala 컴파일 파이프라인으로 포함하도록 수정하라. 파일에서 아래와 같은 행을 찾아라.
    hello_webkit_VALAFLAGS = \
        --pkg gtk+-3.0
    
  5. 위를 제거하고 아래의 행으로 완전히 대체하라.
    hello_webkit_VALAFLAGS = \
        --vapidir . --pkg gtk+-3.0 --pkg webkit-1.0 --pkg libsoup-2.4
    
  6. src 폴더의 hello_webkit.vala 파일을 아래의 내용으로 채워라.
    using GLib;
    using Gtk;
    using WebKit;
    
    public class Main : WebView
    {
        public Main ()
        {
            load_html_string("<h1>Hello</h1>","/");
        }
    
        static int main (string[] args)
        {
            Gtk.init (ref args);
            var webView = new Main ();
            var window = new Gtk.Window();
            window.add(webView);
            window.show_all ();
    
            Gtk.main ();
    
            return 0;
        }
    }
    
  7. 함께 포함되어 있던 webkit-1.0 vapi 파일을 src 폴더에 복사하라. 복사는 꼭 실행해야 하는데, 많은 배포판에서 배포되는 webkit-1.0.vapi 파일이 여전히 GTK+ 버전 2를 사용하기 때문이다.
  8. 위를 실행하면 아래 스크린샷과 같이 Hello 메시지가 표시된 창이 보일 것이다.
GNOME3 Chapter09 01.png


무슨 일이 일어났는가?

우선 WebKit를 네임스페이스로 포함시켜 그로부터 모든 함수와 클래스를 사용할 수 있도록 해야 한다.

using WebKit;


우리 클래스는 WebView 위젯으로부터 파생된다. 이것은 WebKit에서 중요한 위젯으로, 웹 페이지를 표시하는 기능이 있다. 위젯을 표시하면 DOM 을 적절하게 파싱하고 표시할 뿐만 아니라 스크립트를 실행하고 문서에 의해 참조되는 스타일을 표시할 수도 있다는 의미다. 파생의 선언은 아래와 같이 클래스 선언에 들어간다.

public class Main : WebView


생성자 내에서는 문자열을 로딩하여 HTML 문서로 파싱하기만 한다. 문자열은 1 수준 헤딩으로 디자인된 Hello다. 아래 행을 실행한 다음 WebKit는 그 본체에 HTML5 코드의 표현을 파싱하고 표시할 것이다.

public Main ()
{
    load_html_string("<h1>Hello</h1>","/");
}


main 함수에서 할 일은 WebView 위젯을 넣을 창을 생성하는 일이다. 위젯을 추가한 다음에는 창과 위젯을 모두 표시하기 위해 show_all() 함수를 호출해야 한다.

static int main (string[] args)
{
    Gtk.init (ref args);
    var webView = new Main ();
    var window = new Gtk.Window();
    window.add(webView);


이제 window 내용은 유일하게 WebView 위젯만 표시 위젯으로 가진다. 지금은 UI를 표시하기 위해 GTK+를 사용하지 않지만 이는 모두 HTML5에서 쓴 것이다.


JavaScriptCore를 이용한 런타임

HTML5 애플리케이션에는 대부분의 경우 JavaScript로 쓰인 클라이언트측 스크립트와 CSS3으로 쓰인 스타일링 정의 집합이 따라온다. WebKit는 이미 JavaScriptCore 라고 불리는 요소가 있는 클라이언트측 JavaScript를 (웹 페이지에서 스크립트를 실행) 실행하는 기능을 제공한다.


하지만 GNOME 플랫폼과의 연결은 어떻게 될까? 클라이언트측 스크립트를 어떻게 GNOME 객체로 접근하게 만들까? 한 가지 접근법으로, Vala로 쓰인 우리 객체를 노출시켜 클라이언트측 JavaScript가 사용할 수 있도록 만드는 방법이 있다. 여기서 바로 JavaScriptCore를 활용할 것이다.


이것은 프론트엔드(frontend)와 백엔드(backend) 아키텍처 패턴으로 생각할 수 있겠다. GNOME을 다루는 모든 업무 처리는 백엔드에 상주할 것이다. 이는 모두 Vala에서 쓴 것으로, 메인 프로세스에 의해 실행된다. 반대편인 프론트엔드에서는 코드가 JavaScript와 HTML5로 작성되고 WebKit에 의해 내부적으로 실행된다. 프론트엔드는 사용자가 보는 내용이고 백엔드는 뒤에서 발생하는 내용이다.


아래와 같은 애플리케이션의 그림을 살펴보자. 백엔드 부분은 회색 테두리의 박스 안에 위치하여 메인 프로세스에서 실행된다. 프론트엔드는 박스 외부에 위치하고 WebKit에 의해 실행 및 표시된다. 그림을 보면 프론트엔드가 객체를 생성하고, 생성된 객체에서 함수를 호출함을 확인할 수 있다. 우리가 생성하는 객체는 클라이언트측에서 정의되지 않고 사실상 백엔드에서 생성된다. 우리는 백엔드에서 생성된 객체를 프론트엔드 코드에서 접근 가능하도록 연결해주는 다리 역할을 해줄 것을 JavaScriptCore에게 요청한다.


이를 위해서는 백엔드 객체를 JavaScriptCore 클래스와 함수 정의로 자동 줄바꿈(wrap)한다. 프론트엔드에서 이용 가능하게 만들고자 하는 객체마다 JavaScriptCore 측에서 매핑을 생성해야 한다. 아래 그림에서는 MyClass 객체 다음 helloFromVala 함수를, 그 다음으로 intFromVala 순으로 매핑한다.

GNOME3 Chapter09 02.png


실행하기 - 프론트엔드에서 Vala 객체 호출하기

이제 간단한 클라이언트측 JavaScript 코드를 생성하고 백엔드에서 정의된 객체를 호출해보자.

  1. GtkBuilder와 라이센스 없이 빈 Vala 프로젝트를 생성하라. 프로젝트는 hell-jscore로 명명하라.
  2. 앞의 실험과 정확히 동일하게 WebKitGTK+를 포함하도록 configure.ac를 수정하라.
  3. src 폴더에서 Makefile.am을 찾아 WebKitGTK+와 JSCore를 Vala 컴파일 파이프라인으로 포함하도록 수정하라. 파일에서 아래와 같은 행을 찾아라.
    hello_jscore_VALAFLAGS = \
        --pkg gtk+-3.0
    
  4. 위를 제거하고 아래와 같은 행으로 대체하라.
    hello_jscore_VALAFLAGS = \
        --vapidir . --pkg gtk+-3.0 --pkg webkit-1.0 --pkg libsoup-2.4 --pkg javascriptcore
    
  5. src 폴더 내에서 hello_jscore.vala 파일을 아래의 코드로 채워라.
    using GLib;
    using Gtk;
    using WebKit;
    using JSCore;
    
    public class Main : WebView
    {
        public Main ()
        {
                    load_html_string("<h1>Hello</h1>" + "<script>alert(HelloJSCore.hello())</script>","/");
                    window_object_cleared.connect ((frame, context) => { setup_js_class ((JSCore.GlobalContext) context); });
        }
    
        public static JSCore.Value helloFromVala (Context ctx, JSCore.Object function, JSCore.Object thisObject, JSCore.Value[] 
    arguments, out JSCore.Value exception) {
    
                exception = null;
    
                var text = new String.with_utf8_c_string ("Hello from JSCore");
                return new JSCore.Value.string (ctx, text);
            }
        static const JSCore.StaticFunction[] js_funcs = {
                { "hello", helloFromVala, PropertyAttribute.ReadOnly },
                { null, null, 0 }
            };
        static const ClassDefinition js_class = {
                0, // version
                ClassAttribute.None, //attribute
                "HelloJSCore", // className
                null, // parentClass
    
                null, // static values
                js_funcs, // static functions
    
                null, // initialize
                null, // finalize
    
                null, // hasProperty
                null, // getProperty
                null, // setProperty
                null, // deleteProperty
    
                null, // getPropertyNames
                null, // callAsFunction
                null, // callAsConstructor
                null, // hasInstance
                null // convertToType
            };
    
        void setup_js_class (GlobalContext context) {
            var theClass = new Class (js_class);
                var theObject = new JSCore.Object (context, theClass, context);
                var theGlobal = context.get_global_object ();
                var id = new String.with_utf8_c_string ("HelloJSCore");
                theGlobal.set_property (context, id, theObject, PropertyAttribute.None, null);
        }
    
        static int main (string[] args)
        {
            Gtk.init (ref args);
            var webView = new Main ();
            var window = new Gtk.Window();
            window.add(webView);
            window.show_all ();
    
            Gtk.main ();
    
            return 0;
        }
    }
    
  6. 함께 포함되어 있던 webkit-1.0.vapi와 javascriptcore.vapi 파일을 src 폴더로 복사하라. 일부 배포판에서는 저장소에 .vapi 파일이 없기 때문에 javascriptcore.vapi 파일이 필요하다.
  7. 애플리케이션을 실행하라. 아래와 같은 결과가 표시될 것이다.
GNOME3 Chapter09 03.png


무슨 일이 일어났는가?

가장 먼저 WebKit와 JavaScriptCore 네임스페이스를 포함시켜야 한다. 아래 코드 조각에서는 JavaScriptCore 네임스페이스를 줄여 JSCore라고 표시됨을 주목한다.

using WebKit;
using JSCore;


Main 함수에서는 HTML 내용을 WebView 위젯으로 로딩한다. 수준 1 헤딩을 표시하고 alert 함수를 호출한다. alert 함수는 아래에서 확인할 수 있듯이 hello 함수가 리턴한 문자열을 HelloJSCore 클래스 내에 표시한다.

public Main ()
{
    load_html_string("<h1>Hello</h1>" + "<script>alert(HelloJSCore.hello())</script>","/");

alert(HelloJSCore.hello())


위의 코드 조각에서 클라이언트측 JavaScript 코드는 아래와 같음을 확인할 수 있다.

alert(HelloJSCore.hello())


그리고 HelloJSCore 클래스로부터 hello 함수를 static 함수로서 호출한다는 사실도 볼 수 잇다. 즉, hello 함수를 호출하기 전에 HelloJSCore 객체를 인스턴스화하지 않는다는 뜻이다.


WebView에서는 window_object_cleared 시그널을 얻을 때 Vala 클래스에 정의된 클래스를 초기화한다. 해당 시그널은 페이지가 삭제(clear)될 때마다 발생한다. 초기화는 setup_js_class에서 이루어지는데, 이는 JSCore 전역 컨텍스트를 전달하는 곳이기도 하다. 전역 컨텍스트는 JSCore가 전역 변수와 함수를 보관하는 곳이다. 전역 컨텍스트는 모든 코드에서 접근할 수 있다.

window_object_cleared.connect ((frame, context) => { setup_js_class ((JSCore.GlobalContext) context); });


아래의 코드는 클라이언트측 JavaScript로 노출하길 원하는 함수를 포함한다. 이 함수는 단지 Hello from JSCore 문자열 메시지를 리턴할 뿐이다.

public static JSCore.Value helloFromVala (Context ctx, JSCore.Object function, JSCore.Object thisObject, JSCore.Value[] 
arguments, out JSCore.Value exception) {

        exception = null;

    var text = new String.with_utf8_c_string ("Hello from JSCore");

        return new JSCore.Value.string (ctx, text);
}


이제 함수와 클래스의 다른 member들을 노출하는 데에 필요한 상용 코드(boilerplate code)를 넣을 필요가 있다. 코드의 첫 번째 부분은 static 함수 색인이다. 이는 노출된 함수와 래퍼에 정의된 함수명 간 매핑에 해당한다. 아래의 예제에서는 클라이언트측에서 사용할 수 있는 hello 함수를 코드에서 정의된 helloFromVala 함수를 이용해 매핑한다. 이후 색인은 배열의 끝을 표시하기 위해 null을 이용해 끝난다.

static const JSCore.StaticFunction[] js_funcs = {
    { "hello", helloFromVala, PropertyAttribute.ReadOnly },
    { null, null, 0 }
};


다음 코드 부분은 클래스 정의에 해당한다. 이는 우리가 채워야 하는 구조체에 관한 것이므로 JSCore는 클래스에 관해 알 것이다. 우리가 활용하길 원하는 필드를 제외한 모든 필드는 null로 채워진다. 이번 예제에서는 hello 함수에 static 함수를 사용한다. 따라서 static 함수 필드는 앞의 코드 조각에서 정의된 js_funcs로 채운다.

static const ClassDefinition js_class = {
            0, // version
            ClassAttribute.None, //attribute
            "HelloJSCore", // className
            null, // parentClass

            null, // static values
            js_funcs, // static functions

            null, // initialize
            null, // finalize

            null, // hasProperty
            null, // getProperty
            null, // setProperty
            null, // deleteProperty

            null, // getPropertyNames
            null, // callAsFunction
            null, // callAsConstructor
            null, // hasInstance
            null // convertToType
        };


이후 setup_js_class 함수에서는 JSCore 전역 컨텍스트에서 이용 가능한 클래스를 만든다. 먼저 앞서 채운 클래스 정의 구조체를 이용해 JSCore.Class를 생성한다. 이후 전역 컨텍스트에서 생성되는 클래스의 객체를 생성한다. 마지막이지만 이 또한 중요한 단계인데, 바로 객체를 문자열 식별자, 즉 HelloJSCore로 할당하는 일이다. 아래 코드를 실행하고 나면 클라이언트측에서 HelloJSCore를 참조할 수 있을 것이다.

void setup_js_class (GlobalContext context) {
    var theClass = new Class (js_class);
    var theObject = new JSCore.Object (context, theClass, context);
    var theGlobal = context.get_global_object ();
    var id = new String.with_utf8_c_string ("HelloJSCore");
    theGlobal.set_property (context, id, theObject, PropertyAttribute.None, null);
}


Vala 코드를 호출하는 실례는 정적인 함수를 호출하는 것보단 훨씬 흥미로울 것이다. Vala에서 생성된 객체로부터 정적이지 않은 함수를 호출하는 방법을 살펴보도록 하자.


시도해보기 - 구분된 HTML 파일 이용하기

앞절에서는 HTML5 코드를 Vala 코드로 넣었다. 코드가 길어지면 더 복잡해지고 더 이상 유지하기가 힘들어진다. 그렇다면 Vala 코드 밖에, 가령 전용 파일에 넣는다면 어떨까?


이 과제를 끝내기 전에는 다음 내용으로 넘어가지 말길 바란다!


실행하기 - GNOME을 클라이언트측 JavaScript와 연결하기

GNOME 런처를 생성하길 원한다고 가정하자. HTML5를 통해 이용 가능한 프로그램을 시스템에 표시하고 시작할 수 있을 것이다. 이는 어떻게 구현하는지 살펴보자.

  1. GtkBuilder와 라이센스가 없는 빈 Vala 프로젝트를 생성하고, html5-launcher로 명명하라.
  2. WebKitGTK+와 Gee를 프로젝트로 포함시키도록 configure.ac를 수정하라. 파일에서 아래와 같은 행을 찾아라.
    PKG_CHECK_MODULES(HTML5_LAUNCHER, [gtk+-3.0 ])
    
  3. 위의 행을 제거하고 아래의 행으로 대체하라.
    PKG_CHECK_MODULES(HTML5_LAUNCHER, [gtk+-3.0 gee-1.0 webkitgtk-3.0])
    
  4. src 폴더에서 Makefile.am을 찾아 WebKitGTK+, Gio, JavaScriptCore를 Vala 컴파일 파이프라인으로 포함하도록 수정하라. 파일에서 아래와 같은 행을 찾아라.
    html5_launcher_VALAFLAGS = \
        --pkg gtk+-3.0
    
  5. 위의 행을 제거하고 아래의 내용으로 대체하라.
    html5_launcher_VALAFLAGS = \
        --vapidir . --pkg gee-1.0 --pkg gio-unix-2.0 --pkg gtk+-3.0 --pkg webkit-1.0 --pkg libsoup-2.4 --pkg javascriptcore
    
  6. src 폴더에서 html5_launcher.vala 파일을 채워라. 간결성을 위해 전체 코드에서 중요한 부분만 표시하도록 해당 파일을 축소시켜 아래에 표시하겠다.
    public class Main : WebView
    {
        public Main ()
        {
            load_uri("file:///%s/index.html".printf(Environment.get_current_dir()));
            window_object_cleared.connect ((frame, context) => { LauncherJSCore.setup_js_class ((JSCore.GlobalContext) context);
            });
        }
    }
    
    // our Vala class
    public class Launcher
    {
        IconTheme icon = null;
        int ICON_SIZE = 80;
        public HashMap<string,DesktopAppInfo> applications {
            get;
            private set;
        }
    
        public void launch(string name) {
            var app = applications.get(name);
            if (app != null) {
                app.launch(null, new AppLaunchContext());
            }
        }
    
    public Launcher ()
        {
            icon = IconTheme.get_default ();
            applications = new HashMap<string,DesktopAppInfo>();
            var dir = Dir.open("/usr/share/applications");
            if (dir != null) {
                string entry;
                while (true) {
                    entry = dir.read_name();
                    if (entry == null) {
                        break;
                        }
                        var appInfo = new DesktopAppInfo.from_filename("/usr/share/applications/" + entry);
                    if (appInfo != null) {
                        applications.set(entry, appInfo);
                    }
                }
            }
        }
    }
    
    // Our JSCore wrapper
    public class LauncherJSCore
    {
            public static JSCore.Object js_constructor (Context ctx, JSCore.Object constructor, JSCore.Value[] arguments, out 
    JSCore.Value exception) {
                exception = null;
                var c = new Class (js_class);
                var newObject = new JSCore.Object (ctx, c, null);
    
        // register function launch
        var functionName = new String.with_utf8_c_string ("launch");
                var newFunction = new JSCore.Object.function_with_callback (ctx, functionName, js_launch);
                newObject.set_property (ctx, functionName, newFunction, 0, null);
    
        // register function getApplications
        functionName = new String.with_utf8_c_string ("getApplications");
                newFunction = new JSCore.Object.function_with_callback (ctx, functionName, js_getApplications);
                newObject.set_property (ctx, functionName, newFunction, 0, null);
                Launcher* launcher = new Launcher ();
                newObject.set_private (launcher);
                return newObject;
            }
    
            public static JSCore.Value js_getApplications(Context ctx, JSCore.Object function, JSCore.Object thisObject, JSCore.Value
    [] arguments, out JSCore.Value exception) {
                exception = null;
    
        var launcher = thisObject.get_private() as Launcher;
        StringBuilder json = new StringBuilder("[");
    
        if (launcher.applications != null) {
            foreach (var key in launcher.applications.keys) {
                var entry = launcher.applications.get(key) as DesktopAppInfo;
                var name = entry.get_display_name();
                if (entry.get_icon() != null) {
                    var icon = launcher.getIconPath(entry.get_icon().to_string());
                    json.append(("{desktop: '%s', name: '%s', icon: '%s'},").printf (key, name, icon));
                } else {
                    json.append(("{desktop: '%s', name: '%s'},").printf (key, name));
                }
            }
        }
        if (json.str [json.len - 1] == ',') {
            json.erase (json.len - 1, 1); // Remove trailing comma
        }
        json.append("]");
    
        var text = new String.with_utf8_c_string (json.str);
        var obj = ctx.evaluate_script (text, null, null, 0, null);
    
                    return obj;
            }
    
            public static JSCore.Value js_launch (Context ctx, JSCore.Object function, JSCore.Object thisObject, JSCore.Value[] 
    arguments, out JSCore.Value exception) {
                exception = null;
    
        var launcher = thisObject.get_private() as Launcher;
        if (arguments.length == 1 && arguments[0].is_string(ctx)) {
            var parameter = arguments[0].to_string_copy (ctx, null);
            char buffer[1024];
            parameter.get_utf8_c_string (buffer, buffer.length - 1);
            launcher.launch((string) buffer);
        }
        return new JSCore.Value.undefined (ctx);
            }
    
            static const ClassDefinition js_class = {
                0, // version
                ClassAttribute.None, // attribute
                "Launcher", // className
                null, // parentClass
    
                null, // static values
                null, // static functions
    
                null, // initialize
                null, // finalize
    
                null, // hasProperty
                null, // getProperty
                null, // setProperty
                null, // delete Property
    
                null, // getPropertyNames
                null, // callAsFunction
                null, // callAsConstructor
                null, // hasInstance
                null // convertToType
            };
    
            public static void setup_js_class (GlobalContext context) {
            var theClass = new Class (js_class);
                var theConstructor = new JSCore.Object.constructor (context, theClass, js_constructor);
                var theGlobal = context.get_global_object ();
                var id = new String.with_utf8_c_string ("Launcher");
                theGlobal.set_property (context, id, theConstructor, PropertyAttribute.None, null);
        }
    
  7. index.html 이름으로 된 HTML5 파일을 생성하고 src 폴더로 넣어라. 파일을 아래의 내용으로 채워라.
    <!DOCTYPE HTML>
    <html>
    <head>
    <link rel="stylesheet" href="http://code.jquery.com/mobile/1.1.1/
    jquery.mobile-1.1.1.min.css" />
    <script src="http://code.jquery.com/jquery-1.7.1.min.js"></script>
    <script src="http://code.jquery.com/mobile/1.1.1/jquery.mobile-1.1.1.min.js"></script>
    </head>
    <body>
    <div data-role="page">
    <div data-role="header">
    <h1>My Launcher</h1>
    </div>
    <div data-role="content">
    <ul data-role="listview" data-theme="a">
    </ul>
    </div>
    
    </div>
    <script>
    $(document).ready(function() {
        var launcher = new Launcher();
        var apps = launcher.getApplications();
        if (apps != null) {
            for (var i = 0; i < apps.length; i ++) {
                var image = $("<img/>").addClass("ui-li-icon").attr("src", apps[i].icon);
                var link = $("<a/>").attr("href", "#").text(apps[i].name).attr("data-desktop", apps[i].desktop).addClass("desktop-
    launcher").append(image)
                var entry = $("<li/>").append(link)
                $("[data-role=listview]").append(entry);
            }
            $("[data-role=listview]").listview('refresh')
        }
        $(".desktop-launcher").click(function() {
            var desktopFile = $(this).attr("data-desktop");
            if (desktopFile) {
                launcher.launch(desktopFile);
            }
        });
    });
    </script>
    </body>
    </html>
    
  8. 함께 포함되어 있던 webkit-1.0.vapi와 javascriptcore.vapi 파일을 src 폴더로 복사하라.
  9. 애플리케이션을 빌드하여 실행하라. 아래 스크린샷과 같이 런처가 표시될 것이다.
GNOME3 Chapter09 04.png


무슨 일이 일어났는가?

GNOME은 freedesktop.org 데스크톱 시스템을 사용한다. 이 시스템에서는 애플리케이션에 최소 하나의 데스크톱 파일이 함께 포함되어 있다. 이 파일의 확장자는 .desktop 이고, 애플리케이션 제목, 아이콘명을 비롯해 시스템이 애플리케이션을 시작할 때 호출하는 명령, 그리고 많은 언어에서 이용 가능할 경우 애플리케이션 제목의 번역 집합을 포함한다. 이번 예제에서는 시스템으로부터 모든 데스크톱 파일을 얻어 파일에 포함된 정보를 메뉴에 표시하고자 한다. 메뉴 항목 중 하나를 클릭하면 애플리케이션은 그것을 실행한다. 데스크톱 파일은 주로 /usr/share/applications에 상주한다. 따라서 우리 예제에서는 해당 폴더로부터 모든 파일을 얻으면 된다. 그 방법을 자세히 살펴보자.


이 예제에서는 HasMap 데이터 구조체를 활용하기 위해 Gee를 사용한다. 이는 Gee 네임스페이스를 포함시킬 필요가 있다는 의미다.

using Gee;


Vala 코드를 더 깔끔하게 만들 수 있도록 외부 파일로부터 HTML5 파일을 로딩한다. 클라이언트측에서 사용되어야 하는 객체도 구분된 클래스에 넣어야 하고, 마지막으로 JScore를 설정하는 데에 필요한 코드를 다른 클래스에 넣는다. 이렇게 구분하면 코드는 더 훌륭하고 관리하기 쉬운 코드가 된다.


Main 클래스의 생성자에서는 현재 폴더로부터 index.html 파일을 로딩한다. 실제로 구현할 때는 HTML5 파일의 위치는 하드코딩 되어선 안 되고, 유연해야 한다. 파일의 위치가 하드코딩되면 활용하기가 매우 까다로워진다.

public Main ()
{
    load_uri("file:///%s/index.html".printf(Environment.get_current_dir()));


이후 window_object_cleared 시그널에서 LauncherJScore.setup_js_class 함수를 연결한다.

    window_object_cleared.connect ((frame, context) => {
        LauncherJSCore.setup_js_class ((JSCore.GlobalContext) context);
    });


다음으로 우리의 Vala 클래스를 정의한다. 이 클래스는 어떤 JSCore 타입 시스템도 이용해선 안 된다. 클래스는 Launcher라고 부른다.

public class Launcher


HashMap 타입으로 생성된 데이터 구조체가 있다. 이는 데스크톱 파일명과 데스크톱 데이터 구조체를 포함하는 객체 간 매핑을 포함한다. 데스크톱 파일은 Gio 네임스페이스가 제공하는 DesktopAppInfo 객체로 표현된다.

public HashMap<string,DesktopAppInfo> applications {
    get;
    private set;
}


Launcher 생성자에서는 DesktopAppInfo 객체로부터 얻은 아이콘명을 파일시스템 내 실제 경로로 해석하기 위해 IconTheme으로부터 icon 객체를 초기화한다. 이러한 작업은 HTML이 아이콘을 이름으로 표시할 수 없기 때문에 실행하며, 대신 전체 URI를 명시해야 한다. 이후 HashMap 객체를 초기화한다.

public Launcher ()
{
    icon = IconTheme.get_default ();
    applications = new HashMap<string,DesktopAppInfo>();


이후 객체를 /usr/share/applications 에서 찾은 모든 데스크톱 파일로 채운다. 먼저 앞서 언급한 폴더를 가리키는 Dir 객체를 생성하고, 그 read_name() 함수를 반복한다. 함수가 null을 리턴하면 더 이상 파일을 찾을 수 없고 while 루프를 종료해야 한다는 의미다. 종료하지 않으면 무한 루프로 빠지게 되어 애플리케이션은 응답하지 않을 것이다. 그러면 frozen 애플리케이션을 강제로 종료하는 방법 밖에는 없다.

var dir = Dir.open("/usr/share/applications");
if (dir != null) {
    string entry;
    while (true) {
        entry = dir.read_name();
        if (entry == null) {
            break;
        }


read_name() 함수에서 얻은 각 파일명에 대해서는 해당 파일의 DesktopAppInfo 객체를 생성해야 한다. from_filename() 생성자를 이용해 객체를 인스턴스화하고, 데스크톱 파일의 전체 경로를 전달하면 된다. 파일이 데스크톱 파일이 아니라면 인스턴스화가 실패하고, 값은 null이 될 것이다. 값이 null이 아닌 경우 즉시 객체를 HashMap 데이터 구조체로 넣는다.

var appInfo = new DesktopAppInfo.from_filename("/usr/share/applications/" + entry);
if (appInfo != null) {
    applications.set(entry, appInfo);
        }


그리고 나서 아이콘명을 파일시스템 내 아이콘의 전체 경로로 해석하는 일을 실행하는 함수를 준비한다. lookup_icon() 함수를 이용해 현재 활성화된 테마에서 아이콘을 찾는다. 아이콘을 찾으면 아이콘의 전체 경로를 얻고, 그렇지 않으면 원래 아이콘명을 사용한다.

public string getIconPath(string name) {
    var i = icon.lookup_icon (name, ICON_SIZE, IconLookupFlags.GENERIC_FALLBACK);
    if (i != null) {
        return i.get_filename();
    } else {
        return name;
    }
}


드디어 애플리케이션을 시작할 함수가 생겼다. 가장 먼저 HashMap 타입으로부터 DesktopAppInfo 객체를 얻어 DesktopAppInfo가 제공하는 launch() 함수를 호출해야 한다. 시작된 애플리케이션마다 우리는 애플리케이션의 환경설정을 정의하는 새로운 AppLaunchContext 객체를 생성한다.

public void launch(string name) {
    var app = applications.get(name);
    if (app != null) {
        app.launch(null, new AppLaunchContext());
    }
}


이제 JSCore 래퍼로 넘어가보자. 래퍼는 정적 함수만 포함하며, 그 자체는 객체가 아니다.

// Our JSCore wrapper
public class LauncherJSCore


래퍼의 첫 번째 부분은 생성자다. 이는 클라이언트측 JavaScript에서 새로운 Launcher 객체를 호출할 때마다 우리 객체의 생성자가 된다. 생성자에서는 먼저 후에 살펴보게 될 클래스 정의를 기반으로 JSCore 객체를 생성한다.

public static JSCore.Object js_constructor (Context ctx, JSCore.Object constructor, JSCore.Value[] arguments, out JSCore.Value 
exception) {
    exception = null;
    var c = new Class (js_class);
    var newObject = new JSCore.Object (ctx, c, null);


그 다음 클라이언트측으로 노출하고 싶은 함수를 등록한다. 여기서는 launch와 getApplications 함수를 등록한다. 이 때는 함수의 이름을 포함하는 Jscore 문자열을 생성한 다음 함수로 매핑된 JSCore를 생성하는데, 이 곳으로 Vala 함수를 자동 줄바꿈할 것이다. 이런 경우 js_launch 함수로부터 newFunction 객체를 생성한다. 그리고 set_property() 함수를 이용해 functionName 매개변수로 newFunction 함수 객체를 할당한다. 그리고 나면 launch라고 불리는 함수가 Launcher 객체에서 실현된다. 클라이언트측 JavaScript로 노출하길 원하는 함수마다 이를 실행해야 한다.

// register function helloFromVala
var functionName = new String.with_utf8_c_string ("launch");
    var newFunction = new JSCore.Object.function_with_callback (ctx, functionName, js_launch);
    newObject.set_property (ctx, functionName, newFunction, 0, null);


마지막으로 Launcher 객체의 참조를 생성하고 유지하며, 이를 JSCore 객체에서 private으로 설정한다. 따라서 Vala 함수를 호출하고 싶을 때마다 이 객체를 private 영역으로부터 다시 가져와 그곳에서 바로 호출한다.

    Launcher* launcher = new Launcher ();
    newObject.set_private (launcher);
    return newObject;
}


클라이언트측 스크립트로 노출하는 래핑(wrapping) 함수를 하나씩 살펴보자. 첫 번째는 getApplications() 함수다.

public static JSCore.Value js_getApplications(Context ctx, JSCore.Object function, JSCore.Object thisObject, JSCore.Value[] 
arguments, out JSCore.Value exception) {


JavaScript 예외를 생성하지 않는 exception 값을 null 문자열로 설정한다.

exception = null;


앞에서 논했듯이 Vala 객체는 get_private() 함수를 이용해 private 영역에서 가져와 즉시 Launcher 로 캐스팅함으로써 얻는다. 또 애플리케이션 리스트의 JSON 표현을 유지하기 위해 StringBuilder 객체를 준비한다. 문자열은 [ 를 이용해 초기화되는데, JSON을 애플리케이션의 배열로 사용할 것이기 때문이다. 이 문자열에서 데스크톱 파일의 모든 JSON 표현을 결합할 것이다.

var launcher = thisObject.get_private() as Launcher;
StringBuilder json = new StringBuilder("[");


이 시점에서 우리는 리스트의 값이 null인지 아닌지를 확인해야 한다. null이 아니면 HashMap 데이터 구조체로부터 모든 키를 얻음으로써 반복한다. 이미 알고 있겠지만 키는 데스크톱 파일의 이름을 포함하고, 값은 DesktopAppInfo 객체를 포함한다. 따라서 HashMap 데이터 구조체에서 get() 함수를 이용함으로써 DesktopAppInfo 객체를 얻을 수 있다.

if (launcher.applications != null) {
    foreach (var key in launcher.applications.keys) {
        var entry = launcher.applications.get(key) as DesktopAppInfo;


엔트리 변수에 넣었던 DesktopAppInfo 객체를 얻고 나면 get_display_name() 함수를 이용해 애플리케이션의 이름을 얻고, get_icon() 함수를 이용해 아이콘을 얻는다. 파일시스템에서 아이콘의 실제 경로를 얻을 수 있을 때마다 아래와 비슷한 JSON을 생성한다.

{desktop: '%s', name: '%s', icon: '%s'}


그리고 파일시스템에서 아이콘의 실제 경로를 얻을 수 없다면 아래와 같은 JSON을 생성한다.

{desktop: '%s', name: '%s'}


모든 데스크톱 파일이 처리되어 JSON 문자열로 넣을 때까지 이 과정을 반복한다.

var name = entry.get_display_name();
if (entry.get_icon() != null) {
    var icon = launcher.getIconPath(entry.get_icon().to_string());
    json.append(("{desktop: '%s', name: '%s', icon: '%s'},").printf (key, name, icon));
} else {
    json.append(("{desktop: '%s', name: '%s'},").printf (key, name));


루프의 끝에 붙은 콤마는 모두 제거해야 하는데, 그렇지 않으면 이러한 JSCore 함수에서 리턴하게 될 JSCore 객체로 JSON 문자열을 변환하는 동안 JSCore가 실패할 것이다.

if (json.str [json.len - 1] == ',') {
    json.erase (json.len - 1, 1); // Remove trailing comma
}


문자열은 배열의 끝을 표시하는 ]로 종료된다.

json.append("]");


이후 JSON 문자열을 JSCore 문자열로 변환한다. 그리고 나서 evaluate_script() 함수를 이용해 JSON 문자열을 JSCore 객체로 변 환한다. 마지막으로 해당 객체를 리턴한다.

var text = new String.with_utf8_c_string (json.str);
var obj = ctx.evaluate_script (text, null, null, 0, null);

    return obj;


다음으로 살펴볼 함수는 launch() 함수다. 이 함수에서는 우리 JavaScript 코드에 arguments 매개변수를 갖고 있는데, 이는 JSCore 함수의 arguments 매개변수로부터 간단히 얻을 수 있다. Arguments 매개변수의 length 프로퍼티가 1과 같고 arguments 변수의 내용이 문자열일 경우 이것은 해당 함수로 유효한 호출이라고 말할 수 있다. 이런 경우 launcher 객체에서 launcher() 함수를 호출해서 함수로 arguments 변수의 내용을 전달하면 된다. 하지만 그 전에 arguments 변수로부터 문자열, 즉 JSCore 문자열을 C 문자열로 변환할 필요가 있는데, C 문자열은 get_utf8_c_string() 함수를 이용해 Vala 함수가 실현한다.

    public static JSCore.Value js_launch (Context ctx, JSCore.Object function, JSCore.Object thisObject, JSCore.Value[] 
arguments, out JSCore.Value exception) {

        exception = null;

var launcher = thisObject.get_private() as Launcher;
if (arguments.length == 1 && arguments[0].is_string(ctx)) {
    var parameter = arguments[0].to_string_copy (ctx, null);
    char buffer[1024];
    parameter.get_utf8_c_string (buffer, buffer.length - 1);
    launcher.launch((string) buffer);
}


호출이 무효하다면 호출자에게 undefined 값을 리턴하면 된다.

return new JSCore.Value.undefined (ctx);


아래 코드는 JSCore 클래스 정의 구조체를 포함한다. 앞서 논하였듯 js_constructor 함수를 가리키는 생성자 필드와 Launcher 값으로 된 클래스 이름을 제외한 모든 내용은 null로 채운다.

static const ClassDefinition js_class = {
            0, // version
            ClassAttribute.None, // attribute
            "Launcher", // className
            null, // parentClass

            null, // static values
            null, // static functions

            null, // initialize
            null, // finalize

            null, // hasProperty
            null, // getProperty
            null, // setProperty
            null, // delete Property

            null, // getPropertyNames
            null, // callAsFunction
            null, // callAsConstructor
            null, // hasInstance
            null // convertToType
        };


마지막으로 살펴볼 아래의 코드는 앞에서 언급했던 WebView 위젯으로부터 호출되는 setup_js_class 함수를 보여준다. 여기서는 우리의 js_constructor 함수를 가리키는 JSCore 객체 생성자를 생성한다. 이후 전역 컨텍스트에서 Launcher 라는 이름으로 된 생성자 객체를 연결한다. 따라서 클라이언트측 코드가 new Launcher()를 언급할 때마다 생성자가 호출될 것이다.

    public static void setup_js_class (GlobalContext context) {
    var theClass = new Class (js_class);
        var theConstructor = new JSCore.Object.constructor (context, theClass, js_constructor);
        var theGlobal = context.get_global_object ();
        var id = new String.with_utf8_c_string ("Launcher");
        theGlobal.set_property (context, id, theConstructor, PropertyAttribute.None, null);
}


index.html 파일에 대해서는 깊이 살펴보지 않을 것이며 아래의 코드만 살펴볼 것이다.

var launcher = new Launcher();
var apps = launcher.getApplications();


이는 사실상 자동 줄바꿈된 Vala 코드를 호출하는 행이다. 괜찮지 않은가?


Gnome3 notice header.png
사실 이러한 HTML 기반의 런처는 GNOME.Manokwari용으로 개발된 것이다. 이것은 GNOME3를 위한 데스크톱 셸로, 방금 논한 기술을 이용해 개발되었다. http://manokwari.blankonlinux.or.id 를 방문해 소스 코드를 연구하라.
Gnome3 notice footer.png


시도해보기 - index.html을 어디에 놓을 것인가

HTML 파일뿐 아니라 CSS, 이미지 파일, 그 외 다른 파일까지 갖고 있다면 index.html 파일을 html5-launcher 실행 파일이 포함되어 있지 않은 다른 폴더로 넣을 수 있다면 정말 좋을 것이다. 여기서 우리가 할 수 있는 일은 이렇게 중요한 내용들을 포함시켜 실제 환경에서 훌륭하게 활용할 수 있는 곳에 문자열 상수를 넣는 것이다. 개발 중에는 src/ 또는 다른 폴더를 이용할 수 있지만 그들을 활용하는 동안에는 /usr/share/html5-launcher/와 같은 다른 폴더를 이용할 수 있다.


요약

이번 장에서는 JSCore를 활용함으로써 기본적인 GNOME 플랫폼과도 통신이 가능한 HTML5로 쓰인 애플리케이션을 생성하는 방법을 학습하였다. HTML5 페이지를 표시하기 위해 WebKit의 WebView 위젯을 사용한다. 메인 프로세스와 통신이 없다면 해당 위젯만으로도 어떤 HTML5 페이지든 표시할 수 있다.


그리고 나서 Vala 코드에 업무 처리(business process)를 추가하였다. Vala 코드에서 생성된 객체를 HTML5 페이지에 정의된 클라이언트측 JavaScript 코드와 연결하였다. 아쉽게도 상용 코드를 사용해 시작한 다음 우리의 구현부로 채울 필요가 있다. 그리고 JSCore 함수를 이용해 클라이언트측으로 표시하길 원하는 함수마다 자동 줄바꿈을 해야 한다.


이러한 접근법을 이용하면 웹 서버 없이 전체적인 HTML5 애플리케이션을 생성하고 Vala 코드에서 업무 처리를 실행할 수 있다. 추가 기능은 자신의 상상력에 따라서만 좌우되므로 여기서 멈추지 말고 계속하길 바란다. 가령, GtkWebKit에서 특정 HTML5 기능을 지원하지 않는다는 사실을 발견했다면 Vala를 이용해 동일한 API로 자신만의 확장을 생성할 수도 있다.


애플리케이션을 GNOME 데스크톱과 통합하는 방법은 다음 장에서 학습할 것이다.


Notes