기존 코드는 동작을 위한 form 코드, useState로 상태를 관리했었다.
export default function AddInfoForm() {
const { userInfo } = useGetUserInfo();
// form state
const [interests, setInterests] = useState<string[]>([]);
const [name, setName] = useState<string>('');
const [email, setEmail] = useState<string>('');
const [phone, setPhone] = useState<string>('');
const [role, setRole] = useState<string>('');
해당 코드를 하나의 state로 관리하면 어떨까 하는 생각에
아래와 같이 리팩토링을 진행했음
import { useReducer } from 'react';
const reducer = (state: any, action: any) => {
switch (action.type) {
case 'TOGGLE_ARRAY_VALUE':
const arr = state[action.payload.name] as string[];
const value = action.payload.value;
return {
...state,
[action.payload.name]: arr.includes(value) ? arr.filter((v) => v !== value) : [...arr, value],
};
case 'CHANGE_VALUE':
return { ...state, [action.payload.name]: action.payload.value };
default:
return state;
}
};
export const useForm = (initialState: { [key: string]: string | string[] }) => {
const [formValues, dispatch] = useReducer(reducer, initialState);
console.log(formValues);
const handleChange = (e: React.ChangeEvent<HTMLInputElement> | React.ChangeEvent<HTMLSelectElement>) => {
const { name, value } = e.target;
// 배열 필드라면 TOGGLE_ARRAY_VALUE 사용
if (Array.isArray(formValues[name])) {
dispatch({ type: 'TOGGLE_ARRAY_VALUE', payload: { name, value } });
} else {
dispatch({ type: 'CHANGE_VALUE', payload: { name, value } });
}
};
return { formValues, handleChange };
};
import Button from '@/components/common/button';
import Chip from '@/components/common/chip';
import Input from '@/components/common/input';
import { LABELS } from '@/components/common/input/labels';
import { categories } from '@/configs/category';
import { roles } from '@/configs/roles';
import { Select, SelectItem } from '@heroui/react';
import { useForm } from '../hooks/useForm';
export default function Form() {
const { formValues, handleChange } = useForm({
name: '',
email: '',
phone: '',
role: '',
interest: [],
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
};
return (
<>
<form
className="mx-auto w-1/2 flex-1 flex-col gap-6 space-y-6 rounded-3xl border border-white/20 bg-white/10 p-10 shadow-2xl backdrop-blur-2xl"
onSubmit={handleSubmit}
>
<h2 className="mb-2 text-3xl font-extrabold text-white drop-shadow">회원 정보 입력</h2>
<Input
name="name"
label={LABELS.NAME}
type="text"
placeholder="사용하실 이름을 입력해주세요."
description="이름은 최대 10자까지 입력할 수 있습니다."
// isInvalid={!nameSchema.safeParse(name).success}
// errorMessage={nameSchema.safeParse(name).error?.issues[0]?.message}
value={formValues.name}
onChange={handleChange}
/>
<Input
name="email"
label={LABELS.EMAIL}
type="email"
placeholder="example@example.com"
value={formValues.email}
onChange={handleChange}
/>
<Input
name="phone"
label={LABELS.PHONE}
type="tel"
placeholder="010-1234-5678"
value={formValues.phone ?? ''}
onChange={handleChange}
/>
<Select
name="role"
items={roles}
label="역할"
placeholder="참여하실 역할을 선택해주세요."
value={formValues.role}
color="primary"
onChange={handleChange}
variant="underlined"
classNames={{
label: 'text-black dark:text-white/90',
listbox: 'text-black dark:text-white/90',
}}
>
{(role) => <SelectItem>{role.label}</SelectItem>}
</Select>
<div>
<label className="mb-1 block">관심사</label>
<div className="mb-1 flex flex-wrap gap-2">
{categories.map((category) => (
// NOTE: 임시 관심사 리스트임
<Chip
name="interest"
key={category}
onClick={() =>
handleChange({
target: {
name: 'interest',
value: category,
},
} as React.ChangeEvent<HTMLInputElement>)
}
color="danger"
radius="md"
className="w-fit cursor-pointer border border-white/20 shadow backdrop-blur-md"
variant={formValues.interest.includes(category) ? 'flat' : 'bordered'}
>
{category}
</Chip>
))}
</div>
<p className="text-xs text-black/80 dark:text-white/80">관심있는 팝업을 선택해주세요.</p>
</div>
<Button
type="submit"
className="w-full rounded-full bg-gradient-to-r from-pink-400 to-blue-400 font-semibold text-white shadow-lg transition hover:scale-105"
>
완료
</Button>
</form>
</>
);
}
useReducer를 활용해서 작성했는데 처음 input의 e.target.value만을 다룰 때
즉, 코드가 단순할 때는 효율적이였지만
상태관리, 유효성검사, 에러상태처리 등 여러 상태처리를 같이 하려고보니 코드 가독성, 유지보수성 모든 면에서 기존 코드보다 나은점이 없었음 또한 단일원칙또한 지켜지지 않았음
오히려 더 복잡해졌고 코드 쓰레기장이 되어버린 결과가 생김
이를 위해서 어떻게 리팩토링을 할지 생각중에 있다. 한번 더 시도해보고 좋은 결과가 생기면 아래에 추가할 예정
state와 action을 zustand를 사용해 store를 만들어줌
import { create } from 'zustand';
interface AddInfoFormState {
name: string;
email: string;
phone: string;
role: string;
interests: string[];
nameValid: boolean;
emailValid: boolean;
phoneValid: boolean;
}
interface AddInfoFormActions {
setValue: (key: string, value: string) => void;
setInterests: (interests: string[]) => void;
setIsValid: (key: string, isValid: boolean) => void;
}
export const useAddInfoFormStore = create<AddInfoFormState & AddInfoFormActions>((set) => ({
name: '',
email: '',
phone: '',
role: '',
interests: [],
nameValid: false,
emailValid: false,
phoneValid: false,
setValue: (key, value) => set({ [key]: value }),
setIsValid: (key, isValid) => set({ [`${key}Valid`]: isValid }),
setInterests: (interests: string[]) => set({ interests: [...interests] }),
}));
그리고 각 필드를 컴포넌트화 해서
필요한 상태와 action을 import해서 각 input에 해당하는 컴포넌트에 사용해서 해결
이렇게 하니깐 전 보다는 최적화(렌더링)에서는 좋은데 문제는 이제 코드가 생각보다 깔끔하지는 않음
추상화 팩토리 패턴을 도입해서 각 Input을 재사용하는 방향이 필요할거같음