Next.js에서 Supabase 사용하는 법: 설정부터 데이터 조회까지

2026.07.20·약 16분
요약
이 글은 Next.js 프로젝트에 Supabase를 연결하는 기본 흐름을 실습 순서대로 정리합니다. Supabase 프로젝트 준비, .env.local 설정, 클라이언트 분리, 서버 컴포넌트 데이터 조회, 브라우저 인증 처리, 배포 전 확인 사항까지 App Router 기준으로 설명합니다.

추가로 다른 데이터베이스가 궁금하시면 관련 글을 참고해주세요.
URL: Supabase, Neon, Firebase SQL Connect 차이

Next.js에서 Supabase를 쓰는 이유

Supabase 프로젝트에서 API URL과 anon key를 확인하는 화면 예시

Next.js는 화면 구성, 라우팅, 서버 렌더링, API 처리에 강한 프레임워크입니다. 하지만 실제 서비스를 만들다 보면 글, 사용자, 주문, 댓글 같은 데이터를 저장할 데이터베이스가 필요하고, 로그인과 권한 처리도 필요합니다. 이때 Supabase를 붙이면 PostgreSQL 데이터베이스, 인증, 파일 저장소, 실시간 구독 기능을 비교적 빠르게 사용할 수 있습니다.

중요한 점은 Next.js에서 Supabase를 단순히 “연결한다”로 끝내면 안 된다는 것입니다. App Router에서는 서버 컴포넌트와 클라이언트 컴포넌트가 나뉘고, 코드가 실행되는 위치에 따라 사용할 수 있는 키와 API 호출 방식이 달라집니다. 서버에서 데이터를 읽는 코드와 브라우저에서 로그인 상태를 다루는 코드를 같은 방식으로 작성하면 보안 문제나 렌더링 오류가 생길 수 있습니다.

이 글에서는 간단한 posts 테이블을 예시로 사용합니다. 목표는 Next.js 페이지에서 Supabase 데이터를 조회하고, 이후 인증 기능까지 확장할 수 있는 기본 구조를 잡는 것입니다.

Supabase 프로젝트 준비하기

프로젝트 생성과 API 키 확인

먼저 Supabase 대시보드에서 새 프로젝트를 만듭니다. 프로젝트가 준비되면 설정 화면에서 Project URLanon public key를 확인합니다. Project URLSupabase API 서버 주소이고, anon public key는 브라우저에 노출되어도 되는 공개 키입니다.

반대로 service_role key는 절대 브라우저 코드에 넣으면 안 됩니다. 이 키는 RLS 정책을 우회할 수 있는 강한 권한을 가지므로, 필요한 경우에도 서버 전용 환경에서만 사용해야 합니다. 입문 단계에서는 대부분 anon public key만으로 충분합니다.

예시 테이블 만들기

예시로 사용할 테이블은 posts입니다. 최소한 id, title, content, created_at 컬럼을 준비하면 글 목록 조회를 실습하기 좋습니다. id는 기본 키로 두고, created_at은 기본값을 현재 시간으로 설정합니다.

  • id: 각 글을 구분하는 기본 키
  • title: 목록에 표시할 글 제목
  • content: 글 본문 또는 요약 내용
  • created_at: 정렬에 사용할 생성 시간

처음부터 복잡한 스키마를 만들 필요는 없습니다. 연결 구조를 확인하는 단계에서는 작은 테이블 하나로 시작하고, 데이터 조회가 정상적으로 동작하는지 먼저 검증하는 편이 좋습니다.

Next.js 프로젝트에 Supabase 설치하기

Next.js 프로젝트 구조에서 Supabase 클라이언트 파일과 환경 변수 파일 위치

필요한 패키지 설치

Next.js 프로젝트가 이미 있다면 프로젝트 루트에서 @supabase/supabase-js를 설치합니다. 브라우저 데이터 조회만 다룰 때는 이 기본 클라이언트로 시작할 수 있고, 쿠키 기반 인증을 서버 컴포넌트와 함께 다룰 계획이라면 최신 Supabase 문서 기준으로 @supabase/ssr 구성까지 함께 확인합니다.

npm install @supabase/supabase-js

서버 사이드 인증 흐름까지 준비한다면 다음처럼 SSR 헬퍼 패키지를 함께 설치합니다.

npm install @supabase/supabase-js @supabase/ssr

pnpm을 사용한다면 같은 패키지를 pnpm add로 설치하면 됩니다.
자세한 내용은 해당 게시글(클릭)을 참고해보세요.

설치 후에는 Supabase 클라이언트를 프로젝트 어디에서나 재사용할 수 있도록 별도 파일로 분리하는 것이 좋습니다. 예를 들어 src/lib/supabase/browser.ts 또는 src/lib/supabase/client.ts 같은 위치를 사용할 수 있습니다.

.env.local 환경 변수 설정

Next.js에서는 로컬 개발 환경 변수를 .env.local에 저장합니다. 브라우저에서도 접근해야 하는 값은 이름 앞에 NEXT_PUBLIC_을 붙여야 합니다. Supabase 대시보드의 키 이름은 문서와 시점에 따라 anon public key 또는 publishable key로 보일 수 있으므로, 현재 프로젝트의 Connect/API 설정 화면에서 공개용 키를 확인한 뒤 다음처럼 설정합니다.

NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

환경 변수 이름은 코드에서 참조하는 이름과 정확히 일치해야 합니다. 값을 바꾼 뒤에는 개발 서버를 재시작해야 반영됩니다. npm run dev가 이미 실행 중이었다면 종료 후 다시 실행하는 것이 안전합니다.

Supabase 클라이언트 만들기

브라우저용 클라이언트

브라우저에서 사용할 기본 클라이언트는 createClient로 만들 수 있습니다. 예를 들어 src/lib/supabase/client.ts 파일을 만들고 다음처럼 작성합니다.

import { createClient } from '@supabase/supabase-js'

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!

export const supabase = createClient(supabaseUrl, supabaseAnonKey)

여기서 supabase는 브라우저 컴포넌트에서 데이터 조회나 로그인 요청을 보낼 때 사용할 수 있습니다. 단, 이 방식은 공개 가능한 anon public key를 사용하는 구조입니다. 관리자 권한이 필요한 작업이나 민감한 데이터 처리는 이 클라이언트에 맡기면 안 됩니다.

서버에서 사용할 때의 주의점

App Router의 서버 컴포넌트는 서버에서 실행됩니다. 서버 컴포넌트에서는 브라우저 전용 상태나 이벤트를 사용할 수 없지만, 데이터베이스 조회처럼 렌더링 전에 필요한 데이터를 가져오는 작업에는 적합합니다. 단, 로그인한 사용자의 세션을 서버에서 읽어야 한다면 로컬 스토리지 기반 흐름이 아니라 쿠키 기반 SSR 구성을 사용해야 합니다.

따라서 공개 목록 조회는 단순한 @supabase/supabase-js 클라이언트 예시로 시작할 수 있지만, 사용자별 데이터나 인증 상태가 필요한 화면은 최신 Supabase Next.js 문서의 @supabase/ssr 서버 클라이언트, 쿠키 갱신, 미들웨어 또는 프록시 구성을 확인한 뒤 구현합니다.

Next.js 페이지에서 Supabase 데이터 조회하기

Next.js 페이지 요청에서 Supabase 조회와 화면 렌더링으로 이어지는 데이터 흐름

서버 컴포넌트에서 목록 가져오기

App Router의 페이지 파일은 기본적으로 서버 컴포넌트입니다. 아래 코드는 인증 세션을 사용하지 않는 공개 posts 목록을 빠르게 확인하기 위한 단순 예시입니다. 로그인한 사용자 기준으로 행을 제한해야 한다면 이 예시를 그대로 확장하지 말고, 쿠키 기반 서버 클라이언트와 RLS 정책을 먼저 구성합니다.

import { supabase } from '@/lib/supabase/client'

export default async function PostsPage() {
  const { data: posts, error } = await supabase
    .from('posts')
    .select('id, title, content, created_at')
    .order('created_at', { ascending: false })

  if (error) {
    return <p>글 목록을 불러오지 못했습니다.</p>
  }

  return (
    <main>
      <h1>글 목록</h1>
      <ul>
        {posts?.map((post) => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.content}</p>
          </li>
        ))}
      </ul>
    </main>
  )
}

이 예시는 이해를 돕기 위한 공개 데이터 조회 형태입니다. from은 조회할 테이블을 지정하고, select는 가져올 컬럼을 정합니다. order를 사용하면 created_at 기준으로 최신 글을 먼저 보여줄 수 있습니다. 인증이 필요한 쿼리에서는 사용자의 세션이 서버 요청에 어떻게 전달되는지까지 함께 검증합니다.

에러와 로딩 상태 다루기

서버 컴포넌트에서 await로 데이터를 가져오면 렌더링 전에 결과가 준비됩니다. 그래서 클라이언트 컴포넌트처럼 별도의 useEffectuseState를 사용하지 않아도 됩니다. 대신 네트워크 오류, 권한 오류, 테이블 이름 오타를 구분해 로그와 화면 메시지를 남기고, error가 있을 때 빈 목록처럼 보이지 않도록 처리합니다.

Next.jsloading.tsx를 함께 사용하면 페이지 단위 로딩 UI도 만들 수 있습니다. 예를 들어 src/app/posts/loading.tsx를 추가하면 posts 페이지가 데이터를 준비하는 동안 표시할 화면을 분리할 수 있습니다.

인증 기능으로 확장하기

로그인 흐름의 기본 구조

Supabase Auth를 사용하면 이메일 로그인, 소셜 로그인, 매직 링크 같은 인증 기능을 구현할 수 있습니다. 브라우저에서 사용자가 이메일과 비밀번호를 입력하면 클라이언트 컴포넌트에서 signInWithPassword를 호출하는 식으로 시작할 수 있습니다.

'use client'

import { supabase } from '@/lib/supabase/client'

export function LoginForm() {
  async function handleLogin(formData: FormData) {
    const email = String(formData.get('email'))
    const password = String(formData.get('password'))

    const { error } = await supabase.auth.signInWithPassword({
      email,
      password,
    })

    if (error) {
      console.error(error.message)
    }
  }

  return (
    <form action={handleLogin}>
      <input name="email" type="email" />
      <input name="password" type="password" />
      <button type="submit">로그인</button>
    </form>
  )
}

위 코드에서 'use client'는 이 컴포넌트가 브라우저에서 실행되어야 한다는 뜻입니다. 사용자의 입력 이벤트, 폼 처리, 즉각적인 화면 상태 변경은 클라이언트 컴포넌트에서 다루는 편이 자연스럽습니다.

사용자별 데이터 접근 시 고려할 점

로그인 기능을 붙인 뒤에는 “누가 어떤 데이터를 볼 수 있는가”가 중요해집니다. 예를 들어 사용자마다 자신의 글만 볼 수 있어야 한다면 posts 테이블에 user_id 컬럼을 추가하고, auth.uid()를 기준으로 RLS 정책을 작성해야 합니다.

인증은 로그인 화면만 만드는 작업이 아니라 데이터 접근 규칙까지 함께 설계하는 작업입니다. 화면에서 버튼을 숨기는 것만으로는 보안이 되지 않습니다. 실제 데이터베이스 레벨에서 RLS 정책이 올바르게 적용되어야 합니다.

배포 전에 확인할 사항

환경 변수 등록

로컬의 .env.local은 배포 환경에 자동으로 올라가지 않습니다. Vercel 같은 플랫폼을 사용한다면 프로젝트 설정의 환경 변수 화면에 NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEY를 직접 등록해야 합니다.

환경 변수를 추가하거나 수정한 뒤에는 다시 배포해야 합니다. 배포된 앱에서만 오류가 발생한다면 환경 변수 이름, 값, 적용 환경을 먼저 대조합니다. 특히 Production, Preview, Development 환경이 나뉘어 있는 경우 값이 한쪽에만 들어가 있을 수 있습니다.

RLS 정책 확인

Supabase에서 RLS를 켜면 정책이 없는 테이블은 기본적으로 접근이 막힙니다. 로컬에서는 잘 되던 조회가 배포 후 실패한다면 RLS 정책이 없거나 조건이 잘못되었을 가능성이 있습니다. 공개 글 목록이라면 익명 사용자에게 읽기 권한을 주는 정책이 필요하고, 개인 데이터라면 로그인한 사용자만 자신의 행을 읽도록 제한해야 합니다.

테스트 단계에서는 편의를 위해 정책을 넓게 열고 싶을 수 있지만, 실제 서비스에서는 필요한 작업만 허용해야 합니다. insert, select, update, delete 정책을 각각 나누어 확인하면 실수를 줄일 수 있습니다.

자주 하는 실수

Next.jsSupabase를 처음 연결할 때 가장 흔한 문제는 키를 잘못 사용하는 것입니다. anon public keyservice_role key를 구분하지 않으면 보안 사고로 이어질 수 있습니다. 브라우저에 들어가는 코드는 누구나 확인할 수 있다고 보고 설계해야 합니다.

  • service_role key를 클라이언트 코드에 넣는 실수
  • .env.local 수정 후 개발 서버를 재시작하지 않는 실수
  • NEXT_PUBLIC_ 접두사가 필요한 값을 일반 환경 변수로만 선언하는 실수
  • RLS를 켰지만 읽기 정책을 만들지 않는 실수
  • 서버 컴포넌트와 클라이언트 컴포넌트의 실행 위치를 구분하지 않는 실수

또 하나의 실수는 모든 데이터 조회를 클라이언트 컴포넌트에서 처리하는 것입니다. 공개 목록처럼 페이지 렌더링에 바로 필요한 데이터는 서버 컴포넌트에서 가져오는 편이 단순하고 성능 면에서도 유리할 수 있습니다. 반대로 로그인 폼, 사용자 입력, 즉시 반응해야 하는 UI는 클라이언트 컴포넌트가 더 적합합니다.

마무리와 다음 단계

Next.js에서 Supabase를 사용하는 기본 흐름은 프로젝트 준비, 환경 변수 설정, 클라이언트 생성, 데이터 조회, 인증 확장 순서로 이해하면 됩니다. 처음에는 posts 같은 작은 테이블 하나로 시작해 연결이 정상적으로 동작하는지 확인하고, 그다음 로그인과 권한 정책을 붙이는 방식이 안정적입니다.

실무에서 중요한 기준은 실행 위치입니다. 서버 컴포넌트에서는 렌더링 전에 필요한 데이터를 가져오고, 클라이언트 컴포넌트에서는 사용자 입력과 인증 UI를 다룹니다. 여기에 RLS 정책을 더하면 화면 로직과 데이터베이스 보안을 분리해서 관리할 수 있습니다.

기본 연결을 마쳤다면 다음 단계로 Supabase Auth 로그인 구현, 사용자별 RLS 정책, Supabase Storage 이미지 업로드, Next.js 서버 액션과의 조합을 차례로 익히면 됩니다. 인증이나 서버 컴포넌트에서 세션을 다루는 단계로 넘어갈 때는 Supabase의 최신 Next.js SSR 문서를 기준으로 패키지와 쿠키 처리 방식을 다시 확인합니다.

이 글이 마음에 드세요?

RSS 피드를 구독하세요!

댓글 남기기