Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 88 additions & 9 deletions frontend/app/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"use client";

import SignUpForm from "@/components/user/SignUpForm.component";
import { signUp } from "@/src/apis/user";
import { signUp, verifyCode, verifySignUpEmail } from "@/src/apis/user";
import { useMutation } from "@tanstack/react-query";
import { useRouter } from "next/navigation";
import { useReducer } from "react";
import { useReducer, useState } from "react";

interface setFormAction {
name: keyof typeof initState;
Expand Down Expand Up @@ -37,6 +37,10 @@ interface SignUpData {
const SignUpPage = () => {
const navigate = useRouter();
const [signUpData, setForm] = useReducer(formDataReducer, initState);
const [emailCode, setEmailCode] = useState("");
const [isEmailVerified, setIsEmailVerified] = useState(false);
const [isEmailCodeWrong, setIsEmailCodeWrong] = useState(false);
const [isVerificationRequested, setIsVerificationRequested] = useState(false);
Comment on lines +41 to +43

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

이메일 인증과 관련된 여러 상태(isEmailVerified, isEmailCodeWrong, isVerificationRequested)를 개별 useState로 관리하고 있습니다. 기능이 더 복잡해지면 상태 간의 의존성으로 인해 관리가 어려워질 수 있습니다. 이와 같은 관련 상태들은 하나의 객체로 묶어 useState로 관리하거나, 상태 전이가 명확한 useReducer를 사용하여 관리하는 것을 고려해볼 수 있습니다. 이렇게 하면 상태 업데이트 로직을 중앙에서 관리할 수 있고, 유효하지 않은 상태 조합을 방지하는 데 도움이 됩니다.

예시:

type VerificationStatus = "IDLE" | "REQUESTED" | "VERIFIED" | "ERROR";

interface VerificationState {
  status: VerificationStatus;
}

// useReducer나 useState<VerificationState>를 사용하여 상태 관리

(위 코드는 예시이며, 실제 구현은 프로젝트의 컨텍스트에 맞게 조정이 필요합니다.)


const { mutate: submitSignUp, isLoading } = useMutation({
mutationFn: (data: SignUpData) =>
Expand All @@ -47,19 +51,85 @@ const SignUpPage = () => {
year: Number(data.generation),
}),

onSuccess: () => {
alert("회원가입이 완료되었습니다.");
navigate.push("/signin");
},
onError: (error) => {
console.error("Sign up error:", error);
onSuccess: (result) => {
if (result) {
alert("회원가입이 완료되었습니다.");
navigate.push("/signin");
return;
}
alert("회원가입 중 오류가 발생했습니다. 다시 시도해주세요.");
},
});

const { mutate: sendEmailCode, isLoading: isSendingEmailCode } = useMutation({
mutationFn: verifySignUpEmail,
onSuccess: (result) => {
if (result) {
setIsVerificationRequested(true);
setIsEmailCodeWrong(false);
alert("인증번호가 발송되었습니다.");
return;
}
alert("인증번호 발송에 실패했습니다. 다시 시도해주세요.");
},
onError: () => {
alert("인증번호 발송에 실패했습니다. 다시 시도해주세요.");
},
});
Comment on lines +64 to +78

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

현재 useMutationonSuccess 콜백에서 API 호출 결과(result)에 따라 성공과 실패를 모두 처리하고 있으며, onError 콜백과 실패 처리 로직이 중복됩니다. 이는 verifySignUpEmail 함수가 try-catch로 에러를 감싸고 false를 반환하기 때문입니다.

react-query의 디자인에 맞게 API 함수(verifySignUpEmail)가 에러 발생 시 false를 반환하는 대신 에러를 그대로 throw하도록 수정하는 것을 권장합니다. 이렇게 하면 useMutationonError 콜백에서만 실패 로직을 처리하게 되어 코드가 더 명확하고 간결해집니다.

1. API 함수 수정 제안 (frontend/src/apis/user/index.ts)

export const verifySignUpEmail = async ({ email }: VerifyEmailReq) => {
  // try-catch를 제거하여 에러가 발생하면 useMutation의 onError에서 처리되도록 합니다.
  await https.post("/signup/verify", {
    email,
  });
};

2. useMutation 수정 제안
위와 같이 API를 수정하면, useMutation은 다음과 같이 간소화할 수 있습니다. 이 패턴은 signUp, confirmEmailCode 등 다른 뮤테이션에도 일관되게 적용할 수 있어 프로젝트 전체의 코드 품질을 높일 수 있습니다.

  const { mutate: sendEmailCode, isLoading: isSendingEmailCode } = useMutation({
    mutationFn: verifySignUpEmail,
    onSuccess: () => {
      setIsVerificationRequested(true);
      setIsEmailCodeWrong(false);
      alert("인증번호가 발송되었습니다.");
    },
    onError: () => {
      alert("인증번호 발송에 실패했습니다. 다시 시도해주세요.");
    },
  });


const { mutate: confirmEmailCode, isLoading: isCheckingEmailCode } =
useMutation({
mutationFn: verifyCode,
onSuccess: (result) => {
if (result) {
setIsEmailVerified(true);
setIsEmailCodeWrong(false);
alert("이메일 인증이 완료되었습니다.");
return;
}
setIsEmailVerified(false);
setIsEmailCodeWrong(true);
},
onError: () => {
setIsEmailVerified(false);
setIsEmailCodeWrong(true);
},
});

const onSendVerificationCode = () => {
setIsEmailVerified(false);
sendEmailCode({ email: signUpData.email });
};

const onVerifyEmailCode = () => {
confirmEmailCode({
email: signUpData.email,
code: emailCode,
});
};

const onChangeEmailCode = (value: string) => {
setEmailCode(value);
setIsEmailCodeWrong(false);
};

const onChangeForm = (action: setFormAction) => {
if (action.name === "email" && action.value !== signUpData.email) {
setIsEmailVerified(false);
setIsVerificationRequested(false);
setEmailCode("");
setIsEmailCodeWrong(false);
}
setForm(action);
};

const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
if (isLoading) return;
e.preventDefault();
if (!isEmailVerified) {
alert("이메일 인증을 완료해주세요.");
return;
}

submitSignUp(signUpData);
};
Expand All @@ -69,7 +139,16 @@ const SignUpPage = () => {
<SignUpForm
onSubmit={onSubmit}
data={signUpData}
setForm={setForm}
setForm={onChangeForm}
emailCode={emailCode}
onChangeEmailCode={onChangeEmailCode}
onSendVerificationCode={onSendVerificationCode}
onVerifyEmailCode={onVerifyEmailCode}
isEmailVerified={isEmailVerified}
isEmailCodeWrong={isEmailCodeWrong}
isVerificationRequested={isVerificationRequested}
isSendingEmailCode={isSendingEmailCode}
isCheckingEmailCode={isCheckingEmailCode}
isLoading={isLoading}
/>
</div>
Expand Down
61 changes: 59 additions & 2 deletions frontend/components/user/SignUpForm.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const resetWarning = {

interface SignUpProps {
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
onSendVerificationCode: () => void;
onVerifyEmailCode: () => void;
data: {
email: string;
password: string;
Expand All @@ -21,6 +23,13 @@ interface SignUpProps {
name: "email" | "password" | "passwordConfirm" | "username" | "generation";
value: string;
}) => void;
emailCode: string;
onChangeEmailCode: (value: string) => void;
isEmailVerified: boolean;
isEmailCodeWrong: boolean;
isVerificationRequested: boolean;
isSendingEmailCode: boolean;
isCheckingEmailCode: boolean;
isLoading?: boolean;
Comment on lines +26 to 33

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

SignUpForm 컴포넌트로 전달되는 props의 개수가 많아지고 있습니다. 특히 이메일 인증 관련 props가 다수 추가되었습니다. 관련 있는 props들을 하나의 객체로 묶어서 전달하면 prop drilling을 줄이고 코드 가독성을 높일 수 있습니다. 예를 들어, 이메일 인증과 관련된 상태와 핸들러들을 emailVerification과 같은 이름의 객체 prop으로 그룹화할 수 있습니다.

이렇게 하면 SignUpForm을 사용할 때 <SignUpForm {...} emailVerification={emailVerificationProps} /> 와 같이 더 깔끔하게 props를 전달할 수 있습니다.

  emailVerification: {
    emailCode: string;
    onChangeEmailCode: (value: string) => void;
    onSendVerificationCode: () => void;
    onVerifyEmailCode: () => void;
    isEmailVerified: boolean;
    isEmailCodeWrong: boolean;
    isVerificationRequested: boolean;
    isSendingEmailCode: boolean;
    isCheckingEmailCode: boolean;
  };
  isLoading?: boolean;

}

Expand All @@ -33,7 +42,21 @@ interface SignUpProps {
* @returns {JSX.Element} - SignUpForm component
* @constructor
*/
const SignUpForm = ({ onSubmit, data, setForm, isLoading }: SignUpProps) => {
const SignUpForm = ({
onSubmit,
onSendVerificationCode,
onVerifyEmailCode,
data,
setForm,
emailCode,
onChangeEmailCode,
isEmailVerified,
isEmailCodeWrong,
isVerificationRequested,
isSendingEmailCode,
isCheckingEmailCode,
isLoading,
}: SignUpProps) => {
const [isWarning, setWarning] = useState({
email: false,
password: false,
Expand Down Expand Up @@ -71,6 +94,40 @@ const SignUpForm = ({ onSubmit, data, setForm, isLoading }: SignUpProps) => {
isWrong={isWarning.email}
wrongMessage={"잘못된 이메일 형식입니다."}
/>
<div className="flex items-center gap-3">
<button
className="px-4 py-2 rounded-md bg-blue-500 hover:bg-blue-600 text-white disabled:bg-slate-300 disabled:cursor-not-allowed"
type="button"
onClick={onSendVerificationCode}
disabled={isSendingEmailCode || !isEmail(email)}
>
{isSendingEmailCode ? "인증코드 발송 중" : "인증코드 받기"}
</button>
{isEmailVerified && (
<span className="text-sm text-blue-600">이메일 인증 완료</span>
)}
</div>
{isVerificationRequested && !isEmailVerified && (
<div className="flex items-end gap-3">
<InputFormItem
label={"인증코드"}
placeholder={"인증코드를 입력해주세요."}
type="text"
value={emailCode}
onChange={(e) => onChangeEmailCode(e.target.value)}
isWrong={isEmailCodeWrong}
wrongMessage={"인증코드가 올바르지 않습니다."}
/>
<button
className="h-[3.5rem] px-4 py-2 rounded-md bg-blue-500 hover:bg-blue-600 text-white disabled:bg-slate-300 disabled:cursor-not-allowed"
type="button"
onClick={onVerifyEmailCode}
disabled={isCheckingEmailCode || emailCode.trim().length === 0}
>
{isCheckingEmailCode ? "확인 중" : "인증코드 확인"}
</button>
</div>
)}
<InputFormItem
label={"이름"}
placeholder={"에코노"}
Expand Down Expand Up @@ -111,7 +168,7 @@ const SignUpForm = ({ onSubmit, data, setForm, isLoading }: SignUpProps) => {
<button
className="w-full p-4 rounded-md bg-blue-500 hover:bg-blue-600 text-white disabled:cursor-not-allowed"
type="submit"
disabled={isLoading}
disabled={isLoading || !isEmailVerified}
>
회원가입
</button>
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/apis/user/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ interface VerifyEmailReq {
email: string;
}

export const verifySignUpEmail = async ({ email }: VerifyEmailReq) => {
try {
await https.post("/signup/verify", {
email,
});
return true;
} catch (error) {
return false;
}
Comment on lines +64 to +71

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

try-catch 블록에서 에러를 잡아서 false를 반환하는 대신, 에러를 그대로 발생시키는 것이 react-query와 함께 사용하기에 더 좋은 패턴입니다. 이렇게 하면 useMutationonError 콜백에서 에러를 일관되게 처리할 수 있으며, 컴포넌트의 onSuccess 콜백은 성공 케이스만 다루게 되어 코드가 더 간결하고 명확해집니다. 또한, 에러에 대한 자세한 정보를 onError로 전달할 수 있어 디버깅 및 사용자 피드백에 유리합니다.

이러한 변경은 frontend/app/signup/page.tsxuseMutation 로직을 단순화하는 데 도움이 됩니다.

    await https.post("/signup/verify", {
      email,
    });
    return true;

};

export const verifyEmail = async ({ email }: VerifyEmailReq) => {
// TODO: 서버에서 오는 에러를 핸들링해서, 컴포넌트에서 커스텀하게 사용할 수 있게 변경해야 한다.
try {
Expand Down
Loading