# 그림 보기(JavaScript)

간단한 "Hello world" 프로그램에 약간의 무언가가 더 들어갑니다 - GTK+의 그림 보기를 작성합니다. JavaScript 언어 소개 내용도 들어있습니다.
Jonh Wendell
jwendell@gnome.org
Johannes Schmid
jhs@gnome.org
Marta Maria Casetti
mmcasettii@gmail.com 2013
조성호
shcho@gnome.org 2017-2019.

# 그림 보기

      아 따라하기 안내서에서는 그림 파일을 불러와서 나타내는 매우 간단한 GTK 프로그램을 작성해보겠습니다. 다음 내용을 배웁니다:

        JavaScript로 기본 GTK 사용자 인터페이스 작성하기
        시그널 핸들러에 시그널을 연결하여 이벤트 처리하기
        컨테이너로 GTK 사용자 인터페이스 배치하기
        그림 파일 불러오고 표시하기

      이 지침을 따라갈 수 있으려면 다음이 필요합니다:

        안주타 IDE 설치 사본
        gjs 인터프리터 설치 사본
        객체지향 프로그래밍 언어 기본 지식

# 안주타에서 프로젝트 만들기

코딩을 시작하기 전에 안주타에서 새 프로젝트를 설정해야합니다. 이 프로그램은 빌드에 필요한 모든 파일을 만들고 그 다음 코드를 실행합니다. 또한 이 모든 상태를 유지 관리하는데 쓸만합니다.

안주타를 시작하고 파일새로 만들기프로젝트 를 눌러 프로젝트 마법사를 여십시오.

JS 탭에서 일반 Javascript을 선택하고 다음을 누른 후 일부 페이지에 세부 내용을 채워나가십시오 프로젝트 이름과 디렉터리 이름을 image-viewer로 설정하십시오.

적용을 누르면 프로젝트를 만들어줍니다. 프로젝트나 파일탭에서 src/main.js 파일을 여십시오. 매우 기본적인 예제 코드가 들어있습니다.

# JavaScript 기본: Hello World

그림 보기 프로그램 작성을 시작하기 전에 그놈에서 JavaScript를 활용하는 방법을 더 알아보기로 하겠습니다. 아, 물론 어떤 프로그래밍 언어를 처음 접하는 코드는 main.js 파일로도 찾을 수 있는 Hello world 프로그램입니다:

print ("Hello world!");

다른 대부분의 프로그래밍 언어에 익숙하다면 이 부분이 자연스러워 보일지도 모르겠습니다. `print` 함수는 화면에 출력할 `"Hello world!"` 인자 값으로 호출합니다. 참고로 각 코드의 줄은 세미콜론으로 끝납니다.

# JavaScript 클래스

이 방법이 JavaScript에서 클래스를 정의하는 표준 방식입니다:

function MyClass () { this._init (); }

MyClass.prototype = {

_init: function () { this.propertyA = "This is an object's field"; this.propertyB = 10; },

aMethod: function (arg1, arg2) { print ("inside aMethod: " + arg1 + " " + arg2); },

dumpProperties: function () { print (this.propertyA); print (this.propertyB); }

}

`MyClass` 클래스를 정의합니다. 클래스 정의 각 부분을 살펴보겠습니다:

`function MyClass` 는 클래스 생성자입니다. 이름은 클래스 이름과 같아야합니다. `this` 객체를 활용하여 클래서의 어떤 구성원이든 접근할 수 있습니다. 여기서, 생성자는 클래스의 `_init` 메서드를 호출합니다.

`MyClass.prototype` 블록은 클래스의 structure를 정의합니다. 각 클래스는 메서드(함수)와 필드(변수)로 만듭니다. 예제에서는 메서드 3개와 필드 2개로 이루어졌습니다.

여기서 정의한 첫 메서드는 `_init`이며, 인자 값을 주지 않는 함수로 지정합니다:

_init: function ()

중괄호 안에 함수를 작성하겠습니다. `propertyA`와 `propertyB` 필드 두개를 여기에서 정했습니다. 처음은 문자열로 설정하고 두번째는 정수(10진수) 값으로 설정합니다. 이 함수는 어떤 값도 반환하지 않습니다.

다음 메서드는 `aMethod`이며, 이 함수를 호출할 때 화면에 출력할 인자 두 개가 있습니다. 마지막은 `propertyA`, `propertyB` 필드를 출력하는 `dumpProperties`메서드입니다.

클래스 정의(프로토타입) 정돈 법을 정리해두십시오. 각 함수 정의는 쉼표로 구분합니다.

이제 MyClass 클래스를 정의했으니 가지고 놀 수 있습니다:

var o = new MyClass (); o.aMethod ("Hello", "world"); o.propertyA = "Just changed its value!"; o.dumpProperties ();

이 코드는 클래스의 새 인스턴스 `o`를 만들고, `aMethod`를 실행하며 `propertyA` 값을 다른 문자열 값으로 바꾼 후, (필드 내용을 출력하는) `dumpProperties` 메서드를 호출합니다.

main.js 코드를 저장하고 메뉴에서 실행실행을 누르거나 도구 모음의 단추를 눌러 실행하십시오.

# 첫 gtk 프로그램

JavaScript로 작성한 Gtk 프로그램의 가장 기본적인 부분이 어떻게 생겼는지 보도록 하겠습니다:

const Gtk = imports.gi.Gtk;

Gtk.init (null, null);

var w = new Gtk.Window ({title: "Image Viewer Demo"}); w.show ();

Gtk.main ();

어떤 동작이 일어나는지 살펴보죠:

첫 줄은 (Gtk 라이브러리가 있는) Gtk 이름 영역을 임포팅합니다. 라이브러리에는 많은 그놈 라이브러리의 언어 바인딩이 들어있는 GObject 인트로스펙션(gi)이 있습니다.

`Gtk.init`은 Gtk 라이브러리를 초기화합니다. 모든 Gtk 프로그램의 필수 구문입니다.

다음 줄에서는 새 `Gtk.Window` 객체로 메인 창을 만듭니다. `{property: value, property: value, ...}` 문법으로 창 생성자에 몇가지 속성 값을 전달할 수 있습니다. 지금 같은 경우는 창 제목을 설정합니다.

다음 줄에서는 분명하게 창을 보여줍니다. Gtk에서는 모든 창을 기본으로 숨겨둡니다.

마지막으로 `Gtk.main`에서 메인 루프를 실행합니다. 다시 말해, 프로그램을 실행합니다. 메인 루프에서는 사용자 인터페이스의 이벤트(시그널)를 기다리며, 어디선가 내보낸 시그널을 찾으면 쓸만한 시그널 핸들러를 호출합니다. 시그널에 대해 간단하게 앞으로 더 알아볼 예정입니다.

main.js 파일에 코드를 저장하고 실행해보십시오. 창을 닫아도 프로그램이 안 끝나는 모습이 나타납니다. 아직 창의 `destroy`(닫기) 시그널을 시그널 핸들러에서 처리하도록 설정하지 않았기 때문입니다. 곧 이 부분을 진행하겠지만, 지금은 터미널 창에서 CtrlC 키를 눌러 프로그램을 끝낼 수 있습니다.

# 클래스 추가

적당한 Gtk 프로그래밍 방식은 클래스 사용 방식입니다. 간단한 코드를 클래스 코드로 다시 작성해보도록 하겠습니다:

const Gtk = imports.gi.Gtk;

function ImageViewer () { this._init (); }

ImageViewer.prototype = { _init: function () { this.window = new Gtk.Window ({title: "Image Viewer Demo"}); this.window.show (); } }

Gtk.init (null, null); var iv = new ImageViewer (); Gtk.main ();

프로그램이 같다는 사실을 알아채실겁니다. 창을 만드는 코드를 `ImageViewer`클래스로 옮겼을 뿐입니다. 이 클래스 생성자에서는 창을 만들고 보여주는 `_init` 메서드를 호출합니다. 그 다음 메인 루프(`Gtk.main`)를 실행하기 전 클래스의 인스턴스를 만듭니다.

이 코드는 모듈식이며 여러 파일로 쉽게 나눌 수 있습니다. 이렇게 하면 코드가 간명해지고 알아보기 쉬워집니다.

# 시그널

시그널은 Gtk 프로그래밍의 핵심 개념입니다. 객체에 언제 어떤 일이 일어나든지 시그널을 내보냅니다. 예를 들어 단추를 누르면 `clicked` 시그널을 내줍니다. 동작이 일어났을때 프로그램에서 무언가를 하게 하려면 시그널에 함수(시그널 핸들러)를 연결해야합니다. 예제는 다음과 같습니다:

function button_clicked () { print ("you clicked me!"); } var b = new Gtk.Button ({label:"Click me"}); b.connect ("clicked", button_clicked);

마지막 두 줄은 `Gtk.Button`의 인스턴스 `b`를 만들고 `clicked` 시그널을 위에서 정의한 `button_clicked` 함수에 연결합니다. 항상 단추를 누를 때마다 `button_clicked` 함수를 실행합니다. 이 함수는 여기에 적어둔 메시지를 출력하는 동작만 합니다.

시그널을 함수에 연결하는 문법은 다음과 같습니다:

object.connect (<signal_name>, <function_to_be_called>);

GTK 클래스 참고서에서 임의의 객체에 대한 시그널 정의를 찾아볼 수 있습니다.

인라인 함수 정의 활용으로 코드를 단순하게 작성할 수 있습니다:

`b.connect ("clicked", function () { print ("you clicked me!"); });`

# 창 닫기

Gtk 창을 닫을 때 실제로는 창이 닫히지 않고 숨겨져 있습니다. 이렇게 하여 창을 유지할 수 있습니다(예를 들자면 사용자에게 정말 창을 닫을지 물어볼 때 이런 방식이 쓸만합니다).

지금과 같은 경우는 창을 실제로 닫으려고 합니다. 가장 간단하게 처리하는 방법은 프로그램을 닫는 함수에 GtkWindow 객체의 `hide` 시그널을 연결하는 방법입니다. image-viewer.js 파일로 돌아가서 다음 코드를 `_init` 메서드의 `this.window.show` 코드 위에 추가하십시오:

this.window.connect ("hide", Gtk.main_quit);

이 코드는`hide` 시그널을 Gtk 메인 루프 실행을 끝내는 Gtk의 `main_quit` 함수에 연결합니다. 메인 루프를 끝내고 나면 `Gtk.main` 함수에서 반환 값을 내보냅니다. 이 프로그램에서는 `Gtk.main ();` 코드 줄 다음에 작성한 어떤 코드든 계속 실행하려 하겠지만 이후에 어떤 코드도 없기 때문에 프로그램을 그냥 끝냅니다.

# 컨테이너: 사용자 인터페이스를 두는 공간

(단추 레이블 같은 컨트롤) 위젯은 컨테이너를 사용하여 창에서 배치를 정돈할 수 있습니다. 박스, 그리드 등 여러가지 형태의 컨테이너를 섞어 사용하여 배치를 이룰 수 있습니다.

`Gtk.Window`는 그 자체로 컨테이너의 한 종류지만 이 컨테이너에는 위젯을 한번에 하나씩만 넣을 수 있습니다. 여기에 우리는 그림과 단추, 두가지 위젯을 넣으려고 하기 때문에, 창 하나에 다른 위젯을 붙여둘 "공간이 더 많은" 컨테이너를 넣어야합니다. 쓸 수 있는 컨테이너 형식은 여러가지가 있지만, 여기서는 `Gtk.Box`를 사용합니다. `Gtk.Box` 는 가로 세로 방향으로 여러 위젯을 붙일 수 있습니다. 다른 여러가지 박스를 붙여서 보다 더 복잡한 배치를 만들 수 있습니다.

안주타에 통합한 글레이드 사용자 인터페이스 디자인 프로그램이 있습니다. 이 프로그램은 사용자 인터페이스를 쉽게 만들 수 있게 해줍니다. 그러나 이 간단한 예제로 모든 코드를 직접 작성해보겠습니다.

박스와 위젯을 창에 추가해보겠습니다. 다음 코드를 `_init` 메서드의 `this.window.show`줄 바로 위에 넣으십시오:

var main_box = new Gtk.Box ({orientation: Gtk.Orientation.VERTICAL, spacing: 0}); this.window.add (main_box);

첫 줄은 `Gtk.Box`의 인스턴스 `main_box`를 만들고 인스턴스의 속성 두개를 설정합니다. `orientation`은 수직 방향으로 설정(따라서 위젯을 수직 방향으로 배치합니다)하고, 위젯 사이의 공간 `spacing` 값은 0 픽셀로 설정합니다. 다음 줄은 창에 새로 만든 `Gtk.Box`를 추가합니다.

지금까지 창에는 빈 `Gtk.Box`만 넣었기에 지금 프로그램을 실행하면 어떤 변화도 없음을 볼 수 있습니다(`Gtk.Box`는 투명 컨테이너라서 그 무엇도 볼 수 없습니다).

# 패킹: 컨테이너에 위젯 추가

일부 위젯을 `Gtk.Box`에 추가하려면 다음 코드를 직접 `this.window.add (main_box)` 코드 줄 아래에 넣으십시오:

this.image = new Gtk.Image (); main_box.pack_start (this.image, true, true, 0);

첫 줄에서는 그림 파일을 나타낼 때 쓸 새 `Gtk.Image` 인스턴스 `image`를 만듭니다. 그 다음 그림 위젯을 `Gtk.Box`의 `pack_start` 메서드로 `main_box`에 추가(패킹)합니다.

`pack_start` 메서드에는 `Gtk.Box`에 추가할 위젯(`child`) 인자, 새 위젯을 추가했을 때 `Gtk.Box`를 더 크게 할지 여부(`expand`) 인자, `Gtk.Box`가 커졌을 때 모든 여분 공간을 차지할 지 여부(`fill`) 인자, `Gtk.Box`에서 위젯 간 설정할 간격(`padding`) 인자가 있습니다.

Gtk 컨테이너(와 위젯)은 여분 공간을 동적으로 채우도록 설정하면 그렇게 보여줍니다. 창에서 정확한 x,y 좌표 값을 주어 위젯 위치를 설정할 수 없습니다. 다만 서로 상대적 위치에 둘 수 있습니다. 이렇게 하면 창 크기 조절을 더 쉽게할 수 있고, 위젯든 대부분의 경우 타당한 크기를 가집니다.

또한 위젯은 서로 계층적 관계를 가짐을 참고하십시오. `Gtk.Box`에 패킹하면, `Gtk.Image`는 `Gtk.Box`의 하위 요소로 간주합니다. 이렇게 하여 모든 하위 위젯을 그룹 단위로 다룰 수 있습니다. 예를 들면, `Gtk.Box`와 이 컨테이너에 들어있는 모든 하위 컨테이나와 위젯을 동시에 숨길 수 있습니다.

이제 다음 두 줄을 방금 추가한 두 줄 아래에 추가하십시오:

var open_button = new Gtk.Button ({label: "Open a picture..."}); main_box.pack_start (open_button, false, false, 0);

이 줄은 처음 두 줄과 비슷하지만 `Gtk.Button`을 만들어 `main_box`에 추가합니다. 여기서 (두번째) `expand` 인자 값을 `Gtk.Image`에 대해서는 `true` 로 설정했음에 반해 `false`로 설정했음에 유의하십시오. 이 함수로 그림을 활용 가능한 공간을 차지하게 하며 단추는 필요한 공간만 차지하게 합니다. 창을 죄대로 확장하면 단추 크기는 그대로지만, 그림은 창 나머지 공간을 차지하면서 커집니다.

마지막으로 `this.window.show ();` 함수를 다음으로 바꿔야합니다:

this.window.show_all ();

이 코드는 Gtk 창의 하위 구성 요소와 해당 요소의 모든 하위 구성 요소를 보여줍니다(기본적으로 Gtk 위젯은 숨겨둔 상태임을 기억하십시오).

# 그림을 불러옵니다: 단추의 `clicked` 시그널에 연결

사용자가 열기 단추를 누르면, 대화 상자가 나타나 그림을 선택할 수 있습니다. 그림을 선택하고 나면 그림을 불러오고 그림 위젯에 표시합니다.

첫 단계에서는 단추의 `clicked` 시그널을, 우리가 호출할 시그널 핸들러 함수 `_openClicked`에 연결합니다. 이 코드를 단추를 만드는 `var open_button = new Gtk.Button` 줄 바로 다음에 넣으십시오:

open_button.connect ("clicked", Lang.bind (this, this._openClicked));

Lang JavaScript 보조 라이브러리를 사용하고 있습니다. 창의 `hide` 시그널에 대한 (클래스 없는) 생짜 함수를 쓰기보다는, 시그널에 클래스 메서드 를 연결할 수 있습니다. 지금은 걱정하실 필요가 없습니다. 그냥 기술적으로 자세한 이야기일 뿐이죠. 동작하게 하려면 파일의 상단에 다음 줄을 넣어야합니다:

const Lang = imports.lang;

# 그림 불러오기: 시그널 콜백 작성

이제 `_openClicked()` 메서드를 만들었습니다. 다음 코드를 `_init` 메서드 다음의 `ImageViewer.prototype` 코드 블럭에 넣으십시오(그리고 콤마 표시를 빼먹지 마십시오):

_openClicked: function () { var chooser = new Gtk.FileChooserDialog ({title: "Select an image", action: Gtk.FileChooserAction.OPEN, transient_for: this.window, modal: true}); chooser.add_button (Gtk.STOCK_CANCEL, 0); chooser.add_button (Gtk.STOCK_OPEN, 1); chooser.set_default_response (1);

var filter = new Gtk.FileFilter (); filter.add_pixbuf_formats (); chooser.filter = filter;

if (chooser.run () == 1) this.image.file = chooser.get_filename ();

chooser.destroy (); }

지금까지 우리가 다루어왔던 어떤 코드보다 조금 복잡하니 하나씩 뜯어보도록 하겠습니다:

`var chooser`로 시작하는 줄에서는 사용자가 파일을 선택할 수 있는 열기 대화 상자를 만듭니다. 대화 상자 제목, 대화 상자 동작(형식) ("열기" 대화 상자지만, 파일을 저장하려고 할 때는 `SAVE`를 쓸 수 있습니다), 대화 상자의 상위 창을 설정하는 `transient_for`, `true`로 설정하면 대화상자를 닫기 전까지 그 어떤 것도 누를 수 없게 하는 `modal` 속성을 지정합니다.

다음 두 줄은 대화상자에 취소 단추와 열기 단추를 만듭니다. `add_button` 메서드의 두번째 인자는 단추를 눌렀을 때 반환하는 (정수) 값입니다. 취소는 0, 열기는 1입니다.

참고로 "취소", "열기"를 직접 입력하는 대신, Gtk에서 stock 단추 이름을 활용합니다 스톡 이름은 사용자가 쓰는 언어로 미리 번역해둔 단추 레이블을 활용한다는 점입니다.

`set_default_response`에서는 파일을 두번 누르거나 Enter 키를 눌렀을 때 활성화 할 단추를 결정합니다. 지금의 경우는 열기 단추(단추 값은 1)를 기본으로 사용합니다.

다음 두 줄은 열기 대화 상자에서 `Gtk.Image`로만 열 수 있는 파일을 표시하도록 제한합니다. 필터 객체를 우선 만듭니다. 그 다음 `Gdk.Pixbuf`에서 지원하는 파일 종류(PNG와 JPEG 같은 대부분의 그림 형식)를 필터에 추가합니다. 마지막으로 이 필터를 열기 대화 상자의 필터로 설정합니다.

`dialog.run`은 열기 대화 상자를 표시합니다. 대화 상자는 사용자의 그림 선택을 기다립니다. 사용자가 그림을 선택하면 `dialog.run` 에서 1 값을 반환합니다(취소를 누르면 0 값을 반환합니다). `if` 구문에서는 어떤 값을 반환했는지 확인합니다.

사용자가 열기를 눌렀다고 한 상황에서, 다음 줄에서 `Gtk.Image`의 `file` 속성 값을 사용자가 선택한 그림 파일 이름으로 설정합니다. `Gtk.Image`는 선택한 그림을 불러오고 화면에 표시합니다.

이 메서드의 마지막 줄에서는 열기 대화상자는 더 이상 필요 없으니 해체합니다.

# 프로그램 실행

필요한 모든 코드를 제자리에 넣었으니 코드를 실행해보겠습니다. 제대로 동작할겝니다. 이렇게 하여 기능이 완전하게 동작하는 그림 보기 프로그램(JavaScript와 Gtk를 활용한 간단한 맛보기)을 그리 길지 않은 시간 동안에 만들었습니다!

# 참조 구현체

지침서를 따라하는 실행하는 과정에 문제가 있다면, 참조 코드와 여러분의 코드를 비교해보십시오.

# 다음 단계

여기 간단한 시험 프로그램에 여러분이 추가로 넣을 수 있는 몇가지 아이디어가 있습니다:

파일을 선택하기 보단 디렉터리를 선택하게 하고, 디렉터리의 모든 그림을 보여줄 수 있는 컨트롤을 제어 기능을 제공합니다.

사용자가 그림을 불러오고 수정한 그림을 저장할 때 임의 필터와 효과를 그림에 적용하십시오.

GEGL에서는 강력한 그림 편집 기능을 제공합니다.

네트워크 공유, 스캐너, 다른 복잡한 공급원에서 그림을 불러올 수 있습니다.

네트워크 파일 전송 같은 동작을 처리할 때 GIO를 사용할 수 있습니다. 그리고 스캐너를 다룰 때는 그놈 스캔을 활용하시면 됩니다.