현재 진행 중인 프로젝트에서 맡은 파트 중 일부 로그인 & 회원가입, 비밀번호 찾기 등 인증 관련 파트가 있고 기능 개발은 끝난 뒤 다른 기능을 개발하던 도중 필요한 코드를 참고 하려고 들어가봤더니 너무 맘에 들지 않길래 당장 리팩토링
에 들어갔다.
기존 개발 일지: 회원 인증 폼 만들기
먼저 폼에서 자주 쓰이는 input을 재사용하기 위해 하나의 컴포넌트로 분리했다.
const emailInput = {
id: 1,
name: 'email',
type: 'text',
placeholder: '이메일',
};
const passwordInput = {
id: 2,
name: 'password',
type: 'password',
placeholder: '비밀번호: 최소 8자리 이상 25자리 이하 (알파벳, 특수문자 포함)',
};
const passwordConfirmInput = {
id: 3,
name: 'passwordConfirm',
type: 'password',
placeholder: '비밀번호 확인',
};
const businessNumberInput = {
id: 4,
name: 'businessNumber',
type: 'text',
placeholder: '사업자등록번호 (11자리)',
minLength: 11,
maxLength: 11,
onKeyDown: onKeyDownHandler,
};
... 생략
interface InputProps {
value: Record<string, string>;
onChangeHandler?: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
interface InputType {
id: number;
name: string;
type: string;
label?: string;
disabled?: boolean;
placeholder?: string;
}
const Input = ({ value, onChangeHandler }: InputProps) => {
const path = useRouter().pathname;
const { isPasswordValid, passwordValidationMessage } = useErrorMessage(value);
const inputOptions: Record<string, InputType[]> = {
'/auth/login': [emailInput, passwordInput],
'/auth/signup': [emailInput, passwordSignUpInput, passwordConfirmInput, businessNameInput],
'/auth/findPassword': [emailInput],
'/auth/reset': [passwordInput, passwordConfirmInput],
'/admin/store': [storeEmailInput, bnoNumberInput, storeBusineesNameInput],
};
const inputs = inputOptions[path];
return (
<>
{inputs.map((input: InputType) => {
const key = input.name;
const isPasswordConfirm = input.name === 'passwordConfirm' && path === '/auth/signup';
if (input) {
return (
<div key={input.id}>
{path === '/admin/store' && <label htmlFor={input.name}>{input.label}</label>}
{path === '/auth/signup' && <label htmlFor={input.name}>{input.label}</label>}
<input
id={input.name}
className={clsx(styles.input, {
[styles.inputError]: isPasswordConfirm && !isPasswordValid,
})}
name={input.name}
value={value[key]}
onChange={onChangeHandler}
type={input.type}
placeholder={input.placeholder}
disabled={input.disabled}
required
/>
{isPasswordConfirm && (
<span className={isPasswordValid ? styles.match : styles.error}>{passwordValidationMessage}</span>
)}
</div>
);
}
})}
</>
);
기존에 Input 컴포넌트 안에 있던 정적 데이터 들은 별도로 data 폴더 안에 모듈화 시켜서 import 하는 방식으로 가져갔다.
props를 받아오기 위해 기본적으로 타입을 지정 해줬는데 value와 onChangeHandler는 useInput 커스텀 훅을 별도로 만들어서 value 초기값과 onChange 함수를 내려받았고 외부 컴포넌트에서 사용될 Input 컴포넌트에 속성을 부여해 줄 타입들도 지정을 미리해 놓았다.
나는 Form에서 쓰일 해당 컴포넌트를 단 한 번만 호출하고 싶었고, 그래서 진입 경로 별로 어떠한 input이 쓰일지 미리 객체화 시켜놨었던 것이다. (data 폴더로 분리한 input 객체들)
inputOptions
를 보면 경로를 key로 지정해둠.
const { value, changeHandler } = useInput({
email: '',
password: '',
passwordConfirm: '',
businessName: '',
businessNumber: '',
});
<form className={styles.form}>
...
<Input value={value} onChangeHandler={changeHandler} />
...
</form>
input 정보가 담긴 배열을 순회하면서 한 번에 호출만으로도 모든 input이 출력됐다.
별도로 Form도 Input과 동일한 방식으로 페르소나처럼 객체화 시켜서 정보만 전달해주고 Form 컴포넌트를 4개의 페이지에 모두 재사용을 했다.
// auth.ts
export const LOGIN_DATA = {
url: '/auth/signup',
subUrl: '/auth/findPassword',
title: '편리함의 시작',
subTitle: 'Magic Pos',
subName: '비밀번호를 잊으셨나요?',
caption: '아직 회원이 아니신가요? 회원가입하러 가기',
buttonName: '로그인',
buttonSubName: '회원가입',
};
export const SIGNUP_DATA = {
url: '/auth/login',
title: '편리함의 시작',
subTitle: 'Magic Pos',
buttonName: '회원가입',
subButtonName: '인증하기',
comment: '사업자등록번호를 인증해 주세요.',
};
// ... 나머지 생략
// User.tsx
type AuthObjectType = Record<string, string>;
const User = () => {
const auth = useAuthStore(state => state.auth);
const path = useRouter().pathname;
const router = useRouter();
if (auth) router.push('/');
const AuthData: Record<string, AuthObjectType> = {
'/auth/login': LOGIN_DATA,
'/auth/signup': SIGNUP_DATA,
'/auth/findPassword': FIND_PASSWORD_DATA,
'/auth/reset': UPDATE_PASSWORD_DATA,
'/auth/success': SIGNUP_SUCCESS_DATA,
};
return <AuthForm data={AuthData[path]} />;
};
// AuthForm.tsx
const AuthForm = ({ data }: FormProps) => {
const router = useRouter();
const path = router.pathname;
const {
url,
subUrl,
title,
subTitle,
subName,
buttonName,
subButtonName,
description,
buttonSubName,
subDescription,
comment,
} = data;
}
사실 리팩토링과 모듈화의 정답은 없지만 마냥 만족하지는 않는다. 개발 속도가 더디어 질 수 있기에 이쯤에서 마무리를 지어야 했지만 더 좋은 방법이 생각나면 추후 개선시킬 예정이다.