🌐 웹개발

PostgREST

포스트그레스트

PostgreSQL을 RESTful API로 자동 노출. 스키마 기반 API.

📖 상세 설명

PostgREST는 PostgreSQL 데이터베이스를 RESTful API로 자동 변환하는 독립 실행형 웹 서버입니다. 별도의 백엔드 코드 없이 데이터베이스 테이블, 뷰, 함수를 HTTP 엔드포인트로 노출하여 CRUD 작업을 수행할 수 있습니다. 데이터베이스 스키마가 곧 API 명세가 되는 "스키마 퍼스트" 접근 방식입니다.

PostgREST는 PostgreSQL의 Row Level Security(RLS)를 활용한 인증/인가를 지원합니다. JWT 토큰의 클레임을 PostgreSQL role로 매핑하여, 데이터베이스 수준에서 행 단위 접근 제어가 가능합니다. 이는 애플리케이션 로직이 아닌 데이터베이스에서 보안을 강제하므로 더 안전합니다.

API 요청은 URL 파라미터를 통해 필터링, 정렬, 페이지네이션, 컬럼 선택이 가능합니다. `?select=id,name`, `?order=created_at.desc`, `?limit=10&offset=20` 같은 쿼리 문법을 지원하며, 관계형 데이터를 임베딩하여 한 번의 요청으로 조인된 데이터를 가져올 수도 있습니다.

Supabase는 PostgREST를 핵심 컴포넌트로 사용하여 "Firebase 대안"을 제공합니다. PostgREST에 실시간 구독, 인증, 스토리지 등을 추가한 완전한 BaaS(Backend as a Service)입니다. 프로토타입을 빠르게 만들거나, 간단한 CRUD 앱에서 백엔드 개발 시간을 크게 절약할 수 있습니다.

💻 코드 예제

-- === PostgreSQL 스키마 정의 ===
-- 테이블 생성
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    price DECIMAL(10, 2) NOT NULL,
    category VARCHAR(100),
    created_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE reviews (
    id SERIAL PRIMARY KEY,
    product_id INT REFERENCES products(id),
    user_id INT,
    rating INT CHECK (rating >= 1 AND rating <= 5),
    comment TEXT,
    created_at TIMESTAMP DEFAULT NOW()
);

-- Row Level Security 활성화
ALTER TABLE products ENABLE ROW LEVEL SECURITY;
ALTER TABLE reviews ENABLE ROW LEVEL SECURITY;

-- 읽기 정책: 모든 사용자가 읽기 가능
CREATE POLICY "Products are viewable by everyone"
    ON products FOR SELECT
    USING (true);

-- 쓰기 정책: 인증된 사용자만 리뷰 작성 가능
CREATE POLICY "Users can insert their own reviews"
    ON reviews FOR INSERT
    WITH CHECK (auth.uid() = user_id);

-- API를 위한 뷰 생성
CREATE VIEW product_with_rating AS
SELECT
    p.*,
    COALESCE(AVG(r.rating), 0) AS avg_rating,
    COUNT(r.id) AS review_count
FROM products p
LEFT JOIN reviews r ON p.id = r.product_id
GROUP BY p.id;

// === JavaScript/TypeScript 클라이언트 사용 예시 ===

// 기본 CRUD 작업
const API_URL = 'https://your-project.supabase.co/rest/v1';
const API_KEY = 'your-anon-key';

const headers = {
    'apikey': API_KEY,
    'Authorization': `Bearer ${API_KEY}`,
    'Content-Type': 'application/json',
    'Prefer': 'return=representation' // INSERT/UPDATE 후 결과 반환
};

// GET - 전체 상품 조회
const getAllProducts = async () => {
    const response = await fetch(`${API_URL}/products`, { headers });
    return response.json();
};

// GET - 필터링, 정렬, 페이지네이션
const getFilteredProducts = async () => {
    const params = new URLSearchParams({
        'select': 'id,name,price,category', // 컬럼 선택
        'category': 'eq.electronics',        // 필터: category = 'electronics'
        'price': 'lte.1000',                 // 필터: price <= 1000
        'order': 'price.asc',                // 정렬: 가격 오름차순
        'limit': '10',                       // 페이지 크기
        'offset': '0'                        // 시작 위치
    });

    const response = await fetch(`${API_URL}/products?${params}`, { headers });
    return response.json();
};

// GET - 관계 데이터 임베딩 (JOIN)
const getProductsWithReviews = async () => {
    // 상품과 함께 리뷰도 가져오기
    const response = await fetch(
        `${API_URL}/products?select=*,reviews(*)`,
        { headers }
    );
    return response.json();
};

// GET - 뷰 조회 (평균 평점 포함)
const getProductsWithRating = async () => {
    const response = await fetch(
        `${API_URL}/product_with_rating?order=avg_rating.desc`,
        { headers }
    );
    return response.json();
};

// POST - 상품 추가
const createProduct = async (product: {
    name: string;
    price: number;
    category: string;
}) => {
    const response = await fetch(`${API_URL}/products`, {
        method: 'POST',
        headers,
        body: JSON.stringify(product)
    });
    return response.json();
};

// PATCH - 상품 수정
const updateProduct = async (id: number, updates: Partial<Product>) => {
    const response = await fetch(`${API_URL}/products?id=eq.${id}`, {
        method: 'PATCH',
        headers,
        body: JSON.stringify(updates)
    });
    return response.json();
};

// DELETE - 상품 삭제
const deleteProduct = async (id: number) => {
    await fetch(`${API_URL}/products?id=eq.${id}`, {
        method: 'DELETE',
        headers
    });
};

// === Supabase JavaScript 클라이언트 (더 간편한 방식) ===
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
    'https://your-project.supabase.co',
    'your-anon-key'
);

// 동일한 작업을 더 간편하게
const { data, error } = await supabase
    .from('products')
    .select('*, reviews(*)')
    .eq('category', 'electronics')
    .lte('price', 1000)
    .order('price', { ascending: true })
    .range(0, 9);

// 실시간 구독
const subscription = supabase
    .channel('products-changes')
    .on('postgres_changes',
        { event: '*', schema: 'public', table: 'products' },
        (payload) => {
            console.log('변경 감지:', payload);
        }
    )
    .subscribe();

🗣️ 실무 대화 예시

PM

"백엔드 개발자 없이 빠르게 프로토타입을 만들어야 하는데, 어떻게 하면 좋을까요?"

개발자

"Supabase나 PostgREST를 쓰면 됩니다. PostgreSQL에 테이블만 정의하면 자동으로 REST API가 생성돼요. CRUD는 물론 필터링, 페이지네이션, JOIN까지 URL 파라미터로 처리할 수 있어서 별도 백엔드 코드 없이 프론트엔드만으로 기능 구현이 가능해요."

동료

"PostgREST로 API를 만들면 보안은 어떻게 처리해요?"

개발자

"PostgreSQL의 Row Level Security(RLS)를 사용해요. 데이터베이스 수준에서 '이 행은 이 사용자만 접근 가능'처럼 정책을 정의하면, PostgREST가 JWT 토큰을 파싱해서 해당 role로 쿼리를 실행해요. 애플리케이션이 아닌 DB에서 보안을 강제하니까 더 안전합니다."

면접관

"PostgREST의 한계점은 무엇인가요?"

지원자

"복잡한 비즈니스 로직이나 외부 API 연동이 필요한 경우 한계가 있어요. 단순 CRUD를 넘어서는 작업은 PostgreSQL 함수로 구현하거나 별도 서비스가 필요합니다. 또한 스키마 변경이 곧 API 변경이므로, 버전 관리와 하위 호환성 유지에 주의해야 해요."

⚠️ 주의사항

RLS 필수: PostgREST를 프로덕션에서 사용할 때는 반드시 Row Level Security를 활성화하세요. RLS 없이 공개하면 누구나 모든 데이터에 접근할 수 있습니다. 테이블별로 적절한 SELECT, INSERT, UPDATE, DELETE 정책을 정의해야 합니다.

스키마 노출: PostgREST는 데이터베이스 스키마를 그대로 API로 노출합니다. 민감한 컬럼이나 테이블은 별도 스키마로 분리하거나, 뷰를 사용해 필요한 컬럼만 노출하세요. 기본적으로 public 스키마만 노출됩니다.

복잡한 로직: 트랜잭션이 필요한 복잡한 작업, 외부 서비스 연동, 비동기 작업 등은 PostgreSQL 함수나 별도 백엔드가 필요합니다. PostgREST는 CRUD 중심의 간단한 API에 최적화되어 있습니다.

🔗 관련 용어

📚 더 배우기