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. 테이블 파티셔닝 | 데이터베이스는 하나로 유지하되, 물리적인 저장 공간을 | * 인프라 구조가 단순함 | * 프로젝트 생성 시 파티션 테이블을 자동으로 만들어주는 트리거/동적 로직 필요 |
| 2. 멀티 스키마 분리 | Postgres 내부에 프로젝트별 격리 공간(Schema)을 동적으로 생성하여 테이블 세트를 통째로 복제 | * 완벽한 데이터 격리 보장 | * Supabase SDK가 기본적으로 |
| 3. 데이터베이스 샤딩 | 데이터 규모가 수십 TB 이상일 때, | * 서버를 수평 확장(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 범위) 또는 시간별로 파티션 테이블을 자동으로 생성, 유지, 보수해 줍니다.
- 프로젝트 적용 팁: 데이터가 수천만 건으로 늘어날
cells나rows테이블을 프로젝트 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)가 포함된 행만 필터링"하는 기능을 구현할 때, 일반 조회 성능을 수십 배 이상 끌어올릴 수 있습니다.