(loop (print (eval (read))))

;;닭집을 차리기 위한 여정

자바스크립트로 옛날문법으로 OOP 해보기

자바스크립트로 OOP 하기

자바스크립트로 OOP 스타일의 코드를 만드는것은 다양한 방법이 있고, 또 안 건드는것도 좋은 방법이기는 한데,
여기서는 한 가지 방법으로 작성하여 데이터그리드 라는 것을 만들어 보기로 하자.
자바스크립트의 이전 OOP 스타일의 각종 기법들은 ECMA 표준 버전이 올라감에 따라 사라질 운명이라 아예 시작을 class 문법으로 시작하는것도 좋아보이지만
일단 javascript 에 익숙하고 백엔드에 대한 경험이 없는 사람들이 class 구문 없이 OOP 스타일로 코드를 작성해보며 얻는 경험을 목적임.

Component 클래스의 작성

자, 어디서부터 시작해볼까? Animal - Cat 으로 이어지는 많이 알려진 OOP 스타일 코드의 예제부터 시작해 볼까?
그러지 말고 여기서는 약간 더 재미를 더해 유명한 프레임워크인 React 의 Component 클래스를 흉내내 보는것으로 한다. 물론 React 프레임워크를 쓰는게 훨씬 좋겠지만 배우는게 목적이므로.

1
2
3
function Component(state) {
this.state = state;
}

간단한 Component 클래스가 만들어졌다. 이제 이 컴포넌트를 상속받아 가장 그리기 쉬운 Cell 이라는 클래스를 만들어보자. 이 클래스는 최종적으로는 <td></td> 를 그리는 클래스이다.

1
2
3
4
5
6
7
function Cell(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
function Row(state) {
Component.call(this, state);
this.cells = _.map(state.items, function(item){
return new 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 를 상속받은 CellRow 가 모두 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 에 따라 자식요소를 업데이트하고 다시 마운트하게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 컴포넌트를 마운트한다 (위치시킨다).
*/
Component.prototype.mount = function () {
if (!this.$parentNode) {
throw '부모 노드가 정의되지 않아서 마운트할 수 없습니다.'
}
this.children = this.refreshChildren(this.state); // 여기
// 이 순간 렌더링.
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();
});
};

이제 Row 클래스에 refreshChildren 메서드를 오버라이드하자.

1
2
3
4
5
6
7
8
9
10
Row.prototype.refreshChildren = function (state) {
return _.map(this.state.columns, function (column) {
return new Cell({
key: column.key,
value: this.getCellValueByKeys(column.key, state.rowData),
rowData: this.state.rowData,
labelFunction: column.labelFunction
});
}, this);
};

그리고 Row 클래스의 생성자에서는 자식요소를 빼버리자.

1
2
3
4
5
6
7
8
/**
* 데이터그리드의 행.
* @param state
* @constructor
*/
function Row(state) {
Component.call(this, state);
}

급귀찮다…..