FrontEnd에서 Domain 성격의 State를 Model(DTO) 클래스로 관리하기
JavaScript에서 Runtime시에만 발생되는 오류에 대한 해결책으로 Compile시점에 오류를 미리 잡을 수 있도록 TypeScript 문법이 탄생했다.
FrontEnd 뷰 템플릿을 사용하며 여러 의문이 들었다.
FrontEnd Framework인 VueJS나 ReactJS Library 에서는 왜 DTO를 따로 쓰지 않는것일까?
TypeScript 뿐만 아니라 제대로 관리하려면 자바스크립트에서 최신으로 지원하는 클래스 문법도 사용해야 하는것 아닐까?
자바스크립트에서 신버전에 클래스 문법을 괜히 추가해서 제공해주는것이 아닐텐데?
진짜 제대로 관리할거면, 타입스크립트 타입 뿐만 아니라 도메인 성격을 띄우는 Object 타입의 State값들도 자바처럼 클래스로 관리 할 수 있도록 하는게 맞지않을까?
자바를 따라 객체를 관리하는것도, 재사용성 면에서는 장점이 있을거라 생각이 들었고 괜한 고집이 생겼다.
물론 FrontEnd는 BackEnd API, 디자인, 기획 등등 많은 범위에서 잦은 변경에 노출된다.
이 때문에 타입스크립트만을 사용하는것으로 타협을 한것은 아닐까 생각이 든다.
그래도 구현 해볼래!
Model로 관리할 DTO 클래스에 대한 타입 인터페이스를 선언해준다.
/**
* 도메인 성격이 강한 객체 타입 인터페이스 정의
* Person.interface.ts파일로 관리
*/
export interface Person {
name: string;
age: number;
getName(): string;
setName(value: string): void;
getAge(): number;
setAge(value: number): void;
}
인터페이스를 실제로 구현할 구현체 DTO 클래스를 선언한다.
멤버에 pravte 접근제한자를 적용한 뒤 getter setter를 선언함으로써 캡슐화를 적용한다
/**
* 도메인 성격이 강한 객체 클래스 정의
* PersonDTO.class.ts파일로 관리
*/
import { Person } from './Person.interface';
export class PersonDTO implements Person{
private _name: string; // _ prefix는 private 변수, 외부에서 접근하지 않음
private _age: number; // _ prefix는 private 변수, 외부에서 접근하지 않음
constructor(name: string = "", age: number = 0) {
this._name = name;
this._age = age;
}
getName(): string {
return this._name;
}
setName(value: string): void {
this._name = value;
}
getAge(): number {
return this._age;
}
setAge(value: number): void {
this._age = value;
}
}
React
리액트의 경우 state의 변경을 감지 하기 위해서는 `새로운 객체`를 만들어서 변경된 필드에 대한 초기화를 진행하고 state에 set을 해줘야한다.
DTO로 관리되는 PersonDTO 객체는 private 접근제한자를 통해 캡슐화 되어있기 때문에 필드에 직접 접근할 수 없다.
따라서 new 인스턴스로 새로운 인스턴스를 생성하고, setter getter를 통해 필드에 접근하여 값을 초기화 해 준다.
JSX에서는 getter를 통해 접근한다.
/**
* 컴포넌트
*/
import { Person } from './Person.interface';
import { PersonDTO } from './PersonDTO.class';
import { useState, useEffect } from 'react';
export default function App() {
const [person, setPerson] = useState<Person>(new PersonDTO("이름", 30));
const onChangeName = (e: React.ChangeEvent<HTMLInputElement>) => {
const updatePerson = new PersonDTO(); // 새로운 참조 인스턴스 객체 생성
updatePerson.setName(e.target.value); // Name만 직접 수정
updatePerson.setAge(person.getAge()); // Age는 기존값 그대로 세팅
setPerson(updatePerson); // 새로운 인스턴스객체로 초기화
};
const onChangeAge = (e: React.ChangeEvent<HTMLInputElement>) => {
const updatePerson = new PersonDTO(); // 새로운 참조 인스턴스 객체 생성
updatePerson.setAge(e.target.value); // Age만 직접 수정
updatePerson.setName(person.getName()); // Name은 기존값 그대로 세팅
setPerson(updatePerson); // 새로운 인스턴스객체로 초기화
};
useEffect(()=> {
console.log("person 객체 변경되었습니다.")
}, [person])
return (
<div>
<div><p>이름: { person.getName() }</p></div>
<div><p>나이: { person.getAge() }</p></div>
<div>이름 입력: <input type="text" value={person.getName()} onChange={onChangeName}/></div>
<div>나이 입력: <input type="text" value={person.getAge()} onChange={onChangeAge}/></div>
</div>
)
}
REACT 전체 테스트용 코드
import { useState, useEffect } from 'react';
interface Person {
name: string;
age: number;
getName(): string;
setName(value: string): void;
getAge(): number;
setAge(value: number): void;
}
class PersonDTO implements Person{
private _name: string; // _ prefix는 private 변수, 외부에서 접근하지 않음
private _age: number; // _ prefix는 private 변수, 외부에서 접근하지 않음
constructor(name: string = "", age: number = 0) {
this._name = name;
this._age = age;
}
getName(): string {
return this._name;
}
setName(value: string): void {
this._name = value;
}
getAge(): number {
return this._age;
}
setAge(value: number): void {
this._age = value;
}
}
export default function App() {
const [person, setPerson] = useState<Person>(new PersonDTO("이름", 30));
const onChangeName = (e: React.ChangeEvent<HTMLInputElement>) => {
const updatePerson = new PersonDTO(); // 새로운 참조 인스턴스 객체 생성
updatePerson.setName(e.target.value); // Name만 직접 수정
updatePerson.setAge(person.getAge()); // Age는 기존값 그대로 세팅
setPerson(updatePerson); // 새로운 인스턴스객체로 초기화
};
const onChangeAge = (e: React.ChangeEvent<HTMLInputElement>) => {
const updatePerson = new PersonDTO(); // 새로운 참조 인스턴스 객체 생성
updatePerson.setAge(e.target.value); // Age만 직접 수정
updatePerson.setName(person.getName()); // Name은 기존값 그대로 세팅
setPerson(updatePerson); // 새로운 인스턴스객체로 초기화
};
useEffect(()=> {
console.log("person 객체 변경되었습니다.")
}, [person])
return (
<div>
<div><p>이름: { person.getName() }</p></div>
<div><p>나이: { person.getAge() }</p></div>
<div>이름 입력: <input type="text" value={person.getName()} onChange={onChangeName}/></div>
<div>나이 입력: <input type="text" value={person.getAge()} onChange={onChangeAge}/></div>
</div>
)
}
Vue2 - OptionsAPI
vue의 경우에도 private 접근레벨이기 때문에, template영역에서 직접 접근할 수 없다.
react와 마찬가지로 해당 DTO 클래스의 getter로 접근하여 보간법을 통해 값을 바인딩해야한다.
그러나 v-model에도 수정하기위한 필드를 getter로 접근하여 수정하는것은 불가능하다.
(@Input과 setter를 통해 v-model 대신 디테일하게 코드를 구성할경우에는 가능)
따라서 v-model까지 고려한다면 computed를 통해 get과 set을 구현함으로써 객체를 직접 접근하지 않고,
한번 wrapping함으로써 객체의 값도 초기화 하고, 실제 출력하는 값은 computed를 통해 접근하는 원리로 사용하면 문제가 해결된다.
<template>
<div>
<p>이름: {{ personName }}</p> <!-- computed의 get으로 접근 -->
<p>나이: {{ personAge }}</p> <!-- computed의 get으로 접근 -->
<div>이름 입력: <input type="text" v-model="personName"/></div> <!-- computed의 set으로 접근 -->
<div>나이 입력: <input type="text" v-model="personAge"/></div> <!-- computed의 set으로 접근 -->
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { PersonDTO } from './PersonDTO.class';
import { Person } from './Person.interface';
export default {
data() {
return {
// PersonDTO 객체로 초기화
person: new PersonDTO("John", 30) as Person,
};
},
computed: {
personName: {
get(): string {
return this.person.getName();
},
set(value: string) {
this.person.setName(value);
},
},
personAge: {
get(): number {
return this.person.getAge();
},
set(value: number) {
this.person.setAge(value);
},
},
},
};
</script>
전체 테스트용 코드
<template>
<div>
<p>이름: {{ personName }}</p> <!-- computed의 get으로 접근 -->
<p>나이: {{ personAge }}</p> <!-- computed의 get으로 접근 -->
<div>이름 입력: <input type="text" v-model="personName" /></div> <!-- computed의 set으로 접근 -->
<div>나이 입력: <input type="text" v-model="personAge" /></div> <!-- computed의 set으로 접근 -->
</div>
</template>
<script lang="ts">
import Vue from "vue";
interface Person {
name: string;
age: number;
getName(): string;
setName(value: string): void;
getAge(): number;
setAge(value: number): void;
}
class PersonDTO implements Person {
private _name: string; // _ prefix는 private 변수, 외부에서 접근하지 않음
private _age: number; // _ prefix는 private 변수, 외부에서 접근하지 않음
constructor(name: string = "", age: number = 0) {
this._name = name;
this._age = age;
}
getName(): string {
return this._name;
}
setName(value: string): void {
this._name = value;
}
getAge(): number {
return this._age;
}
setAge(value: number): void {
this._age = value;
}
}
// Vue.extend 방식을 사용하여 컴포넌트 정의
export default {
data() {
return {
// PersonDTO 객체로 초기화
person: new PersonDTO("John", 30) as Person,
};
},
computed: {
personName: {
get(): string {
return this.person.getName();
},
set(value: string) {
this.person.setName(value);
},
},
personAge: {
get(): number {
return this.person.getAge();
},
set(value: number) {
this.person.setAge(value);
},
},
},
};
</script>
Vue3 - CompositionAPI (SETUP)
vue3도 vue2와 똑같이 computed를 사용해야 한다.
<template>
<div>
<p>이름: {{ personName }}</p> <!-- computed의 get으로 접근 -->
<p>나이: {{ personAge }}</p> <!-- computed의 get으로 접근 -->
<div>이름 입력: <input type="text" v-model="personName"/></div> <!-- computed의 set으로 접근 -->
<div>나이 입력: <input type="text" v-model="personAge"/></div> <!-- computed의 set으로 접근 -->
</div>
</template>
<script setup lang="ts">
import { reactive, computed } from "vue";
import { Person } from "./Person.interface";
import { PersonDTO } from "./PersonDTO.class";
const person = reactive<Person>(new PersonDTO("John", 30));
// computed를 사용하여 캡슐화된 값에 접근
const personName = computed({
get: () => {
return person.getName()
},
set: (value: string) => {
person.setName(value)
}
});
const personAge = computed({
get: () => {
return person.getAge()
},
set: (value: number) => {
person.setAge(value)
}
});
</script>
전체 테스트용 코드
<template>
<div>
<p>이름: {{ personName }}</p> <!-- computed의 get으로 접근 -->
<p>나이: {{ personAge }}</p> <!-- computed의 get으로 접근 -->
<div>이름 입력: <input type="text" v-model="personName"/></div> <!-- computed의 set으로 접근 -->
<div>나이 입력: <input type="text" v-model="personAge"/></div> <!-- computed의 set으로 접근 -->
</div>
</template>
<script setup lang="ts">
import { reactive, computed } from "vue";
interface Person {
name: string;
age: number;
getName(): string;
setName(value: string): void;
getAge(): number;
setAge(value: number): void;
}
class PersonDTO implements Person{
private _name: string; // _ prefix는 private 변수, 외부에서 접근하지 않음
private _age: number; // _ prefix는 private 변수, 외부에서 접근하지 않음
constructor(name: string = "", age: number = 0) {
this._name = name;
this._age = age;
}
getName(): string {
return this._name;
}
setName(value: string): void {
this._name = value;
}
getAge(): number {
return this._age;
}
setAge(value: number): void {
this._age = value;
}
}
const person = reactive<Person>(new PersonDTO("John", 30));
// computed를 사용하여 캡슐화된 값에 접근
const personName = computed({
get: (): string => {
return person.getName()
},
set: (value: string) => {
person.setName(value)
}
});
const personAge = computed({
get: () => {
return person.getAge()
},
set: (value: number) => {
person.setAge(value)
}
});
</script>
직접 코드로 구현해보고 느낀점은, 이러하다.
각 컴포넌트의 State 값들은 UI중심적이기에 DTO처럼 모델이 강제된다면 DTO와 UI 간의 업데이트가 까다롭다는것을 느꼈다.
캡슐화라는 필드 접근 제약 규칙으로 인해
`React`에서는 새 객체를 만들어서 setter를 통해 객체를 초기화 해주고 추가로 state에 대한 set을 해줘야하며,
`Vue`에서는 Template 영역의 input 태그 v-model 디렉티브에 getter 함수를 바인딩 해준다는 것이 매커니즘적 측면에서 말이 안되는 행위고, 실제로 변경도 이루어지지 않기에 Computed로 Wrapping해줘야만 했다.
그래서 한번 더 생각해본것은 API 호출시 Request용 파라미터 객체나 Response용 응답 객체를 DTO 클래스로 사용하는건 어떨까? 였다.
하지만 이 역시도 리터럴 객체 선언 후 매개변수 타입과 반환 타입에 타입스크립트의 interface나 type을 지정하는것이 클래스로 강제화 해서 관리하는것과 다를것 없지 않나? 하는 생각이 들었다.
이러한 이유 때문에 FrontEnd에서는 굳이 클래스로 까지 관리하지 않고 타입스크립트의 타입으로 손쉽게 객체를 관리하는것이 아닐까 하는 결론을 내렸다.
'Typescript' 카테고리의 다른 글
Declaration Files & JSDoc (0) | 2023.11.28 |
---|---|
TypeScript 설치 및 세팅 (Terminal 명령어 .json파일 등) (1) | 2023.11.27 |