컴포넌트가 렌더링되기 전에 대기상태에서 처리할 컴포넌트를 Suspense에 전달해서 사용할 수 있다. 즉, 기존에 react-query로 로딩상태를 특정 컴포넌트에서 컴포넌트를 불러와서 사용하고 있었다면, 그 부분을 외부 컴포넌트에서 Suspense로 감싸서 로딩상태일 때 보여줄 컴포넌트를 전달해서 보다 선언적으로 코드를 구성할 수 있다.
보통 회원가입을 하나의 form으로 관리한다. form안에 input 태그를 두고 입력을 받으며, 그와 관련된 상태관리 로직과 onChange와 관련된 함수도 관리한다. 이때 form과 관련된 UI로직과 비즈니스로직을 최대한 분리해서 컴포넌트가 최대한 단일책임을 가질 수 있도록 구현하려고 한다.
그렇게 하기 위해서 일단은 처음부터 UI로직과 비즈니스 로직을 바로 분리하려고 하기에는 아직 내 실력이 부족하다. 그래서 일단 하나의 컴포넌트에서 모든 로직을 작성하고 정상적으로 동작하는지를 먼저 확인했다.
'use client';
import {
isEmailCheck,
isNameCheck,
isPasswordCheck,
isPhoneCheck,
} from '@/utils/isValidationCheck';
import { useRouter } from 'next/navigation';
import { ChangeEvent, FormEvent, MouseEventHandler, useState } from 'react';
import Button from '../Button';
import AuthInput from './AuthInput';
import TermsOfUse from './TermsOfUse';
import ValidationMessage from './ValidationMessage';
import ValidationMessages from './ValidationMessages';
interface UserInfo {
email: string;
password: string;
name: string;
phone: string;
}
interface ValidationFunctions {
[key: string]: (val1: string) => boolean;
}
const validationFunctions: ValidationFunctions = {
email: isEmailCheck,
password: isPasswordCheck,
name: isNameCheck,
phone: isPhoneCheck,
};
const RegisterForm = () => {
const router = useRouter();
const [isEnterUserInfo, setIsEnterUserInfo] = useState(false);
const [userValue, setUserValue] = useState({
email: '',
password: '',
name: '',
phone: '',
});
const [isValid, setIsValid] = useState({
email: false,
password: false,
name: false,
phone: false,
});
const [isEmpty, setIsEmpty] = useState({
email: true,
password: true,
name: true,
phone: true,
});
const handleInputChange = (e: ChangeEvent) => {
const { value, name } = e.target;
if (value !== '') {
setIsEmpty((prev) => ({ ...prev, [name]: false }));
}
setUserValue((prev) => ({ ...prev, [name]: value }));
const isValidFunction = validationFunctions[name];
if (isValidFunction) {
const isValid = isValidFunction(value);
setIsValid((prev) => ({ ...prev, [name]: isValid }));
}
};
const handleClickContinue: MouseEventHandler = (e) => {
const isEnteredAllUserInfo = Object.values(userValue).every(
(value) => value !== '',
);
setIsEnterUserInfo(isEnteredAllUserInfo);
};
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (isEnterUserInfo) router.push('/login');
};
return (
export default RegisterForm;
유저 정보와 관련된 이메일, 비밀번호, 이름, 휴대폰 번호등을 하나의 객체로 선언하고 관리하는 방식을 선택했으며, 그와 관련해서 발생하면 상태변화를 감지해서 validation을 체크해서 boolean 값으로 저장해서 관리하고 실제 input창이 비어있는지 아닌지를 감지할 수 있도록 구현했다.
그리고 현재는 handleSubmit에서는 특별한 로직이 없다. 그냥 단순하게 페이지 이동 처리 로직만 넣어놓을 생각이다. 그럼 이제 RegisterForm 컴포넌트에서는 최대한 UI로직만 가지고 있고, 외부에 useForm이라는 커스텀 훅을 선언해서 RegisterForm에서는 그걸 이용해서 비즈니스 로직을 최소화해서 사용해보려고 했다.
import ...
const initialValue = {
email: '',
password: '',
name: '',
phone: '',
};
const initialValidValue = {
email: false,
password: false,
name: false,
phone: false,
};
const initialEmptyValue = {
email: true,
password: true,
name: true,
phone: true,
};
const useForm = ({ formType, onSubmit, validate }: useFromProps) => {
const [values, setValues] = useState<UserInfo>(initialValue);
const [isValid, setIsValid] = useState(initialValidValue);
const [isEmpty, setIsEmpty] = useState(initialEmptyValue);
const [isEnterUserInfo, setIsEnterUserInfo] = useState(false);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const { value, name } = e.target;
if (value !== '') {
setIsEmpty((prev) => ({ ...prev, [name]: false }));
}
setValues((prev) => ({ ...prev, [name]: value }));
const isValidFunction = validate[name];
if (isValidFunction) {
const isValid = isValidFunction(value);
setIsValid((prev) => ({ ...prev, [name]: isValid }));
}
handleAllUserInfoCheck();
};
const handleAllUserInfoCheck = () => {
if (formType === 'login') {
const isEmailAndPasswordEntered =
values.email !== '' && values.password !== '';
setIsEnterUserInfo(isEmailAndPasswordEntered);
} else {
const isEnteredAllUserInfo = Object.values(values).every(
(value) => value !== '',
);
setIsEnterUserInfo(isEnteredAllUserInfo);
}
};
const handleClickContinue: MouseEventHandler<HTMLButtonElement> = () => {
handleAllUserInfoCheck();
};
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
onSubmit();
};
return {
values,
isValid,
isEmpty,
isEnterUserInfo,
handleAllUserInfoCheck,
handleClickContinue,
handleChange,
handleSubmit,
};
};
export default useForm;
form과 관련된 로직을 전부 useForm으로 가지고 온다. 그리고 handleSubmit의 경우 회원가입이 폼이나 로그인 폼의 경우 로직을 재사용하기가 힘든 중복로직이 아닌 경우가 많을 수 있기 때문에 onSubmit으로 인자로 받을 수 있게 했다. 그리고 formType을 받아서 그에 따라 다른 로직을 구현할 수 있도록 했다. 당연히 validate도 폼마다 달라질 수 있으니 인자로 받을 수 있도록 했다. 일단은 RegisterForm에서 분리한다는 목적으로 빼낸 것 까지는 잘했다고 생각하지만, useForm 자체를 보면 재사용성이 그렇게 높아보이지는 않는다. formType에 따라서 달라지는 로직도 점점 추가되다보면 추상화레벨이 좀 낮을 수 있다고 생각한다. 하지만 아직은 거기까지는 내 실력이 부족한 것 같다. 그래서 일단 당분간은 여기서 만족하고 나중에 다시 시도해보려 한다.
'use client'
import useForm from '@/hooks/useForm'
const RegisterForm = () => {
const onSubmit = () => {
if (isEnterUserInfo) router.push('/login')
}
const {
isValid,
isEmpty,
isEnterUserInfo,
handleClickContinue,
handleChange,
handleSubmit
} = useForm({
formType: 'register',
onSubmit,
validate: validationFunctions
})
return <form onSubmit={handleSubmit}>...</form>
}
export default RegisterForm
useForm을 사용하면서 비즈니스 로직이 확 줄어버렸다. 이것만으로도 로직을 재사용할 수 있기 되었기 때문에 어느 정도 수확이 있다고 생각하지만 사실 UI로직과 비즈니스 로직이 완전히 분리되어 있는 것이 최선일 것이다. 그리고 Next13의 경우는 서버 컴포넌트와 클라이언트 컴포넌트의 구분을 명확하게 두고 있기 때문에 그 이유 때문이라도 명확하게 분리를 시도해보는 것이 좋다고 생각한다. 그래서 이 부분은 Context API를 사용해서 진행해보려고 한다. 이 부분은 추후에 다시 시도해보고 TIL에 정리하도록 하자.