Skip to content

Baserow:CloneCoding

Baserow 클론 코딩을 위한 인사이트

1. 아키텍처 접근 방식 선택

Baserow나 Airtable 같은 No-code/Low-code DB 서비스를 Supabase(PostgreSQL) 상에서 구축할 때 고려할 수 있는 핵심 패턴 두 가지입니다. 본 프로젝트에서는 구현 난이도와 유연성을 고려하여 방식 B(메타데이터 방식)를 채택합니다.

방식 A: 동적 DDL 방식 (Baserow 원본 방식)

  • 개념: 사용자가 테이블을 만들 때마다 DB에 실제로 CREATE TABLE user_table_1 ... SQL 명령어를 날려 물리적 테이블을 생성하는 방식입니다.
  • 장점: Postgres의 인덱싱, 정렬, 타입 시스템을 그대로 사용할 수 있어 대용량 조회 성능이 뛰어납니다.
  • 단점: Supabase 클라이언트 SDK(JS/TS)만으로는 구현이 불가능하며, DDL을 실행할 별도의 백엔드(Edge Functions 등)가 필요합니다. 스키마 마이그레이션 및 권한 관리가 극도로 복잡해집니다.

방식 B: 메타데이터 스키마 방식 (추천)

  • 개념: 물리적인 테이블 구조는 고정해 두고, 사용자가 생성하는 테이블과 컬럼 정보를 전부 데이터(Row) 형태로 저장하는 방식(EAV 패턴의 변형)입니다.
  • 장점: Supabase 환경에서 구현이 가장 안전하고 직관적입니다. RLS(Row Level Security) 설정이 용이하며, 스키마 변경이 자유롭습니다.
  • 단점: 데이터가 많아질수록 조인(Join) 연산이 늘어나므로 촘촘한 인덱싱 및 최적화 전략이 필수적입니다.

2. 추천 테이블 스키마 설계 (방식 B 기준)

Supabase에서 구현하기 가장 매끄러운 핵심 테이블 5개의 DDL 구조입니다. 기존의 Workspace 개념은 Project로 대체합니다.

1) projects (프로젝트)

사용자 그룹이나 조직을 관리하며, Supabase Auth와 연동됩니다.

CREATE TABLE projects (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name TEXT NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

2) tables (가상 테이블 정의)

사용자가 프로젝트 내에 생성한 "가상 테이블"의 이름과 정보를 저장합니다.

CREATE TABLE tables (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
    name TEXT NOT NULL, -- 예: "고객 명단", "할 일 목록"
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

3) columns (가상 컬럼 정의)

테이블에 포함된 컬럼의 이름과 데이터 타입(텍스트, 숫자, 날짜, 체크박스 등)을 정의합니다.

CREATE TABLE columns (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    table_id UUID REFERENCES tables(id) ON DELETE CASCADE,
    name TEXT NOT NULL, -- 예: "이메일", "나이"
    type TEXT NOT NULL, -- 예: 'text', 'number', 'date', 'boolean', 'select'
    options JSONB,      -- 선택지(Select) 타입일 경우의 옵션 목록 {'choices': ['A', 'B']}
    order_index INT NOT NULL -- 컬럼 출력 순서
);

4) rows (행 데이터 뼈대)

실제 데이터가 들어갈 가로 줄(Row)입니다. 데이터 자체는 이 테이블에 담기지 않고 순서와 생성일만 관리합니다.

CREATE TABLE rows (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    table_id UUID REFERENCES tables(id) ON DELETE CASCADE,
    order_index INT NOT NULL, -- 행 정렬 순서
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

5) cells (실제 셀 데이터 값)

가장 핵심적인 테이블로, 실제 값이 저장되는 공간입니다. Postgres의 JSONB 타입을 활용하여 다양한 데이터 타입을 유연하게 수용합니다.

CREATE TABLE cells (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    row_id UUID REFERENCES rows(id) ON DELETE CASCADE,
    column_id UUID REFERENCES columns(id) ON DELETE CASCADE,
    value JSONB, -- 어떤 타입이든 JSON 형태로 저장 (예: "홍길동", 25, true)

    -- 성능 최적화 및 데이터 무결성을 위한 복합 유니크 제약
    CONSTRAINT unique_row_column UNIQUE (row_id, column_id)
);

3. 데이터 조작 및 조회 예시

데이터 입력 (Cell 저장)

사용자가 "이메일" 컬럼에 [email protected]을 입력했을 때 cells 테이블에 적재되는 형태입니다.

{
  "row_id": "행_UUID",
  "column_id": "이메일_컬럼_UUID",
  "value": "\"[email protected]\""
}

데이터 조회 (Grid UI 출력용)

Supabase JS SDK를 사용하여 엑셀 형태의 그리드 화면을 그리기 위해 데이터를 단일 쿼리로 조회하는 예시입니다.

const { data, error } = await supabase
  .from('rows')
  .select(`
    id,
    order_index,
    cells (
      column_id,
      value
    )
  `)
  .eq('table_id', '조회하려는_테이블_UUID')
  .order('order_index', { ascending: true });
  • 프론트엔드단에서 받아온 cells 배열을 column_id를 키(Key)로 하는 오브젝트로 가공하여 UI에 매핑합니다.

4. 대용량 데이터 대응을 위한 Project 단위 지역화/격리 전략

프로젝트와 셀 데이터가 기하급수적으로 늘어날 경우 단일 테이블 구조는 성능 한계에 직면하므로 아래와 같은 격리 전략을 검토합니다.

전략

개념

장점

단점

1. 테이블 파티셔닝

데이터베이스는 하나로 유지하되, 물리적인 저장 공간을 project_id 기준으로 쪼개는 방식

* 인프라 구조가 단순함
* Supabase RLS 및 Realtime 기능 100% 활용 가능

* 프로젝트 생성 시 파티션 테이블을 자동으로 만들어주는 트리거/동적 로직 필요

2. 멀티 스키마 분리

Postgres 내부에 프로젝트별 격리 공간(Schema)을 동적으로 생성하여 테이블 세트를 통째로 복제

* 완벽한 데이터 격리 보장
* 동적 DDL 방식(방식 A) 채택 시 테이블명 충돌 방지

* Supabase SDK가 기본적으로 public 스키마를 바라보므로 백엔드에서 커넥션 제어 로직 필요

3. 데이터베이스 샤딩

데이터 규모가 수십 TB 이상일 때, project_id에 따라 실제 물리 DB 서버 자체를 분리

* 서버를 수평 확장(Horizontal Scaling)하여 무한대 대응 가능

* 비용이 높고 여러 DB 인스턴스의 마이그레이션 관리가 극도로 복잡함

5. 프로젝트 개발 로드맵 제안

초기 구현부터 무리하게 샤딩을 도입하기보다는 단계별 스케일아웃 전략을 추천합니다.

  • Phase 1 (프로토타입): 단일 테이블 구조로 시작하되, 향후 격리를 위해 모든 데이터 테이블(rows, cells 등)에 project_id 컬럼을 반드시 포함시킵니다. 인덱스는 복합 인덱스(project_id, row_id, column_id) 구조로 촘촘히 설계합니다.
  • Phase 2 (성장기): 데이터가 수천만 건을 상회하면 기존 코드를 거의 수정하지 않고 성능을 확보할 수 있는 1. 테이블 파티셔닝(Table Partitioning) 구조로 전환합니다.
  • Phase 3 (엔터프라이즈): 특정 대형 프로젝트의 부하가 전체 시스템에 영향을 줄 경우, 해당 프로젝트만 별도의 물리 Supabase 인스턴스로 이주시키는 3. 샤딩(Sharding) 체계로 전환합니다.

6. 성능 최적화를 위한 추천 Postgres Extensions

Supabase에서 제공하는 PostgreSQL 확장 기능을 활용하면 대용량 데이터 조회 및 자동 파티셔닝 구조를 훨씬 효율적으로 구축할 수 있습니다.

1) pg_partman (자동 파티셔닝 관리) ⭐️ 가장 추천

앞서 언급한 '프로젝트 단위 테이블 파티셔닝(전략 1)'을 수동으로 구현하려면 매번 테이블 생성 트리거를 짜야 해서 번거롭습니다. pg_partman은 이를 자동화해 주는 확장 기능입니다.

  • 주요 용도: ID 범위(예: Project ID 범위) 또는 시간별로 파티션 테이블을 자동으로 생성, 유지, 보수해 줍니다.
  • 프로젝트 적용 팁: 데이터가 수천만 건으로 늘어날 cellsrows 테이블을 프로젝트 ID 기반으로 동적 파티셔닝할 때 백엔드 공수를 획기적으로 줄여줍니다.

2) pg_stat_statements (쿼리 성능 병목 분석)

서비스가 느려질 때, 어떤 사용자 정의 테이블이나 어떤 셀 조회 쿼리가 병목을 일으키는지 정확히 찾아내야 합니다.

  • 주요 용도: 실행된 모든 SQL 쿼리의 실행 시간, 호출 횟수, 메모리 사용량 등의 통계를 기록합니다.
  • 프로젝트 적용 팁: Supabase 대시보드 내의 대시보드 성능 분석 탭의 기반이 되는 확장 기능입니다. 프론트엔드에서 그리드를 로딩할 때 발생하는 느린 쿼리(Slow Query)를 실시간으로 모니터링하고 인덱스를 추가할 타이밍을 잡는 데 필수적입니다.

3) pgcrypto (암호화 및 고성능 UUID)

No-code 툴 특성상 모든 가상 테이블, 컬럼, 행(Row)은 고유한 ID(UUID)를 가집니다. UUID 생성 효율이 전체 레코드 삽입 속도에 영향을 줍니다.

  • 주요 용도: gen_random_uuid() 같은 고성능 UUID 생성 함수 및 데이터 암호화 기능을 제공합니다.
  • 프로젝트 적용 팁: 기본적으로 Supabase에 내장되어 있으나, 메타데이터 구조상 수억 개의 행/셀 UUID를 생성할 때 무결성과 성능을 보장하는 핵심 뼈대가 됩니다.

4) intarray / btree_gin (JSONB 및 배열 인덱스 최적화)

메타데이터 방식에서는 columns의 옵션이나 cells의 실제 값을 JSONB 타입으로 저장합니다. 이 JSON 내부 데이터를 빠르게 검색해야 할 때 필요합니다.

  • 주요 용도: 기본 B-Tree 인덱스가 지원하지 않는 JSONB 내부 값이나 복잡한 배열 데이터에 대용량 고성능 인덱스(GIN)를 걸 수 있도록 도와줍니다.
  • 프로젝트 적용 팁: 예를 들어 사용자가 "특정 텍스트가 포함된 셀만 필터링"하거나 "특정 태그(Array)가 포함된 행만 필터링"하는 기능을 구현할 때, 일반 조회 성능을 수십 배 이상 끌어올릴 수 있습니다.

See also