🌱앱 식물 키우기 오픈!

풀스택 - 1주차

풀스택 - 1주차

강의 수강

supabase 장점

  • 오픈소스 프로젝트 (자체 서버구축 가능)

  • PostgreSQL 기반 (관계형 DB 장점을 살릴 수 있다)

  • Firebase 대비 저렴

  • 다양한 연동방식 지원

NEXT.JS 특징

  • “use server” → 서버에서만 동작함. (DB를 연결해도 됨)

  • fetch + REST API 조합 api/route.ts를 사용하여 api를 만들수 있음.

     

  • Server Actions api 구현을 하지않아도 데이터를 구현할 수 있다.

     

  • SEO를 위한 Metadata

     

투두리스트 CRUD 기능 구현

actions/todo-action.ts

"use server"; // Next.js의 서버 액션(Server Action)으로 사용하기 위해 명시해야 함.

import { Database } from "types_db";
import { createServerSupabaseClient } from "utils/supabase/server";

// 📝 todo 테이블에서 조회할 때 반환되는 데이터의 타입
export type TodoRow = Database["public"]["Tables"]["todo"]["Row"];
// 📝 todo 테이블에 새로운 데이터를 추가할 때 필요한 데이터의 타입
export type TodoRowInsert = Database["public"]["Tables"]["todo"]["Insert"];
// 📝 todo 테이블에서 기존 데이터를 수정할 때 사용할 데이터의 타입
export type TodoRowUpdate = Database["public"]["Tables"]["todo"]["Update"];

// 🔹 에러 핸들링 함수
function handleError(error: any) {
  console.error(error); // 에러 로그 출력
  throw new Error(error.message); // 에러 메시지를 포함한 예외 발생
}

export async function getTodos({ searchInput = "" }) {
  const supabase = await createServerSupabaseClient();

  // 🔹 Supabase를 통해 todo 테이블에서 데이터 조회
  const { data, error } = await supabase // ✅ `await` 추가하여 비동기 데이터 처리
    .from("todo") // 📌 todo 테이블에 접근
    .select("*") // 📌 모든 컬럼 조회
    .like("title", `%${searchInput}%`) // 📌 LIKE 연산자로 title에 searchInput이 포함된 데이터 필터링
    .order("created_at", { ascending: true }); // 📌 생성 날짜 기준 오름차순 정렬

  if (error) {
    handleError(error); // 에러 발생 시 핸들링
  }

  return data; // 조회된 데이터 반환
}

export async function createTodo(todo: TodoRowInsert) {
  const supabase = await createServerSupabaseClient(); // ✅ Supabase 서버 클라이언트 생성

  // 🔹 Supabase를 사용하여 todo 테이블에 새로운 데이터 삽입
  const { data, error } = await supabase
    .from("todo") // 📌 todo 테이블에 접근
    .insert({
      ...todo, // 📌 클라이언트에서 받은 데이터(todo) 삽입
      created_at: new Date().toISOString(), // 📌 클라이언트에서 전달된 값이 이상할 수 있으므로, 서버에서 직접 현재 시간을 생성하여 추가
    });

  if (error) {
    handleError(error); // 🔹 에러 발생 시 핸들링
  }

  return data; // ✅ 삽입된 데이터 반환
}

//todo 업데이트
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); // 📌 id가 일치하는 행만 업데이트

  if (error) {
    handleError(error);
  }

  return data;
}

//todo 삭제
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;
}

  • new Date().toISOString() 이란?

     

    현재 시간을 ISO 8601 형식의 문자열로 변환하는 메서드입니다.

    1⃣ 시간 표준화 (Time Standardization)

    문제: 서버와 클라이언트의 시간대가 다를 수 있음

    • 사용자가 다른 시간대(한국, 미국, 유럽 등)에서 앱을 사용할 수 있음.

    • 예를 들어, 한국(KST)에서 저장한 날짜가 미국(PST)에서 보면 이상하게 보일 수 있음.

    해결책:

    • 모든 시간을 UTC 기준으로 저장하여 시간대를 통일하면 문제 해결!

    • new Date().toISOString()항상 UTC(세계 표준시)로 변환되므로, 서버-클라이언트 간 시간 오차 없이 일관성 유지 가능.


    2⃣ 클라이언트와 서버 간 시간 차이 해결

    문제: 클라이언트에서 new Date()를 사용하면 각 기기의 로컬 시간이 저장됨

    • 만약 한국(KST)에서 new Date()를 저장하면 2025-03-02T23:30:45

    • 미국(PST)에서 new Date()를 저장하면 2025-03-02T06:30:45

    • 서버에서 보면 시간이 뒤죽박죽이 됨 😵

    해결책:

    • 모든 시간을 UTC 기준으로 저장(new Date().toISOString())

    • 클라이언트가 받을 때 필요한 시간대로 변환해서 사용

    • 예시:

      js
      복사편집
      const utcTime = new Date().toISOString();
      console.log(utcTime); // "2025-03-02T14:30:45.123Z" (UTC)
      
      
      • 한국(KST)에서는 2025-03-02 23:30:45로 변환해서 보여주면 됨.

      • 미국(PST)에서는 2025-03-02 06:30:45로 변환해서 보여주면 됨.


    3⃣ 데이터베이스와의 호환성

    문제: DB에서 created_at 필드를 올바르게 저장하려면?

    • Supabase(PostgreSQL)에서는 TIMESTAMP 또는 TIMESTAMP WITH TIME ZONE 타입을 사용함.

    • 클라이언트에서 로컬 시간을 넣으면, DB에서 잘못 해석할 수 있음.

    해결책:

    • ISO 8601 형식(2025-03-02T14:30:45.123Z)을 사용하면 DB에서 자동 변환 가능!

    • Supabase에서 created_atTIMESTAMP 필드라면, new Date().toISOString()을 넣으면 자동으로 올바른 값이 저장됨.

 

 

미션

  • TODO 항목 옆에 생성시간을 표시하기
    테이블에서 생성시간을 뽑아오니 utc기준으로 보여지고 있었다. 한국시간으로 바꾸기위해 day.js 라이브러리를 활용하였다. 🤟🏻 created_at(UTC)을 로컬 시간으로 변환 로컬 시간(Local Time)이란 현재 사용자의 컴퓨터(브라우저) 또는 서버에서 설정된 시간대(Time Zone)에 맞는 시간을 의미

    **npm install dayjs**
    
       <p>{dayjs(todo.created_at).format("YYYY-MM-DD HH:mm:ss")}</p>
    

     

  • completed_at 필드를 추가하여 완료한 시간도 함께 저장하기

  1. supabase에서 compledte_at 컬럼 추가

  2. 터미널에서 npm run generate-types 입력하면 types_db.ts 에 타입정보가 뜬다. completed_at 타입 추가하면 완성.

    //todo 업데이트
    export async function updateTodo(todo: TodoRowUpdate) {
      const supabase = await createServerSupabaseClient();
      const { data, error } = await supabase
        .from("todo")
        .update({
          ...todo,
          updated_at: new Date().toISOString(),
          completed_at: new Date().toISOString(), //추가
        })
        .eq("id", todo.id); // 📌 id가 일치하는 행만 업데이트
    
      if (error) {
        handleError(error);
      }
    
      return data;
    }
    
  • 검색 입력창 디바운스 적용하여 렌더링 개선.

    export default function UI() {
      const [searchInput, setSearchInput] = useState("");
      const [debouncedSearch, setDebouncedSearch] = useState(searchInput);
    
      // 검색어 입력 시 `debounce` 적용 (100ms 딜레이)
      useEffect(() => {
        const handler = setTimeout(() => {
          setDebouncedSearch(searchInput);
        }, 100);
        return () => clearTimeout(handler);
      }, [searchInput]);
    
      const {
        data: todosQuery,
        isPending,
        refetch,
      } = useQuery({
        queryKey: ["todos", debouncedSearch], // 검색어가 변경될 때마다 쿼리 새로 실행
        queryFn: () => getTodos({ searchInput: debouncedSearch }),
      });
    
    

회고

퇴근 후 매일 1시간씩 꾸준히 공부한 내 자신을 칭찬하고 싶다. 완강을 목표로 하고 있지만, 한 단계 더 나아가 다양한 기능을 직접 구현하며 부딪혀 보고 익히고 싶다. 이를 통해 한 달 후에는 스스로 미니 프로젝트를 완성해 보는 것이 목표다.

시간이 날 때 조금 더 개선해 보고 싶은 사항:

  • 리액트 훅을 hooks 폴더로 분리 → 비즈니스 로직을 컴포넌트에서 분리하여 코드의 가독성과 재사용성을 높이기

  • 리액트 쿼리 키를 별도로 관리 → 쿼리 키를 체계적으로 관리하여 유지보수성과 확장성을 강화하기

댓글을 작성해보세요.

채널톡 아이콘