내가 원래 알던 상식대로
server action 내부에서 throw를 해도 error.tsx로 넘어가게 된다
지금까지 useActionState를 사용할 경우 state.error로 자동으로 넘어가는줄 알았는데
이러면 필요 없는 코드들을 잔뜩 삭제할 수 있다
이 부분도 try Catch문이 있다면
내부에서 발생한 throw는 catch 문으로 이동하게 된다.
---------------
혹시 몰라 마지막으로 테스트 해봤는데
return {
sucess: false,
error: new Error("모든 필드를 입력해주세요."),
value: { inputData },
};
안에서 해도 괜찮았다 다른 문제가 있었던거 같다
굳이 다른 생성 함수를 만들지 않아도 될 것 같다.
------------------------------
이제 에러 관리에 대해 확실히 알 것 같다
일단 지금까지 했던 오해를 풀고 가야하는데
return {
sucess: false,
error: new Error("모든 필드를 입력해주세요."),
value: { inputData },
};
server action에서 이렇게 생성해서 하면 그대로 리턴될 것 같지만
new Error에서 트리거 발생해 error.tsx로 이동하게 된다
이를 해결하려면
return {
sucess: false,
error: createDisplayError("게시글 수정에 실패했습니다."),
value: { inputData },
};
이렇게 별도의 함수를 만들어서 사용해야한다
그리고 위 사례와 마찬가지로
throw createDisplayError(
"게시글 수정중에 예상치 못한 에러가 발생하였습니다."
);
server action 내부에서 throw를 해도 error.tsx로 넘어가게 된다
지금까지 useActionState를 사용할 경우 state.error로 자동으로 넘어가는줄 알았는데
이러면 필요 없는 코드들을 잔뜩 삭제할 수 있다
export const updateArticle = async (
_: updateArticleState,
formData: FormData
): Promise<updateArticleState> => {
try {
const cookieStore = await cookies();
const token = cookieStore.get("token")?.value;
if (!token) {
return {
sucess: false,
error: createDisplayError("로그인이 필요합니다."),
value: {
inputData: {
title: "",
description: "",
body: "",
tagList: [],
},
},
};
}
const slug = formData.get("slug")?.toString() || "";
const tagListStr = formData.get("tagList")?.toString();
const tagList = tagListStr ? (JSON.parse(tagListStr) as string[]) : [];
const inputData = {
title: formData.get("title")?.toString() || "",
description: formData.get("description")?.toString() || "",
body: formData.get("body")?.toString() || "",
tagList: tagList,
};
if (!inputData.title || !inputData.description || !inputData.body) {
return {
sucess: false,
error: createDisplayError("모든 필드를 입력해주세요."),
value: { inputData },
};
}
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/articles/${slug}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
article: inputData,
}),
}
);
if (!response.ok) {
console.error(response);
return {
sucess: false,
error: createDisplayError("게시글 수정에 실패했습니다."),
value: { inputData },
};
}
return {
sucess: true,
value: { inputData },
};
} catch (e) {
console.error(e);
throw new Error("게시글 수정중에 예상치 못한 에러가 발생하였습니다.");
}
};
그래서 최종적으로는 이런 형태로 server action에서의 코드를 작성할 수 있을 것같다
사용자에게는 직관적인 에러를 전달하고 console.error로 에러 사항을 파악하는게 좋은 것 같다
server error code나 message들을 그대로 전달하는게 좋다고 생각했는데 아니었다.
또한 모든 useActionState에 들어갈 server 함수만 state를 만들어서 success와 error를 관리했는데
당장은 사용자에게 그 메시지를 보여주지 않더라도
동일한 포멧으로 응답을 관리하는게 더 좋다는 것을 알았다.
----------------------------------------------------------------------------------------------------
아래 코드대로 하면 너무 필요없는 상태관리가 든다
useState나 useEffect를 쓰게 되는데 쓸데가 없다
"use client";
import { Button } from "@/components/ui/Button/Button";
import { useActionState, useState } from "react";
import { updatePassword } from "@/actions/auth";
import { UpdatePasswordState } from "@/types/authTypes";
import { ErrorDisplay } from "@/components/ErrorDisplay";
import { InputWithError } from "../InputWithError";
import { isUnexpectedError, ValidationError } from "@/types/error";
import { validatePassword } from "@/utils/validations";
import { useRouter } from "next/navigation";
import logout from "@/utils/auth/authUtils";
const initialState: UpdatePasswordState = {
error: undefined,
value: {
inputData: {
currentPassword: "",
password: "",
passwordConfirm: "",
},
token: null,
},
success: undefined,
};
export default function SecurityForm() {
const [state, formAction, isPending] = useActionState(
updatePassword,
initialState
);
const [clientErrors, setClientErrors] = useState<Record<string, string>>({});
const [isValid, setIsValid] = useState(false);
const router = useRouter();
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const formData = new FormData(e.currentTarget.form!);
const values = Object.fromEntries(formData) as Record<string, string>;
const errors = validatePassword(values);
if (errors) {
setClientErrors(errors);
setIsValid(false);
} else {
setClientErrors({});
setIsValid(true);
}
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
if (!isValid) {
e.preventDefault();
}
};
const handleDeleteUser = () => {
router.push("/settings/deleteUser", {
scroll: false,
});
};
if (state.error && isUnexpectedError(state.error)) {
throw state.error;
}
return (
<div className="flex gap-8 flex-col">
<section>
<h3 className="text-xl font-bold text-gray-800 dark:text-gray-100 mb-4">
비밀번호 변경
</h3>
<form action={formAction} onSubmit={handleSubmit}>
<ErrorDisplay message={state.error?.message} />
<InputWithError
props={{
type: "password",
name: "currentPassword",
placeholder: "기존 비밀번호",
defaultValue: state.value.inputData.currentPassword,
onChange: handleChange,
}}
className="mb-4"
/>
<InputWithError
errorMessage={
clientErrors["password"] ??
(state.error?.name === "ValidationError"
? (state.error as ValidationError)?.fieldErrors?.password
: "")
}
props={{
type: "password",
name: "password",
placeholder: "새로운 비밀번호",
defaultValue: state.value.inputData.password,
onChange: handleChange,
}}
className="mb-4"
/>
<InputWithError
errorMessage={
clientErrors["passwordConfirm"] ??
(state.error?.name === "ValidationError"
? (state.error as ValidationError)?.fieldErrors?.passwordConfirm
: "")
}
props={{
type: "password",
name: "passwordConfirm",
placeholder: "새로운 비밀번호 확인",
defaultValue: state.value.inputData.passwordConfirm,
onChange: handleChange,
}}
className="mb-4"
/>
<div className="flex justify-end">
<Button
type="submit"
variant="primary"
size="lg"
disabled={isPending || !isValid}
>
패스워드 수정하기
</Button>
</div>
</form>
</section>
<section className="flex gap-4 flex-col">
<Button
variant="outline-danger"
className="w-full"
size="lg"
onClick={logout}
>
로그아웃
</Button>
<Button
variant="outline-danger"
className="w-full"
size="lg"
onClick={handleDeleteUser}
>
회원탈퇴
</Button>
</section>
</div>
);
}
if (state.error && isUnexpectedError(state.error)) {
throw state.error;
}
이 짧은 코드로 쓸모없는 다른 코드들 모두를 대체했다.
----------------------
nextjs는 많은 부분들이 추상화 되어있다
error boundary도 마찬가지인데
폴도 내에 error.js(또는 error.tsx)를 선언하면
해당경계가 자동으로 errorBoundary 처리가 되어 throw error를 하면 그곳으로 들어가게 된다.
하지만
이것의 예외 사항이 있었는데
오류 경계는 다음의 오류를 포착하지 않습니다.
- 이벤트 핸들러(자세히 알아보기)
- 비동기 코드(예: setTimeout 또는 requestAnimationFrame 콜백)
- 서버 측 렌더링
- 오류 경계 자체(자식이 아닌)에서 발생한 오류
errorBoundary는 위 사항에서 동작을 안한다고 한다
지금까지 이걸 모르고 왜 이곳으로 안넘어가지 고민을 했었는데 풀렸다.
"use client";
import { deleteUser } from "@/actions/auth";
import { ErrorDisplay } from "@/components/ErrorDisplay";
import { Input } from "@/components/Input";
import { Button } from "@/components/ui/Button/Button";
import Modal from "@/components/ui/Modal";
import convertAuthSupabaseErrorToKorean from "@/error/convertAuthSupabaseErrorToKorean";
import logout from "@/utils/auth/logout";
import { AuthError } from "@supabase/supabase-js";
import { useEffect, useState } from "react";
const CHECK_TEXT = "탈퇴하겠습니다.";
export default function DeleteUserModalPage() {
const [valid, setValid] = useState(false);
const handleCheckText = (e: React.ChangeEvent<HTMLInputElement>) => {
setValid(e.target.value === CHECK_TEXT);
};
const [error, setError] = useState<AuthError | null>(null);
const [unexpectedError, setUnexpectedError] = useState<Error | null>(null);
useEffect(() => {
if (unexpectedError) {
throw unexpectedError;
}
}, [unexpectedError]);
const handleDeleteUser = async () => {
if (!valid) return;
try {
const { success, error } = await deleteUser();
if (!success) {
setError(error || null);
} else {
logout();
}
} catch (error) {
setUnexpectedError(error as Error);
}
};
return (
<>
<Modal>
<Modal.Header>회원 탈퇴</Modal.Header>
<Modal.Content>
<ErrorDisplay
message={
convertAuthSupabaseErrorToKorean(error?.code) || error?.message
}
/>
<p>회원 탈퇴 시 모든 데이터가 삭제됩니다.</p>
<p>
정말 탈퇴하시겠습니까? 원한다면 해당 문구를 입력하고 버튼을
눌러주세요.
</p>
<Input
type="text"
placeholder="탈퇴하겠습니다."
onChange={handleCheckText}
/>
</Modal.Content>
<Modal.Footer>
<Button disabled={!valid} onClick={handleDeleteUser}>
회원 탈퇴
</Button>
</Modal.Footer>
</Modal>
</>
);
}
위와 같이 useEffect을 사용해서 관리를 하면 성공적으로 처리된다.
출처:
https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
Component – React
The library for web and native user interfaces
react.dev
'개발 > NEXTJS' 카테고리의 다른 글
supabase에서 회원 탈퇴 구현하기 (0) | 2025.02.10 |
---|---|
모달창에서 스크롤 방지 (0) | 2025.02.10 |
nextjs 에러 관리 (0) | 2025.02.05 |
사용자 avata image 변경했을 때 (0) | 2025.01.31 |
좋은 블로그 글 모음 (1) | 2025.01.27 |