티스토리 뷰
Form을 작성하면서, FormData와 서버 도메인 객체를 분리하는 것은 매우 중요합니다. 누구에게는 당연한 이야기이지만, 개발을 하면서 Domain Object를 그대로 사용하여 Form 안정성 및 확장성을 저해하는 경우를 종종 접해서 이 글을 작성합니다. 이번 글에서는 FormData와 서버 도메인 객체를 분리해야 하는 이유와 그로 인해 얻을 수 있는 다양한 장점을 설명하겠습니다.
1. 서버 도메인 객체란?
서버 도메인 객체(Server Domain Object)는 서버와 클라이언트 간에 주고받는 데이터 구조입니다. 보통 서버에서 정의된 데이터 모델을 따르며, 데이터베이스와 직접 상호작용하는 객체입니다.
// 서버 도메인 객체 예시
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: string;
updatedAt: string;
}
2. UI와 API 로직 분리의 필요성
코드의 가독성과 유지보수성 향상
UI 로직과 API 로직을 분리하면 각각의 역할이 명확해집니다. UI 로직은 사용자 인터페이스의 상태와 동작에 집중하고, API 로직은 서버와의 데이터 통신에 전념하게 됩니다. 이렇게 역할이 분리되면 코드가 더 명확해지고, 각 부분을 독립적으로 유지보수할 수 있어 개발 효율성이 크게 향상됩니다.
FormData = UI 로직 데이터
FormData는 사용자 입력을 처리하기 위한 UI 상태입니다. 따라서 UI와 밀접하게 연관된 상태를 관리해야 합니다.
Domain Object = API 로직 데이터
서버 도메인 객체는 API 통신에만 집중해야 합니다. 이렇게 분리하면 UI와 API 간의 명확한 경계를 설정할 수 있습니다.
3. FormData와 서버 도메인 객체 분리의 중요성
메모리 낭비 방지
서버 도메인 객체를 그대로 FormData에 사용하면 메모리 낭비가 발생할 수 있습니다. 서버 도메인 객체는 많은 데이터를 포함할 수 있으며, 이러한 데이터를 FormData에 모두 담으면 메모리 사용량이 불필요하게 증가하게 됩니다. 이는 특히 서버 도메인 객체가 클 때, onChange에서 오버헤드를 발생시킬 수 있습니다.
특정 필드의 조합 문제
서버 도메인 객체의 특정 필드는 여러 입력 필드의 조합으로 만들어질 수 있습니다. 예를 들어, 사용자의 전체 이름은 이름과 성을 조합하여 생성될 수 있습니다. 이러한 조합은 클라이언트에서만 유효하며, 서버에서는 조합된 최종 데이터만 필요할 수 있습니다. 따라서, FormData는 서버 도메인 객체와 다를 가능성이 큽니다.
수정되지 않는 필드의 접근 문제
서버 도메인 객체를 FormData에 그대로 사용하면, 수정이 필요 없는 데이터까지 FormData에 포함됩니다. 이는 개발자가 실수로 불필요한 필드를 수정하거나 접근하게 되는 상황을 초래할 수 있습니다. 예를 들어, 비밀번호와 같은 중요한 정보가 포함될 경우, 실수로 이러한 필드가 변경되거나 노출될 위험이 있습니다. 변경된 데이터가 서버까지 전달된다면 의도치 않은 버그가 발생할 수 있습니다.
타입 안정성 확보
FormData와 서버 도메인 객체를 분리하면, 각 데이터 구조의 타입을 명확하게 정의할 수 있습니다. 이는 타입스크립트와 같은 정적 타입 언어에서 특히 중요합니다. UI 상태와 서버 도메인 객체의 타입을 분리하면, 타입스크립트 컴파일러가 각 데이터 구조의 타입을 정확히 추론하고 검증할 수 있어, 타입 오류를 방지하고 안정성을 높일 수 있습니다.
우연히 domain객체의 모든 필드가 수정되고, 필드 조합을 하는 경우도 없어서 FormData의 구조와 Domain Object가 같아질 수 있습니다. 하지만 이는 우연의 일치일 뿐이며, 그렇다고 해서 Domain Object를 그대로 FomData로 사용하면 기획 변경에 유연하게 대응하지 못합니다.
4. 서버 도메인 객체를 FormData에 그대로 사용했을 때의 문제점 예시
import React, { useState } from 'react';
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: string;
updatedAt: string;
}
interface UserUpdateFormProps {
user: User;
}
const UserUpdateForm: React.FC<UserUpdateFormProps> = ({ user }) => {
//XXX: 실제 수정 되는 데이터는 name과 email뿐이나 그 외 데이터도 formData에 포함되어 있음. 접근 및 수정 가능.
const [formData, setFormData] = useState<User>(user);
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
await fetch('/api/updateUser', {
method: 'POST',
body: {
...formData, // XXX: 불필요한 데이터도 접근가능. password, createAt등...
},
});
};
return (
<form onSubmit={handleSubmit}>
<input type="text" name="name" value={formData.name} readOnly /> {/* XXX: name 필드가 firstname과 lastname으로 구성된 경우 조합하기 위해 별도 상태 필요함 */}
<input type="email" name="email" value={formData.email} readOnly />
<button type="submit">Update</button>
</form>
);
};
- 메모리 낭비: 폼 로직과는 관계없는 데이터가 formData에 포함되어있음.
- 특정 필드 조합의 어려움: 서버 도메인 객체와 다른 데이터 구조로 인해 발생하는 문제. 예를 들어, name 필드가 firstname과 lastname으로 나뉘어 있을 때, 서버 도메인 객체와 FormData 간의 조합이 맞지 않음.
- 수정되지 않는 필드의 접근 문제: 불필요한 필드에 대한 접근 가능성 증가로 인해, 실수로 중요한 데이터(예: 비밀번호)가 개발자 실수로 인해 변경되거나 노출될 위험. 타입 안정성 떨어짐.
개선된 예시
import React, { useState } from 'react';
interface User {
id: number;
firstname: string;
lastname: string;
email: string;
password: string;
createdAt: string;
updatedAt: string;
}
interface UserUpdateFormProps {
user: User;
}
interface IFormData {
firstname: string;
lastname: string;
email: string;
}
const UserUpdateForm: React.FC<UserUpdateFormProps> = ({ user }) => {
//Form에 필요한 데이터만 따로 타입을 정의하여 들고 있음.
const [formData, setFormData] = useState<IFormData>({
firstname: user.firstname,
lastname: user.lastname,
email: user.email,
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
await fetch('/api/updateUser', {
method: 'POST',
body: {
name: `${formData.firstName}${formData.lastName}`,
email: formData.email
// formData.password는 접근 불가
},
});
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
name="firstname"
value={formData.firstname}
onChange={handleChange}
/>
<input
type="text"
name="lastname"
value={formData.lastname}
onChange={handleChange}
/>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
<button type="submit">Update</button>
</form>
);
};
위 개선된 예시에서는 필요한 데이터만 FormData에 추가하여, 불필요한 데이터 노출을 방지하고 데이터의 일관성을 유지할 수 있습니다. 또한, useState를 사용하여 FormData의 상태를 관리하고 각 필드를 handleChange 핸들러로 업데이트합니다.
결론
FormData와 서버 도메인 객체를 분리하는 것은 코드의 가독성과 유지보수성을 높이고, 데이터 일관성을 유지하며, 타입 안정성을 확보하는 데 중요한 역할을 합니다. 특히, 수정되지 않는 필드에 대한 접근을 방지함으로써 중요한 데이터가 실수로 변경되거나 노출될 위험을 줄일 수 있습니다. 이러한 원칙을 염두에 두고 개발하면, 더 견고하고 유지보수하기 쉬운 애플리케이션을 만들 수 있습니다.
+ 또한, 최근에는 React Hook Form과 같은 라이브러리를 사용하여 이러한 작업을 더욱 효율적으로 처리할 수 있습니다. React Hook Form은 폼 상태 관리와 유효성 검사를 쉽게 처리할 수 있게 도와주어, 개발 생산성을 높이고 유지보수를 용이하게 합니다.
import React from 'react';
import { useForm } from 'react-hook-form';
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: string;
updatedAt: string;
}
interface IFormData {
name: string;
email: string;
}
const UserUpdateForm: React.FC<{ user: User }> = ({ user }) => {
const { register, handleSubmit } = useForm<IFormData>({
defaultValues: {
name: user.name,
email: user.email,
},
});
const onSubmit = async (data: IFormData) => {
await fetch('/api/updateUser', {
method: 'POST',
body: {
name: data.name,
email: data.email,
// data.password는 접근 불가.
},
});
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} type="text" />
<input {...register('email')} type="email" />
<button type="submit">Update</button>
</form>
);
};