Electric SQL 완벽 가이드 — 로컬 우선 Postgres·Shape 동기화·SQLite·React
이 글의 핵심
Electric은 PostgreSQL에서 변경분을 읽어 Shape 단위로 클라이언트에 실시간 전달하는 동기화 엔진입니다. 읽기 경로는 Electric, 쓰기 경로는 API·낙관적 상태·로컬 DB 등 팀이 선택해 설계하는 구조를 이해하면 로컬 우선 앱을 안전하게 만들 수 있습니다.
이 글의 핵심
Electric SQL(이하 Electric)은 PostgreSQL에 적재된 데이터의 변경을 추적하고, 정의한 Shape(부분 집합) 단위로 클라이언트·엣지·다른 서비스에 실시간·증분으로 전달하는 오픈소스 동기화 엔진입니다. “로컬 우선(local-first)” 앱에서 말하는 빠른 읽기·오프라인 읽기를 가능하게 하는 읽기 경로 동기화에 초점을 맞추며, 쓰기를 Postgres에 반영하는 방식은 프레임워크가 하나로 강제하지 않고 기존 백엔드·패턴과 조합할 수 있게 설계되어 있습니다.
이 글에서는 다음을 순서대로 다룹니다.
- Electric의 역할과 경계(무엇을 해 주고, 무엇을 개발자가 맡는지)
- Shape 기반 동기화의 정의, HTTP·TypeScript 사용법, 보안·성능 이슈
- 로컬 저장소로서의 SQLite·PGlite·메모리 Shape의 선택 기준
- Electric이 직접 제공하지 않는 쓰기 경로와 그에 따른 충돌·머지·롤백 전략
- TypeScript 타입·파싱·스키마 연계
- React(
@electric-sql/react) 통합과 프로덕션 프록시 패턴 - 실전 오프라인 지향 앱을 위한 아키텍처 체크리스트
참고: 제품명·플래그·패키지 버전은 시기에 따라 달라질 수 있습니다. 운영 전 Electric 공식 문서와 GitHub 저장소의 최신 안내를 반드시 확인하시기 바랍니다.
1. Electric SQL의 핵심 개념
1-1. Postgres를 “진실 공급원”으로 두는 읽기 동기화
Electric은 논리적 복제(logical replication) 등을 통해 Postgres의 변경 스트림을 받아, 구독 중인 Shape에 해당하는 행·열의 갱신을 클라이언트에 푸시에 가까운 방식으로 전달합니다. 애플리케이션 입장에서는 서버 DB 상태가 곧 권위 있는 읽기 소스가 되며, 클라이언트는 동기화된 로컬 표현(메모리, SQLite, PGlite 등) 위에서 UI를 그립니다.
1-2. “동기화 엔진”과 “앱 스택”의 분리
최근 문서에서는 Postgres Sync를 중심으로, UI·상태 계층으로 TanStack DB 등과 조합하는 예시가 강조됩니다. 즉 Electric은 데이터 파이프의 한 구간이고, 라우팅·인증·비즈니스 규칙·쓰기 API는 기존과 같이 백엔드·BaaS에서 다루는 것이 일반적입니다.
1-3. 로컬 우선에서의 책임 분리
| 영역 | Electric의 위치 | 애플리케이션/백엔드가 맡는 일 |
|---|---|---|
| 읽기 | Postgres→클라이언트 Shape 스트림 | Shape 범위 설계, 캐시 정책 |
| 쓰기 | 직접 제공하지 않음 | API, 트랜잭션, 권한, 검증 |
| 보안 | Shape 파라미터는 민감 | 토큰 검증, 행 단위 접근 제어, 프록시 |
| 오프라인 | 읽기 스트림 재개·로컬 보관 | 쓰기 큐, 재시도, 충돌 UX |
이 표가 곧 아키텍처 논의의 출발점입니다. “Electric만 켰다고 오프라인 쓰기가 자동 완성”되지는 않으며, 쓰기 경로를 의도적으로 설계해야 합니다.
2. Shape 기반 동기화
2-1. Shape란?
Shape는 “클라우드 Postgres에 있는 많은 데이터 중, 이 기기·이 세션에 실어 나를 subset”을 정의하는 핵심 단위입니다. 전 테이블을 무조건 내려받는 대신, 필요한 행·열만 동기화하여 네트워크·저장·개인정보 노출을 제어합니다.
공식 가이드에 따르면 Shape는 최소 다음으로 정의됩니다.
- 테이블: 루트가 되는 Postgres 테이블(스키마 접두어
schema.table가능) - 선택적 WHERE: 포함할 행을 SQL 표현식으로 필터
- 선택적 columns: 동기화할 열 목록(단, 기본키 열은 반드시 포함)
여러 클라이언트가 동일 Shape를 구독할 수 있고, Shape끼리 겹치는 행이 있을 수 있습니다.
2-2. HTTP API로 구독하기
클라이언트는 일반적으로 GET /v1/shape에 쿼리 파라미터로 Shape를 지정합니다. 초기 스냅샷 이후에는 라이브 모드(long-polling 등)로 증분 로그를 이어 받습니다. 응답은 Shape Log 항목의 나열로, TypeScript 클라이언트가 이를 머티리얼라이즈해 객체 형태로 쓰기 쉽게 합니다.
개발 환경에서는 로컬 Electric 인스턴스에 직접 붙을 수 있으나, 프로덕션에서는 백엔드 API 뒤에 숨기는 것이 권장됩니다(아래 React 절 참고).
2-3. WHERE 절과 제약
WHERE는 Postgres에서 유효한 결정적(deterministic) 표현이어야 하며, now() 등 비결정적 함수는 제약 대상입니다. 비교·논리 연산·LIKE 등이 문서화된 범위에서 지원됩니다. 서브쿼리로 다른 테이블을 참조하는 필터는 실험적 기능으로, 별도 기능 플래그가 필요할 수 있습니다.
사용자 입력을 WHERE에 넣을 때는 문자열 연결로 SQL을 조립하지 말고, $1 같은 위치 매개변수와 함께 값을 분리해 SQL 인젝션을 방지해야 합니다.
2-4. 컬럼 프로젝션과 처리량
columns로 동기화 열만 줄이면 네트워크와 로컬 저장 비용이 줄어듭니다. 반대로 WHERE 평가·복잡한 필터는 처리량에 영향을 줄 수 있어, 문서에서는 최적화된 WHERE 패턴을 별도로 다룹니다. 대용량 테이블일수록 Shape 경계 설계가 성능과 직결됩니다.
2-5. 보안: Shape는 “권한 모델”이 아니다
Electric은 Shape를 동기화 경계로 제공합니다. 그러나 클라이언트가 임의의 table·where를 조합할 수 있다면, 그것은 애플리케이션의 권한 모델을 우회하는 통로가 될 수 있습니다. 그래서 공식 문서는 프로덕션에서 백엔드 API를 통해 Shape 요청을 검증할 것을 반복해서 강조합니다. 인증된 사용자·테넌트에 맞는 파라미터만 서버가 Electric에 전달하도록 프록시 레이어를 두는 것이 정석입니다.
3. 로컬 SQLite와 브라우저 환경
3-1. “로컬 DB”의 역할
읽기 동기화된 데이터는 최종적으로 다음 중 하나 이상에 머티리얼라이즈됩니다.
- 메모리:
Shape객체·정규화 스토어(간단·휘발) - SQLite: 임베디드·모바일·일부 데스크톱에서 널리 쓰임
- PGlite: 브라우저·Node 등에서 Postgres 호환 엔진을 WASM 등으로 구동하는 선택지
Electric 문서의 예시는 웹에서 PGlite로 정규화 저장을 구현하는 흐름을 자주 보여 줍니다. 반면 SQLite는 Capacitor·Tauri·네이티브처럼 OS에 임베디드 DB가 자연스러운 환경에서 여전히 강력한 선택입니다.
3-2. SQLite를 고르는 경우
- 오프라인 읽기 캐시를 디스크에 오래 유지하고 싶다
- 기존 모바일 팀이 SQLite·FTS·백그라운드 동기화에 익숙하다
- 동일 SQL을 클라이언트·서버에서 공유하고 싶다(Drizzle 등으로 스키마 일치)
3-3. PGlite를 고르는 경우
- 웹에서 Postgres 타입·쿼리를 그대로 쓰고 싶다
- Electric 예제·TanStack DB와 같은 생태계를 따르고 싶다
- 로컬 스키마를 Postgres 방언으로 통일하고 싶다
3-4. 동기화 저장소 설계 시 유의점
로컬 테이블에 서버에서 내려온 읽기 전용 복제본과 사용자의 임시 쓰기를 한 테이블에 섞으면, 나중에 머지·충돌 처리가 어려워집니다. 공식 through-the-database 예제는 동기화된 테이블·로컬 섀도 테이블·뷰로 읽기 합성으로 관심사를 분리합니다. 이 패턴은 복잡도를 올리는 대신 롤백·리베이스를 구조화합니다.
4. 충돌 해결과 쓰기 전략
Electric은 읽기 동기화에 집중하므로, 다중 사용자가 동시에 같은 행을 고치는 문제는 애플리케이션의 쓰기 경로에서 다룹니다. 공식 Writes 가이드는 다음 네 가지 패턴을 제시합니다.
4-1. 온라인 쓰기
가장 단순합니다. 읽기는 Electric으로 빠르게, 쓰기는 항상 API로 보냅니다. 오프라인 쓰기가 없으면 충돌 표면이 줄어듭니다. 대시보드·분석·AI 임베딩 파이프처럼 쓰기가 드물거나 서버 조율이 필수인 경우 적합합니다.
4-2. 낙관적 상태(Optimistic state)
React의 useOptimistic 등으로 UI는 즉시 갱신하고, API 응답 후 서버 상태가 Shape로 다시 들어오면 낙관적 레이어를 버립니다. 오프라인에서도 “일단 보여 주기”는 가능하지만, 컴포넌트 범위에만 두면 다른 화면과 불일치가 날 수 있습니다.
4-3. 공유·영속 낙관적 상태
Valtio·TanStack Query·로컬스토리지 등으로 낙관적 쓰기 큐를 공유하고, Electric이 반영한 서버 데이터와 머지(match) 로직으로 재조정합니다. 문서에서는 다른 사용자의 변경이 스트림으로 들어왔을 때 로컬 낙관적 변경을 리베이스하는 예시를 설명합니다. 실무 로컬 우선 SaaS에서 균형이 좋은 지점입니다.
4-4. DB를 통한 동기화(Through-the-database)
PGlite 등에 동기화 테이블·로컬 섀도·트리거·변경 로그를 두고, 백그라운드 유틸이 서버로 쓰기를 재생합니다. 완전한 오프라인 쓰기에 가장 가깝지만, 스키마·롤백·거절 처리가 가장 무겁습니다. 예제의 롤백이 “단순히 전부 비우기” 수준인 점은 실서비스에서 반드시 고도화해야 함을 시사합니다.
4-5. “충돌 해결”을 설계할 때의 원칙
- 서버 권위: 최종적으로 Postgres의 제약·트랜잭션이 진실이면, 클라이언트는 거절·재시도를 UX로 노출해야 합니다.
- 단순 정책: 많은 업무 앱에서는 최종 쓰기 우선(LWW)·서버 타임스탬프로 충분합니다. 실시간 공동 편집은 CRDT 등 별도 계층이 필요할 수 있습니다.
- 머지와 롤백 분리: 스트림으로 들어온 남의 변경과 내 낙관적 변경이 겹칠 때의 리베이스 규칙을 미리 문서화합니다.
- YAGNI: 동시 편집 충돌은 생각보다 드물다는 현장 보고도 인용됩니다. 과도한 CRDT 도입 전 제품 요구를 확인합니다.
5. TypeScript 타입 생성과 정확한 파싱
5-1. 제네릭으로 행 타입 지정
@electric-sql/client의 ShapeStream·Shape, @electric-sql/react의 useShape<T> 등에 행 타입을 제네릭으로 넘겨 컴파일 타임 안전성을 확보합니다.
import { useShape } from '@electric-sql/react'
type ItemRow = {
id: string
title: string
status: 'active' | 'archived'
}
export function ItemList() {
const { data, isLoading, isError, error } = useShape<ItemRow>({
url: '/api/shape/items', // 프로덕션: 백엔드 프록시
params: {
table: 'items',
where: "status = 'active'",
columns: ['id', 'title', 'status'],
},
})
if (isLoading) return <p>불러오는 중…</p>
if (isError) return <p>동기화 오류: {String(error)}</p>
return (
<ul>
{data.map((row) => (
<li key={row.id}>{row.title}</li>
))}
</ul>
)
}
UseShapeResult는 data, isLoading, lastSyncedAt, isError, error 등을 제공하므로, 로딩·에러·마지막 동기 시각을 UI 정책에 녹이기 좋습니다.
5-2. electric-schema 헤더와 커스텀 파서
HTTP 응답에는 열 타입 정보를 담은 electric-schema 헤더가 포함될 수 있습니다. 기본 파서가 다루는 Postgres 타입은 문서에 열거되어 있으며, 나머지는 문자열로 남을 수 있습니다. timestamp·jsonb·도메인 타입 등을 앱에서 객체로 쓰려면 ShapeStream 옵션의 커스텀 파서를 지정합니다.
5-3. 스키마에서 타입을 “생성”하는 실무 패턴
Electric 단독으로 모든 테이블 타입을 자동 생성해 주는 도구에 의존하기보다, 다음을 조합하는 경우가 많습니다.
- Drizzle ORM·Prisma 등으로 Postgres 스키마에서 TypeScript 타입을 생성하고, Shape의
columns와 동일 열 집합을 맞춘다 - Zod로 런타임 검증 레이어를 두어 스트림 데이터를 입구에서 걸러 낸다
이렇게 하면 서버 마이그레이션→클라이언트 타입의 단방향 흐름을 유지하기 쉽습니다.
6. React 통합
6-1. 패키지와 훅
@electric-sql/react는 useShape, preloadShape, 스트림 캐시용 getShapeStream·getShape 등을 제공합니다. 라우트 로더에서 preloadShape로 데이터 선요청을 걸면 초기 페인트가 안정적입니다.
6-2. 개발 vs 프로덕션
문서 예시는 개발 편의를 위해 http://localhost:3000/v1/shape에 직접 붙지만, 프로덕션에서는 반드시 자사 API URL을 사용합니다. 프록시 레이어에서 세션·JWT·테넌트 ID를 검증한 뒤 Electric에 넘길 table·where·params를 결정합니다.
// 권장: API가 Shape 엔드포인트 역할
const { data } = useShape<TaskRow>({
url: `${import.meta.env.VITE_API_BASE}/sync/tasks`,
})
백엔드 구현 시 스트리밍·에러 처리·인증 헤더 전달은 Auth 가이드를 따르는 것이 안전합니다.
6-3. 구독 해제와 AbortController
useShape에 AbortController의 signal을 넘기면 라이브 업데이트를 중단할 수 있습니다. 다만 전역 캐시된 스트림을 공유하는 경우, 한 컴포넌트에서 abort하면 다른 구독자에게도 영향을 줄 수 있어, 문서에서도 주의를 요합니다. 라우트·화면 단위로 Shape 생명주기 정책을 팀에서 합의해야 합니다.
6-4. TanStack DB·Query와의 조합
Electric은 읽기 스트림이고, TanStack Query는 서버 상태 캐시·재시도에 강합니다. 쓰기 API를 Mutation으로 두고, Electric 스트림으로 읽기 무효화를 대체하거나 보완하는 하이브리드가 흔합니다. 최신 스택은 TanStack DB와의 연동 문서를 참고하시기 바랍니다.
7. 실전: 오프라인 지향 앱 구축 체크리스트
7-1. 아키텍처 개요
[PostgreSQL] --복제--> [Electric Sync Service] --HTTP/Shape--> [클라이언트]
|
로컬 DB / 메모리 / UI
^
[사용자 쓰기] ----REST/GraphQL----------------> [백엔드 API] ----> [PostgreSQL]
- 읽기: Electric이 빠르고 일관된 복제본을 제공
- 쓰기: API가 권위를 가지며, 검증·권한·부작용을 처리
- 오프라인: 읽기는 로컬 캐시로 버티고, 쓰기는 큐+재시도 또는 through-the-db로 심화
7-2. 네트워크·개발 환경
로컬 개발 시 브라우저에서 HTTP/2 성능 이슈가 보고된 바 있어, 문서에서는 Caddy 등으로 HTTP/2를 켜는 것을 권장합니다. 느린 Shape는 트러블슈팅 가이드의 “slow shapes” 절을 참고합니다.
7-3. 운영 체크리스트
- Shape 최소화: 테넌트·사용자·기간으로 WHERE를 좁힌다
- 프록시 필수: 클라이언트가 임의 Shape를 요청하지 못하게 한다
- 쓰기 패턴 선택: 온라인만인지, 낙관적·영속 낙관적·로컬 DB인지 요구사항에 맞게
- 머지·롤백 플레이북: API 거절·버전 충돌 시 사용자 메시지와 데이터 복구 절차
- 관측: Electric·Postgres·API의 지연·오류율을 대시보드화
- 배포: Electric Cloud 또는 셀프호스팅 중 선택
7-4. 학습용 예제
공식 저장소의 examples에는 write-patterns, linearlite 등 학습 가치가 높은 프로젝트가 있습니다. 특히 write-patterns 데모는 동일 앱에서 패턴 1~4를 비교할 수 있어 팀 교육에 적합합니다.
8. 정리
Electric SQL은 PostgreSQL을 중심에 둔 현대적 로컬 우선 아키텍처에서 읽기 동기화를 담당하는 강력한 레이어입니다. Shape로 동기화 범위를 명시하고, TypeScript·React 클라이언트로 스트림을 소비하며, 쓰기·충돌·권한은 백엔드와 클라이언트 패턴으로 보완하는 설계가 실무에서 가장 균형이 좋습니다. SQLite와 PGlite 중 플랫폼에 맞는 로컬 저장소를 고르고, 공식 Writes·Auth·Security 가이드를 배포 전 필독 자료로 삼으시기 바랍니다.