Supabase와 NextJS로 블로그 만들기 - Modify, Delete
안녕하세요?
오늘은 Supabase + NextJS 블로그 만들기 시리즈 마지막인 Post의 Modify, Delete에 대해 알아보겠습니다.
일단 /post/modify.tsx 파일을 만들었고 무엇을 수정, 삭제할지에 대한 정보는 브라우저의 주소창 쿼리를 이용할 예정입니다.
NextJS의 다이내믹 라우팅으로도 할 수 있지만 좀 더 간단한 주소창 쿼리를 이용할 예정입니다.
http://localhost:3000/post/modify?id=1
이런 식으로 주소 링크를 주면 해당 modify.tsx 파일에서 해당 id를 이용해서 수정, 삭제할 예정입니다.
일단 /post/modify.tsx의 로그인된 유저만 볼 수 있게 하는 템플릿을 준비해 보겠습니다.
import React, { useEffect } from "react";
import { useRouter } from "next/router";
import { GetServerSideProps } from "next";
import { useMessage } from "../../lib/message";
import { useFormFields } from "../../lib/utils";
import { supabase } from "../../lib/supabase";
import { User } from "@supabase/supabase-js";
import classNames from "classnames";
const ModifyPost = ({ user }: { user: User }) => {
const { messages, handleMessage } = useMessage();
return (
<div className="flex flex-col items-center justify-start py-4 min-h-screen">
<h2 className="text-lg my-4">
Hello, {user && user.email ? user.email : "Supabase User!"}
</h2>
{messages &&
messages.map((message, index) => (
<div
key={index}
className={classNames(
"shadow-md rounded px-3 py-2 text-shadow transition-all mt-2 text-center",
message.type === "error"
? "bg-red-500 text-white"
: message.type === "success"
? "bg-green-300 text-gray-800"
: "bg-gray-100 text-gray-800"
)}
>
{message.message}
</div>
))}
<h1 className="py-3 font-semibold text-2xl text-blue-600">
Modify a Post
</h1>
);
};
export default ModifyPost;
export type NextAppPageUserProps = {
props: {
user: User;
loggedIn: boolean;
};
};
export type NextAppPageRedirProps = {
redirect: {
destination: string;
permanent: boolean;
};
};
export type NextAppPageServerSideProps =
| NextAppPageUserProps
| NextAppPageRedirProps;
export const getServerSideProps: GetServerSideProps = async ({
req,
}): Promise<NextAppPageServerSideProps> => {
const { user } = await supabase.auth.api.getUserByCookie(req);
if (!user) {
return {
redirect: {
destination: "/auth",
permanent: false,
},
};
}
return {
props: {
user,
loggedIn: !!user,
},
};
};
역시나 이번 코드도 지난 시간에 썼던 create.tsx 파일과 똑같습니다.
그럼 본격적으로 코드를 짜 볼까요?
먼저, next/router를 이용한 쿼리를 파싱 하는 코드부터 만들도록 하겠습니다.
const router = useRouter();
const { id } = router.query;
위와 같이 router.query 객체를 이용해서 id를 뽑아냅니다.
그러면 modify.tsx 파일을 부를 때는 어떻게 해야 할까요?
해당 링크는 지난 시간에 배웠던 상세 페이지에서 링크를 생성하겠습니다.
/pages/post/[id].tsx
<h3 className="py-3 font-semibold text-lg text-blue-600">
Post Detail List
</h3>
<div className="flex flex-row items-center justify-center space-x-4 mb-4">
<Link href={`/post/modify?id=${post.id}`}>
<a className="bg-transparent hover:bg-blue-600 text-sm text-blue-600 hover:text-white font-semibold py-2 px-4 border border-blue-500 hover:border-transparent rounded-lg">
Modify
</a>
</Link>
<div>or</div>
<a
className="bg-transparent hover:bg-gray-600 text-sm text-gray-600 hover:text-white font-semibold py-2 px-4 border border-gray-500 hover:border-transparent rounded-lg"
onClick={deletePost}
>
Delete
</a>
</div>
<Link href={`/post/modify?id=${post.id}`}>
상기 코드처럼 자바스크립트의 리터럴 방식으로 주소창에 /post/modify?id=1 이런 식으로 처리했습니다.
참고로 /pages/post/[id].tsx 파일에도 deletePost 함수가 있는데요.
나중에 modify.tsx 파일에서 deletePost 함수를 작성할 때 여기 있는 deletePost 함수도 처리할 예정이니까 일단 넘어가도록 하겠습니다.
/pages/post/[id].tsx 파일의 스크린숏은 아래처럼 Modify, Delete 버튼이 생기게 됩니다.
이제 다시 modify.tsx 파일로 와서 마저 진행해 볼까요?
type FormFieldProps = {
title: string;
content: string;
};
const FORM_VALUES: FormFieldProps = {
title: "",
content: "",
};
const ModifyPost = ({ user }: { user: User }) => {
const { messages, handleMessage } = useMessage();
const [values, handleChange, resetFormFields] =
useFormFields<FormFieldProps>(FORM_VALUES);
const [refresh, setRefresh] = useState<boolean>(false);
const router = useRouter();
const { id } = router.query;
useEffect(() => {
fetchPosts();
}, []);
const fetchPosts = async () => {
let { data: post, error } = await supabase
.from("posts")
.select("*")
.eq("id", Number(id))
.limit(1)
.single();
if (error) {
console.log(error);
handleMessage({ message: "Error : No post", type: "error" });
} else {
values.title = post.title;
values.content = post.content;
setRefresh(!refresh);
}
};
위 코드를 보면 useEffect 훅에서 fetchPosts 함수를 이용해서 수정하려고 할 post의 정보를 가져옵니다.
supabase.from.select.eq.limit.single 메써드를 섰습니다.
그리고 useFormFields 훅의 values 값에 title과 content 값을 업데이트했습니다.
그런데, values 값에 title과 content 값을 업데이트했어도 화면에 반영이 안 되는데요.
왜냐하면, useFormFields useState 훅과 useEffect 훅과의 race condition 때문인데요.
useState 초기화와 useEffect과 순서대로 useState 다음에 useEffect가 실행되지 않고, 병렬로 처리되기 때문에 그렇습니다.
그래서 values.title 값을 변경한 후에 강제로 리 렌더링 해야 하기 때문에 임의의 state를 만들었습니다.
바로 refresh, setRefresh인데요.
values.title = post.title;
values.content = post.content;
후에 setRefresh(!refresh)를 실행해서 스테이트(State)가 변경되어 리액트가 화면을 리프레쉬 되게 만드는 겁니다.
이렇게 하지 않을 경우는 아예 NextJS 다이내믹 라우팅으로 서버 사이드 쪽에서 post 자료를 리액트 컴포넌트로 넘겨주면 되거든요.
여러분께서 한번 해보시기 바랍니다.
이제 다음으로 handleSubmit함수를 살펴볼까요?
const handleSumbit = async (event: React.FormEvent) => {
console.log(user);
event.preventDefault();
const { data, error } = await supabase
.from("posts")
.update([
{
title: values.title,
content: values.content,
},
])
.match({
id: Number(id),
});
if (error) {
console.log(error);
handleMessage({
message: "Error : Create A Post",
type: "error",
});
} else {
console.log(data);
handleMessage({
message: "Success : Modified A Post",
type: "success",
});
}
resetFormFields();
router.push("/posts");
};
const deletePost = async () => {
const { data, error } = await supabase
.from("posts")
.delete()
.match({
id: Number(id),
});
if (error) {
console.log(error);
handleMessage({
message: "Error : Delete A Post",
type: "error",
});
} else {
console.log(data);
handleMessage({
message: "Success : Deleted A Post",
type: "success",
});
}
router.push("/posts");
};
form의 handleSubmit 함수와 deletePost함수입니다.
handleSubmit 함수에서는 supabase.from.update.match 메써드를 썼습니다.
사용방법은 위 코드처럼 별로 어려운 거는 없습니다.
그리고 deletePost 함수는 supabase.from.delete.match 메써드를 썼습니다.
이것도 또한 별로 어려운 건 없고요.
/pages/post/[id].tsx 파일에도 deletePost 함수를 넣어주면 됩니다.
이제 UI 부분인데요.
UI 부분은 create.tsx 파일을 그대로 썼습니다.
<form
onSubmit={handleSumbit}
className="bg-white w-3/4 md:w-1/2 shadow-md rounded px-8 pt-6 pb-8 mb-4"
>
<div className="mb-4">
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="title"
>
Title
</label>
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id="title"
name="title"
required
value={values.title}
onChange={handleChange}
/>
</div>
<div className="mb-6">
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="content"
>
Content
</label>
<textarea
className="h-72 shadow appearance-none border border-red-500 rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
id="content"
name="content"
value={values.content}
onChange={handleChange}
/>
</div>
<div className="flex gap-2">
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
type="submit"
>
Modify
</button>
<a
className="bg-transparent hover:bg-gray-600 text-sm text-gray-600 hover:text-white font-semibold py-2 px-4 border border-gray-500 hover:border-transparent rounded-lg"
onClick={deletePost}
>
Delete
</a>
</div>
</form>
이제 Modify와 Delete가 정상적으로 작동됩니다.