서버 Postgres를 브라우저 안에 실시간으로 미러링하기 — ElectricSQL Shapes와 PGlite로 구현하는 로컬 퍼스트 앱
네트워크가 불안정한 상황에서도 UI가 즉각 반응하고, 오프라인에서도 동작하면서, 여러 사용자 간 데이터가 실시간으로 맞춰져야 하는 앱. 이 세 가지를 동시에 구현하려다 WebSocket 서버, Redux, 캐시 레이어, 오프라인 큐를 차례로 쌓다 보면 코드베이스가 걷잡을 수 없이 복잡해집니다.
이 세 요구사항을 한 번에 다루는 패러다임이 **로컬 퍼스트(Local-First)**입니다. 클라이언트가 서버 DB의 필요한 부분을 로컬에 복사해 두고, UI는 항상 로컬에서만 읽습니다. 네트워크 상태와 무관하게 UI가 즉각 반응하고, 백그라운드에서 서버와 동기화가 이루어집니다.
현재 이 패턴을 웹에서 가장 깔끔하게 구현할 수 있는 조합이 ElectricSQL Shapes + PGlite입니다. 서버 PostgreSQL 변경 사항을 ElectricSQL Shape 스트림으로 받아 브라우저 내장 PostgreSQL인 PGlite에 실시간으로 반영하고, 오프라인 쓰기는 낙관적 업데이트로 즉시 UI에 반영하는 구조입니다. 이 글에서는 동작 원리, 실제 코드, 그리고 실무에서 자주 겪는 함정을 순서대로 다룹니다.
핵심 개념
로컬 퍼스트 아키텍처
전통적인 클라이언트-서버 모델에서 UI는 서버 DB에서 직접 데이터를 받아옵니다. 네트워크가 느리면 UI도 느려지고, 오프라인이면 앱이 멈춥니다. 로컬 퍼스트는 이 흐름을 뒤집습니다. 클라이언트가 서버 DB의 필요한 부분을 로컬에 복사해 두고, UI는 항상 로컬 DB에서만 읽습니다.
읽기 경로와 쓰기 경로가 분리되어 있습니다. 읽기는 항상 로컬 PGlite → UI이고, 쓰기는 UI → 백엔드 API → 서버 Postgres → Electric 스트림 → PGlite 순서입니다. Electric은 이 흐름에서 읽기 전용 단방향 싱크 엔진입니다. 클라이언트에서 PGlite에 직접 쓴다고 그 변경이 서버로 올라가지 않습니다.
ElectricSQL Shapes
ElectricSQL은 서버 PostgreSQL의 변경 사항을 클라이언트로 실시간 스트리밍하는 Postgres 싱크 엔진입니다. 2025년 3월 v1.0 GA가 출시되면서 핵심 API가 안정화됐고, v1.1에서는 새 스토리지 엔진을 도입해 v1.0 대비 서버 사이드 쓰기 처리량이 크게 개선되었습니다.
Shape는 ElectricSQL에서 동기화할 데이터의 부분 집합을 선언하는 프리미티브입니다. "이 테이블에서 이 조건에 맞는 행의 이 컬럼만 내려줘"라고 지정하면, Electric이 PostgreSQL 논리 복제 스트림을 Shape 단위로 필터링해 HTTP 스트림으로 내보냅니다.
PGlite: 브라우저 내장 PostgreSQL
PGlite는 PostgreSQL을 WebAssembly로 컴파일해 TypeScript 라이브러리로 패키징한 것입니다. 별도 OS 패키지나 외부 프로세스 없이 브라우저, Node.js, Bun, Deno에서 실행됩니다. 인메모리 또는 IndexedDB(브라우저)/파일시스템(Node) 기반 영구 저장을 지원하며, 압축 기준 수 MB 수준의 번들로 배포됩니다. 정확한 번들 크기는 버전마다 다르므로 공식 릴리스 노트에서 확인하세요.
import { PGlite } from '@electric-sql/pglite'
import { live } from '@electric-sql/pglite/live'
const db = new PGlite('idb://my-app-db', {
extensions: { live },
})
const result = await db.live.query(
'SELECT * FROM issues ORDER BY created_at DESC',
[],
(updatedResult) => {
console.log('데이터 변경됨:', updatedResult.rows)
}
)live.query가 특히 유용합니다. PGlite 내 데이터가 바뀌면 콜백이 자동으로 호출되므로, React 컴포넌트와 연결하면 별도 상태 관리 레이어 없이 UI가 반응형으로 동작합니다. 데이터베이스 자체가 상태가 됩니다.
두 가지 통합 패턴: A vs B
ElectricSQL을 React와 연결하는 방식은 크게 두 가지로 나뉘며, 이 두 패턴을 혼동하면 불필요한 복잡도가 생깁니다.
패턴 A: useShape → React 상태 (PGlite 불필요)
@electric-sql/react의 useShape 훅으로 Shape 데이터를 React 상태로 직접 받아옵니다.
import { useShape } from '@electric-sql/react'
function IssueList({ userId }: { userId: string }) {
const { isLoading, data } = useShape<{ id: string; title: string; status: string }>({
url: `${ELECTRIC_URL}/v1/shape`,
params: {
table: 'issues',
where: `user_id = '${userId}'`,
},
})
if (isLoading) return <div>로딩 중...</div>
return <ul>{data.map(issue => <li key={issue.id}>{issue.title}</li>)}</ul>
}설정이 간단하지만 오프라인 영구 저장이 없고, SQL JOIN이나 집계 쿼리를 실행할 수 없습니다. 실시간 읽기만 필요한 간단한 화면에 적합합니다.
패턴 B: syncShapeToTable → PGlite → useLiveQuery (이 글의 주된 아키텍처)
@electric-sql/pglite-sync로 Shape 스트림을 브라우저 내 PGlite 테이블에 동기화하고, @electric-sql/pglite-react의 useLiveQuery로 쿼리합니다. 오프라인 영구 저장, 복잡한 SQL 쿼리, 낙관적 쓰기 패턴이 필요하다면 패턴 B를 선택하세요. 이 글의 나머지는 패턴 B 기준입니다.
핵심 패키지 정리
| 패키지 | 역할 | 패턴 |
|---|---|---|
@electric-sql/react |
useShape 훅 |
A |
@electric-sql/pglite |
브라우저/Node용 WASM PostgreSQL | B |
@electric-sql/pglite-sync |
Electric Shape → PGlite 동기화 | B |
@electric-sql/pglite-react |
useLiveQuery 훅 |
B |
@electric-sql/pglite/worker |
UI 블로킹 방지용 Worker 래퍼 | B |
@electric-sql/pglite/live |
라이브 쿼리로 DB 변경 시 자동 리렌더링 | B |
@electric-sql/react와 @electric-sql/pglite-react는 이름이 비슷하지만 다른 패키지입니다. 패턴 B에서는 반드시 @electric-sql/pglite-react를 설치해야 합니다.
실전 적용
시나리오: 오프라인 지원 이슈 트래커
Linear 스타일의 이슈 트래커를 만드는 상황입니다. 오프라인에서도 이슈를 생성하고 상태를 변경할 수 있어야 하고, 온라인 복귀 시 자동으로 서버와 동기화되어야 합니다. ElectricSQL 공식 데모인 Linearlite가 정확히 이 패턴을 사용합니다.
보안 경계: Shape 필터는 인가가 아니다
코드를 작성하기 전에 반드시 짚고 넘어가야 할 점이 있습니다.
params: {
table: 'issues',
where: `user_id = '${userId}'`,
}이 where 절을 두고 "SQL 인젝션 위험"이라고 표현하는 경우가 있는데, 정확하지 않습니다. Electric의 where는 브라우저에서 실행되는 SQL이 아니라 Electric 서버가 평가하는 필터 표현식입니다. 진짜 문제는 다른 곳에 있습니다.
클라이언트가 where 절을 직접 선언하는 구조는 인가(authorization) 경계가 아닙니다. 악의적인 클라이언트는 where: "1=1" 또는 다른 사용자의 user_id를 그냥 넣어 Electric 서버에 요청할 수 있습니다. where 절은 조회 최적화 힌트이지 보안 경계가 아닙니다.
실무 구현에서는 Electric 앞에 인증 프록시를 두어야 합니다.
클라이언트 → 인증 미들웨어 / 게이트키퍼 → Electric 서버두 가지 접근법이 있습니다:
- Electric JWT auth 활용: Electric 서버를 JWT 인증과 함께 구성하면, 미들웨어에서 사용자 컨텍스트를 기반으로
where조건을 서버 측에서 강제할 수 있습니다. - 리버스 프록시: Nginx나 서버 미들웨어에서 요청의 JWT를 검증하고,
where절을 검증된 사용자 ID로 고정해 Electric으로 전달합니다.
프로토타입 단계에서는 클라이언트 선언 where도 충분하지만, 프로덕션 배포 전에는 이 경계를 반드시 서버 측으로 옮겨야 합니다.
전체 설정 코드
1단계: 패키지 설치
npm install @electric-sql/pglite @electric-sql/pglite-sync @electric-sql/pglite-react2단계: Worker 파일 작성
PGlite는 WASM 기반이라 메인 스레드에서 실행하면 UI가 블로킹됩니다. 항상 Worker에서 실행하세요. 중요한 점은 확장(extensions)은 Worker 내부의 PGlite 인스턴스에 등록해야 한다는 것입니다. 메인 스레드의 PGliteWorker는 Worker로 요청을 위임하는 프록시 역할만 합니다.
// pglite-worker.js
import { PGlite } from '@electric-sql/pglite'
import { live } from '@electric-sql/pglite/live'
import { electricSync } from '@electric-sql/pglite-sync'
import { registerWorkerMessageHandler } from '@electric-sql/pglite/worker'
const db = new PGlite('idb://issues-app', {
extensions: {
live,
electric: electricSync(),
},
})
registerWorkerMessageHandler(db)// lib/db.ts
import { PGliteWorker } from '@electric-sql/pglite/worker'
export const db = new PGliteWorker(
new Worker(new URL('./pglite-worker.js', import.meta.url), { type: 'module' })
)3단계: DB 초기화 및 Shape 동기화
낙관적 쓰기를 올바르게 구현하려면 테이블 설계가 중요합니다. syncShapeToTable이 소유한 issues 테이블에 직접 낙관적 상태를 INSERT하면, 다음 Electric 싱크 이벤트가 해당 행을 삭제하거나 덮어쓸 수 있습니다. 대신 별도의 issues_local 테이블에 낙관적 상태를 저장하고, 뷰(View)로 병합하는 패턴을 사용합니다.
// lib/db.ts (계속)
export async function initDb(userId: string) {
await db.exec(`
-- Electric이 소유하는 서버 동기화 테이블
CREATE TABLE IF NOT EXISTS issues (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'open',
user_id TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
-- 완전히 로컬 전용: 낙관적 상태 보관
CREATE TABLE IF NOT EXISTS issues_local (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'open',
user_id TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
-- 두 테이블을 병합한 뷰: UI는 이 뷰에서만 읽음
CREATE OR REPLACE VIEW issues_merged AS
SELECT id, title, description, status, user_id, created_at, false AS is_local
FROM issues
UNION ALL
SELECT id, title, description, status, user_id, created_at, true AS is_local
FROM issues_local
WHERE id NOT IN (SELECT id FROM issues);
`)
-- Electric은 issues 테이블만 관리. issues_local은 건드리지 않음.
await db.electric.syncShapeToTable({
shape: {
url: `${process.env.ELECTRIC_URL}/v1/shape`,
params: {
table: 'issues',
where: `user_id = '${userId}'`,
},
},
table: 'issues',
primaryKey: ['id'],
})
}issues_merged 뷰는 issues에 없는 issues_local 행만 보여줍니다. 서버에서 확정된 데이터가 issues에 들어오는 순간, issues_local의 해당 행은 뷰에서 자동으로 숨겨집니다.
4단계: 라이브 쿼리로 React 컴포넌트 연결
// hooks/useIssues.ts
import { useLiveQuery } from '@electric-sql/pglite-react'
import { db } from '../lib/db'
export function useIssues(statusFilter?: string) {
return useLiveQuery(
statusFilter
? `SELECT * FROM issues_merged WHERE status = $1 ORDER BY created_at DESC`
: `SELECT * FROM issues_merged ORDER BY created_at DESC`,
statusFilter ? [statusFilter] : [],
db
)
}5단계: 오프라인 큐와 낙관적 쓰기 구현
// lib/offline-queue.ts
type QueueEntry = {
id: string
type: string
payload: unknown
timestamp: number
}
const QUEUE_KEY = 'offline-queue'
export const offlineQueue = {
enqueue(entry: Omit<QueueEntry, 'id' | 'timestamp'>) {
const queue = this.getAll()
queue.push({ ...entry, id: crypto.randomUUID(), timestamp: Date.now() })
localStorage.setItem(QUEUE_KEY, JSON.stringify(queue))
},
getAll(): QueueEntry[] {
try {
return JSON.parse(localStorage.getItem(QUEUE_KEY) ?? '[]')
} catch {
return []
}
},
remove(id: string) {
const filtered = this.getAll().filter(e => e.id !== id)
localStorage.setItem(QUEUE_KEY, JSON.stringify(filtered))
},
}// hooks/useCreateIssue.ts
import { db } from '../lib/db'
import { offlineQueue } from '../lib/offline-queue'
export function useCreateIssue() {
const createIssue = async (title: string, description: string, userId: string) => {
const optimisticId = crypto.randomUUID()
// 낙관적 상태: Electric이 관리하는 issues가 아닌 issues_local에 저장
await db.query(
`INSERT INTO issues_local (id, title, description, status, user_id)
VALUES ($1, $2, $3, 'open', $4)`,
[optimisticId, title, description, userId]
)
try {
const response = await fetch('/api/issues', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: optimisticId, title, description }),
})
if (!response.ok) {
await db.query(`DELETE FROM issues_local WHERE id = $1`, [optimisticId])
throw new Error('이슈 생성에 실패했습니다.')
}
// 서버 쓰기 성공: 낙관적 상태 제거
// Electric이 issues 테이블에 서버 데이터를 동기화하면 뷰가 자동으로 서버 버전을 표시함
await db.query(`DELETE FROM issues_local WHERE id = $1`, [optimisticId])
} catch (error) {
if (!navigator.onLine) {
// 오프라인: 낙관적 상태 유지, 재시도 큐에 등록
offlineQueue.enqueue({
type: 'create_issue',
payload: { optimisticId, title, description, userId },
})
} else {
// 온라인이지만 서버 에러: 롤백
await db.query(`DELETE FROM issues_local WHERE id = $1`, [optimisticId])
throw error
}
}
}
return { createIssue }
}낙관적 쓰기 전체 흐름을 시퀀스 다이어그램으로 보면 이해가 빠릅니다.
오프라인 상태에서는 API 요청이 실패하지만 issues_local의 낙관적 상태가 유지되어 UI는 계속 동작합니다. 온라인 복귀 시 오프라인 큐를 처리해 API 요청을 재시도하고, Electric이 확정된 데이터를 스트리밍해 오면 뷰가 자연스럽게 서버 버전으로 전환됩니다.
// components/IssueList.tsx
import { useIssues } from '../hooks/useIssues'
import { useCreateIssue } from '../hooks/useCreateIssue'
export function IssueList({ userId }: { userId: string }) {
const { rows: issues } = useIssues()
const { createIssue } = useCreateIssue()
return (
<div>
<button onClick={() => createIssue('새 이슈', '설명...', userId)}>
이슈 추가
</button>
<ul>
{issues?.map(issue => (
<li key={issue.id} style={{ opacity: issue.is_local ? 0.6 : 1 }}>
{issue.title} — <span>{issue.status}</span>
{issue.is_local && <span> (동기화 중...)</span>}
</li>
))}
</ul>
</div>
)
}is_local 플래그를 활용하면 아직 서버에 확정되지 않은 항목을 시각적으로 구분할 수 있습니다.
멀티 탭 지원
PGlite는 기본적으로 단일 커넥션만 지원합니다. 브라우저 탭 두 개가 같은 IndexedDB를 동시에 열면 충돌이 납니다. 앞서 소개한 일반 Worker 패턴으로는 이 문제를 해결할 수 없습니다. 탭마다 별도의 Worker가 생성되어 같은 IndexedDB를 각각 열기 때문입니다.
멀티 탭 환경에서는 SharedWorker를 사용해 단일 PGlite 인스턴스를 여러 탭이 공유해야 합니다.
// shared-worker.js
import { PGlite } from '@electric-sql/pglite'
import { live } from '@electric-sql/pglite/live'
import { electricSync } from '@electric-sql/pglite-sync'
import { registerWorkerMessageHandler } from '@electric-sql/pglite/worker'
const db = new PGlite('idb://issues-app', {
extensions: {
live,
electric: electricSync(),
},
})
registerWorkerMessageHandler(db)// lib/db.ts (멀티 탭 환경)
import { PGliteWorker } from '@electric-sql/pglite/worker'
export const db = new PGliteWorker(
new SharedWorker(new URL('./shared-worker.js', import.meta.url), { type: 'module' })
)Worker 파일 내부 코드는 동일하지만 메인 스레드에서 new Worker() 대신 new SharedWorker()를 사용합니다. SharedWorker는 같은 오리진의 여러 탭이 하나의 Worker 인스턴스를 공유합니다. 단일 탭 앱이라면 일반 Worker로 충분하고, 멀티 탭 지원이 필요한 순간에 SharedWorker로 교체하면 됩니다. 상세한 설정은 PGlite 공식 문서 Multi-tab Worker를 참고하세요.
Shape 범위 결정 흐름
어떤 데이터를 Shape으로 묶을지 고민될 때 아래 흐름이 도움이 됩니다.
IndexedDB 저장소는 브라우저 정책에 따라 용량 제한이 있습니다. Shape 범위를 필요한 최소 데이터로 유지하는 것이 초기 동기화 속도와 스토리지 효율 모두에 중요합니다.
장단점 분석
전통적 방식 vs 로컬 퍼스트 비교
| 항목 | 전통적 클라이언트-서버 | ElectricSQL + PGlite |
|---|---|---|
| 읽기 레이턴시 | 네트워크 왕복 시간 | 초기 동기화 완료 후 로컬 쿼리는 사실상 0 |
| 오프라인 읽기 | 불가 또는 별도 캐시 필요 | 완전 지원 |
| 오프라인 쓰기 | 불가 또는 큐 직접 구현 | 낙관적 상태로 즉시 반영 |
| 실시간 멀티 유저 | WebSocket 서버 별도 필요 | Electric이 팬아웃 처리 |
| 상태 관리 | Redux, Zustand 등 필요 | DB 자체가 상태 |
| 브라우저 SQL | 불가 | 완전한 Postgres SQL |
| 충돌 해결 | 직접 구현 | 기본 Last-Write-Wins |
"읽기 레이턴시가 사실상 0"이라는 표현에는 조건이 있습니다. 앱 첫 로드 시 PGlite WASM 번들 다운로드와 Shape 최초 동기화 시간이 필요하고, 이 구간에서는 UI가 로딩 상태입니다. 이후 재방문부터는 IndexedDB에 캐시된 데이터로 즉시 렌더링되고, 변경 사항만 증분 동기화됩니다.
경쟁 기술 비교
| ElectricSQL + PGlite | PowerSync | Zero (Rocicorp) | |
|---|---|---|---|
| 서버 DB | Postgres 전용 | Postgres / MySQL / MongoDB | Postgres |
| 동기화 방향 | 단방향 읽기 + 별도 쓰기 API | 양방향 | 양방향 |
| 모바일 SDK | 제한적 | 성숙 | 웹 중심 |
| 브라우저 DB | PGlite (Postgres) | SQLite (wa-sqlite) | 자체 캐시 |
| 오픈 소스 | Apache 2.0 | BSL | 서버 비공개, 클라이언트 npm 공개 |
| 충돌 해결 | LWW 기본, CRDT 별도 구현 | LWW 기본, CRDT 별도 구현 | 서버 권위적 |
Zero(Rocicorp)는 전체 서버 코드가 비공개이지만 클라이언트 패키지는 npm에 공개되어 있습니다. PowerSync는 충돌 해결 면에서 Last-Write-Wins를 기본으로 하며, CRDT는 별도로 구현해야 한다는 점에서 ElectricSQL과 유사합니다. Postgres 기반 웹 앱에서 오픈 소스와 셀프 호스팅을 선호한다면 ElectricSQL + PGlite 조합이 가장 강점을 발휘합니다.
실무에서 흔히 빠지는 함정
함정 1: "Electric이 쓰기도 처리해 주겠지"
ElectricSQL은 읽기 전용 단방향 싱크 엔진입니다. 클라이언트에서 PGlite에 직접 쓴다고 그 변경이 서버로 올라가지 않습니다. 쓰기는 반드시 별도의 REST API나 GraphQL 뮤테이션을 통해 서버 Postgres에 반영한 뒤, Electric이 그 변경을 다시 스트리밍해 오는 구조입니다.
함정 2: 낙관적 상태를 Electric 관리 테이블에 직접 쓰기
// 문제가 되는 패턴: syncShapeToTable이 관리하는 테이블에 직접 INSERT
await db.query(`INSERT INTO issues (id, title, ...) VALUES (...)`)
// 다음 Electric 싱크 이벤트가 이 행을 삭제하거나 덮어쓸 수 있음
// 올바른 패턴: 별도 로컬 테이블에 낙관적 상태 보관
await db.query(`INSERT INTO issues_local (id, title, ...) VALUES (...)`)
// issues_merged 뷰로 병합해 UI에 표시함정 3: Shape를 너무 넓게 정의하기
// 피하는 것이 좋은 패턴: 테이블 전체 구독
params: { table: 'issues' }
// 권장 패턴: 현재 사용자 데이터만, 필요한 컬럼만
params: {
table: 'issues',
where: `user_id = '${userId}'`,
columns: ['id', 'title', 'status'],
}Shape 범위가 넓을수록 IndexedDB 용량과 초기 동기화 시간이 늘어납니다.
함정 4: 멀티 탭에서 DB 중복 열기
각 탭에서 new PGlite('idb://my-db')를 독립적으로 호출하면 같은 IndexedDB를 여러 커넥션이 열려 충돌이 납니다. SharedWorker 패턴으로 단일 커넥션을 공유해야 합니다.
함정 5: Last-Write-Wins를 동시 편집 앱에서 그대로 사용하기
Electric의 기본 충돌 해결 전략은 Last-Write-Wins입니다. 단순한 CRUD 앱에서는 충분하지만, 여러 사용자가 동시에 같은 필드를 편집하는 협업 앱에서는 데이터 손실이 발생할 수 있습니다.
이런 경우 CRDT(Conflict-free Replicated Data Types) 라이브러리를 통합하는 방법이 있습니다.
- Yjs: 문서 편집 중심. PGlite 컬럼에 Yjs 문서를 직렬화해 저장하고, Y.js의 병합 알고리즘으로 충돌을 해결합니다. 리치 텍스트나 협업 에디터에 적합합니다.
- Automerge: 구조화된 JSON 데이터에 적합. Automerge 문서를 PGlite 컬럼에 저장하고, 서버 병합 후 Electric으로 배포하는 패턴을 사용할 수 있습니다.
두 라이브러리 모두 PGlite와 독립적으로 동작하므로, PGlite는 데이터 저장 레이어로, CRDT 라이브러리는 충돌 해결 레이어로 역할을 분리해 사용합니다. 시작점으로는 Materialized View 팟캐스트의 ElectricSQL + CRDT 에피소드와 각 라이브러리의 공식 문서가 도움이 됩니다.
마치며
ElectricSQL Shapes + PGlite 조합의 핵심 가치는 세 가지입니다. 첫째, 초기 동기화 완료 후 UI가 로컬 DB에서 읽기 때문에 읽기 레이턴시가 사실상 없습니다. 둘째, 낙관적 쓰기 덕분에 오프라인 상태에서도 앱이 완전히 동작합니다. 셋째, Electric이 서버 변경을 모든 Shape 구독자에게 팬아웃하기 때문에 별도 WebSocket 서버 없이 실시간 멀티 유저 기능을 구현할 수 있습니다.
항상 염두에 둬야 할 것들도 있습니다. Electric은 읽기 전용 단방향 싱크이며 쓰기는 별도 API를 통해야 합니다. 낙관적 상태는 Electric 관리 테이블에 직접 쓰지 말고 별도 로컬 테이블에 보관해야 합니다. Shape 필터는 인가 경계가 아니므로 프로덕션에서는 반드시 인증 프록시를 둬야 합니다.
지금 시작해볼 수 있는 3단계:
-
Linearlite 예제 앱 클론해서 실행해 보기 — ElectricSQL 공식 GitHub에서 Linearlite를 클론하면, 실제 규모의 앱에서 이 패턴이 어떻게 동작하는지 직접 확인할 수 있습니다.
-
Electric Cloud로 기존 Postgres에 연결해 보기 — 2025년 4월 퍼블릭 베타로 출시된 Electric Cloud를 통해 셀프 호스팅 없이 기존 Postgres에 Electric을 연결해 볼 수 있습니다. 개념 검증에는 Cloud가 가장 빠른 경로입니다.
-
기존 앱의 읽기 집약적 화면 하나에 패턴 B 적용해 보기 — 전체 앱을 바꿀 필요 없이, 로딩이 느린 리스트 화면 하나에만
syncShapeToTable과useLiveQuery를 적용해 보세요. 초기 동기화 이후에는 네트워크 왕복이 없는 읽기 경험을 바로 확인할 수 있습니다.
참고 자료
- Electric 1.0 Released — ElectricSQL 공식 블로그
- Shapes - Guide — ElectricSQL 공식 문서
- Writes - Guide — ElectricSQL 공식 문서
- Sync using Electric — PGlite 공식 문서
- Getting started with PGlite — PGlite 공식 문서
- Multi-tab Worker — PGlite 공식 문서
- GitHub: electric-sql/pglite
- GitHub: electric-sql/electric
- Local-first with your existing API — ElectricSQL 블로그
- Electric Cloud 퍼블릭 베타 출시 — ElectricSQL 블로그
- Linearlite 예제 앱 — GitHub
- Write Patterns for ElectricSQL — QueryPlane
- ElectricSQL, PGLite, CRDTs, and Elixir with James Arthur — Materialized View
- Comparing local-first frameworks and approaches — Neon 블로그
- Using ElectricSQL to build a local-first application — LogRocket Blog
- Yjs CRDT 라이브러리
- Automerge CRDT 라이브러리