귀차니즘의 영향으로 분명 늦은 포스팅이지만 상당히 인상깊은 행사였다고 생각하며 꼭 블로그에 글 써야지 하고 생각했던 행사였다. 사람들이 정말 전통적인 소프트웨어 강의를 얼마나 익숙치 않아하고 반대로 눈에 보이는 그래픽-한 과제에는 흥미를 보이는구나. 생각해보면 이런 종류의 교육용 커리큘럼은 plt-racket에 이미 있었고 꽤 흥미로웠던 기억이 난다. 그런데 plt-racket에 있는 http2/image 패키지랑은 다르게 quil은 좀 더 낮은 api만 제공하므로 이 글을 쓰는 계기가 됨.
quil로 도형 그리기
1
lein new quil foo
정도의 명령어로 quil 라이브러리가 포함되고 실행만 하면 되는 얼개가 구현된 상태의 quil 프로젝트가 만들어진다.
기본적으로 생성되는 프로젝트는 사실 너무 복잡하므로 그냥 도형만 그려보는 것으로 수정..
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(ns foo.core
(:require [quil.core :refer:all]
[quil.middleware :as m]))
(defn setup []
(frame-rate30)
(color-mode:rgb))
(defn draw-state [state]
(background255255255)
(fill25500) ;; red
(rect75755050))
(defsketch foo
:title"foo"
:size [200200]
:setup setup
:draw draw-state
:features [:keep-on-top]
:middleware [m/fun-mode])
일단은 여기서 draw-state 함수만 고치면 뭔가를 그려볼 수 있는 상태가 됨. 라켓의 http2/image 패키지에는 여러 레이아웃 함수들이 있어 편리하다. 그래서 그런걸 만들어보려는 마음이 생겼다.
처음 만들어 볼 것은 여러 모양을 가로로 늘어놓는 모양을 만드는 beside를 만들어보자. 그럴려면 모든 모양들은 아직 그려지지 않은 상태에서 그려지기 위한 데이터를 가지고 있어야 하며, 또한 한 가지 원칙으로 그려져야 괜찮을 것 같다-예를들어 삼각형은 각 꼭지점의 위치를 인자로 줘야 하는데 사각형과 마찬가지로 그냥 좌-상단위치와 가로길이,세로길이 를 인자로 받도록 하는게 더 좋을듯.
그럼 그리기! 함수를 한번 생각해보면 그리기!는 어떤 모양을 그리든지 나머지는 모양이 가진 데이터에 기초해서 그리되 함께 넘겨받은 left 위치와 top 위치 인자를 가지고 그리도록 하는게 좋겠다.
1
2
3
4
5
6
7
(defn 그리기!
"좌-상단 위치를 기준으로 모양을 그린다."
[left top 모양]
(rect left
top
(:width 모양)
(:heigth 모양)))
그리고 다음과 같이 draw-state 함수를 수정하면
1
2
3
4
(defn draw-state [state]
(background255255255)
(fill25500) ;; red
(그리기! 7575 {:width50:heigth50}))
이전과 동일하게 동작하게 된다. 그리기! 함수는 무조건 rect 를 그리게 구현되었다. 이는 우리가 원한게 아니므로 그리기! 함수가 어떤 모양인지를 따져보고 알맞은 함수를 호출하도록 바꿔보자. 이럴때 단순히 조건분기하는 if 나 cond를 쓸 수도 있지만 어떤 모양인지 따져보고 알맞은 함수를 호출한다는 것은 clojure 에서 지원하는 문법인 defmulti 를 쓰기에 알맞아 보인다. defmulti는 실제 함수 구현부인 defmethod 와 짝을 이루며 인자를 살펴보거나 따져보아서 어떤 값을 기준으로 method를 호출할지 결정할 수 있고, defmethod 에서는 어떤 값에 따라 호출될지 구현부 앞에 밝혀적음으로써 누구나 의도를 파악하기 쉽도록 할 수 있다.
하지만 한가지 문제가 있는데 defmulti 는 repl 이 다시 실행될때까지 덮어 써지지 않는다;; 수정하려면 뭔가 특별한 조치를 취하지 않는이상 ide 에서 프로젝트 커넥션등을 다시 실행하거나 해야한다는것..
1
2
3
4
5
6
7
8
9
10
11
12
(defmulti 그리기!
"좌-상단 위치를 기준으로 모양을 그린다."
(fn [_ _ 모양] ;; _ 로 관심없는 인자를 표시..
(:type 모양))) ;; 어떤 값을 기준으로 할지 골라내는 방법
(defmethod 그리기!
::rect;; 골라낸 값이 일치하는 경우 실행될 메서드임을 밝힘.
[left top 모양]
(rect left
top
(:width 모양)
(:heigth 모양)))
여기서는 모양 맵의 :type 을 보고 ::rect 인 경우에만 사각형을 그리도록 함수가 구현되어 있다. 이는 모양 맵이 :type 이 있음을 가정하고 있으므로 draw-state 함수는 다음과 같이 고쳐야 한다.
1
2
3
4
5
6
(defn draw-state [state]
(background255255255)
(fill25500) ;; red
(그리기! 7575 {:type::rect
:width50
:heigth50}))
어떤 모양을 그릴때 이 모양은 하나가 아닐 수 있다. 특히나 우리가 구현하려고 하는 beside는 각 모양들의 집합이다. 따라서 이 모양들의 좌-상단위치가 같아서는 안 된다. 이를 나타내는 offset 을 모양맵에 넣도록 하자. 덤으로 색깔도..
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
;; 그리기! 함수를 이렇게..
(defmethod 그리기!
::rect
[left top 모양]
(when (:stroke 모양)
(apply stroke (:stroke 모양))) ;; apply 를 썼다.
(when (:fill 모양)
(apply fill (:fill 모양))) ;; apply 를 썼다.
(rect (+ left (:offset-left 모양))
(+ top (:offset-top 모양))
(:width 모양)
(:heigth 모양)))
;; draw-state 함수를 이렇게...
(defn draw-state [state]
(background255255255)
(그리기! 00 {:type::rect
:fill [25500]
:stroke [000]
:offset-left75
:offset-top75
:width50
:heigth50}))
stroke 나 fill 함수는 인자들을 여러개를 받아서 색을 결정한다. 따라서 우리는 데이터를 벡터 형으로 기록하도록 하고 나중에 함수를 실행할때 벡터를 여러 인자를 넣어 실행한 것처럼 호출 해야한다. 이때 apply 라는 함수를 쓸 수 있다. 또, 어떤 경우에만 실행한다 라는 의미로 when 을 쓸 수 있다. 그리기! 메서드가 너무 길어져서 번잡해졌다. 한번 개선해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(defmethod 그리기!
::rect
[left top 모양]
(let [{:keys [offset-left
offset-top
width
height
stroke
fill]} 모양]
(when stroke
(apply quil.core/stroke stroke))
(when fill
(apply quil.core/fill fill))
(rect (+ left offset-left)
(+ top offset-top)
width
height)))
let은 내부에 밝혀적은 식대로 destructuring(구조분해) 해서 각 심볼로 그 값을 쓸 수 있게 해준다. 여기서는 모양맵을 구조분해 해서 각 키워드에 해당하는 값을 그대로 심볼로 쓸 수 있게 한 것. 그런데 여기서 stroke는 quil.core/stroke 함수를 가려버리게 되므로 stroke가 quil의 함수임을 밝혀적었다.
이제 beside 를 만들어보자.
옆으로 늘어놓을때, 각각의 세로길이가 다른 모양을 늘어놓을 것이므로 이 모양들은 세로 기준 중앙 정렬 되어야 할 것이다.
그러므로 먼저 모양들 중 가장 긴 세로길이 를 구해야 하고,
1
(apply max (map:height 모양들)) ;; 모양들의 가장-긴-세로길이 를 구한다.
이 가장 긴 세로길이를 기준으로 offset-top 을 바꿔줘야 한다.
1
(quot (- 가장-긴-세로길이 height) 2) ;; 각 모양의 (가장-긴-세로길이 - 세로길이) / 2
또한 모든 모양들에 대해서 앞선 모양의 offset-left 에 현재 모양의 가로길이를 더한것이 다음번 모양의 offset-left 가 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(defn beside
"모양들을 옆으로 늘어놓는다. 이때 세로가 가장 긴 모양의 세로축 중심을 기준으로 중앙정렬한다."
[& 모양들]
(let [가장-긴-세로길이 (apply max (map:height 모양들))
자식요소 (loop [모양들 모양들
left 0
result []]
(if 모양들
(let [[이번-모양 & 남은-모양] 모양들
{:keys [width height]} 이번-모양
top (quot (- 가장-긴-세로길이 height) 2)
조정된-모양 (assoc 이번-모양
:offset-left left
:offset-top top)] ;; 중앙정렬
(recur 남은-모양
(+ left width) ;; 다음 모양은 현재 가로길이를 더한 값으로 offset-left 가 계산되도록 더해준다.
(conj result 조정된-모양)))
result))]
{:type::grouped
:width (reduce + (map:width 모양들)) ;; 모양들의 가로길이 합이 합쳐진 모양의 가로길이가 된다.
:height 가장-긴-세로길이
:offset-left0
:offset-top0
:children 자식요소}))
& 모양들은 이 함수에 여러개의 인자를 넣어서 호출 할 수 있음을 밝히는 구문이다. loop 는 초기값을 선언하고 몸체내의 식을 recur 로 반복할 수 있도록 만든 설탕문법이다. loop는 다른 함수나 letfn 등으로 바꿔 쓸 수 있다. assoc은 해당 키의 값을 덮어 쓰는데 사용한다.
즉, 모양들 을 순회하며 offset-left 와 offset-top을 갱신하는 코드가 된다. 마지막으로 이 결과를 가지고 :type이 ::grouped인 모양맵을 만들어낸다.
이 ::grouped 를 그리기!하는 method는 아직 없으므로 하나 만들어주자.
1
2
3
4
5
6
7
8
9
10
11
12
13
(defmethod 그리기!
::grouped
[left top 모양]
(let [{:keys [type
offset-left
offset-top
width
height
children]} 모양]
(doseq [child children]
(그리기! (+ offset-left left)
(+ offset-top top)
child))))
deseq 는 다른 플랫폼의 for-each 구문과 비슷하게 동작한다. child of children을 모두 그리기!하는 함수이되 ::grouped 모양도 결국은 offset 을 가지므로 이를 좌-상단 위치에 반영해준다.
이를 draw-state 함수를 고쳐 한번 테스트해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(defn draw-state [state]
(background255255255)
(그리기! 2525 (beside
{:type::rect
:fill [25500]
:stroke [000]
:offset-left0
:offset-top0
:width25
:height25}
{:type::rect
:fill [255255255]
:stroke [000]
:offset-left0
:offset-top0
:width25
:height25}
{:type::rect
:fill [25500]
:stroke [000]
:offset-left0
:offset-top0
:width25
:height25})))
clojure에는 cycle이라는 함수가 있어서 반복되는 값을 표현하기에 알맞다. 빨간색 사각형과 흰 사각형이 교대로 늘어서는 모양을 생각해보자. 이는 이렇게 표현할 수 있을 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(clojure.pprint/pprint
(apply beside
(take10
(cycle
[{:type::rect
:fill [25500]
:stroke [000]
:offset-left0
:offset-top0
:width50
:height50}
{:type::rect
:fill [25500]
:stroke [000]
:offset-left0
:offset-top0
:width50
:height50}]))))
이를 repl 에서 실행시켜 보면 offset-left 차이나는 10개의 모양맵들이 보일 것이다. clojure.pprint/pprint 함수는 데이터를 예쁘게 출력해주는 함수다. draw-state를 고치면 실제로 그려지는 모양을 확인할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(defn draw-state [state]
(background255255255)
(그리기! 2525
(apply beside
(take10
(cycle
[{:type::rect
:fill [25500]
:stroke [000]
:offset-left0
:offset-top0
:width15
:height15}
{:type::rect
:fill [255255255]
:stroke [000]
:offset-left0
:offset-top0
:width15
:height15}])))))
이 beside와 비슷한 above 함수를 만들어보자. 이 함수는 가로가 아닌 세로로 늘어놓는 함수다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(defn above
"모양들을 위아래로 늘어놓는다. 이때 가로가 가장 긴 모양의 가로축 중심을 기준으로 중앙정렬한다."
[& 모양들]
(let [가장-긴-가로길이 (apply max (map:width 모양들))
자식요소 (loop [모양들 모양들
top 0
result []]
(if 모양들
(let [[지금-모양 & 남은-모양] 모양들
{:keys [width height]} 지금-모양
left (quot (- 가장-긴-가로길이 width) 2)
조정된-모양 (assoc 지금-모양
:offset-left left
:offset-top top)] ;; 중앙정렬
(recur 남은-모양
(+ top height)
(conj result 조정된-모양)))
result))]
{:type::grouped
:width 가장-긴-가로길이
:height (reduce + (map:height 모양들))
:offset-left0
:offset-top0
:children 자식요소}))
이제 이 모양들로 체스판을 만들어보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
(defn draw-state [state]
(background255255255)
(그리기! 2525
(apply above
(take10
(cycle
[(apply beside
(take10
(cycle
[{:type::rect
:fill [25500]
:stroke [000]
:offset-left0
:offset-top0
:width15
:height15}
{:type::rect
:fill [255255255]
:stroke [000]
:offset-left0
:offset-top0
:width15
:height15}])))
(apply beside
(take10
(cycle
[{:type::rect
:fill [255255255]
:stroke [000]
:offset-left0
:offset-top0
:width15
:height15}
{:type::rect
:fill [25500]
:stroke [000]
:offset-left0
:offset-top0
:width15
:height15}])))])))))
일단 ::grouped 모양을 만들면 이 모양을 그리기!하는 함수는 이미 있으므로 이 모양을 다시 cycle을 하든 그걸 다시 above로 묶던 내부적으로 이를 그리는 방법은 신경 쓸 필요가 없다.
이제 이 함수를 가지고 plt-racket의 홈페이지 대문 예제 중 하나인 시어핀스키의 삼각형을 그려보자.
먼저 삼각형을 그리기!하는 함수를 만들어보면,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(defmethod 그리기!
::triangle
[left top 모양]
(let [{:keys [offset-left
offset-top
width
height
stroke
fill]} 모양]
(when stroke
(apply quil.core/stroke stroke))
(when (:fill 모양)
(apply quil.core/fill fill))
(let [left (+ left offset-left)
top (+ top offset-top)]
(triangle (+ left (quot width 2)) top
left (+ top height)
(+ left width) (+ top height)))))
이렇게 하면 사실 정삼각형을 그리기 힘들다 (가로 세로만 입력가능하니까..)
또, 시어핀스키의 삼각형을 만드는 함수를 만들어보자. above, beside 면 간단히 만들 수 있다.
자바스크립트로 OOP 스타일의 코드를 만드는것은 다양한 방법이 있고, 또 안 건드는것도 좋은 방법이기는 한데, 여기서는 한 가지 방법으로 작성하여 데이터그리드 라는 것을 만들어 보기로 하자. 자바스크립트의 이전 OOP 스타일의 각종 기법들은 ECMA 표준 버전이 올라감에 따라 사라질 운명이라 아예 시작을 class 문법으로 시작하는것도 좋아보이지만 일단 javascript 에 익숙하고 백엔드에 대한 경험이 없는 사람들이 class 구문 없이 OOP 스타일로 코드를 작성해보며 얻는 경험을 목적임.
Component 클래스의 작성
자, 어디서부터 시작해볼까? Animal - Cat 으로 이어지는 많이 알려진 OOP 스타일 코드의 예제부터 시작해 볼까? 그러지 말고 여기서는 약간 더 재미를 더해 유명한 프레임워크인 React 의 Component 클래스를 흉내내 보는것으로 한다. 물론 React 프레임워크를 쓰는게 훨씬 좋겠지만 배우는게 목적이므로.
1
2
3
functionComponent(state) {
this.state = state;
}
간단한 Component 클래스가 만들어졌다. 이제 이 컴포넌트를 상속받아 가장 그리기 쉬운 Cell 이라는 클래스를 만들어보자. 이 클래스는 최종적으로는 <td></td> 를 그리는 클래스이다.
1
2
3
4
5
6
7
functionCell(state) {
Component.call(this, state);
}
Cell.prototype = new Component();
Cell.prototype.render = function () {
return $('<td></td>')
};
이로써 컴포넌트를 상속받은 Cell 클래스가 만들어졌다. 이 클래스는 render 메서드가 호출될 때 jquery 오브젝트를 생성하여 돌려준다. 이번에는 이 클래스를 자식요소로 갖는 Row 클래스를 만들어보자.
1
2
3
4
5
6
7
8
9
10
functionRow(state) {
Component.call(this, state);
this.cells = _.map(state.items, function(item){
returnnew Cell(item);
});
}
Row.prototype = new Component();
Row.prototype.render = function () {
return $('<tr></tr>');
};
이로써 컴포넌트를 상속받은 Row 클래스가 만들어졌다. 이 클래스는 Cell 클래스와 마찬가지로 render 메서드가 호출될 때 jquery 오브젝트를 생성하여 돌려준다. Row 클래스와 Cell 클래스에 선언된 render 메서드를 잘 생각해보면 이러한 컴포넌트가 당연히 가져야될 메서드임이 자명하다. 따라서 이 메서드는 Component 클래스에서 쓰일 수 있도록 Component 클래스에서도 render 메서드를 구현해 놓을 수 있다.
1
2
3
Component.prototype.render = function () {
throw"렌더 메서드를 구현해 주세요."
};
이 Row 생성자에는 주어진 state 변수로부터 Cell의 인스턴스를 얻어내어 프로퍼티로 가지게끔 하였다. 이번에는 이 두 클래스의 render 함수를 호출하여 지정된 부모 jquery 오브젝트에 얹히는 (mount) 메서드를 구현해 보자. 이 메서드가 호출되기 전에 컴포넌트는 미리 render에 의해 생성되는 jquery 오브젝트가 얹혀질 부모 오브젝트를 가지고 있어야 한다.
1
2
3
4
5
6
7
/**
* 이 컴포넌트가 마운트될 부모 DOM 요소를 지정한다.
* @param $parentNode
*/
Component.prototype.setParentNode = function ($parentNode) {
this.$parentNode = $parentNode;
};
이러면 Component 를 상속받은 Cell 과 Row 가 모두 setParentNode 라는 메서드를 가지게 된다. 이어서 setParentNode 를 이용해서 mount 메서드를 구현해보면,
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 컴포넌트를 마운트한다 (위치시킨다).
*/
Row.prototype.mount = function () {
if (!this.$parentNode) {
throw'부모 노드가 정의되지 않아서 마운트할 수 없습니다.'
}
var $el = this.render();
var $parentNode = this.$parentNode;
this.$el = $el;
$parentNode.append($el);
};
여기까지 작성하고 새로 Row 클래스의 인스턴스를 만들어서 mount 해 보자. 크롬 개발자도구 콘솔을 열어서 다음과 같이 입력하면,
1
2
3
var row = new Row({items: [1,2,3,4]});
row.setParentNode($(document.body));
row.mount();
document.body 영역에 tr 태그가 삽입된 것이 확인된다. 여기에 Row의 자식요소인 cells 는 어떻게 마운트를 할까? 우리가 이미 구현한 mount 메서드에서 cells 를 순회하며 mount 메서드를 호출하면 될 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 컴포넌트를 마운트한다 (위치시킨다).
*/
Component.prototype.mount = function () {
if (!this.$parentNode) {
throw'부모 노드가 정의되지 않아서 마운트할 수 없습니다.'
}
// 이 순간 렌더링.
var $el = this.render();
var $parentNode = this.$parentNode;
this.$el = $el;
$parentNode.append($el);
// 자식컴포넌트들도 마운트한다.
_.each(this.children, function (child) {
// 마운트된 이 컴포넌트의 DOM 요소를 자식 컴포넌트가 렌더링 될 부모 DOM 요소로 지정한다.
child.setParentNode($el);
child.mount();
});
};
반대 메서드인 unmount 메서드도 구현할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 컴포넌트를 언마운트한다 (제거한다).
*/
Component.prototype.unmount = function () {
// 자식 컴포넌트들 부터 언마운트 한다.
_.each(this.children, function (child) {
child.unmount();
});
this.children = undefined;
if(this.$el) {
this.$el.remove(); // 날리고
this.$el = undefined; // 없애고
}
};
그런데 이상의 코드에서 this.children 이라는 자식요소도 반복되는 요소이므로 Component 의 메서드로 정의될 필요가 있어보인다.
1
2
3
4
5
6
7
8
/**
* 자식 요소들을 state 로 부터 다시 생성한다.
* @param state
* @returns {*}
*/
Component.prototype.refreshChildren = function (state) {
return [];
};
최종적으로 mount 메서드는 다음과 같이 변경하면 state 에 따라 자식요소를 업데이트하고 다시 마운트하게 된다.
개발자라면 AWS 계정이 없는것이 이상할정도로 거의 무제한 공짜(1년간 마이크로 인스턴스 무료. 그러나 메일계정만 있으면 계속 생성가능) 이지만 그래도 아직 가입하지 않았다면 가입하는것이 좋다.
가입중 신용카드 정보를 입력하는 부분이 등장한다. 외화결제가 가능해야 하므로 준비해놓아야 한다.
가입하고 tokyo 리젼 콘솔로 접근해서 ec 인스턴스를 생성하고 pem 파일을 다운로드 받아 놓는다. 이때 free tier eligible을 선택해야 공짜가 됨. 공짜 조건은 micro 급 ec 인스턴스 한개이므로 다른 스토리지, rds, 로드밸런서 따위를 선택해서도 안된다.
빙 둘러 말하기로 프로젝트 위키에다가 써 보았다. 물론 프로젝트 위키에다가 썼으므로 팀원만 보겠지 ㅎㅎ…
스크럼 이걸 왜 하나요?
원활한 개발을 방해하는 요소들은 셀 수 없이 많습니다. 하지만, 그런 방해 요소들을 적극적으로 해결하고자 하는 노력은 그에 비해 턱없이 부족합니다. 늦어지는 기획, 너무 잦은 회의, 본부장의 잦은 간섭, 협업 요청에 대한 동료의 무응답, 작업을 더디게하는 수많은 요소들은 오늘도 우리 주위에 산재해 있습니다. 하지만, 이런 것들에 문제를 제기하면 왠지 분위기가 어색해질 것 같고 눈치가 보여서 포기하고 맙니다. 결국 시간이 갈수록 즐겁게 개발 할 수 있는 환경은 사라져갑니다. 스크럼은 목표를 달성하는데 방해가 되는 요소들을 그 즉시 해결해야 한다고 이야기합니다.