회원가입 페이지 (SignUpPage.jsx)에서의 로직이 과도하게 많아 가독성이 정말 좋지 않았다.
따라서 로직을 분리는 해야겠고, 컴포넌트를 분리하는 것이 아닌, 로직을 분리할때의 가장 적절하고 권장되는 방식을 공부하였다.
커스텀 훅이란?
React 컴포넌트에서 사용하는 상태 로직( useState, useEffect) 등을 재사용하기 위한 함수.
쉽게말하면, 특정 컴포넌트의 JSX를 제외한, 로직/상태/이벤트 핸들링을 분리하는 방식이다.
설계한 컴포넌트가 과도하게 무겁다면 여러가지 원인이 있겠는데,
1. 상태를 정의하거나
2. 유효성 검사를 하거나
3. 입력 처리 핸들러가 들어있거나
4. 조건부 렌더링 플래그가 있거나.
위 4가지는 사실 일반적인 컴포넌트에 필요한 로직이고 컴포넌트를 설계할때 늘 저 4가지 케이스에 대한 코드가 있었던 것 같다.
커스텀 훅을 왜 도입하려 했는가?
사실 커스텀훅의 정확한 의도는 상태 로직의 재사용이다. 그러나, 경험적으로 생각했을때 내가 만든 커스텀훅으르 재사용 한적이 있었나?를
고민해보았다.
"컴포넌트 내부 코드가 더러워진걸 보기 힘든것이지, 이 훅을 재사용할 것 같지는 않은데?"
이런 고민을 했었다.
명확하게 재사용하는 커스텀 훅의 예시로. 아래와 같은 것들은 명확하게 다른 컴포넌트에서 재사용할 가능성이 높다.
// useForm.js
const useForm = (initialValues) => {
const [values, setValues] = useState(initialValues);
const handleChange = (e) => {
const { name, value } = e.target;
setValues(prev => ({ ...prev, [name]: value }));
};
return { values, handleChange };
};
const useFetch = (url) => {
const [data, setData] = useState(null);
const [isLoading, setLoading] = useState(true);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(json => setData(json))
.finally(() => setLoading(false));
}, [url]);
return { data, isLoading };
};
그러나, 내가 회원가입 페이지에서 짠 복잡한 로직을 커스텀 훅으로 뺸다해도 그걸 재사용 할 것인가?
그렇다면 재사용을 하지 않는다면 커스텀훅으로 따로 뺄 이유가 없는 것일까?
사실 커스텀 훅은 본래 목적은 "재사용 + 관심사 분라" 이다.
즉, 재사용성이 명확하지 않아도 복잡하면 커스텀 훅으로 빼도 전혀 무방 하다는 것이다.
본론으로 넘어가, 회원가입 컴포넌트의 관심사 분리를 왜 굳이 했을까?
SignUpPage에는 다음과 같은 상태와 로직들이 있었다.
• 이름, 이메일, 비밀번호, 학과 등 입력값 상태
• 각 입력값의 유효성 검사
• 조건부 필드 표시 (showPasswordField, showGender 등)
• 단계별 전개(step1 → step2)
• 가입 요청 및 후처리 (성공 시 이동)
이 5가지의 기능에만 집중하여 코드를 짰더니 하나의 컴포넌트에 300줄이 넘는 복잡한 컴포넌트가 되었다.
커스텀훅의 리턴 타입을 뭘로 정해야하는가?
일반적으로 커스텀훅의 리턴 타입은 객체로, 그 해당 객체 내부에 상태, 상태변경함수 등을 넣는다.
당연히 객체로 리턴하는 이유는 구조분해할당이란 편한 문법으로 필요한 값(상태)만 가져올 수 있으므로, 또한 순서도 신경쓰지 않아도 괜찮았다.
리턴 구조 설계 : 역할별 그룹화
역할별 그룹화하면 저걸 Import할때 헷갈리지 않고 가져올 수 있겠다.
return {
values: {
name, email, password, confirmPassword,
department, studentId, gender, nickname,
},
setters: {
setName, setEmail, setPassword, ...
},
errors: {
passwordError, MjuEmailError,
},
flags: {
isMjuEmail, isStepOneValid, step, ...
},
display: {
showEmail, showPasswordField, ...
},
actions: {
handleNextStep, handleSubmit, setShowPassword,
},
};
커스텀훅과 별도로 REGEX(정규 검증)을 하는 부분은 커스텀 훅에서 제외하고, 유틸함수로 분리.
즉 로직을 외부 함수로 추출한 것이다!
// utils/verifyRegex.js
export const verifyMjuEmail = (email) => /@mju\.ac\.kr$/.test(email);
export const verifyPassword = (password) => /^(?=.*[A-Za-z])(?=.*\d)(?=.*[!@#$%^&*]).{8,16}$/.test(password);
커스텀훅에 포함시키지 않은 이유는 이 부분은 오직 정규식 검증에만 쓰인다.
즉, 상태관리를 전혀하지 않기에 일반 유틸 함수로 뺐다.
//SignUpPage 컴포넌트의 과도한 부담을 줄이기 위한 컴포넌트.
import { useEffect, useState } from 'react';
import { useAuth } from '@/context/AuthContext';
import { useNavigate } from 'react-router-dom';
import { verifyMjuEmail, verifyPassword } from '@/util/verifyRegex';
const useSignupForm = () => {
const [step, setStep] = useState(1);
const [isSignUpComplete, setIsSignUpcomplete] = useState(false);
const { signup } = useAuth();
// 입력 상태
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [department, setDepartment] = useState('');
const [studentId, setStudentId] = useState('');
const [gender, setGender] = useState('');
const [nickname, setNickname] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [passwordError, setPasswordError] = useState('');
const [isMjuEmail, setIsMjuMail] = useState(false);
const [MjuEmailError, setMjuEmailError] = useState('');
const [isStepOneValid, setIsStepOneVaild] = useState(false);
//비밀번호가 입력칸이 바뀔때 -> effect passWord 상태를 최신화 (의존성 배열에 password추가)
useEffect(() => {
if (!verifyPassword(password))
setPasswordError(
'비밀번호는 영문, 숫자, 특수문자 포함 8-16자여야 합니다.'
);
else {
setPasswordError('');
}
}, [password]);
// 상태 변경을 위한 useEffect
const [showEmail, setShowEmail] = useState(false);
const [showPasswordField, setShowPasswordField] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [showStudentId, setShowStudentId] = useState(false);
const [showGender, setShowGender] = useState(false);
const [showNickname, setShowNickname] = useState(false);
const navigate = useNavigate();
useEffect(() => {
if (name.length >= 2) setShowEmail(true);
else setShowEmail(false);
}, [name]);
useEffect(() => {
if (!verifyMjuEmail(email) && email.length > 1) {
setShowPasswordField(false);
setIsMjuMail(false);
setMjuEmailError(
'이메일 형식은 명지대학교의 공식 학생 이메일이어야만 합니다.'
);
}
if (verifyMjuEmail(email)) {
setShowPasswordField(true);
setMjuEmailError('');
}
}, [email]);
useEffect(() => {
if (password.length >= 8) setShowConfirmPassword(true);
else setShowConfirmPassword(false);
}, [password]);
useEffect(() => {
if (password === confirmPassword && confirmPassword.length > 0) {
console.log('현재 비밀번호와 확인 비밀번호가 일치합니다.');
setIsStepOneVaild(true);
} else {
setIsStepOneVaild(false);
console.log('현재 비밀번호와 일치하지 않습니다.');
}
}, [password, confirmPassword]);
useEffect(() => {
if (department.length > 2) setShowStudentId(true);
else setShowStudentId(false);
}, [department]);
useEffect(() => {
if (studentId.length >= 6) setShowGender(true);
else setShowGender(false);
}, [studentId]);
useEffect(() => {
if (gender) setShowNickname(true);
else setShowNickname(false);
}, [gender]);
//(2상태) 두번째 네개의 값이 다 채워지면 2상태
const isStepTwoValid = department && studentId && gender && nickname;
const handleNextStep = () => {
if (step === 1 && isStepOneValid) {
setStep(2);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
//서버에 보낼 객체값가공
const newUser = {
name,
email,
password,
department,
studentId,
gender,
nickname,
};
try {
await signup(newUser);
setIsSignUpcomplete(true);
//회원가입 성공하면 로그인 페이지로 리다리엑션
setTimeout(() => {
navigate('/login');
}, 500);
} catch (e) {
alert('회원가입에 실패했습니다.');
console.log('회원가입 실패', e);
}
};
// 리턴값 자체를 객체 타입으로 value 속성에 어떤값 이런식으로 주는게 더 현명
//그럼 원본 컴포넌트에서 받아볼수 있기때문임!
return {
values: {
name,
email,
password,
confirmPassword,
department,
studentId,
gender,
nickname,
},
setters: {
setName,
setEmail,
setPassword,
setConfirmPassword,
setDepartment,
setStudentId,
setGender,
setNickname,
},
errors: {
passwordError,
MjuEmailError,
},
flags: {
isMjuEmail,
isStepOneValid,
isStepTwoValid,
isSignUpComplete,
step,
},
display: {
showEmail,
showPasswordField,
showConfirmPassword,
showStudentId,
showGender,
showNickname,
showPassword,
},
actions: {
setShowPassword,
handleNextStep,
handleSubmit,
},
};
};
export default useSignupForm;
'FrontEnd Develop > Project : Team Nova MJ Search' 카테고리의 다른 글
비동기 함수와 useEffect에서의 처리 방식 쉽게 이해하기 (0) | 2025.03.20 |
---|---|
Server Request Parameter Handling : 동적 키를 활용한 상태 관리 vs 개별 상태 관리 (무엇이 더 확장성에 도움 되는가?) (0) | 2025.03.16 |
LocalStorage와 SessionStorage를 활용한 로그인 전역 상태 유지 및 Fetching UI JANK, UX 개선 (0) | 2025.03.06 |
프로젝트에 Open API 적용하기 (0) | 2025.01.14 |
<React + Vue.js> 프로젝트 내 회원가입 기능 구현 가이드 (0) | 2025.01.12 |