문미새 개발일지

supabase 연결, 랭킹 페이지 구현 본문

개발 TIL

supabase 연결, 랭킹 페이지 구현

문미새 2025. 2. 25. 22:15
728x90

무료로 사용하기 좋은 supabase를 퍼즐 프로젝트에 연결했다.

 

supabase를 사용한 이유

supabase는 어느 회사에서 면접 보는 중에 면접관님이 알려주셨다. 협업도 중요한데 개인의 기량을 위해 개인 프로젝트 하는 것도 괜찮을 것 같다며, 무료에다 sql기반으로 혼자서 풀스택처럼 할 수 있다고 사용을 추천해주셨다.

supabase는 firebase의 대체제로 firebase와 다르게 PostgreSQL 기반의 서비스를 제공하기 때문에 데이터베이스나 파일 저장 등의 기능들이 가능하다고 한다.

Supabase의 주요 기능

  1. PostgreSQL 데이터베이스
    • 강력한 SQL 데이터베이스(JSON, 관계형 데이터, 실시간 변경 감지 지원)를 제공하며, Firebase와 다르게 완전한 SQL을 지원한다.
    • 리얼타임 데이터베이스
  2. 인증(Auth) 기능
    • 이메일, 비밀번호 로그인 지원
    • OAuth(Google, GitHub, Facebook 등) 지원
  3. 스토리지(Storage)
    • 이미지, 비디오, 파일 업로드 가능
    • S3와 비슷한 방식으로 파일 저장 및 관리 가능
  4. Edge Functions (서버리스 함수)
    • Firebase의 Cloud Functions와 유사
    • JavaScript/TypeScript로 API 엔드포인트 생성 가능
  5. 자동 API 생성
    • PostgreSQL 테이블을 만들면 자동으로 RESTful API 제공
    • API 키를 사용하여 보안 설정 가능

수파베이스에 로그인해서 테이블 생성으로 넘어가면 테이블 설정을 할 수 있다.

간단하게 테이블 이름과 설명을 작성하고 하단의 컬럼 란에 작성하면 된다.

 

테이블을 생성하면 이 페이지에서 확인할 수 있고, 실시간으로 데이터가 생성되거나 삭제되는걸 확인할 수 있다.

 

테이블을 만들었으면 클라이언트에서 연결해야 한다.

npm i @supabase/supabse-js

프로젝트에 supabase를 설치해주고

NEXT_PUBLIC_SUPABASE_URL="수파베이스 url"
NEXT_PUBLIC_SUPABASE_ANON_KEY="수파베이스 키"

.env.local 파일을 만들어서 url과 키 값을 입력해주는데, 이 값은 수파베이스 홈페이지에서 확인할 수 있다.

 

그리고 src안에 lib폴더를 만들고 supabaseClient.ts라는 요청 대체 코드를 입력해준다.

import { createClient } from "@supabase/supabase-js";

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;

export const supabase = createClient(supabaseUrl, supabaseKey);

이 코드는 다른 페이지에서 fetch나 axios역할을 하며 해당 코드로 요청을 보낼 시 CRUD값에 따라 supabase가 알아서 처리를 해준다.

 

  // 퍼즐 조각 드롭
  const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
    event.preventDefault();
    if (draggingPiece === null || changePiece === null) return;

    const newPieces = [...pieces];
    const draggedIndex = pieces.findIndex((p) => p.id === draggingPiece.id);

    const previousPieceIndex = newPieces[draggedIndex].originalIndex;
    const currentPieceIndex = newPieces[changePiece].originalIndex;

    if (previousPieceIndex !== currentPieceIndex) {
      [newPieces[draggedIndex], newPieces[changePiece]] = [
        newPieces[changePiece],
        newPieces[draggedIndex],
      ];

      setMoveCount((prev) => prev + 1);
    }

    setPieces(newPieces);
    setDraggingPiece(null);
    setChangePiece(null);

    setTimeout(() => {
      if (newPieces.every((piece, index) => piece.originalIndex === index)) {
        setIsCompleted(true);
        alert("퍼즐을 완성했습니다!");

        // 스코어 계산(이동보너스 + 시간보너스)
        let score = 1000; // 기본 점수
        const minMoves = puzzleSize * puzzleSize * 2; // 최소 이동 횟수
        let moveBonus = Math.max(0, (minMoves * 2 - moveCount) * 10);
        let timeBonus = Math.max(0, (300 - seconds) * 5);
        score += moveBonus + timeBonus;

        // 랭킹 등록
        const saveRanking = async () => {
          const { data, error } = await supabase.from("projects").insert([
            {
              name: name,
              moveCount: moveCount,
              timer: seconds,
              puzzle: puzzle,
              date: new Date().toISOString(),
              score: score,
            },
          ]);

          if (error) {
            console.error("랭킹 등록 실패", error);
            alert("랭킹 등록에 실패했습니다.");
          } else {
            console.log("랭킹 등록 성공", data);
            router.push("/ranking");
          }

          return data;
        };

        saveRanking();
      }
    }, 500);
  };

조각을 놓았을 때의 이벤트핸들러 코드이며, 조각을 놓았을 때 퍼즐이 완성되면 완성됐다는 alert과 함께 서버로 데이터를 전송하게 된다.

스코어의 경우 이동횟수와 타이머를 토대로 최소 점수를 추가해 합산하여 기록하고 있다.

 

해당 코드에선 supabase로 데이터를 보내며 등록이 성공하면 랭킹페이지로 이동한다.

"use client";
import { supabase } from "@/lib/supabaseClient";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";

interface RankingData {
  id: number;
  name: string;
  moveCount: number;
  timer: number;
  date: string;
  score: number;
}

const Ranking = () => {
  const [rankings, setRankings] = useState<RankingData[]>([]);
  const router = useRouter();

  // 랭킹 데이터 불러오기
  useEffect(() => {
    const fetchRankings = async () => {
      const { data, error } = await supabase
        .from("projects")
        .select("*")
        .order("score", { ascending: false }); // 높은 점수 순 정렬

      if (error) {
        console.error("랭킹 데이터 로드 실패", error);
        alert("랭킹 데이터를 불러오는 데 실패했습니다.");
      } else {
        setRankings(data || []);
      }
    };

    fetchRankings();
  }, []);

supabase의 기능으로 쿼리문처럼 코드를 작성할 수 있어 axios처럼 api로 요청해 데이터를 받아올 필요없이 이것도 supabase가 알아서 처리해준다. 매우 편리한듯

 

화면에 데이터가 올바르게 출력되는걸 볼 수 있고, supabase에도 데이터가 등록된 것을 확인할 수 있다.


혹시나 supabase 사용 중 401에러가 떴을 때 코드 수정으로도 안된다면, supabase에 RLS를 확인해보면 해결할 수 있다.

RLS은 Real-time Subscription Layer의 약자이며, 리얼 타임 데이터베이스의 기능이다.

해당 버튼은 테이블 페이지에서 찾을 수 있으며, 401이 떴다면  enable(활성화)이 되어있을 건데, 이 기능이 활성화되어 있다면, 인증된 사용자만 데이터에 접근이 가능하기 때문에 권한 문제로 에러가 뜬 것이다. 버튼을 클릭해서 disabled로 변경하면 따로 코드 문제가 없는 한 올바르게 데이터를 받아오고 삽입할 수 있을 것이다.

 

간단한 프로젝트지만 aws도 직접 만져보고 싶어 배포까지 진행할 예정이다. 근데 지금 페이지로는 배포하기 좀 부족해보여 따로 추가할 페이지나 기능들이 뭐가 있는지 생각 좀 해봐야할 것 같다. 아예 퍼즐만이 아니라 미니게임처럼 여러 게임을 넣는 방식도 좋아보이고, 본격적으로 퍼즐을 깊게 파도 좋을 것 같고.. 고민이다.

728x90

'개발 TIL' 카테고리의 다른 글

개발 회고  (0) 2025.03.26
vercel 배포  (0) 2025.02.27
퍼즐 만들기 메인 페이지 추가  (0) 2025.02.19
next.js에서 tailwind 에러  (0) 2025.02.18
퍼즐 만들기  (0) 2025.02.13