
서비스를 운영하기 위해서는 사용자 인증 기능이 필요하다.
이번 프로젝트에서는 회원가입과 로그인을 구현하면서 입력값 검증, API 요청, 토큰 저장, 로그인 상태 관리까지 함께 처리하였다.
특히 사용자가 잘못된 형식의 데이터를 입력한 상태로 서버에 요청을 보내지 않도록 react-hook-form과 yup을 사용하여 유효성 검사를 적용하였다.
폼 상태 관리와 유효성 검사를 위해 필요한 패키지를 설치하였다.
npm i react-hook-form yup @hookform/resolvers
회원가입 폼에서는 닉네임, 이메일, 비밀번호, 비밀번호 확인 값을 검증하였다.
import * as yup from 'yup';
export const signupSchema = yup.object().shape({
name: yup
.string()
.required('닉네임을 입력해주세요'),
email: yup
.string()
.required('이메일을 입력해주세요')
.email('올바른 이메일 형식을 입력하세요'),
password: yup
.string()
.required('비밀번호를 입력해주세요')
.matches(/^[a-zA-Z0-9]*$/, '비밀번호에는 특수문자가 포함될 수 없어요')
.matches(/[a-z]/, '비밀번호에는 1개 이상의 소문자가 포함되어야 해요')
.matches(/[A-Z]/, '비밀번호에는 1개 이상의 대문자가 포함되어야 해요')
.matches(/\d/, '비밀번호에는 1개 이상의 숫자가 포함되어야 해요')
.min(12, '비밀번호는 12자 이상이어야 해요')
.max(25, '비밀번호는 25자를 넘을 수 없어요'),
confirmPassword: yup
.string()
.required('비밀번호를 다시 입력해주세요')
.oneOf([yup.ref('password')], '비밀번호가 일치하지 않아요'),
});
회원가입 요청은 별도의 API 함수로 분리하였다.
import { api } from '../../../app/API';
export const postSignupAPI = async (data) => {
try {
const response = await api.post('sign-up', data);
return response.data;
} catch (error) {
console.error('API 요청 중 오류 발생:', error);
throw error;
}
};
useForm에 yupResolver를 연결하여
스키마 기반 유효성 검사가 동작하도록 설정하였다.
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: yupResolver(signupSchema),
});
폼 제출 시에는 입력값을 서버에서 요구하는 형식으로 가공한 뒤 API를 호출하였다.
const onSubmit = async (data) => {
try {
const { name, email, password } = data;
const payload = {
nickname: name,
email,
password,
};
await postSignupAPI(payload);
setIsModalOpen(true);
} catch (error) {
console.error('회원가입 실패:', error);
alert('회원가입 중 문제가 발생했습니다. 다시 시도해 주세요.');
}
};
각 input에는 register를 연결하고, 에러가 있을 경우 메시지를 출력하였다.
<StyledInput
type="email"
{...register('email')}
/>
{errors.email && (
<ErrorMessage>
{errors.email.message}
</ErrorMessage>
)}
로그인에서는 이메일과 비밀번호 입력값을 검증하였다.
export const loginSchema = yup.object().shape({
email: yup
.string()
.email('올바른 이메일 형식을 입력하세요')
.required('이메일은 필수항목이에요'),
password: yup
.string()
.required('비밀번호는 필수항목이에요'),
});
export const postLoginAPI = async (data) => {
try {
const response = await api.post('login', data);
return response.data;
} catch (error) {
console.error('API 요청 중 오류 발생:', error);
throw error;
}
};
로그인 역시 react-hook-form과 yupResolver를 사용하여
입력값 검증 후 API 요청을 보냈다.
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: yupResolver(loginSchema),
});
로그인에 성공하면 서버에서 받은 토큰을 localStorage에 저장하였다.
const onSubmit = async (data) => {
try {
const response = await postLoginAPI(data);
localStorage.setItem(
'accessToken',
response.accessToken
);
if (response.refreshToken) {
localStorage.setItem(
'refreshToken',
response.refreshToken
);
}
alert('로그인에 성공했습니다.');
navigate('/');
} catch (error) {
console.error(
'로그인 실패:',
error.response?.data || error.message
);
alert('로그인에 실패했습니다. 이메일 또는 비밀번호를 확인하세요.');
}
};
로그인 여부는 localStorage에 저장된 토큰을 기준으로 판단하였다.
const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => {
const token = getAccessToken();
setIsLoggedIn(!!token);
const handleStorageChange = () => {
const token = getAccessToken();
setIsLoggedIn(!!token);
};
window.addEventListener('storage', handleStorageChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
};
}, []);
토큰이 존재하면 로그인 상태로 판단하고,
헤더에서는 로그인 상태에 따라 버튼을 다르게 렌더링하였다.
{isLoggedIn ? (
<StyledButton onClick={handleLogout}>
로그아웃
</StyledButton>
) : (
<StyledButton onClick={() => handleNavigation('/login')}>
로그인
</StyledButton>
)}
로그아웃 시에는 저장된 토큰을 삭제하고 로그인 상태를 변경하였다.
const handleLogout = () => {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
setIsLoggedIn(false);
alert('로그아웃 되었습니다.');
navigate('/');
};
회원가입과 로그인 기능을 구현하면서 다음과 같은 구조를 적용할 수 있었다.
- react-hook-form을 활용한 폼 상태 관리
- yup을 활용한 입력값 유효성 검사
- 회원가입 / 로그인 API 요청 분리
- 로그인 성공 시 토큰 저장
- 토큰 여부에 따른 로그인 상태 관리
- 로그아웃 시 토큰 삭제 및 UI 업데이트
인증 기능은 서비스 전반에서 사용되는 중요한 기능이기 때문에,
폼 검증과 API 요청 로직을 분리하여 관리하는 것이 유지보수 측면에서 훨씬 편리했다.
또한 로그인 상태를 헤더와 연결하여 사용자가 현재 로그인 상태인지 직관적으로 확인할 수 있도록 구성하였다.