PowerSync 완벽 가이드 | 로컬 우선 동기화·Sync Rules·React Native·SQLite

PowerSync 완벽 가이드 | 로컬 우선 동기화·Sync Rules·React Native·SQLite

이 글의 핵심

PowerSync는 클라이언트의 SQLite와 서버 측 데이터베이스(PostgreSQL, MySQL, MongoDB, SQL Server 등) 사이에서 증분 동기화를 제공하는 로컬 우선(local-first) 동기화 엔진입니다. 오프라인에서도 읽기·쓰기가 가능하고, 온라인이 되면 변경분을 안전하게 맞춥니다. 본 글에서는 아키텍처, Sync Rules, 로컬 스키마, React Native 통합, 백엔드 커넥터, 충돌 해결, 실무 모바일 패턴까지 한 흐름으로 정리합니다.

왜 PowerSync인가: 모바일에서 “항상 API 호출 → 로딩 스피너” 모델은 네트워크 지연과 불안정한 연결에 취약합니다. 로컬 SQLite를 단일 진실 소스처럼 쓰면 UI는 즉시 반응하고, 동기화 레이어가 서버와 일관성을 유지합니다. PowerSync는 그 동기화 레이어를 제품화한 플랫폼입니다.

들어가며: “오프라인에서도 같은 화면을 보여주고 싶어요”

실무 문제 시나리오

시나리오 1: 필드 작업 중 통신이 끊겨요

현장·지하·이동 중에는 연결이 자주 끊깁니다. 로컬 DB에 먼저 기록하고 나중에 동기화하면 업무가 멈추지 않습니다.

시나리오 2: 사용자마다 다른 데이터만 받아야 해요

전 테이블을 내려받는 방식은 비용·보안·성능 모두 비효율적입니다. Sync Rules로 사용자·조직 단위로 부분 복제(partial replication)를 정의합니다.

시나리오 3: 서버와 로컬이 동시에 바뀌면 어떻게 합치나요?

동시 수정은 피할 수 없습니다. PowerSync의 기본 전략과 커스텀 업로드 처리, 필요 시 CRDT 같은 상위 전략을 함께 설계합니다.


1. PowerSync란 무엇인가

1.1 로컬 우선이 의미하는 것

로컬 우선은 “사용자가 보는 데이터의 1차 저장소가 단말의 SQLite”라는 뜻입니다. 읽기 경로는 네트워크 왕복 없이 SQL 쿼리로 끝나고, 쓰기는 로컬에 먼저 반영된 뒤 업로드 큐를 통해 서버로 전달됩니다. 서버는 권한·비즈니스 규칙·충돌 정책을 적용한 뒤, 변경이 다시 동기화 스트림을 통해 클라이언트에 흘러들어옵니다.

1.2 구성 요소 개요

구성은 대략 다음과 같이 이해할 수 있습니다.

역할설명
클라이언트 SDKSQLite 래핑, 동기화 세션, 감시 쿼리(watched queries), CRUD 트랜잭션 API
PowerSync 서비스백엔드 DB와 연동하여 변경분을 스트리밍, Sync Rules에 따라 버킷 단위로 클라이언트에 전달
백엔드 커넥터PostgreSQL 등과의 연결·복제·스냅샷 처리(플랫폼/셀프호스트에 따라 구성)
애플리케이션 백엔드클라이언트가 보낸 쓰기를 검증·저장하는 API(예: uploadData)

즉, PowerSync는 “읽기 동기화 파이프라인 + 로컬 쓰기 큐 업로드”를 표준화한 스택입니다.

1.3 기존 방식과의 차이

전통적인 온라인 우선 앱은 화면마다 REST/GraphQL 호출 → 캐시 → 재요청 패턴이 일반적입니다. 로컬 우선에서는 캐시가 아니라 SQLite가 주 저장소이며, 동기화는 백그라운드에서 지속적으로 수행됩니다. 그 결과 체감 지연(latency)이 크게 줄고, 오프라인 UX를 일관되게 유지할 수 있습니다.


2. 핵심 개념: 버킷, 복제, 스트림

2.1 Sync Bucket(동기화 버킷)

PowerSync는 데이터를 논리적 단위인 버킷으로 나누어 클라이언트에 공급합니다. 예를 들어 “특정 user_id에 속한 주문 목록”, “특정 project_id의 이슈”처럼, 앱 도메인에 맞는 단위로 쪼갭니다. 버킷은 Sync Rules의 매개변수(parameter)와 연결되며, 사용자마다 다른 버킷 집합이 생성될 수 있습니다.

2.2 부분 복제

전체 테이블 동기화는 모바일에서 비현실적일 때가 많습니다. 부분 복제는 WHERE 조건·관계·매개변수 쿼리로 “이 단말에 필요한 행만” SQLite로 가져오는 방식입니다. 이 설계가 잘못되면 동기화 데이터가 너무 커지거나, 반대로 필요한 행이 빠져 버그가 납니다.

2.3 읽기 경로와 쓰기 경로의 분리

  • 읽기: 서버 → PowerSync → 클라이언트 SQLite (스냅샷·증분)
  • 쓰기: 클라이언트 SQLite 변경 → 업로드 API → 서버 DB 반영 → 다시 동기화로 전파

이 분리 덕분에 UI는 항상 로컬 SQL만 보면 되고, 네트워크는 백그라운드 책임으로 둘 수 있습니다.


3. Sync Rules 심화

Sync Rules는 YAML(또는 프로젝트 설정에 따라 동등한 형식)으로 작성하며, “누가 어떤 버킷을 받는지”와 “버킷 안에 어떤 행이 들어가는지”를 선언적으로 정의합니다. 핵심은 매개변수 쿼리(parameter query)데이터 쿼리(data query)의 조합입니다.

3.1 구조적 이해

의사 코드로 보면 다음과 같은 레이어로 생각할 수 있습니다.

# 개념 예시 — 실제 프로젝트의 스키마·함수명은 문서와 DB에 맞게 조정해야 합니다.
bucket_definitions:
  # 예: "내가 멤버인 프로젝트" 단위로 버킷을 나눔
  project_data:
    # 어떤 bucket 파라미터 조합이 존재하는지 정의
    parameters: |
      SELECT id AS project_id
      FROM project_members
      WHERE user_id = request.user_id()
    data:
      - |
        SELECT *
        FROM issues
        WHERE project_id = bucket.project_id
      - |
        SELECT *
        FROM issue_comments
        WHERE project_id = bucket.project_id

매개변수 쿼리는 “이 사용자에게 어떤 project_id 버킷들이 필요한가”를 결정하고, 데이터 쿼리는 각 버킷에 채워 넣을 행을 고릅니다. 데이터 쿼리는 정의된 버킷 파라미터를 모두 참조해야 한다는 제약이 있으므로, 설계 시 컬럼·별칭을 일관되게 맞추는 것이 중요합니다.

3.2 보안과 최소 권한

Sync Rules는 사실상 서버 측 필터입니다. 클라이언트가 임의로 “남의 user_id”를 넣어 전 테이블을 받아가는 일을 막으려면, request.user_id() 같은 서버가 주입하는 컨텍스트와 백엔드의 인증·권한 모델이 일치해야 합니다. 규칙만으로 끝내지 말고, 업로드 API에서도 동일한 도메인 규칙을 강제하는 것이 안전합니다.

3.3 설계 시 흔한 실수

  • 버킷 단위가 너무 큼: 한 번에 너무 많은 행이 동기화되어 초기 동기가 느려집니다.
  • 버킷 단위가 너무 잘게 쪼개짐: 버킷 수가 폭증하면 메타데이터·동기화 오버헤드가 커질 수 있습니다.
  • 파라미터와 데이터 쿼리 불일치: 버킷 컬럼 이름 불일치로 일부 행이 누락됩니다.

운영 중에는 스키마 마이그레이션 시 Sync Rules와 클라이언트 SQLite 마이그레이션을 같은 릴리스 계획으로 묶는 것이 좋습니다.

3.4 제품 로드맵과의 정렬

PowerSync 생태계는 문서와 제품 업데이트에 따라 동기화 표현 방식이 확장될 수 있습니다. 새 프로젝트를 시작할 때는 공식 문서의 Sync Streams 등 최신 권장 방식을 확인하고, 기존 Sync Rules 기반 프로젝트는 마이그레이션 가이드를 참고하는 것이 바람직합니다.


4. 로컬 SQLite: 스키마, 마이그레이션, 쿼리

4.1 클라이언트 스키마는 “복제 모델”

서버의 정규화된 테이블을 그대로 옮기기보다, 읽기 최적화·동기화 키·메타 컬럼을 고려한 SQLite 스키마를 설계하는 경우가 많습니다. PowerSync가 내려주는 행은 앱에서 일반 SQL로 조회합니다. 즉, RN 상태 관리의 중심을 Redux 대신 SQL에 두는 패턴과 잘 맞습니다.

4.2 마이그레이션 전략

앱 버전업 시 SQLite 스키마를 바꾸려면:

  1. 로컬 마이그레이션 스크립트로 테이블·인덱스 변경
  2. PowerSync 초기 동기화 또는 증분 동기화와 충돌 없는지 검증
  3. 필요 시 한 번만 도는 데이터 정리 배치

프로덕션에서는 “마이그레이션 실패 시 롤백/재시도” 정책을 명확히 하는 것이 좋습니다.

4.3 감시 쿼리(Watched Queries)

PowerSync SDK는 특정 SQL 결과가 바뀔 때 UI를 갱신할 수 있는 감시 쿼리를 지원합니다. 화면 단위로 SELECT를 걸어 두면, 동기화로 로컬 DB가 갱신될 때 자동으로 리렌더링 트리거가 됩니다. 이는 수동으로 동기화 이벤트를 흩뿌리는 것보다 예측 가능한 데이터 흐름을 만듭니다.

일부 환경(Expo 등)에서는 Async Iterator 기반 API를 쓸 때 폴리필·Babel 플러그인이 필요합니다. 공식 React Native 문서의 “Polyfills” 절을 프로젝트에 맞게 적용해야 합니다.


5. React Native 통합

5.1 패키지와 피어 의존성

Expo 기준으로는 대략 다음과 같은 설치 흐름을 따릅니다(버전은 프로젝트에 맞게 고정).

npx expo install @powersync/react-native
npx expo install @journeyapps/react-native-quick-sqlite

OP-SQLite 등 대체 SQLite 백엔드를 쓰는 베타 옵션도 있으므로, 팀 표준과 빌드 파이프라인에 맞게 선택합니다.

5.2 앱 부트스트랩

앱 시작 시:

  1. SQLite 파일 경로·이름 확정(사용자별 분리 여부 포함)
  2. PowerSyncDatabase 또는 SDK가 요구하는 팩토리로 DB 인스턴스 생성
  3. 인증 토큰·PowerSync 엔드포인트와 연결
  4. React 트리 상위에 Provider를 두고 하위 화면에서 훅으로 접근

패턴은 “한 사용자 세션당 DB 한 벌”이 일반적이며, 로그아웃 시 연결 해제·파일 정리 정책을 정해야 합니다.

@powersync/reactPowerSyncContextusePowerSync, useQuery, useStatus 등을 함께 쓰면, 로컬 SQLite 결과가 바뀔 때 화면이 반응형으로 갱신되기 쉽습니다. 아래는 개념 정리용 최소 예시이며, 스키마·연결 옵션·인증은 공식 Usage Examples를 따라야 합니다.

import React, { useMemo } from 'react';
import { PowerSyncDatabase } from '@powersync/react-native';
import { PowerSyncContext } from '@powersync/react';

export function AppProviders({ children }: { children: React.ReactNode }) {
  const db = useMemo(() => {
    return new PowerSyncDatabase({
      // schema: AppSchema — 프로젝트의 Drizzle/Zod 등 스키마 정의와 일치시킴
      database: { dbFilename: 'app.sqlite' },
    });
    // 이후 connect(connector) 등으로 PowerSync 서비스와 세션 연결
  }, []);

  return (
    <PowerSyncContext.Provider value={db}>
      {children}
    </PowerSyncContext.Provider>
  );
}

// 하위 컴포넌트: usePowerSync(), useQuery('SELECT ...'), useStatus() 등

5.3 Expo·네이티브 빌드 유의사항

  • 네이티브 모듈이므로 실기기·시뮬레이터에서의 검증이 필수입니다.
  • expo-updates 등과 SQLite 네이티브 의존성 충돌이 나면, 문서에 안내된 Podfile 설정 등으로 서드파티 SQLite 사용을 조정할 수 있습니다.
  • 순수 RN(Expo 없음)에서는 Metro inlineRequires 관련 이슈가 보고된 바 있으므로, 공식 문서의 metro.config.js 가이드를 참고합니다.

5.4 UI와의 연결

화면은 가능한 한 로컬 SQL 결과만 구독하게 두고, “동기화 중”은 별도 인디케이터로 표시합니다. 사용자 경험상 읽기는 항상 로컬 속도, 동기화 상태는 부가 정보로 두는 편이 자연스럽습니다.


6. 백엔드 커넥터와 서버 측 책임

6.1 지원 데이터베이스

PowerSync는 일반적으로 PostgreSQL, MySQL, MongoDB, SQL Server 등과 연동합니다. 커넥터는 변경 캡처(논리 복제, CDC, 폴링 등)와 스냅샷 생성을 담당하고, PowerSync 서비스가 이를 클라이언트 규칙에 맞게 스트리밍합니다.

6.2 셀프호스트 vs 클라우드

배포 모델에 따라 운영 주체(DB 권한, 네트워크, 모니터링)가 달라집니다. 엔터프라이즈에서는 VPC·방화벽·감사 로그 요구사항을 충족하는지 검토해야 합니다.

6.3 애플리케이션 서버의 역할: uploadData

클라이언트의 로컬 변경은 결국 백엔드 API로 업로드됩니다. 일반적인 흐름은 다음과 같습니다.

  1. 클라이언트가 로컬에서 PUT / PATCH / DELETE 형태의 작업을 큐에 쌓음
  2. 네트워크 가능 시 서버의 업로드 핸들러로 전송
  3. 서버가 트랜잭션·권한·비즈니스 규칙을 적용해 권위 있는 DB에 반영
  4. 반영 결과가 다시 동기화 채널을 통해 다른 단말로 전파

여기서 서버는 단일 진실 소스 역할을 하며, PowerSync는 읽기 복제와 쓰기 큐 전달을 매끄럽게 잇습니다.


7. 충돌 해결(Conflict Resolution)

7.1 기본 동작: 필드 단위 마지막 쓰기 우선

PowerSync는 흔히 필드 단위 last-write-wins에 가까운 기본 충돌 해석을 사용합니다. 서로 다른 필드를 두 사용자가 동시에 고치면 둘 다 살아남을 수 있고, 같은 필드에 동시에 쓰면 나중에 서버에 도착한 쪽이 이깁니다. 이는 구현이 단순하지만, 재고 차감·잔액 같이 “덧셈이 의미 있는” 도메인에는 부족할 수 있습니다.

7.2 커스텀 업로드 처리

복잡한 규칙이 필요하면 업로드 핸들러에서 CrudEntry를 해석해 직접 합치 로직을 구현합니다. 예를 들어 재고는 “새 값으로 덮기”가 아니라 “증감 연산”으로 적용하거나, 주문 상태는 허용된 전이만 받도록 검증합니다.

7.3 CRDT·blob 전략

협업 문서·리치 텍스트처럼 자동 병합이 필요하면 Yjs 등 CRDT를 쓰고, 바이너리 상태를 PostgreSQL의 BYTEA 등에 저장하는 패턴도 소개된 바 있습니다. 이 경우 PowerSync는 blob 동기화를 담당하고, 의미 있는 병합은 CRDT 라이브러리가 처리합니다.

7.4 제품 관점의 조언

충돌 설계는 기술만의 문제가 아니라 도메인 규칙 문제입니다. “누가 이기는가”를 화면과 알림 정책까지 포함해 정의하고, 가능하면 동시 편집을 줄이는 UX(잠금, 낙관적 UI, 버전 표시)와 함께 가져가면 분쟁이 줄어듭니다.


8. 실전 모바일 앱 동기화 패턴

8.1 인증과 Sync Rules 정합성

JWT 등의 사용자 ID가 Sync Rules의 request.user_id()와 항상 일치해야 합니다. 토큰 갱신·계정 전환 시에는 PowerSync 연결을 재수립하고, 로컬 DB를 사용자별로 분리할지 삭제 후 재동기할지 정책을 문서화합니다.

8.2 초기 로그인과 대량 동기화

첫 로그인 후 초기 스냅샷이 클 수 있습니다. UX 대응:

  • 진행률·예상 시간(대략) 표시
  • 핵심 화면에 필요한 버킷 우선순위 검토
  • Wi-Fi에서만 대량 동기 옵션

8.3 오프라인 쓰기와 실패 복구

업로드가 실패하면 큐에 남습니다. 앱은 재시도 백오프, 사용자에게 재시도 버튼, 서버 오류 코드별 메시지를 제공하는 것이 좋습니다. 영구 실패 시 로컬 변경 폐기 vs 유지 정책도 팀 합의가 필요합니다.

8.4 테스트 전략

  • 단위 테스트: 순수 SQL·도메인 로직
  • 통합 테스트: SQLite 인메모리 또는 임시 파일 + 동기화 시뮬레이션
  • E2E: 실제 기기에서 비행기 모드 전환 시나리오

동기화 버그는 재현이 어려우므로, 로깅(동기화 세션 ID, 버킷, 버전)을 충분히 남깁니다.

8.5 관측 가능성(Observability)

프로덕션에서는 동기화 지연, 업로드 실패율, 큐 길이, 버전 충돌 비율을 대시보드로 모니터링하는 것이 좋습니다. 클라이언트와 서버 로그를 상관 ID로 묶으면 사고 분석이 빨라집니다.


9. 정리

PowerSync는 SQLite를 단말의 중심에 두고, 서버 DB와의 양방향 동기화를 제품 수준으로 제공하는 스택입니다. 성공적인 도입은 Sync Rules로 최소 권한·적절한 버킷 크기를 만드는 일과, 업로드 API에서 도메인 규칙·충돌 정책을 완성하는 일이 한 세트입니다. React Native에서는 네이티브 SQLite·빌드 설정·폴리필까지 모바일 특유의 난이도를 감안해야 하며, 그 대가로 오프라인 우선 UX빠른 읽기 경로를 얻을 수 있습니다.

추가로 공식 데모 앱, Discord 커뮤니티, SDK 변경 로그를 북마크해 두면 신규 멤버 온보딩과 장애 대응에 도움이 됩니다.


FAQ

Q. PowerSync만 쓰면 오프라인 동기화가 완전히 끝나나요?

A. 아닙니다. PowerSync는 동기화 인프라이고, 비즈니스 규칙·권한·충돌 처리·스키마 진화는 애플리케이션과 백엔드가 함께 설계해야 합니다.

Q. Sync Rules만으로 보안이 충분한가요?

A. Sync Rules는 중요한 서버 측 필터이지만, 업로드 API에서도 동일한 도메인 검증을 해야 악의적 클라이언트 시나리오를 막을 수 있습니다.

Q. React Native에서 가장 흔한 이슈는 무엇인가요?

A. 네이티브 빌드·SQLite 피어 의존성·Metro/폴리필 설정 이슈가 자주 보고됩니다. 공식 문서의 플랫폼별 노트를 초기 스프린트에 반영하는 것이 좋습니다.

Q. 충돌이 잦은 도메인은 어떻게 하나요?

A. 필드 단위 last-write-wins로 부족하면 커스텀 업로드 병합, 필요 시 CRDT·상태 머신(허용 전이)으로 승격합니다.


참고

  • PowerSync 공식 문서: https://docs.powersync.com/
  • React Native·Expo SDK 개요: 클라이언트 SDK 레퍼런스 내 React Native/Expo 섹션
  • 데모 앱: 공식 문서의 Demo Apps / Example Projects 갤러리