객체의 런타임 체크가 가능하기 때문이다.
이를테면
/* class의 세계 */
class Person {
fName: string;
lName: string;
constructor({ fName, lName }: Person) {
this.fName = fName;
this.lName = lName;
}
}
const me = new Person({
fName: 'sungchun',
lName: 'park',
});
이 있다고 한다면, 외부로부터(API로부터, 혹은 메소드 호출로부터) 주입된 변수가 Person 인스턴스인지 판별할 때 방식은 아래와 같이 짧다.
/* class의 세계 */
const isPerson = me instanceof Person;
if (isPerson) {
// ...
}
if 구문으로 type guard를 설치하면 이제 이 다음부터는 me가 Person이 된다. (*tsc가 그렇게 알도록 하는 게 중요하다.)
만약 Person이 type이라 한다면, type, interface 같은 것들은 모두 Javascript 런타임에 소거되기 때문에 위와 같이 쓸 수 없다.
/* type의 세계 */
type Person = {
fName: string,
lName: string,
};
const me = {
fName: 'sungchun',
lName: 'park',
};
1번 안, 간단한 방법.
/* type의 세계 */
const isPerson = Boolean(
typeof me === 'object' &&
me && // null도 object이기 때문이다.
'fName' in me &&
'lName' in me &&
typeof me.fName === 'string' &&
typeof me.fName === 'string'
);
써놓고 보니 전혀 간단하지도 않고 아무도 이렇게 살기는 싫을 것이다. 무엇보다 isPerson을 평가하고 난 다음에도 me는 여전히 unknown. 그렇다면,
2번 안으로 가보자. type predicate를 사용하는 방식이다.
/* 아직도 type의 세계 */
const isPerson = (target: unknown): target is Person => Boolean(
typeof person === 'object' &&
person &&
'fName' in me &&
'lName' in me &&
typeof me.fName === 'string' &&
typeof me.fName === 'string'
);
(마음에 들지 않는다는 것을 안다.)
주로 다루는 모델들에 대한 class 선언이 되지 않은 어플리케이션 코드를 인수하여 유지보수를 할 때 위와 같은 공략이 필요하다. 함수 바디 내부는 여전히 지저분할 수 있지만, 적어도 판정식을 캡슐화할 수 있다. 적어도 테스트는 할 수 있다. 그러나,
리팩터링을 포함한 이슈 처리와, 리팩터링이 없는 이슈 처리를 여럿 지나온 지난 시간들을 회고해보건데, class 작성부터 하고 감이 가장 빠르다고 생각한다. 보통 가장 빠른 길이 안정적일 수는 없는 법인데, 이 길은 안정적이기까지 하다.
class를 선언하면 어플리케이션 개발 플로우 중 "modeling"이라는 작업 개념의 관점에서 데이터를 다루게 된다. 메소드가 있고, getter/setter가 있으며 접근 한정자를(Javascript 런타임에도!) 사용할 수 있기 때문이기도 하다. 내가 다루는 어플리케이션을 얼만큼 자르고 감추고 격리해야 할 지에 대해서도 생각할 겨를이 생긴다. 이제 프론트엔드 분야라기엔 관심 영역이 더 넓어지므로 여기까지만 쓰자.
런타임에도 유지되는 불변 프로퍼티의 선언도 가능하다. 제일 매력적이라고 생각하는 부분인데,
class Person {
#fName: string;
#lName: string;
constructor({ fName, lName }: Person) {
this.#fName = fName;
this.#lName = lName;
}
get fName() {
return this.#fName;
}
get lName() {
return this.#lName;
}
}
계산된 값으로서의 getter를 작성할 수도 있는 건, 사실 리터럴 객체도 완전 가능하기 때문에 class만의 이점은 아니다.
// ...
get name() {
return [this.#fName, this.#lName].join(' ');
}
// ...
class를 선언하는 일은 아무것도 아니다. 일단 써놓고 보자, 나중에 다시 합치거나 없애도 늦지 않다. 그런 경우가 잘 없었다.
단점은 무엇일까? 있을까?
기존 코드, 그러니까 데이터를 fetch하는 영역 및 메소드 호출부 같은 데에서 이제 이 class 인스턴싱 구문을 모두 추가해주어야 한다는 것이다.
이 단점 때문에 나는 많은 이슈 처리를, 그냥 user-defined type guard 함수를 만들어서 대응했던 것 같다. 후회를 하는 지점이다.