오늘도 어제에 이어서 바닐라 자바스크립트로(타입스크립트로) 컴포넌트를 만드는 연습을 했다. 그래도 어제에 비해서 조금 더 유연하게 설계한 것 같아서 기쁘다. 아직 클래스 상속에 대하여 개념을 잘 몰라서 체계적인 컴포넌트를 만드는 것이 어렵다. 일단 상속에 대한 개념을 조금 익혀둔 다음, 이를 이용하여 구현하는 연습을 꾸준히 해나가야겠다.
객체 지향 프로그래밍의 가장 큰 특징은 다형성, 캡슐화, 상속이다. 이 중 상속은 부모 클래스로부터 특징을 내려받아 자식클래스에서 사용할 수 있다는 특징을 만족하며, 메서드를 오버라이딩 할 수 있으며, 자식은 부모 클래스의 메서드 + 자식 클래스에서 고유 메서드를 구현하여 확장이 가능하다. 부모 클래스가 사용되던 곳에 자식 클래스로 대체하더라도, 완벽하게 동작할 수 있어야 한다.
class Parent {
constructor(value) {
this.value = value;
this.setup();
}
setup() {
}
}
이렇게 부모 클래스가 있을 때, 자식클래스는 extends
예약어를 통하여 부모 클래스를 상속한다.
class Child extends Parent {
constructor(value) {
super(value);
this.childrenValue = value;
}
}
Child
클래스는 Parent
클래스를 상속받는다. 자식클래스는 항상 super()
메서드를 통하여 부모의 constructor
(생성자 함수)를 먼저 실행한다.
Parent
클래스의 constructor
를 보면, setup()
메서드가 초기 생성 시 실행되도록 구현이 되어 있다. 만약 Child
클래스에서 setup()
메서드가 따로 구현되어 있지 않다면, 자동으로 Parent
클래스의 setup()
메서드를 실행하고, 만약 Child
클래스에 setup()
메서드를 명시적으로 구현하였다면, Child
클래스에서는 Child
클래스에서 구현한 setup()
메서드를 이용하여 constructor
을 실행한다. 이를 메서드 오버라이딩이라고 한다.
생성자 오버라이딩도 있다. Child
클래스에서 constructor()
를 이용하여 새로운 value
값을 지정해주었다. 만약 어떠한 새로운 인자들을 추가할 필요가 없다면 constructor()
를 작성하지 않아도 된다. 만약 자식 클래스에서 새 생성자들을 추가하고 싶다면, super()
를 이용하여 부모 클래스의 constructor()
에 인자를 넘겨 주어야 한다.
어제는 버튼 클래스를 상속하는 토글 버튼을 만들었는데, 오늘은 좀 더 유연한 컴포넌트를 만들기 위해 버튼 클래스의 부모 클래스인 Component
클래스를 만들고 이를 상속하는 것을 만들어 보기로 한다.
먼저 모든 컴포넌트에 대해 공통적으로 구현해야 하는 것을 생각해본다.
1. $target
에 컴포넌트 붙이기
2. setState
로 state
바꾸기
3. render
로 브라우저에 렌더하기
class Component<S> {
$target: Element | null;
state?: S;
constructor({ $target }: { $target: Element | null }) {
this.$target = $target;
}
setState(nextState: S) {
this.state = { ...nextState };
this.render();
}
render() {}
}
export { Component };
다음과 같이 컴포넌트 클래스를 구현하였다.
1. state
는 필요한 컴포넌트도 있고, 필요 없는 컴포넌트도 있기 때문에 제네릭으로 구성하였고, 처음에는 비어있는 상태로 정의하여 하위 클래스에서 필요할 경우 state
를 구현하도록 하였다. constructor
에서부터 $target
을 정의하여 하위 클래스에서 필요할 경우 appendChild
할 수 있도록 했다.
2. render
메서드는 constructor
내부에 넣어도 가능하지만, 현재 appendChild
방식으로 컴포넌트를 붙일지, template
방식(innerHTML
) 방식으로 붙일지 확실히 결정하지 않았기 때문에 빼놓았다.
3. setState
메서드는 state
를 명시했든, 명시하지 않았든 동일하게 동작하므로 가장 상위 컴포넌트에 작성 후, 하위 클래스에서는 메서드를 따로 구현하지 않도록 만들었다.
잘 작동하는지 일단 Component
를 상속받는 Button
클래스를 만들어보자.
class Button<T> extends Component<T> {
state: T;
button: HTMLElement;
constructor({ $target, initialState }: ButtonProps<T>) {
super({ $target });
this.state = initialState;
this.button = document.createElement("button");
this.$target?.appendChild(this.button);
this.render();
this.setEvent();
}
setEvent(): void {}
}
Button
클래스의 constructor
는initialState
를 추가로 입력받아 state
를 정의하한다.button
요소를 만들어 타겟에 붙인다.render
함수를 실행하여 버튼을 렌더링한다.보면 target
을 등록하지 않았는데도 this.target
을 다룰 수 있는데, 부모 클래스에서 이를 미리 정의하였기 때문이다.
render
메서드, setEvent
역시 Button
에 따로 등록하지 않았지만, 부모 클래스에서 메서드를 정의하였으므로 사용에 문제가 없다. 아무 내용도 없지만, Button
클래스를 상속받는 ToggleButton
에서 이벤트와 렌더 함수를 정의하도록 하겠다.
class ToggleButton extends Button<ToggleButtonState> {
setEvent() {
this.button.addEventListener("click", (e) => {
console.log(e.target);
this.setState({
...this.state,
toggled: !this.state.toggled,
clickCount: this.state.clickCount + 1,
});
});
}
render() {
this.button.textContent = `${this.state.clickCount}번 클릭했습니다.`;
this.button.className = this.state.className;
}
}
constructor
를 정의하지 않았다. 정의하지 않았더라도 ToggleButton
에는 두 가지 인자가 필요하다.render
함수를 정의하였다.state
를 업데이트 하는 함수를 간단하게 만들었다. setState
를 현재 클래스에서 따로 정의하지 않았지만, Component
클래스에서 이미 정의하였으므로 사용에 문제가 없다.index파일을 만들고 한번 테스트 해보았다.
import { ToggleButton } from "./components/Button.js";
const $app = document.querySelector("#app");
new ToggleButton({
$target: $app,
initialState: {
clickCount: 0,
toggled: false,
innerText: "0번 클릭했습니다.",
className: "1",
},
});
new ToggleButton({
$target: $app,
initialState: {
clickCount: 0,
toggled: false,
innerText: "0번 클릭했습니다.",
className: "2",
},
});
new ToggleButton({
$target: $app,
initialState: {
clickCount: 0,
toggled: false,
innerText: "0번 클릭했습니다.",
className: "3",
},
});
new ToggleButton({
$target: $app,
initialState: {
clickCount: 0,
toggled: false,
innerText: "0번 클릭했습니다.",
className: "4",
},
});
new ToggleButton({
$target: $app,
initialState: {
clickCount: 0,
toggled: false,
innerText: "0번 클릭했습니다.",
className: "5",
},
});
야호! 의도한대로 잘 동작한다.
아직 다듬어야 할 부분이 많지만, 그래도 의도한 대로는 동작하는 것 같아 기쁘다. 클래스의 상속과 유연하게 만드는 과정은 이론으로만 알고 있었기 때문에 실제로 코드를 쳐보면서 작성하는 것은 또 다른 느낌이었고, 매우 어려웠다... 연습할 때는 항상 코드를 치면서 연습을 하는 습관을 갖도록 하자.