출처: https://okky.kr/article/409329
추상화, 의존성 분리, 캡슐화 등등... 말은 참 쉽지만 실전에서 와닿기 힘들 수 있습니다.
자바스크립트로 [Button]을 눌렀을 때 [Text]에 버튼을 누른 횟수를 표시하는 코드를 모델링해보겠습니다. (객체를 설계해보겠습니다.)
1. 어설픈 모델링
class Text {
constructor(){
this.view = document.createElement('div');
this.view.textContent = "[0]";
document.body.append(this.view);
}
}
class Button {
constructor(){
this.view = document.createElement('button');
this.view.textContent = "INCREMENT";
document.body.append(this.view);
this.clicked = 0;
this.view.onclick = () => {
text.view.textContent = `[${this.clicked++}]`;
}
}
}
let text = new Text();
let button = new Button();
위 코드는 잘 작동하지만 Button
을 사용하기 위해서는 다음과 같은 의존성이 있습니다.
- 전역 스코프에
text
라는 객체가 있을 것 text
는view
라는 객체를 속성으로 가질 것text.view
는textContent
라는String
을 속성으로 가질 것
혹은
- 전역 스코프의
text
라는 객체가 있을 것 text
는 view라는HTMLElement
를 속성으로 가질 것
이 때문에
let text = new Text();
let button = new Button();
위 코드를
let button = new Button();
let text = new Text();
이렇게만 바꿔도 작동하지 않으며, 현실적으로 재사용하고 확장하기에 무리가 있습니다.
2. 추상화
- 전역 스코프에
text
라는 객체가 있을 것 text
는view
라는 객체를 속성으로 가질 것text.view
는textContent
라는String
을 속성으로 가질 것
이 세가지 항목을 최대한 줄여볼 수 있을까요?
전역 스코프 사용을 자제하고, 모델을 약간 추상화해보겠습니다.
class Text {
constructor(){
this.view = document.createElement('div');
document.body.append(this.view);
this.render(0);
}
render(number){
this.view.textContent = `[${number}]`;
}
}
class Button {
constructor(text){
this.view = document.createElement('button');
this.view.textContent = "INCREMENT";
document.body.append(this.view);
this.clicked = 0;
this.view.onclick = () => {
text.render(this.clicked++);
}
}
}
let text = new Text();
let button = new Button(text);
이런 방식으로 의존성이 줄어들 수 있겠습니다. 위 코드에서 Button
의 의존성은
- 생성자의 인자가
render(Number)
를 속성으로 갖는 객체 일 것
의존성을 줄이려다보니, Text
에 render
라는 메소드를 만들게 됐습니다.
화면에 보이는 결과는 같지만 의존성이 줄었을뿐만 아니라, Text
의 모습을 제어하는 코드 역시 자연스럽게 Text
에 캡슐화가 되었습니다.
// 의사 코드
interface Renderable {
void render(Number number);
}
class Button {
constructor(Renderable renderer){
// ...
}
}
JS가 정적 타입 언어라면, 위처럼 Text의 생성자에 좀 더 제약사항을 걸어서 코드의 안정성과 가독성을 높힐 수도 있겠습니다.
이렇게 Renderable
이라는 추상계층을 통해서 의존성을 줄일 수 있습니다.
3. 확장성
의존성이 단순하기 때문에 Button
의 역할을 확장하기가 수월합니다.
새로운 요구사항
Button
에Renderable
한 객체들을 여러개 연결하고 싶음
class Text {
// ...
}
class Balls {
constructor(){
this.view = document.createElement('div');
document.body.append(this.view);
}
render(number){
this.view.innerHTML = '';
for(let i=0; i<number; i++) {
let ball = document.createElement('div');
ball.style.background = 'orange';
ball.style.width = ball.style.height = '1em';
ball.style.borderRadius = '50%';
ball.style.display = 'inline-block';
this.view.appendChild(ball);
}
}
}
class Button {
constructor(renderers){
this.view = document.createElement('button');
this.view.textContent = "INCREMENT";
document.body.append(this.view);
this.clicked = 0;
this.view.onclick = () => {
this.clicked++;
renderers.forEach(renderer => renderer.render(this.clicked));
}
}
}
let text = new Text();
let balls = new Balls();
let button = new Button([text, balls]);
이렇게 해서 Button
은 여러 Renderable
과 느슨하게 결합 할 수 있습니다.
이제 Button
의 의존성은 (엄밀하게 말하면 의존성은 Renderable 인터페이스 뿐입니다)
- 생성자 인자의 타입은
Array<Renderable>
일것
4. 동적인 참조
그런데 아직도
let text = new Text();
let balls = new Balls();
let button = new Button([text, balls]);
를
let button = new Button([text, balls]);
let text = new Text();
let balls = new Balls();
로 바꿀 수는 없는 점이 아쉽습니다.
새로운 요구사항
Button
을 생성하는 시점이 아니라, 생성된 이후에도Button
에Renderable
들을 연결하고 싶음
class Text {
// ...
}
class Balls {
// ...
}
class Button {
constructor(renderers = []){ // 기본값을 할당하는 코드입니다.
this.view = document.createElement('button');
this.view.textContent = "INCREMENT";
document.body.append(this.view);
this.clicked = 0;
this.renderers = renderers;
this.view.onclick = () => {
this.clicked++;
this.renderers.forEach(renderer => renderer.render(this.clicked));
}
}
connect(renderer){
this.renderers.push(renderer);
}
}
let text = new Text();
let button = new Button([new Balls()]);
button.connect(text);
이제 Button
에 동적으로 Renderable
들을 연결 할 수 있습니다.
5. 의존성 없애기!
Button
이 Renderable
들의 render
코드를 호출하는 대신에,
이벤트 주도(감시자 패턴, 출판-구독 패턴..) 방식으로 Renderable
이라는 제약사항을 없앨 수도 있습니다.
새로운 요구사항
Balls, Text
을Renderable
에서 탈피시키며 의존성을 완전히 없애기
class Text {
constructor(){
this.view = document.createElement('div');
document.body.append(this.view);
this.number = 0;
this.count();
}
count(){
this.view.textContent = `[${this.number++}]`;
}
}
class Balls {
constructor(){
this.view = document.createElement('div');
document.body.append(this.view);
}
generate(){
this.view.innerHTML = '';
let number = Math.floor(Math.random()*10);
for(let i=0; i<number; i++) {
let ball = document.createElement('div');
ball.style.background = 'orange';
ball.style.width = ball.style.height = '1em';
ball.style.borderRadius = '50%';
ball.style.display = 'inline-block';
this.view.appendChild(ball);
}
}
}
class Button {
constructor(text){
this.view = document.createElement('button');
this.view.textContent = text;
document.body.append(this.view);
this.clickHandlers = [];
this.view.onclick = () => {
this.clicked++;
this.clickHandlers.forEach(handler => handler(this.clicked));
}
}
addClickHandler(handler) {
this.clickHandlers.push(handler);
}
}
let balls = new Balls();
let text = new Text();
// increment button
let button = new Button("INCREMENT");
button.addClickHandler(() => {
text.count();
});
// button for both
let button2 = new Button("CLICK ME");
button2.addClickHandler(() => {
balls.generate();
text.count();
});
이제 Button
은 인스턴스별로 원하는 hook(callback, handler, listener, observer 등..)을 등록하여 입맛대로 쓸 수 있습니다.
* 아시는 것처럼 DOM에는 당연히 native로 각종 이벤트 핸들러를 부착할 수 있습니다, JS를 예시로 들다보니 같은 작업을 중복한 꼴이 되었습니다만, 지금 예시로 든 GUI 뿐 아니라 그 어떤 코드에서도 의존성에서 탈피하기 위해서 같은 원리를 적용 할 수 있음을 말하고자 합니다.
맺음말.
의존성에서 벗어나야 좋은 코드라는데, 위 처럼 의존성이 하나도 없는게 최고군요!
- 의존성이 없으면 그만큼 확장성이 있지만, 원하는 대로 그 쓰임새를 강제 할수가 없습니다.
예를 들어서 현금
,부동산
등을 등록 할 수 있는 회계사
가 있다고 해봅시다.회계사
는 현금
, 부동산
과 현금으로평가할수있는
인터페이스로 느슨하게 연결되어 있습니다.
interface 현금으로평가할수있는 {
금액 현금으로평가();
}
이를 통해서 회계사
는 현금
이나 부동산
등을 현금으로 평가하여 다룰 수 있습니다. 또한 비트코인
이라는 새로운 객체에도 현금으로평가할수있는
이라는 명시적인 인터페이스만 구현해준다면 쉽게 회계사
와 연결 할 수 있습니다.
이렇게 적절한 의존성을 통해서 쓰임새의 방향을 어느 정도 강제하고, 확장의 가이드가 될 수 있습니다.
---
디자인 패턴을 공부해도 막상 실제 코드엔 적용하기가 쉽지 않습니다. 저는 코드의 구조에 그 합당한 이유가 있어야 한다는 생각을 합니다. 아무런 필요도 준비도 없이, 디자인패턴들을 되새김질하며 마치 공식처럼 적용하려고 하기보다는, 재사용, 확장성, 추상화의 키워드를 핵심 컨셉으로 "필요에 따라서 리팩토링" 해나가면 좋겠습니다.
'IT > Javascript|Jquery' 카테고리의 다른 글
자바스크립트와 이벤트 루프 (0) | 2018.02.01 |
---|---|
Scope의 이해 (0) | 2018.01.25 |
비밀번호검증 스크립트(연속,반복) (0) | 2018.01.22 |
통화형식 (3자리 콤마) 사용하기 예제 (0) | 2018.01.19 |
Jquery Input Name Array 값 가져오기 (1) | 2018.01.16 |