TheSpecUIframework:Chapter 08

From 흡혈양파의 번역工房
Jump to navigation Jump to search
동적 Spec

동적 Spec

지금까지는 정적인 사용자 인터페이스를 살펴봤습니다. UI 가 열리고 나서 위젯이 변경되지 않으며 UI 레이아웃도 변경되지 않습니다. 또한, UI 의 코드를 작성할 때 프로그래머는 정확히 얼만큼의, 어떤 위젯을 사용해야 하는지 알고 있습니다. 그렇지만, 사용자 인터페이스는 보다 동적이어야 할 필요가 있는 경우도 있습니다. 예를 들어, 파일 대화 상자는 현재 선택된 파일에 대한 미리보기 영역을 가질 수 있습니다. 파일이 텍스트인 경우 해당 내용은 텍스트 필드에 표시되며, 이미지인 경우에는 이미지가 표시됩니다. 또 다른 경우는, 개발시점에서 데이터 항목이 어떻게 표시 될지 결정할 수 없는 데이터 모음을 시각화하는 것입니다. 예를 들어, 족보(genealogy) 응용 프로그램에서 부모를 클릭하면 모든 자식에 대한 정보가 표시됩니다. 각 자식은 복잡하며 편집 가능한 위젯을 사용하여 표시됩니다.


Spec 의 동적 기능 덕분에 이러한 종류의 사용자 인터페이스를 지원할 수 있으며, 관련된 내용을 이제부터 설명하겠습니다. 먼저 이미 열려있는 UI 를 동적으로 변경하는 방법을 살펴보겠습니다. 그 다음에, UI 를 작성할 때 이를 하드 코딩하는 대신 UI 가 열리는 시점에서 위젯의 선택을 연기하는 방법을 보여드리겠습니다. 세번째로, 하나의 (큰)코드 내 에서 완전한 UI 를 스크립팅 하겠습니다.


이미 열려있는 UI를 동적으로 변경

Spec 의 첫 번째 동적 기능은 이미 열려있는 UI 의 레이아웃과 내용을 변경하는 것입니다. 이 작업은 needRebuild: 및 buildWithSpecLayout: 메소드를 사용하여 수행됩니다.

  • needRebuild: 이 메서드는 전체 UI 를 처음부터 다시 작성하지 않아도 된다는 신호로 사용됩니다
  • buildWithSpecLayout: 이 메서드는 UI 재작성을 시작시킵니다. 재작성되는 UI 는 인수로 주어진 SpecLayout 인스턴스에 지정된대로 위젯을 레이아웃하는 것으로 제한됩니다.


작동 예제

이 두 가지 방법을 사용하기 위해 다중 데이터 뷰어 예제 사용자 인터페이스를 만듭니다. 왼쪽에는 다양한 종류의 데이터 목록이 나와 있으며, 오른쪽에는 이 데이터를 가장 적절한 방식으로 보여주는 view 위젯이있다. 다중 데이터 뷰어는 가로형에서 세로형으로 레이아웃을 변경할 수 있으며, 변경한 경우에는 데이터 목록이 맨 위이고 뷰 위젯이 맨 아래에 있게 됩니다. 이런 변경작업은 버튼을 클릭하여 수행됩니다. 그림 8-1 에서 멀티 데이터 뷰어 및, 그림 8-2 에서 세로 모드에 선택한 항목이 있는걸 확인할 수 있습니다.

그림 8-1 방금 시작된 상태의 다중 데이터 뷰어
그림 8-2 양식이 선택된 상태에서의, 세로 레이아웃 다중 데이터 뷰어


UI 를 열어서 변경하기 전에, 클래스 정의, 창 크기 및 제목 정의부터 시작해서 목록의 내용을 설정하는 기능등의 UI 를 설정하는 작업을 먼저 해야합니다.

ComposableModel subclass: #DynamicViewer
	instanceVariableNames: 'list view button state'
	classVariableNames: ''
	package: 'Spec-BuildUIWithSpec'
DynamicViewer >> extent
	^ 400@350
DynamicViewer >> title
	^ 'Dynamic Multi-Data Viewer'
DynamicViewer >> items: aCollection
	list items: aCollection


비슷하지만, initializeWidgets 메소드는 간단합니다. 목록에는 15 자로 축소된 printString 이 표시됩니다. UI 는 $h 문자에 state 를 설정하여 가로 레이아웃을 사용하고 있으며 레이블을 기본값으로 설정합니다(그림 8-1 참고).

DynamicViewer >> initializeWidgets
	list := self newList.
	list displayBlock: [ :item | item printString contractTo: 15 ].
	button := self newButton.
	button label: 'Change!'.
	state := $h.
	view := self defaultView.
DynamicViewer >> defaultView
	| cm |
	cm := self newLabel.
	cm label: 'Select something from the list please.'.
	^cm


윈도우의 레이아웃은 아래의 horizontalSpec 메소드에 의해 주어진다. 다시 말하지만, 여기서 놀랄만한 부분은 없습니다. <spec : #default> 프라그마(pragma) 덕분에 기본 레이아웃이 됩니다. (자세한 내용은 5 장 참조).

DynamicViewer class >> horizontalSpec
	<spec: #default>
	^ SpecColumnLayout composed
		newRow: [ :r |
			r newColumn: [:c | c add: #list] left: 0 right: 0.7.
			r newColumn: [:c | c add: #view] left: 0.32 right: 0];
		newRow: [ :r | r add: #button ] height: self toolbarHeight;
      yourself.


이제부터, 이 UI 를 열고 표시할 항목의 목록을 제공 할 수 있습니다. 예를 들어, 아래 코드는 그림 8-1 과 같은 UI 를 생성합니다.

| viewer |
viewer := DynamicViewer new.
viewer openWithSpec.
viewer items: {
	42 .
	'Everberg' . 
	#thumbsUp asIcon . 
	#(SGO CDG ZYR BRU) . 
	(OrderedCollection withAllSubclasses
	  collect:[: cls | cls comment]) asArray}.


UI 레이아웃의 변경

'Change!' 버튼을 클릭하지만, 아직은 아무 것도 하지 않습니다. 무언가를 작동하게 하기위해, initializePresenter 에서 다음과 같이 버튼의 액션 블록을 정의해야 합니다. 이 액션 블록에서 먼저 $h$v 사이의 상태를 적절하게 전환합니다. 두번째로, self needRebuild: false 구문을 사용해서 완전한 UI 가 처음부터 재구성 될 필요는 없다는 신호를 보냅니다. 덕분에, UI 의 다음 재빌드는 레이아웃 작업만 수행하는 것으로 제한되게 됩니다. 세 번째이자 마지막으로, 레이아웃 변경은 buildWithSpecLayout: 메시지에 의해 시동됩니다. 인수로 SpecLayout 을 가지게 되며, UI 의 레이아웃을 지정된 레이아웃으로 변경합니다.

DynamicViewer >> initializePresenter
	button action: [
		state := (state = $v) ifTrue: [ $h ] ifFalse: [ $v ].
		self needRebuild: false.
		self buildWithSpecLayout: self currentSpec.
		].


UI 의 새로운 레이아웃은, 아래에 표시된 currentSpec 메소드에 의해 반환됩니다. state 에 저장된 값에 따라, horizontalSpec 또는 verticalSpec 메서드로 간단하게 위임합니다.

DynamicViewer >> currentSpec.
	^ state = $v
		ifTrue: [ self class verticalSpec ]
		ifFalse: [ self class horizontalSpec ]
DynamicViewer class >> verticalSpec
	^ SpecColumnLayout composed
      newRow: [:r | r add: #list] top: 0 bottom: 0.7;
      newRow: [:r | r add: #view] top: 0.32 bottom: 0.02;
      newRow: [:r | r add: #button] height: self toolbarHeight;
      yourself.

앞의 내용은, 뷰어의 레이아웃을 가로 모드에서 세로 모드로 전환 할 수 있도록 하기 위해 필요한 모든 내용입니다. 핵심은 self needRebuild: falseself buildWithSpecLayout: <XXX> 의 두 줄입니다. 나머지는 실제로 <XXX> 값을 정의하는 데 사용되는 장치입니다.


위젯을 다른것으로 교체하기

근본적으로, 위젯을 다른 것으로 바꾸는 것은 UI 의 레이아웃을 변경하는 특별한 경우에 해당될 뿐입니다. 사용 된 레이아웃은 실제로 동일 하며, 하나의 위젯 인스턴스가 먼저 다른 인스턴스로 대체됩니다.


이를 표시하기 위해서, view 위젯에 표시해서 목록의 선택된 항목을 시각화하는 코드를 구현합니다. 아래 코드와 같이 initializePresenter 에 새 코드를 추가하면 됩니다. 목록 선택 변경 블록은 앞에서 설명한 버튼 동작 블록과 동일한 레이아웃 동작을 수행합니다. 차이점은 새 위젯을 view 인스턴스 변수에 지정하는 부분입니다. 예를 들자면, UI 는 그림 8-1 처럼 Form 을 표시하게 됩니다.

DynamicViewer >> initializePresenter
	button action: [
		state := (state = $v)	ifTrue: [ $h ] ifFalse: [ $v ].
		self needRebuild: false.
		self buildWithSpecLayout: self currentSpec.
		].

	list whenSelectedItemChanged: [ :new |
			view := self widgetFor: new.
			self needRebuild: false.
			self buildWithSpecLayout: self currentSpec.
		].


widgetFor: 메소드의 의무는, 선택된 아이템을 보여주는 정확한 위젯의 인스턴스를 반환하는 것입니다. 이 예제를 작게 유지하기 위해서, 기본적으로 선택된 항목의 유형에 대한 큰 switch 문을 근본적으로 구현하는, 객체지향적이지 않은 코드를 제공합니다. 깨끗한 객체 지향 구현은 이중-디스패치를 ​​사용해야하며, 이것을 읽고 있는 당신에게 연습으로 남겨두겠습니다.

DynamicViewer >> widgetFor: aDatum
	| cm |
	aDatum isNil ifTrue: [ ^ self defaultView ].
	aDatum isForm ifTrue: [ 
		cm := self newImage.
		cm image: aDatum.
		^ cm ].
	aDatum isArray ifTrue: [ 
		cm := self newList.
		cm items: aDatum.
		^ cm ].
	
	"default case"
	cm := self newText.
	cm text: aDatum asString.
	^cm
Gnome3 notice header.png
widgetFor: 에 대한 코드는 좋은 객체지향의 사례가 아닙니다. 이 장의 문맥에서 우리는 예제를 자급자족[1] 하게 하기위해서, 못생긴 코드를 작성하기로 선택했습니다. 이와 같은 코드를 작성하는 학생이라면, 우리 사무실에 초대해서 긴 대화를 나눠야 할겁니다.
Gnome3 notice footer.png


UI 와 위젯을 이용해서 동적으로 채우기(populating)

UI 에 표시 될 위젯의 수를 언제나 미리 결정할 수있는 것은 아니며, 그렇다면 UI 를 열때 위젯을 결정해야 합니다. Spec 은 이런 방법을 지원합니다. 동적 채우기(Dynamic population)는 assign:to: 메서드를 사용하여 다른 위젯에 대한 가상 인스턴스 변수를 동적으로 만들고, 인스턴스인 경우라면 레이아웃을 정의하여 ComposableModel 대신 DynamicComposableModel 을 서브클래싱해서 수행됩니다.


다중 데이터 뷰어에 적용하기

이를 보여주기 위해, 이전 섹션의 동적 다중 데이터 뷰어를 확장해 보겠습니다. 그림 8-3 에서 보이는것 처럼, 목록보기 에서와는 다르게 배열의 내용을 다르게 표시합니다. 이 상세보기를 위해 새로운 DynamicArrayViewer 위젯을 만들예정이며, 이를 위해서 일단 DynamicViewer >> widgetFor: 구현을 변경해서 위젯을 반환하십시오:

그림 8-3 배열 내용을 표시하는 확장된 다중 데이터 뷰어
DynamicViewer >> widgetFor: aDatum
	| cm |
	aDatum isNil ifTrue: [ ^ self defaultView ].
	aDatum isForm ifTrue: [ 
		cm := self newImage.
		cm image: aDatum.
		^ cm ].
	aDatum isArray ifTrue: [ 
		cm := DynamicArrayViewer on: aDatum.
		cm owner: self.  " needed because we do not use 'instantiate:' "
		^ cm].
	
	"default case"
	cm := self newText.
	cm text: aDatum asString.
	^ cm


배열 내용을 보여주는 위젯 구현하기

이제부터 배열의 자세한 내용을 보여주는 위젯을 구현합니다. DynamicComposableModel 의 하위 클래스이며 배열을 보유하고있는 인스턴스 변수 collection 과, 맨 위에 레이블을 보유하고있는 label 이 있습니다.

DynamicComposableModel subclass: #DynamicArrayViewer
	instanceVariableNames: 'collection label'
	classVariableNames: ''
	package: 'Spec-BuildUIWithSpec'


collection 인스턴스 변수는 다음과 같이 on: 메시지를 사용해서 위젯이 인스턴스화 될 때 초기화됩니다. initialize 메소드가 호출되기 전에 이 collection 변수를 초기화 해야하는데, 왜냐하면 initializeWidgets 에서 표시 할 항목을 결정해야 하기 때문입니다.

DynamicArrayViewer class >> on: aCollection
	| inst |
	inst := self basicNew.
	inst collection: aCollection.
	inst initialize.
	^inst.


initializeWidgets 메서드는 컬렉션을 반복하고 각각의 항목에 대해 TextModel 을 인스턴스화 합니다. 인스턴스화 이후에, 다음에서 보이듯이 assign:to: 메서드를 사용해서 UI 에 추가됩니다.


assign:to: 는 ComposableModel 의 인스턴스와 해당 인스턴스를 나타내는데 사용되는 심볼을 사용합니다. Spec 은 (가상의)접근자 메소드와 함께 변수를 가상으로 만듭니다. 예를 들어, 다음의 코드는 txt_1:, txt_2: 처럼, 접근자 txt_1, txt_2 등을 생성합니다.

DynamicArrayViewer >> initializeWidgets
	
	label := self newLabel.
	label label: 'An array with ', collection size asString , ' elements:'.
		
	1 to: collection size do: [ :count | | model |
		model := self newText.
		model text: (collection at: count) asString.
		self assign: model to: ('txt_',count asString) asSymbol ]


Gnome3 notice header.png
이러한 가상 접근자를 생성하면, 마치 일반 변수인 것처럼 접근자를 통해 나머지 코드에서 위젯을 참조 할 수 있습니다.
Gnome3 notice footer.png


이제부터 해야할 일은 이 UI 의 위젯에 대한 레이아웃을 정의하는 것입니다. ComposableModel 의 클래스 측면 레이아웃 기능 외에도 DynamicComposableModel 을 사용하면 레이아웃을 인스턴스 측면에서 지정할 수도 있습니다. 필요한 SpecLayout 인스턴스를 반환하기 위해서, layout 메서드를 재정의하여 이 작업을 진행하거나, layout: 접근자를 사용하여 레이아웃을 설정해도 됩니다. 이 두 가지 옵션은 6 장에서 설명한 창 제목 및 크기에서 가능한 옵션과 유사합니다.


지금의 예제에서는, 위젯 수가 인스턴스마다 다르므로 인스턴스 측면에서 레이아웃을 정의해야 합니다. 여기서는 다음과 같이 layout 메서드를 오버라이드 하도록 선택합니다. 이 코드는, 열을 생성하며, initializeWidgets 에서 생성된 모든 위젯을 추가합니다. 동적으로 생성된 위젯은 컬렉션을 반복하여 재구성(reconstructed)된 이름인 txt_1, txt_2 등으로 참조됩니다. 하지만 이런 방법은 차선책이며, 다른 구현방법은 이 장의 끝에서 보여드리도록 하겠습니다.

DynamicArrayViewer >> layout
	| col |
	col := SpecColumnLayout composed.
	col add: #label.
	1 to: collection size do: [:count|
		col add: ('txt_',count asString) asSymbol ].
	^ col

이것이 UI 를 동적으로 채우는 데 필요한 전부입니다. 이 UI 는 ComposableModel 의 하위 클래스이므로 독립실행형으로 열 수도 있습니다. 예를 들어, 아래의 코드조각은 그림 8-3 처럼 열리게 됩니다.

(DynamicArrayViewer on:
       (OrderedCollection withAllSubclasses
           collect:[: cls | cls comment]))
   openWithSpec.


initializeWidgets 의 대체 구현

지금까지의 내용에서, 위젯의 가상변수 이름을 만들 때 initializeWidgets 과 layout 이 두 번 작동된다는 것을 확인했습니다. layout 메서드를 재정의하는 대신에, 다음과 같이 initializeWidgets 내부에서 layout: 접근자를 사용한다면 두번 작동되는 문제를 피할 수 있습니다.:

DynamicArrayViewer >> initializeWidgets
	| col |
	col := SpecColumnLayout composed.

	label := self newLabel.
	label label: 'An array with ', collection size asString , ' elements:'.
	col add: #label.	
		
	1 to: collection size do: [ :count | | model nam |
		model := self newText.
		model text: (collection at: count) asString.
		nam := ('txt_',count asString) asSymbol.
		self assign: model to: nam.
		col add: nam ]

	self layout: col.


이렇게 구현하게 되면 위젯이 만들어질때 레이아웃을 만들게 됩니다. 레이아웃이 생성된 직후에 각 위젯에 레이아웃의 참조를 추가합니다. 메소드가 끝나면, UI 의 레이아웃은 layout: 메서드를 사용해서 설정되기 때문에, 앞의 구현 에서처럼 layout 을 재정의 할 필요가 없습니다.


Playground 에서 UI 를 해킹하기

DynamicComposableModel 덕분에 Spec 은 스크립팅 같은 개발 스타일을 허용할 수 있으며, 전체 UI 는 대부분 점차적으로 구축 된 하나의 거대한 코드에 정의됩니다. 이 내용은, 여러 메서드의 정의 뿐만 아니라 UI 를 위한 클래스의 생성을 생략하고 싶은 "quick and dirty" 프로토 타이핑 작업에 유용합니다. 이 섹션에서는 Playground 안에서 간단한 UI, 스크립팅 스타일을 함께 해킹 할 수 있는 방법을 살펴보겠습니다.


여기서 만들어진 내용은, UI 는 숫자와 버튼을 표시하는 간단한 예입니다. 하나는 숫자를 증가시키고 하나는 감소시키기위한 것입니다. 이 예제는 Pharo 5 이미지에 있는 프로그램 코드에서 변형된 것입니다: DynamicSpecExample >> openOnInteger 를 작성한 Torsten Bergmann 에게 감사드립니다.


DynamicComposableModel 을 인스턴스화하는 것으로 예제 코드는 시작되며, title: 및 extent: 메서드를 사용해서 UI 의 제목과 크기를 설정합니다(5장 참조). 지역 변수 num 은 UI 에 표시된 숫자를 가지게 됩니다.

	| ui num |
	
	num := 0.
	ui := DynamicComposableModel new.
	ui title: 'I am dynamic'.
	ui extent: 250@70.


그런 다음 instantiateModels: 메소드를 사용하여 위젯을 UI 에 추가합니다. 이 메소드는 인수로 한쌍의 콜렉션을 가지게 됩니다. 쌍의 첫 번째 요소는 위젯의 이름인데, 인스턴스 변수가 정상적인 정적 UI 인 경우라면, 이 인스턴스 변수의 이름이 됩니다. 쌍의 두 번째 요소는, 위젯의 유형을 결정하는 ComposableModel 의 하위 클래스입니다.

	
	ui instantiateModels: #( 
		text LabelModel
		plus ButtonModel
		minus ButtonModel ).


그러므로 이 UI 는 LabelModel 인스턴스인 text 위젯과 plus 및 minus 의 두 개 ButtonModel 위젯을 각각 보유합니다. 근본적으로, 위의 코드는 앞에서 살펴봤던 내용과 비교했을때 유일한 차이점입니다. 이전 섹션에 표시된 assign:to: 메소드에서와 같이, Spec 은 위젯에 대한 접근자(가상)를 자동으로 생성하기 때문에, 나머지 코드에서도 접근자를 사용할 수 있기 때문입니다.


위젯을 정의하고 인스턴스화하면 일반적인 initializeWidgets 또는 initializePresenter 메소드에서 처럼 위젯을 구성 할 수 있습니다.

	ui text label: num asString.
	
	ui minus 
		label: '-';
		state: false;
		action: 	[ 
			num := num -1.
			ui text label: num asString ].

	ui plus 
		label: '+';
		state: false;
		action: 	[ 
			num := num +1.
			ui text label: num asString ].

마지막으로 해야 할 일은, UI 에 레이아웃을 제공하는 것입니다. 이 작업은 레이아웃을 지정하는 SpecLayout 인스턴스를 전달하는 layout: 메서드를 호출하여 수행됩니다.

	ui layout: (SpecLayout composed
		newColumn: [ :c |
			c
				add: #text height: 25;
				newRow: [ :r | r add: #minus ; addSplitter; add: #plus ] height: 25 ];
		yourself).
				
	ui openWithSpec.


Gnome3 notice header.png
이렇게 하면 Pharo 가 제공해야하는(예를들면 브라우저) 모든 코드 작성 및 관리 기능을 무시하게 되며, UI 에 대한 코드의 서로 다른 책임을 명확하게 구분하지 않습니다. 그리고 앞으로 서브클래싱을 통해 (프로그램을) 확장하는것이 힘들어집니다. 돌려 말하자면, 이런 방법은 사용자 인터페이스를 해킹하는 빠르고 쉬운 방법이지만 실제 프로그램 작성에 사용해서는 안되는 방법입니다.
Gnome3 notice footer.png


결론

이 장에서는 보다 동적인 사용자 인터페이스를 허용하는 Spec 의 기능에 대해 논의했습니다. Spec 이 제공하는 기능을 통해, 레이아웃 및 콘텐츠를 즉시 변경하는 것은 물론이거니와, 프로그램이 동작하는 시점에서 UI 의 위젯을 결정할 수도 있었습니다. 그리고 동적기능에 대한 설명으로 끝을 맺었습니다. 덕분에 하나의 커다란 코드 블록, 스크립팅 스타일로 UI 를 완벽하게 구성할 수 있었습니다.


Notes

  1. 다른 코드와 의존성을 가져가지 않게 하려는 의도