Supabase와 NextJS로 블로그 만들기 - Post Create
안녕하세요?
지난 시간에 이어 Supabase와 NextJS로 블로그 만들기에 도전해 보겠습니다.
오늘은 Create A Post를 만들어 보겠는데요.
기존에 만들어 놨던 템플릿만 잘 활용하면 오늘 주제도 금방 만들 수 있을 거 같습니다.
먼저 /pages/post/create.tsx 파일을 만들 예정입니다.
이렇게 만들면 browser 상의 주소가 http://localhost:3000/post/create 가 됩니다.
일단 User-Authenticated Page 템플릿을 준비합니다.
이 템플릿은 앞에서 벌써 여러 번 써먹었으니 꼭 알아 두시기 바랍니다.
/pages/post/create.tsx
import React from "react";
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 CreatePost = ({ user }: { user: User }) => {
const { messages, handleMessage } = useMessage();
return (
<div className="flex flex-col items-center justify-start py-4 min-h-screen">
Create A Post
</div>
);
};
export default CreatePost;
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,
},
};
};
이 템플릿은 getServerSideProps로 서버 사이드 상에서 유저 로그인 상태를 확인하고 로그인 상태가 아니면 로그인 화면으로 이동하게끔 합니다.
그래서 로그인되지 않은 상태에서는 브라우저에서 상에서는 아무것도 보이지 않게 되는 거죠.
만약 서버 사이드 상이 아니라 클라이언트 상에서 유저 로그인 상태를 확인하려면 isLoading 같은 함수를 이용해서 페이지가 로드 중이라고 표시해야 할 겁니다.
안 그러면 잠깐이나마 로그인되고 나서 보여줘야 할 페이지가 보이게 되기 때문이죠.
그래서 서버사이드 상에서 User-Authenticated Page를 구현하는 게 좀 더 안전합니다.
일단 Create A Post를 만들려면 Form이 필요하겠죠.
예전에 만들어 놓은 /lib/utils.tsx 파일에 있는 useFormFields 훅을 사용할 차례입니다.
import { useFormFields } from "../../lib/utils";
type FormFieldProps = {
title: string;
content: string;
};
const FORM_VALUES: FormFieldProps = {
title: "",
content: "",
};
const CreatePost = ({ user }: { user: User }) => {
const { messages, handleMessage } = useMessage();
const [values, handleChange, resetFormFields] =
useFormFields<FormFieldProps>(FORM_VALUES);
const handleSumbit = async (event: React.FormEvent) => {
// console.log(user);
event.preventDefault();
....
....
....
....
resetFormFields();
};
handleSubmit 함수를 처리하기 전에 먼저 Form UI를 만들어 보겠습니다.
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">
Create a Post
</h1>
<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"
>
Create
</button>
</div>
</form>
</div>
);
};
예전 로그인 화면 Form을 응용했습니다.
그런데 useFormFields 훅 관련해서 에러가 떴습니다.
다름이 아니라 TextArea 태그 쪽인데요.
아래처럼 useFormFields 훅은 HTMLInpuElement로 설정되어 있는데 TextArea는 HTMLTextAreaElement로 이루어져 있기 때문에 타입 에러가 떴습니다.
어떻게 처리해야 할까요?
/lib/utils.ts 파일을 열어서 아래와 같이 수정하면 됩니다.
import { useState } from 'react'
export function useFormFields<T>(
initialValues: T
): [T, (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void, () => void] {
const [values, setValues] = useState<T>(initialValues);
const handleChange = (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
event.persist();
const { target } = event;
const { name, value } = target;
setValues({ ...values, [name]: value });
}
const resetFormFields = () => setValues(initialValues);
return [values, handleChange, resetFormFields];
}
바뀐 부분은 바로 (event: React.ChangeEvent <HTMLInputElement | HTMLTextAreaElement>)처럼 event의 타입 부분인데요.
바로 HTMLInputElement | HTMLTextAreaElement처럼 | 연산자를 써서 두 가지의 경우가 될 수 있게끔 수정했습니다.
이제 코드가 정상 작동되는 걸 볼 수 있습니다.
이제 handleSubmit 부분에서 supabase로 데이터를 저장하는 부분만 확인하면 됩니다.
const handleSumbit = async (event: React.FormEvent) => {
console.log(user);
event.preventDefault();
const { data, error } = await supabase.from("posts").insert([
{
user_id: user.id,
user_email: user.email,
title: values.title,
content: values.content,
},
]);
if (error) {
console.log(error);
handleMessage({
message: "Error : Create A Post",
type: "error",
});
} else {
console.log(data);
}
resetFormFields();
};
SQL에서 테이블에 데이터를 추가하는 명령어는 바로 insert라는 명령어를 씁니다.
그래서 supabase.from.insert라고 사용하면 됩니다.
사용 방법은 위 코드처럼 간단하고요.
여기서 user_id 부분과 user_email 부분은 user라는 getServerSideProps에서 가져온 로그인된 user 정보를 이용했습니다.
user_id 부분은 굉장히 중요합니다.
user_id 부분은 나중에 Row Level Security에 의해 다른 사용자가 본인의 글을 보지 못하게 하는 중요한 키가 됩니다.
이제 테스트해볼까요?
위 그림처럼 Create A Post 버튼을 누르면 바로 아래 창으로 이동하게 됩니다.
여기서 테스트 겸 아무렇게나 입력해 볼까요?
위 그림처럼 브라우저 콘솔 창에 해당 정보가 아주 잘 나오고 있습니다.
그리고 supabase 대시보드로 가볼까요?
몇 가지 테스트로 만들어 놓았던 Post가 보이네요.
이제 다시 NavBar에서 Posts를 클릭해서 Posts List를 볼까요?
test 3과 test4가 잘 나오고 있습니다.
그리고 상세 페이지로 넘어가 볼까요?
상세 페이지에도 잘 나오고 있습니다.
그럼 다음 시간에는 Update 부분을 살펴보도록 하겠습니다.