[Next.js] Supabase와 Server Action으로 TodoList 만들기

Jane·2024년 8월 15일
0

Next.js

목록 보기
11/12
post-thumbnail
post-custom-banner

저번 포스트에서 Supabase로 생성한 DB를 Supabase-SSR을 통해 Next.js 프로젝트에 연동하는 방법에 대해 다뤄보았다. 이번에는 불러온 데이터를 기반으로 간단하게 Next.js 14의 Server Action을 활용한 투두 리스트를 완성해본 내용을 기록할 예정이다.

🌱 이 포스트는 해당 강의를 수강한 후 수업 내용을 기반으로 작성되었으며, 이미지 클릭 시 강의 소개 페이지로 이동합니다.

🚀 Next.js 14의 Server Action

이번 포스트는 서버 액션 자체에 대한 깊이있는 설명보다는 서버 액션을 통해 Supabase로 생성된 데이터를 관리하는 방식에 대해 다룹니다.

Server Action이란

  • Next.js의 Server Action은 서버에서만 실행되는 비동기 함수를 정의할 수 있게 해주는 기능으로, 주로 데이터베이스와의 상호작용을 처리하는 데 사용된다.
  • Server Action 함수는 서버, 클라이언트 컴포넌트 내부에서 호출되어 Next.js 애플리케이션 상의 폼 제출과 데이터 mutation을 관리할 수 있다.
  • Server Action을 설정하기 위해 함수 상단에 use server 지시어를 추가하면, Next.js는 해당 함수를 클라이언트 컴포넌트에 전달할 수 있는 참조로 변환한다.
  • Server Action임을 표시하는 방법은 다양하다.
    • 지시문을 async 함수 상단에 배치해 해당 함수를 서버 액션으로 표현할 수 있다.
    • 별도의 파일 상단에 배치해서 해당 파일의 모든 export를 서버 액션으로 표시할 수도 있다.

🤠 Next.js 14에서 Server Action이 안정화되었습니다!

  • Next.js의 App Router는 프레임워크가 새로운 기능을 채택하기에 안정적인 환경을 제공하기 위해 React의 새로운 버전인 canary 채널을 기반으로 하여 구축되었다. Next.js 14버전부터는 안정적인 서버 액션을 포함하는 최신 React canary 를 지원한다.
  • 서버 액션은 폼과 FormData Web API와 같은 웹 기본 요소들을 기반으로 구현되었다.
    • FormData Web API
      • 폼 데이터를 쉽게 생성하고 관리할 수 있게 해주는 API이다.
      • 폼의 각 필드에 입력된 값을 키/값 쌍으로 만들어 서버에 전송하거나 JS단에서 쉽게 조작할 수 있게 한다.
      • FormData 객체를 통해 생성된 데이터를 mlutipart/form-data 형식으로 인코딩하여 fetch, XMLHttpRequest를 통해 서버에 전송한다.
  • 폼을 통한 서버 액션의 사용은 Progressive Enhancement(점진적 향상) 을 위해 유용하지만 필수 사항으로 요구되지는 않는다.
    • 폼 없이 직접 함수를 호출하는 것도 가능하다.
    • TypeScript 사용 시 클라이언트와 서버 간 완전한 타입 안정성 보장이 가능하다.
  • 데이터를 변경(mutate)하고, 페이지를 리렌더링하거나 리디렉션하는 행위가 한 번의 네트워크 왕복으로 이루어질 수 있다.
    • 네트워크 왕복: 네트워크 요청을 시작한 후 응답을 받는 행위
    • 이를 통해 업스트림 공급자의 속도가 느리더라도 클라이언트에 올바른 데이터가 표시되게 한다.

🌱 점진적 향상이란?

  • 필요한 모든 코드를 실행할 수 있는 최신 브라우저 사용자에게 최상의 경험을 제공하되 가능한 많은 사용자가 필수 콘텐츠와 기능을 사용할 수 있도록 하는 설계 철학
  • 기능이 제한되는 이전 브라우저 및 기기 사용자를 위해서도 완전히 동일하진 않더라도 사용 가능한 환경을 제공한다.
  • 이와 동시에 최신 브라우저/기기 사용자에게는 더욱 향상된 사용자 경험을 제공한다.

✍️ Server Action으로 투두리스트 구현하기

  • Supabase에서 제공하는 유틸 함수, 추출해온 타입 값과 Next.js 14버전의 서버 액션만을 사용하여 간단한 CRUD 기능을 구현해보았다.

작고 소중한 나의 투두리스트,,,

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
    • 해당 코드가 서버 측에서만 실행되는 것을 보장하기 위한 지시어
  • Supabase에서 DB 인트로스펙션 기능을 통해 추출해온 Database를 사용한다.
  • 데이터베이스 구조를 기반으로 필요한 값에 대한 타입을 지정해주었다.
    • 이를 통해 타입 안정성을 유지하며 Supabase 데이터베이스와의 상호작용을 진행할 수 있다.

에러 정의하기

type ErrorType = {
  message: any;
  details?: string;
  hint?: string;
  code?: string;
};

function handleError(error: ErrorType) {
  console.error(error);
  throw new Error(error.message);
}
  • supabase 측에서 전달 받은 에러를 적절한 형태로 가공하여 새로운 예외를 던지도록 하였다.

Create

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()
    • Todo 테이블에 전달 받은 새로운 할 일 값을 삽입한다.
    • created_at의 경우 클라이언트 측에서 날짜를 입력하지 않거나 잘못된 값을 전달해도 서버 측에서 알맞은 시간 값을 정의하여 안정적인 데이터 생성이 가능하게 하였다.

Read

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)
    • todo 필드가 앞뒤로 searchInput 값을 가지고 있는지를 검사한다.
    • 일치하는 값을 포함하는 데이터만 필터링하여 반환하고, 빈 문자열이 들어오면 모든 값을 조회하는 것이 된다.
  • order('created_at', {ascending: true})
    • 생성일시를 기준으로 오름차순 정렬한다.

Update

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 테이블에서 todo.idid 값이 동일한 항목을 조회하여 업데이트 대상으로 지정해준다.

Delete

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가 굉장히 편리하게 만들어져 있다는 점을 느낄 수 있었다.

React Query로 데이터 관리하기

  • Supabase와 Server Action을 통해 관리되는 데이터를 캐싱으로 관리함으로써 사용자 경험을 더욱 향상시키기 위해 React Query를 함께 사용해보았다.
  • 구현한 여러 기능 중 데이터를 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,
  };
}

🔎 References

profile
An investment in knowledge pays the best interest🙃
post-custom-banner

0개의 댓글