백엔드 개발자라면 데이터베이스 관리의 복잡함을 경험해봤을 것이다. 특히 자체 서버에 MySQL을 관리하거나, 클라우드 데이터베이스 설정에 골치가 아픈 경험을 많이 한다. 다만 대부분의 개발자들은 서버리스 데이터베이스의 존재를 알면서도 실제로 어떻게 연동하는지, 인증은 어떻게 처리하는지 정확히 모른 채 복잡한 자체 구축 방식만 고집한다. 이번에는 Supabase라는 오픈소스 Firebase 대안을 PHP에서 어떻게 활용하는지, 데이터베이스 생성부터 CRUD 작업까지 완벽하게 정리해서 소개하겠다.
Supabase는 PostgreSQL 기반의 오픈소스 백엔드-as-a-Service(BaaS) 플랫폼이다. Firebase처럼 인프라 관리 없이 데이터베이스, 인증, 스토리지 등을 제공하지만, 오픈소스이고 self-hosted가 가능하며 비용이 훨씬 저렴하다는 게 특징이다.
간단하게 말하면:
- 자체 MySQL 서버 관리할 필요 없음
- PostgreSQL 쿼리를 직접 실행 가능
- REST API와 WebSocket을 기본 제공
- JWT 기반 인증 시스템 내장
- 무료 플랜에서 충분한 용량 제공
따라서 소규모~중규모 서비스나 MVP 개발에 최적화되어 있다.
먼저 Supabase 계정을 만들고 프로젝트를 설정해야 한다.
Supabase 공식 웹사이트(https://supabase.com)에 접속해서 GitHub나 이메일로 계정을 만든다. 프로젝트를 생성할 때는 다음 항목을 입력한다:
- 프로젝트 이름: 원하는 이름 입력
- 데이터베이스 비밀번호: 강력한 암호 설정 (나중에 필요함)
- 지역(Region): 가장 가까운 지역 선택 (한국 사용자라면 Singapore 권장)
프로젝트 생성에 약 2~3분 소요된다. 완료되면 대시보드로 이동한다.
왼쪽 사이드바에서 Settings → API를 클릭한다. 여기서 다음 정보를 확인할 수 있다:
- Project URL: REST API 엔드포인트 (예: https://your-project.supabase.co)
- anon key: 클라이언트 측에서 사용하는 공개 키
- service_role key: 서버 측에서 사용하는 비공개 키 (PHP 백엔드에서 사용)
이 세 가지를 복사해서 PHP 설정 파일에 저장해둔다.
Supabase는 REST API 기반이므로 별도의 PHP 라이브러리 없이 cURL로도 통신할 수 있지만, composer를 통해 공식 라이브러리를 설치하는 것이 더 간편하다.
composer require supabase/supabase-php
프로젝트 루트에 .env 파일을 생성하고 다음 정보를 입력한다:
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=eyJhbGc...
SUPABASE_SERVICE_KEY=eyJhbGc...
이제 실제로 Supabase에서 데이터를 조회, 삽입, 수정, 삭제해보자. 먼저 Supabase 대시보드에서 테이블을 하나 만들어야 한다.
좌측 메뉴 SQL Editor에서 다음 쿼리를 실행한다:
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
age INT,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
✗ 잘못된 코드 - 직접 cURL 요청을 매번 작성하는 경우:
<?php
$url = 'https://your-project.supabase.co/rest/v1/users';
$headers = [
'Authorization: Bearer ' . $_ENV['SUPABASE_ANON_KEY'],
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
echo json_encode($data);
?>
이 방식은 작동하지만 매번 URL, 헤더, cURL 설정을 반복해야 하고 에러 처리도 불완전하다.
✓ 올바른 코드 - Supabase PHP 클라이언트 라이브러리 사용:
<?php
require 'vendor/autoload.php';
use SupabaseSupabase;
$supabase = Supabase::initializeApp(
$_ENV['SUPABASE_URL'],
$_ENV['SUPABASE_SERVICE_KEY']
);
try {
$response = $supabase
->from('users')
->select('*')
->execute();
$data = $response->data;
echo json_encode($data);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
?>
결과 출력:
[
{"id": 1, "email": "user@example.com", "name": "John", "age": 30, "created_at": "2024-01-15T10:30:00+00:00"},
{"id": 2, "email": "jane@example.com", "name": "Jane", "age": 28, "created_at": "2024-01-15T11:00:00+00:00"}
]
이 방식은 자동으로 에러 처리를 포함하고 코드가 훨씬 간결하다.
✗ 잘못된 코드 - 필터를 제대로 적용하지 않은 경우:
<?php
// 모든 데이터를 가져온 후 PHP에서 필터링 (비효율적)
$response = $supabase->from('users')->select('*')->execute();
$data = $response->data;
foreach ($data as $user) {
if ($user['age'] > 25) {
// 처리
}
}
?>
이는 불필요한 데이터 전송으로 대역폭 낭비가 크다.
✓ 올바른 코드 - 서버 측 필터링 적용:
<?php
$response = $supabase
->from('users')
->select('*')
->gt('age', 25) // age > 25
->execute();
$data = $response->data;
echo json_encode($data);
?>
주요 필터 함수:
| 함수 | 의미 | 예제 |
|---|---|---|
eq() | 같음 (=) | ->eq('status', 'active') |
neq() | 다름 (!=) | ->neq('status', 'deleted') |
gt() | 큼 (>) | ->gt('age', 25) |
gte() | 이상 (>=) | ->gte('score', 80) |
lt() | 작음 (<) | ->lt('age', 30) |
lte() | 이하 (<=) | ->lte('price', 100) |
like() | 부분 일치 | ->like('name', '%john%') |
in() | 리스트 포함 | ->in('status', ['active', 'pending']) |
✗ 잘못된 코드 - 유효성 검증 없이 데이터 삽입:
<?php
$email = $_POST['email'];
$name = $_POST['name'];
$age = $_POST['age'];
$response = $supabase
->from('users')
->insert([
'email' => $email,
'name' => $name,
'age' => $age
])
->execute();
?>
이렇게 하면 SQL 인젝션 위험과 잘못된 데이터 삽입 가능성이 있다.
✓ 올바른 코드 - 유효성 검증 및 에러 처리 포함:
<?php
require 'vendor/autoload.php';
use SupabaseSupabase;
header('Content-Type: application/json');
$email = trim($_POST['email'] ?? '');
$name = trim($_POST['name'] ?? '');
$age = intval($_POST['age'] ?? 0);
// 유효성 검증
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid email format']);
exit;
}
if (strlen($name) < 2) {
http_response_code(400);
echo json_encode(['error' => 'Name must be at least 2 characters']);
exit;
}
if ($age < 0 || $age > 150) {
http_response_code(400);
echo json_encode(['error' => 'Invalid age']);
exit;
}
try {
$supabase = Supabase::initializeApp(
$_ENV['SUPABASE_URL'],
$_ENV['SUPABASE_SERVICE_KEY']
);
$response = $supabase
->from('users')
->insert([
'email' => $email,
'name' => $name,
'age' => $age
])
->execute();
http_response_code(201);
echo json_encode([
'message' => 'User created successfully',
'data' => $response->data
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
?>
성공 응답:
{
"message": "User created successfully",
"data": [
{
"id": 3,
"email": "newuser@example.com",
"name": "Alice",
"age": 32,
"created_at": "2024-01-15T14:30:00+00:00"
}
]
}
✗ 잘못된 코드 - ID 확인 없이 업데이트:
<?php
$userId = $_POST['id'];
$name = $_POST['name'];
$response = $supabase
->from('users')
->update(['name' => $name])
->eq('id', $userId)
->execute();
?>
ID 타입 검증 없이 문자열이 들어올 수 있으므로 위험하다.
✓ 올바른 코드 - ID 검증 및 업데이트 확인:
<?php
$userId = intval($_POST['id'] ?? 0);
$name = trim($_POST['name'] ?? '');
if ($userId <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Invalid user ID']);
exit;
}
if (strlen($name) < 2) {
http_response_code(400);
echo json_encode(['error' => 'Name must be at least 2 characters']);
exit;
}
try {
$supabase = Supabase::initializeApp(
$_ENV['SUPABASE_URL'],
$_ENV['SUPABASE_SERVICE_KEY']
);
// 사용자 존재 여부 확인
$check = $supabase
->from('users')
->select('id')
->eq('id', $userId)
->execute();
if (empty($check->data)) {
http_response_code(404);
echo json_encode(['error' => 'User not found']);
exit;
}
// 업데이트 실행
$response = $supabase
->from('users')
->update(['name' => $name, 'updated_at' => date('c')])
->eq('id', $userId)
->execute();
http_response_code(200);
echo json_encode([
'message' => 'User updated successfully',
'data' => $response->data
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
?>
✗ 잘못된 코드 - 삭제 확인 없음:
<?php
$userId = $_POST['id'];
$response = $supabase
->from('users')
->delete()
->eq('id', $userId)
->execute();
?>
✓ 올바른 코드 - 존재 확인 후 삭제:
<?php
$userId = intval($_POST['id'] ?? 0);
if ($userId <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Invalid user ID']);
exit;
}
try {
$supabase = Supabase::initializeApp(
$_ENV['SUPABASE_URL'],
$_ENV['SUPABASE_SERVICE_KEY']
);
// 삭제 전 존재 확인
$check = $supabase
->from('users')
->select('id')
->eq('id', $userId)
->execute();
if (empty($check->data)) {
http_response_code(404);
echo json_encode(['error' => 'User not found']);
exit;
}
$response = $supabase
->from('users')
->delete()
->eq('id', $userId)
->execute();
http_response_code(200);
echo json_encode(['message' => 'User deleted successfully']);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
?>
✗ 문제 1: "Invalid API key" 에러가 계속 발생
원인: 환경 변수가 제대로 로드되지 않음 또는 API 키 복사 실수
해결:
.env파일이 프로젝트 루트에 있는지 확인dotenv라이브러리로 명시적으로 로드:$dotenv = DotenvDotenv::createImmutable(__DIR__); $dotenv->load();- API 키에 공백이나 줄바꿈이 없는지 확인
- Supabase 대시보드에서 API 키 다시 복사
✗ 문제 2: 데이터를 삽입했는데 조회가 안 됨
원인: 테이블 RLS(Row Level Security) 정책 문제
해결: Supabase 대시보드 → Authentication → Policies에서 테이블 정책 확인. 테스트 단계에서는 모든 작업 허용으로 설정한 후 나중에 구체적인 정책 적용
✗ 문제 3: 대량 데이터 삽입 시 타임아웃
원인: 한 번에 너무 많은 레코드를 삽입하려는 시도
해결: 배치 처리로 1000개씩 나눠 삽입
<?php
$records = [...]; // 대량의 데이터
$batch_size = 1000;
for ($i = 0; $i < count($records); $i += $batch_size) {
$batch = array_slice($records, $i, $batch_size);
$supabase->from('users')->insert($batch)->execute();
}
?>
Supabase PostgreSQL API 연동은 자체 데이터베이스 관리 부담을 크게 줄여준다. 간단한 REST API 호출로 복잡한 데이터 조작이 가능하며, JWT 기반 인증과 RLS 정책으로 보안도 뛰어나다. 유효성 검증과 에러 처리라는 작은 습관이 모여서 안정적인 백엔드를 만든다는 점을 잊지 말자. 이 글의 CRUD 코드 패턴을 참고해 실제 프로젝트에 적용하면, 데이터베이스 관리 시간을 절감하고 개발 속도를 크게 높일 수 있을 것이다.