저번 포스트에서 Supabase로 생성한 DB를 Supabase-SSR을 통해 Next.js 프로젝트에 연동하는 방법에 대해 다뤄보았다. 이번에는 불러온 데이터를 기반으로 간단하게 Next.js 14의 Server Action을 활용한 투두 리스트를 완성해본 내용을 기록할 예정이다.
🌱 이 포스트는 해당 강의를 수강한 후 수업 내용을 기반으로 작성되었으며, 이미지 클릭 시 강의 소개 페이지로 이동합니다.
이번 포스트는 서버 액션 자체에 대한 깊이있는 설명보다는 서버 액션을 통해 Supabase로 생성된 데이터를 관리하는 방식에 대해 다룹니다.
use server
지시어를 추가하면, Next.js는 해당 함수를 클라이언트 컴포넌트에 전달할 수 있는 참조로 변환한다. async
함수 상단에 배치해 해당 함수를 서버 액션으로 표현할 수 있다.export
를 서버 액션으로 표시할 수도 있다.canary
채널을 기반으로 하여 구축되었다. Next.js 14버전부터는 안정적인 서버 액션을 포함하는 최신 React canary
를 지원한다.FormData
객체를 통해 생성된 데이터를 mlutipart/form-data
형식으로 인코딩하여 fetch
, XMLHttpRequest
를 통해 서버에 전송한다.🌱 점진적 향상이란?
- 필요한 모든 코드를 실행할 수 있는 최신 브라우저 사용자에게 최상의 경험을 제공하되 가능한 많은 사용자가 필수 콘텐츠와 기능을 사용할 수 있도록 하는 설계 철학
- 기능이 제한되는 이전 브라우저 및 기기 사용자를 위해서도 완전히 동일하진 않더라도 사용 가능한 환경을 제공한다.
- 이와 동시에 최신 브라우저/기기 사용자에게는 더욱 향상된 사용자 경험을 제공한다.
action
파일 초기 설정"use server";
import { Database } from "types_db";
import { createServerSupabaseClient } from "utils/supabase/server";
// 최상단에 사용할 타입 지정
export type TodoRow = Database["public"]["Tables"]["Todo"]["Row"];
export type TodoRowInsert = Database["public"]["Tables"]["Todo"]["Insert"];
export type TodoRowUpdate = Database["public"]["Tables"]["Todo"]["Update"];
use server
Database
를 사용한다.type ErrorType = {
message: any;
details?: string;
hint?: string;
code?: string;
};
function handleError(error: ErrorType) {
console.error(error);
throw new Error(error.message);
}
insert
export async function createTodo(todo: TodoRowInsert) {
const supabase = await createServerSupabaseClient();
const { data, error } = await supabase.from("Todo").insert({
...todo,
created_at: new Date().toISOString(),
});
if (error) {
handleError(error);
}
return data;
}
createServerSupabaseClient
함수를 통해 생성된 Supabase 클라이언트 객체를 사용해 데이터베이스와 상호작용하게 된다..from('Todo').insert()
created_at
의 경우 클라이언트 측에서 날짜를 입력하지 않거나 잘못된 값을 전달해도 서버 측에서 알맞은 시간 값을 정의하여 안정적인 데이터 생성이 가능하게 하였다.export async function getTodos({ searchInput = "" }): Promise<TodoRow[]> {
const supabase = await createServerSupabaseClient();
const { data, error } = await supabase
.from("Todo")
.select("*")
.like("todo", `%${searchInput}%`)
.order("created_at", { ascending: true });
if (error) {
handleError(error);
}
return data;
}
.from("Todo").select("*")
를 통해 Todo 테이블 전체 데이터에 대한 조회를 진행했다.like("todo", searchInput)
order('created_at', {ascending: true})
export async function updateTodo(todo: TodoRowUpdate) {
const supabase = await createServerSupabaseClient();
const { data, error } = await supabase
.from("Todo")
.update({
...todo,
updated_at: new Date().toISOString(),
})
.eq("id", todo.id);
if (error) {
handleError(error);
}
return data;
}
insert
대신 update
를 사용한다.eq('id', todo.id)
todo.id
와 id 값이 동일한 항목을 조회하여 업데이트 대상으로 지정해준다.export async function deleteTodo(id: number) {
const supabase = await createServerSupabaseClient();
const { data, error } = await supabase.from("Todo").delete().eq("id", id);
if (error) {
handleError(error);
}
return data;
}
eq
메서드를 사용하여 삭제할 대상을 조회한 뒤 삭제 로직을 실행한다.서버쪽 기능까지 혼자 구현해보면서 '이렇게 쉽게 구현할 수 있다고?'라는 생각이 들 정도로 Supabase가 굉장히 편리하게 만들어져 있다는 점을 느낄 수 있었다.
GET
해오고, 검색하는 부분의 코드를 소개하고자 한다.export default function useTodo() {
const [searchInput, setSearchInput] = useState("");
// 입력받은 searchInput 값으로 데이터를 조회한다.
const { data, isLoading, refetch } = useQuery({
queryKey: [QUERY_KEY.TODO],
queryFn: () => getTodos({ searchInput }),
});
// 생성, 수정에 사용되는 mutation 함수이다.
// 데이터 변형이 성공적으로 이루어졌다면 refetch를 통해 데이터를 새롭게 불러온다.
const { mutate, isPending } = useMutation({
mutationFn: () => createTodo({ todo: "", isDone: false }),
onSuccess: () => {
refetch();
},
});
// 검색값이 제출되면 기존 조회 캐시를 무효화하고 검색 결과만을 조회한다.
const searchTodo = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
queryClient.invalidateQueries({ queryKey: [QUERY_KEY.TODO] });
refetch();
};
return {
refetch,
searchInput,
setSearchInput,
todoList: data,
isGetTodoListLoading: isLoading,
createTodo: mutate,
isCreateTodoPending: isPending,
searchTodo,
};
}