Supabase는 오픈 소스 Firebase 대안으로, 웹 및 모바일 애플리케이션 개발에 필요한 백엔드 서비스를 제공합니다. Supabase는 다음과 같은 핵심 기능을 제공합니다:
Supabase를 사용하면 백엔드 개발 없이도 강력한 애플리케이션을 "주말에 구축하고 수백만 명에게 확장"할 수 있습니다.
특징 | Firebase | Supabase |
---|---|---|
데이터베이스 | NoSQL(Firestore) | PostgreSQL(관계형) |
오픈소스 | ❌ | ✅ |
SQL 지원 | ❌ | ✅ |
마이그레이션 용이성 | 어려움 (독점 시스템) | 쉬움 (표준 Postgres) |
실시간 기능 | ✅ | ✅ |
인증 | ✅ | ✅ |
파일 스토리지 | ✅ | ✅ |
서버리스 함수 | Cloud Functions | Edge Functions |
가격 정책 | 사용량 기반, 높은 확장 비용 | 예측 가능한 요금제 |
로컬 개발 | 제한적 | 완전한 로컬 개발 지원 |
Supabase에서 새로운 프로젝트를 만드는 방법:
알아두면 좋은 점: Supabase는 각 프로젝트마다 완전한 PostgreSQL 데이터베이스를 제공합니다. 이는 여러분이 완전한 데이터베이스 관리 시스템에 접근할 수 있음을 의미합니다.
users
)id
, name
, email
등)uuid
, text
, varchar
, integer
등)Primary Key (기본 키):
id
열Foreign Key (외래 키):
comments
테이블의 user_id
열이 users
테이블의 id
를 참조제약 조건 설정하기:
Users 테이블 생성:
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
Posts 테이블 생성 (외래 키 포함):
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
title TEXT NOT NULL,
content TEXT,
user_id UUID REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
또는 SQL 에디터에서 INSERT 문 사용:
INSERT INTO users (name, email)
VALUES ('홍길동', 'hong@example.com');
INSERT INTO posts (title, content, user_id)
VALUES (
'첫 번째 게시물',
'안녕하세요, 이것은 제 첫 게시물입니다.',
'여기에 user_id 값 입력'
);
npm install @supabase/supabase-js
// src/supabase.js
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = '<https://your-project-url.supabase.co>';
const supabaseAnonKey = 'your-anon-key';
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
데이터 조회 (Read):
import { supabase } from './supabase';
import { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchUsers() {
try {
const { data, error } = await supabase
.from('users')
.select('*');
if (error) throw error;
setUsers(data || []);
} catch (error) {
console.error('Error fetching users:', error.message);
} finally {
setLoading(false);
}
}
fetchUsers();
}, []);
if (loading) return <div>데이터를 불러오는 중...</div>;
return (
<div>
<h2>사용자 목록</h2>
<ul>
{users.map(user => (
<li key={user.id}>{user.name} ({user.email})</li>
))}
</ul>
</div>
);
}
데이터 생성 (Create):
import { supabase } from './supabase';
import { useState } from 'react';
function CreateUser() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
async function handleSubmit(e) {
e.preventDefault();
if (!name || !email) {
setMessage('이름과 이메일을 모두 입력해주세요.');
return;
}
setLoading(true);
try {
const { data, error } = await supabase
.from('users')
.insert([{ name, email }])
.select();
if (error) throw error;
setMessage('사용자가 성공적으로 추가되었습니다!');
setName('');
setEmail('');
} catch (error) {
setMessage(`오류: ${error.message}`);
} finally {
setLoading(false);
}
}
return (
<div>
<h2>사용자 추가</h2>
{message && <p>{message}</p>}
<form onSubmit={handleSubmit}>
<div>
<label>이름:</label>
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
/>
</div>
<div>
<label>이메일:</label>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
</div>
<button type="submit" disabled={loading}>
{loading ? '처리 중...' : '사용자 추가'}
</button>
</form>
</div>
);
}
데이터 업데이트 (Update):
const { data, error } = await supabase
.from('users')
.update({ name: '새 이름' })
.eq('id', userId);
데이터 삭제 (Delete):
const { data, error } = await supabase
.from('users')
.delete()
.eq('id', userId);
Node.js와 Express로 Supabase를 사용한 API 서버를 구축하는 방법:
npm install express @supabase/supabase-js cors dotenv
// server.js
const express = require('express');
const { createClient } = require('@supabase/supabase-js');
const cors = require('cors');
require('dotenv').config();
const app = express();
app.use(cors());
app.use(express.json());
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseServiceKey = process.env.SUPABASE_SERVICE_KEY; // 서비스 롤 키 사용
const supabase = createClient(supabaseUrl, supabaseServiceKey);
// 사용자 목록 조회
app.get('/api/users', async (req, res) => {
try {
const { data, error } = await supabase
.from('users')
.select('*');
if (error) throw error;
res.json(data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 특정 사용자 조회
app.get('/api/users/:id', async (req, res) => {
try {
const { id } = req.params;
const { data, error } = await supabase
.from('users')
.select('*')
.eq('id', id)
.single();
if (error) throw error;
if (!data) {
return res.status(404).json({ error: '사용자를 찾을 수 없습니다.' });
}
res.json(data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 사용자 생성
app.post('/api/users', async (req, res) => {
try {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: '이름과 이메일은 필수 항목입니다.' });
}
const { data, error } = await supabase
.from('users')
.insert([{ name, email }])
.select();
if (error) throw error;
res.status(201).json(data[0]);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 사용자 업데이트
app.put('/api/users/:id', async (req, res) => {
try {
const { id } = req.params;
const { name, email } = req.body;
const { data, error } = await supabase
.from('users')
.update({ name, email })
.eq('id', id)
.select();
if (error) throw error;
if (data.length === 0) {
return res.status(404).json({ error: '사용자를 찾을 수 없습니다.' });
}
res.json(data[0]);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 사용자 삭제
app.delete('/api/users/:id', async (req, res) => {
try {
const { id } = req.params;
const { data, error } = await supabase
.from('users')
.delete()
.eq('id', id)
.select();
if (error) throw error;
if (data.length === 0) {
return res.status(404).json({ error: '사용자를 찾을 수 없습니다.' });
}
res.json({ message: '사용자가 성공적으로 삭제되었습니다.' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`서버가 <http://localhost>:${PORT} 에서 실행 중입니다.`);
});
중요: 서버에서는 서비스 롤 키를 사용합니다. 이 키는 RLS 정책을 우회할 수 있으므로 환경 변수로 안전하게 보관하고, 클라이언트 코드에는 절대 노출하지 마세요.
React와 Axios를 사용하여 Express API를 호출하는 방법:
npm install axios
// src/services/api.js
import axios from 'axios';
const API_URL = '<http://localhost:3001/api>';
export const userService = {
// 사용자 목록 조회
getUsers: async () => {
try {
const response = await axios.get(`${API_URL}/users`);
return response.data;
} catch (error) {
throw error.response?.data || error.message;
}
},
// 특정 사용자 조회
getUser: async (id) => {
try {
const response = await axios.get(`${API_URL}/users/${id}`);
return response.data;
} catch (error) {
throw error.response?.data || error.message;
}
},
// 사용자 생성
createUser: async (userData) => {
try {
const response = await axios.post(`${API_URL}/users`, userData);
return response.data;
} catch (error) {
throw error.response?.data || error.message;
}
},
// 사용자 업데이트
updateUser: async (id, userData) => {
try {
const response = await axios.put(`${API_URL}/users/${id}`, userData);
return response.data;
} catch (error) {
throw error.response?.data || error.message;
}
},
// 사용자 삭제
deleteUser: async (id) => {
try {
const response = await axios.delete(`${API_URL}/users/${id}`);
return response.data;
} catch (error) {
throw error.response?.data || error.message;
}
}
};
import { useState, useEffect } from 'react';
import { userService } from '../services/api';
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function loadUsers() {
try {
const data = await userService.getUsers();
setUsers(data);
} catch (err) {
setError(err.error || '사용자 데이터를 불러오는 중 오류가 발생했습니다.');
} finally {
setLoading(false);
}
}
loadUsers();
}, []);
async function handleDeleteUser(id) {
if (window.confirm('정말 이 사용자를 삭제하시겠습니까?')) {
try {
await userService.deleteUser(id);
// 삭제 후 목록 업데이트
setUsers(users.filter(user => user.id !== id));
} catch (err) {
alert(err.error || '사용자를 삭제하는 중 오류가 발생했습니다.');
}
}
}
if (loading) return <div>로딩 중...</div>;
if (error) return <div>오류: {error}</div>;
return (
<div>
<h2>사용자 목록</h2>
{users.length === 0 ? (
<p>사용자가 없습니다.</p>
) : (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} ({user.email})
<button onClick={() => handleDeleteUser(user.id)}>삭제</button>
</li>
))}
</ul>
)}
</div>
);
}
Supabase에서 데이터를 조회할 수 없는 가장 흔한 이유는 Row Level Security(RLS) 설정 때문입니다.
Row Level Security(RLS)는 PostgreSQL의 보안 기능으로, 데이터베이스 행(row) 수준에서 접근을 제어합니다. 즉, 사용자별로 볼 수 있는 데이터 행을 제한할 수 있습니다.
Supabase에서는 기본적으로 새 테이블을 만들 때 RLS가 활성화되어 있으며, 명시적인 정책을 설정하지 않으면 모든 데이터 접근이 거부됩니다.
RLS의 주요 이점:
Supabase 대시보드에서 RLS 정책을 설정하는 방법:
또는 SQL 에디터에서 직접 정책 설정:
-- 모든 사용자가 users 테이블의 데이터를 읽을 수 있도록 허용
CREATE POLICY "사용자 데이터 읽기 허용" ON users
FOR SELECT USING (true);
각 작업별로 RLS 정책을 설정하는 방법과 예시를 살펴보겠습니다.
모든 로그인한 사용자가 모든 데이터 조회 가능:
CREATE POLICY "인증된 사용자 읽기 허용" ON posts
FOR SELECT USING (auth.role() = 'authenticated');
자신의 게시물만 조회 가능:
CREATE POLICY "자신의 게시물만 읽기 허용" ON posts
FOR SELECT USING (auth.uid() = user_id);
공개된 게시물은 모든 사용자가 조회 가능:
CREATE POLICY "공개 게시물 읽기 허용" ON posts
FOR SELECT USING (is_public = true);
로그인한 사용자만 게시물 생성 가능:
CREATE POLICY "인증된 사용자 삽입 허용" ON posts
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
자신의 데이터만 삽입 가능하도록 제한:
CREATE POLICY "자신의 ID로만 게시물 생성 가능" ON posts
FOR INSERT WITH CHECK (auth.uid() = user_id);
자신의 게시물만 수정 가능:
CREATE POLICY "자신의 게시물만 수정 허용" ON posts
FOR UPDATE USING (auth.uid() = user_id);
관리자는 모든 게시물 수정 가능:
CREATE POLICY "관리자 수정 허용" ON posts
FOR UPDATE USING (
auth.uid() IN (
SELECT id FROM users WHERE is_admin = true
)
);
자신의 게시물만 삭제 가능:
CREATE POLICY "자신의 게시물만 삭제 허용" ON posts
FOR DELETE USING (auth.uid() = user_id);
오래된 게시물만 삭제 가능:
CREATE POLICY "오래된 게시물 삭제 허용" ON posts
FOR DELETE USING (created_at < NOW() - INTERVAL '1 year');
ALTER TABLE your_table DISABLE ROW LEVEL SECURITY;
-- 특정 조건에서 true/false를 반환
SELECT auth.uid() = user_id FROM posts WHERE id = 'some-post-id';
const { data: { user } } = await supabase.auth.getUser();
console.log('현재 인증된 사용자 ID:', user?.id);
Supabase는 데이터베이스 변경 사항을 실시간으로 클라이언트에 전송하는 기능을 제공합니다. 이를 통해 채팅 앱, 실시간 대시보드, 협업 도구 등을 쉽게 구현할 수 있습니다.
import { useEffect, useState } from 'react';
import { supabase } from './supabase';
function RealtimePostsList() {
const [posts, setPosts] = useState([]);
useEffect(() => {
// 초기 데이터 로드
fetchPosts();
// 실시간 구독 설정
const channel = supabase
.channel('public:posts')
.on('postgres_changes',
{
event: '*',
schema: 'public',
table: 'posts'
},
(payload) => {
console.log('실시간 변경 감지:', payload);
// 데이터 변경 유형에 따라 상태 업데이트
if (payload.eventType === 'INSERT') {
setPosts(prev => [...prev, payload.new]);
} else if (payload.eventType === 'UPDATE') {
setPosts(prev =>
prev.map(post => post.id === payload.new.id ? payload.new : post)
);
} else if (payload.eventType === 'DELETE') {
setPosts(prev =>
prev.filter(post => post.id !== payload.old.id)
);
}
}
)
.subscribe();
// 컴포넌트 언마운트 시 구독 해제
return () => {
supabase.removeChannel(channel);
};
}, []);
async function fetchPosts() {
const { data, error } = await supabase
.from('posts')
.select('*')
.order('created_at', { ascending: false });
if (error) {
console.error('게시물을 불러오는 중 오류 발생:', error);
return;
}
setPosts(data);
}
return (
<div>
<h2>실시간 게시물 목록</h2>
<ul>
{posts.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</li>
))}
</ul>
</div>
);
}
Supabase는 백엔드 개발 경험이 부족한 프론트엔드 개발자에게 훌륭한 솔루션입니다. PostgreSQL의 강력함과 Firebase의 편리함을 결합하여, 빠르게 확장 가능한 애플리케이션을 구축할 수 있습니다.
주요 장점:
Supabase를 사용하면서 데이터베이스 개념과 SQL에 익숙해진다면, 백엔드 개발 역량도 함께 향상될 것입니다. 이는 풀스택 개발자로 성장하는 데 큰 도움이 될 것입니다.
시작하기 쉬운 Supabase로 여러분의 다음 프로젝트를 빠르게 구축해보세요!