Supabase 학습 전에 확인할 선수 지식
이 글은 Supabase를 처음 연결하는 독자를 기준으로 시작합니다. React 컴포넌트와 Next.js App Router의 기본 구조, JavaScript의 async/await, 터미널에서 npm 명령을 실행하는 정도만 알고 있으면 됩니다. 아직 Next.js 프로젝트나 Supabase 프로젝트가 없어도 아래 순서대로 만들 수 있습니다.
- Node.js와 npm 확인
- Next.js 프로젝트 생성
- Supabase 프로젝트 생성
- Supabase SDK 설치
- 환경 변수 연결
- 서버 클라이언트 초기화
- 개발 서버 실행 확인
- 테이블·RLS 설정 후 첫 데이터 조회
1. Node.js와 npm 확인
먼저 터미널에서 Node.js와 npm을 사용할 수 있는지 확인합니다. 버전 번호가 출력되면 다음 단계로 진행할 수 있습니다.
node -v
npm -v
명령을 찾을 수 없다는 메시지가 나오면 Node.js 설치부터 진행해야 합니다. 이 글의 예제는 Node 24.19.0 환경에서 검증했습니다.
2. Next.js 프로젝트 생성
기존 Next.js 프로젝트가 없다면 새 App Router 프로젝트를 만듭니다. 아래 명령은 TypeScript와 npm을 사용하는 기본 프로젝트를 생성합니다.
npx create-next-app@latest --ts --use-npm supabase-start
cd supabase-start
이미 Next.js 프로젝트가 있다면 이 단계는 건너뛰어도 됩니다. Supabase 공식 문서에는 with-supabase 템플릿도 있지만, 이 글에서는 설치와 연결 과정을 직접 확인할 수 있도록 일반 Next.js 프로젝트에서 시작합니다. 글 하단의 실습 ZIP을 사용할 경우에는 새 프로젝트를 만들지 않고 ZIP 폴더에서 진행해도 됩니다.
3. Supabase 프로젝트 생성
Supabase Dashboard에 로그인한 뒤 개발용 프로젝트를 하나 만듭니다. 조직을 선택하고 프로젝트 이름, 데이터베이스 비밀번호, 리전을 설정합니다. 프로젝트 생성이 끝나면 Connect 화면에서 다음 두 값을 확인합니다.
- Project URL
- Publishable key
이 글에서는 공개 클라이언트용 publishable key만 사용합니다. secret 키나 기존 service_role 키는 관리자 권한을 가질 수 있으므로 NEXT_PUBLIC_ 환경 변수나 브라우저 코드에 넣으면 안 됩니다.
4. Supabase SDK 설치
프로젝트 폴더에서 Supabase JavaScript SDK와 서버 전용 모듈 표시용 패키지를 설치합니다.
npm install @supabase/supabase-js server-only
@supabase/supabase-js가 실제 Supabase 클라이언트 라이브러리이고, server-only는 이 글의 서버 조회 helper가 클라이언트 컴포넌트로 잘못 import되는 것을 막는 용도로 사용합니다.
5. 환경 변수 연결
프로젝트 루트에 .env.local 파일을 만들고 Supabase의 Connect 화면에서 확인한 값을 넣습니다. 아래 값은 형식 예시이므로 실제 프로젝트 값으로 교체합니다.
NEXT_PUBLIC_SUPABASE_URL=https://YOUR_PROJECT_REF.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=YOUR_PUBLISHABLE_KEY
실제 키가 들어 있는 .env.local은 Git에 커밋하지 않습니다. 공유용 .env.example에는 변수 이름만 남기고 값은 비워 둡니다.
6. 서버 클라이언트 초기화
이 글의 첫 목표는 로그인 없는 공개 데이터를 Server Component에서 읽는 것입니다. lib/supabase/public-server.ts를 만들고 다음처럼 클라이언트를 초기화합니다.
import 'server-only';
import { createClient } from '@supabase/supabase-js';
export function createPublicClient() {
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
const key = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY;
if (!url || !key) {
throw new Error('Supabase 공개 설정이 필요합니다.');
}
return createClient(url, key, {
auth: {
persistSession: false,
autoRefreshToken: false,
detectSessionInUrl: false,
},
});
}
여기서는 인증 세션을 다루지 않으므로 무세션 공개 조회용 클라이언트만 만듭니다. 로그인과 쿠키 기반 SSR 인증은 뒤의 별도 학습 글에서 @supabase/ssr 구조로 확장합니다.
7. 개발 서버 실행 확인
아직 데이터 조회 코드를 붙이기 전이라도 먼저 Next.js 프로젝트 자체가 정상 실행되는지 확인합니다.
npm run dev
브라우저에서 http://localhost:3000을 열어 기본 화면이 나오면 개발 환경 준비는 끝난 것입니다. .env.local을 수정했다면 개발 서버를 다시 시작합니다. 이제 테이블과 RLS 정책을 만들고 실제 데이터를 조회합니다.
8. 공개 글 한 개를 Next.js 서버에서 조회하기
이 실습은 로그인 없는 공개 읽기 전용 목록을 완성합니다. 목표는 공개 키·테이블 권한·RLS·서버 조회의 역할을 연결하고 성공 목록, 빈 목록, 연결 오류를 구분하는 것입니다. 로그인 세션과 개인 데이터는 뒤의 별도 과정에서 다룹니다.

실습 기준 환경과 준비 원칙
이 글의 검증 환경은 Node 24.19.0, Next.js 16.3.5, React 19.3.0, @supabase/supabase-js 2.115.0입니다. 직접 새 프로젝트를 만든 독자는 현재 설치된 버전으로 진행할 수 있고, 글 하단 ZIP과 동일한 상태를 재현하려면 제공된 package-lock.json 기준으로 npm ci를 사용합니다.
본인 개발 Supabase 프로젝트의 Connect 화면에서 URL과 publishable key를 확인합니다. 실제 키를 본문·저장소·로그에 적지 않습니다. publishable key는 공개 클라이언트에서 사용할 수 있는 키이며, 실제 데이터 접근 범위는 테이블 권한과 RLS 정책으로 제한합니다. Data API를 꺼 둔 프로젝트라면 Integrations의 Data API 설정도 확인합니다.
파일 구성과 요청 흐름
app/page.tsx → lib/supabase/public-server.ts → Supabase Data API → SELECT 권한과 RLS 순서입니다. 브라우저 로그인 인스턴스를 서버에 import하지 않습니다. public-server.ts는 server-only 모듈이며 요청마다 무세션 클라이언트를 만듭니다. HTML 문서는 app/layout.tsx, 스타일은 app/globals.css, 화면은 page.tsx로 나뉩니다. Next.js JSX 파일은 프레임워크 구조를 따릅니다.
서버에서 실행해도 publishable key의 권한이 관리자 권한으로 올라가지는 않습니다. 이 앱은 쿠키를 읽지 않고 인증 저장도 꺼 두었으므로 anon 역할의 공개 행만 읽습니다. force-dynamic은 요청 시 조회를 실행하게 하는 이 예제의 선택이며 모든 페이지에 무조건 적용하는 최적화 규칙은 아닙니다.
SQL: 테이블을 만들고 공개 읽기만 허용하기
schema.sql 전체를 개발 프로젝트 SQL Editor에서 한 번 실행합니다. public_posts_lab 이름이 이미 있으면 중단하고 기존 용도를 확인합니다. 이 SQL은 기존 테이블을 삭제하지 않습니다. 샘플 데이터는 모두에게 공개할 수 있는 문자열만 사용합니다. INSERT는 SQL Editor에서 준비하고 브라우저/익명 쓰기 권한은 주지 않습니다.
begin;
create table public.public_posts_lab (
id bigint generated always as identity primary key,
title text not null,
content text not null,
created_at timestamptz not null default now()
);
insert into public.public_posts_lab (title, content) values
('첫 연결 성공', '공개 읽기 전용 데이터입니다.');
alter table public.public_posts_lab enable row level security;
revoke all on public.public_posts_lab from public, anon, authenticated;
grant select on public.public_posts_lab to anon;
create policy public_posts_read on public.public_posts_lab
for select to anon using (true);
commit;
GRANT SELECT는 읽기 작업의 문을 열고, USING(true)는 anon이 테이블의 모든 행을 읽게 합니다. 따라서 이 테이블에 개인 정보를 저장해서는 안 됩니다. RLS를 켜기만 하고 정책을 만들지 않으면 데이터가 있어도 읽지 못할 수 있습니다. 반대로 정책이 있어도 테이블 권한 자체가 없으면 권한 오류가 납니다.

프로젝트 전체 파일
다음 코드는 표시한 경로의 전체 내용입니다. ZIP의 package-lock.json은 설치로 생성되는 파일이라 본문에서는 생략하고 다운로드에 포함합니다. .env.local은 사용자가 직접 작성하며 ZIP에는 들어 있지 않습니다.
package.json
{
"name": "supabase-public-posts",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build --webpack",
"start": "next start",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"next": "16.3.5",
"react": "19.3.0",
"react-dom": "19.3.0",
"@supabase/supabase-js": "2.115.0",
"server-only": "0.0.1"
},
"devDependencies": {
"typescript": "^5.9.0",
"@types/node": "^24",
"@types/react": "^19",
"@types/react-dom": "^19"
},
"engines": {
"node": ">=24.19.0 <25"
}
}
.env.example
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=
.gitignore
node_modules/
.next/
.env.local
*.tsbuildinfo
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"strict": true,
"noEmit": true,
"module": "esnext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"resolveJsonModule": true,
"esModuleInterop": true,
"skipLibCheck": true,
"plugins": [
{
"name": "next"
}
],
"allowJs": true,
"incremental": true,
"isolatedModules": true
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}
next-env.d.ts
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/types/root-params.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
app/layout.tsx
import type { ReactNode } from 'react';
import './globals.css';
export default function RootLayout({ children }: { children: ReactNode }) {
return <html lang="ko"><body>{children}</body></html>;
}
app/globals.css
body { margin: 0; font-family: system-ui, sans-serif; line-height: 1.6; }
main { max-width: 48rem; margin: 3rem auto; padding: 0 1rem; }
li { margin-block: 1rem; }
lib/supabase/public-server.ts
import 'server-only';
import { createClient } from '@supabase/supabase-js';
export function createPublicClient() {
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
const key = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY;
if (!url || !key) throw new Error('Supabase 공개 설정이 필요합니다.');
return createClient(url, key, {
auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false },
});
}
app/page.tsx
import { createPublicClient } from '../lib/supabase/public-server';
export const dynamic = 'force-dynamic';
export default async function PostsPage() {
try {
const supabase = createPublicClient();
const { data: posts, error } = await supabase
.from('public_posts_lab')
.select('id, title, content, created_at')
.order('created_at', { ascending: false });
if (error) return <main><h1>공개 글</h1><p>조회 실패: 프로젝트와 읽기 정책을 확인하세요.</p></main>;
return (
<main>
<h1>공개 글</h1>
{posts.length === 0 ? <p>등록된 글이 없습니다.</p> : (
<ul>{posts.map((post) => (
<li key={post.id}><h2>{post.title}</h2><p>{post.content}</p></li>
))}</ul>
)}
</main>
);
} catch {
return <main><h1>공개 글</h1><p>연결 설정과 네트워크를 확인하세요.</p></main>;
}
}
app/loading.tsx
export default function Loading() {
return <main><p role="status">공개 글을 불러오는 중입니다.</p></main>;
}
실행하고 세 가지 화면을 구분하기
npm ci
npm run build
npm run typecheck
npm run start
처음 파일을 손으로 만든 경우에는 npm ci 대신 npm install을 먼저 실행합니다. Next.js가 생성하는 라우트 타입이 필요하므로 처음에는 build 후 typecheck 순서를 지킵니다. 브라우저에서 http://localhost:3000을 열었을 때 “공개 글” 아래 “첫 연결 성공” 한 항목이 기대 결과입니다. 개발 중에는 npm run dev를 사용하고 환경 파일을 바꾼 뒤 서버를 재시작합니다.
| 결과 | 해석 | 확인 위치 |
|---|---|---|
| 첫 연결 성공 항목 1개 | API가 공개 행 반환 | schema.sql의 샘플과 화면을 대조 |
| 등록된 글이 없습니다 | 정상 빈 결과 또는 정책 필터링 | SQL 데이터 존재, anon SELECT 정책 |
| 조회 실패 안내 | API가 오류 반환 | Data API 활성화·테이블 이름·GRANT·RLS |
| 연결 설정과 네트워크 안내 | 설정 누락 또는 연결 예외 | 변수 이름과 존재 여부, 호스트·네트워크 |
이 예제는 오류 세부 내용과 비밀값을 화면에 표시하지 않습니다. 같은 프로젝트인지 확인할 때도 URL·키의 실제 값을 공개 로그에 출력하지 말고 본인 대시보드와 로컬 파일 안에서 대조하세요. 오류 화면의 HTTP 상태가 항상 API 오류 상태와 같지는 않으므로 화면 문구와 서버/API 결과를 따로 해석합니다.
검증 범위와 다음 학습
2026-09-13, Node 24.19.0 / npm 11.9.0에서 의존성 설치, tsc –noEmit, Next.js production build를 실제 실행해 통과했습니다. 로컬 production 서버에서 HTTP 200과 공개 설정 누락 안내 문구를 확인했습니다. 실제 Supabase 프로젝트의 SQL 실행·목록 반환·브라우저 렌더링·로그인은 검사하지 않았습니다. 성공 목록은 본인 개발 프로젝트에서 확인해야 합니다.
공개 목록은 인증 과정의 출발점입니다. 가입 확인 메일 → 사용자별 CRUD와 RLS → SSR 쿠키 인증 순서로 확장하세요. 개인 데이터를 다룰 때는 이 무세션 클라이언트 대신 요청 쿠키를 읽는 @supabase/ssr 구성이 필요합니다.
공식 확인일 2026-09-16: Next.js 빠른 시작, supabase-js 설치, API 키 안내, RLS, 쿠키 기반 SSR 클라이언트. 공식 인증 템플릿과 달리 본 실습은 공개 데이터 조회만 독립적으로 구현했습니다.

이 글이 도움이 되었나요?
Supabase 학습 순서
필수 4개 · 전체 5개
읽음 기록 관리
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.