개인의 기록
  • 소개
  • 프로젝트
  • 글
  • 링크

© 2026 newgirok

← 글 목록

Supabase 프로젝트 구조 — 실무 폴더 구성 방법

2025년 5월 2일
Supabase프로젝트 구조lib/supabaseServicesHooks

Supabase를 도입하면 초반에는 컴포넌트 안에서 직접 supabase.from을 호출하는 방식으로 빠르게 시작한다. 하지만 프로젝트가 커질수록 비즈니스 로직과 DB 접근 코드가 뒤섞여 유지보수가 어려워진다. 역할을 명확히 나눈 폴더 구조가 필요한 이유다.

전체 폴더 구조

src/
├── lib/
│   └── supabase.ts          # 클라이언트 싱글톤
├── types/
│   └── database.types.ts    # supabase gen으로 자동 생성
├── repositories/
│   └── postRepository.ts    # 순수 DB 쿼리
├── services/
│   └── postService.ts       # 비즈니스 로직
└── hooks/
    └── usePosts.ts          # React 상태 + 호출

각 레이어는 단방향으로만 의존한다. Hooks → Services → Repositories → Supabase Client 순이다.

lib/supabase.ts — 클라이언트 초기화

클라이언트 싱글톤은 앱 전체에서 하나의 인스턴스를 공유한다. 환경에 따라 브라우저용과 서버용을 분리한다.

// src/lib/supabase.ts
import { createBrowserClient } from "@supabase/ssr";
import type { Database } from "@/types/database.types";

export const supabase = createBrowserClient<Database>(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

createBrowserClient: 브라우저 환경에서 세션 쿠키를 자동으로 관리하는 Supabase SSR 패키지의 함수. 서버 컴포넌트에서는 createServerClient를 사용한다.

types/database.types.ts — 자동 생성 타입

타입은 직접 작성하지 않는다. Supabase CLI로 DB 스키마에서 자동 생성한다.

npx supabase gen types typescript \
  --project-id YOUR_PROJECT_ID \
  --schema public \
  > src/types/database.types.ts

생성된 파일을 수정하지 않는다. 스키마가 바뀔 때마다 재생성한다.

repositories — 순수 DB 쿼리

Repository 패턴은 DB 접근 코드를 한 곳에 모은다. 비즈니스 로직 없이 쿼리만 담는다.

// src/repositories/postRepository.ts
import { supabase } from "@/lib/supabase";
import type { Database } from "@/types/database.types";

type Post = Database["public"]["Tables"]["posts"]["Row"];

export const postRepository = {
  async findAll(): Promise<Post[]> {
    const { data, error } = await supabase
      .from("posts")
      .select("*")
      .order("created_at", { ascending: false });

    if (error) throw error;
    return data;
  },

  async findById(id: string): Promise<Post | null> {
    const { data, error } = await supabase
      .from("posts")
      .select("*")
      .eq("id", id)
      .single();

    if (error) throw error;
    return data;
  },
};

services — 비즈니스 로직

Service 레이어는 Repository를 조합해 실제 애플리케이션 규칙을 구현한다. 예를 들어 게시글 작성 시 slug 중복 검사나 권한 확인이 여기에 들어간다.

// src/services/postService.ts
import { postRepository } from "@/repositories/postRepository";

export const postService = {
  async getPublishedPosts() {
    const posts = await postRepository.findAll();
    return posts.filter((p) => p.status === "published");
  },
};

Service 레이어: Controller(또는 Hook)와 Repository 사이에 위치하며, 단일 책임 원칙에 따라 비즈니스 규칙만 담당한다.

hooks — React 상태 연결

Custom Hook은 Service를 호출하고 로딩·에러 상태를 컴포넌트에 노출한다.

// src/hooks/usePosts.ts
import { useEffect, useState } from "react";
import { postService } from "@/services/postService";

export function usePosts() {
  const [posts, setPosts] = useState<Awaited<ReturnType<typeof postService.getPublishedPosts>>>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    postService.getPublishedPosts()
      .then(setPosts)
      .finally(() => setLoading(false));
  }, []);

  return { posts, loading };
}

레이어별 책임 요약

레이어위치책임
Clientlib/supabase.ts인스턴스 생성 및 설정
Typestypes/database.types.tsDB 스키마 타입 (자동 생성)
Repositoryrepositories/SQL 쿼리 추상화
Serviceservices/비즈니스 로직
Hookhooks/React 상태 관리

컴포넌트는 Hook만 호출하고, DB 접근 방식이 바뀌어도 Repository만 수정하면 된다. 레이어가 명확하면 테스트 작성도 쉬워진다. Service는 Repository를 Mock으로 교체해 순수 함수처럼 테스트할 수 있다.

← 이전 글Supabase 보안 — RLS와 API 키 관리 체크리스트
다음 글 →Redis — 메모리에 데이터를 저장하는 데이터베이스