- 복잡한 프런트엔드 앱은 API 응답 캐시에서 출발해 수동 인덱스와 캐시 무효화까지 떠안으며, 프로젝트마다 작은 데이터베이스를 다시 구현하는 상태에 가까워짐
- React 같은 선언형 프레임워크에서는 렌더마다 API를 호출하지 않기 위해 로컬 상태나 Redux 계층에 캐시를 두고, 이 계층이 점차 중앙 저장소 역할을 맡게 됨
- ID 기반 저장소와 날짜별 조회 구조는 조회를 빠르게 만들지만,
CACHE와ENTRIES_BY_DATE같은 여러 구조 사이의 일관성 유지가 테스트와 코드 리뷰 부담으로 커짐 - 낙관적 변경은 서버 응답 전 UI를 즉시 갱신해 체감 속도를 높이지만, 클라이언트·서버 로직 중복, 진행 중 변경 추적, 오류 롤백, 앱 재시작 후 조정 같은 정합성 비용이 따라옴
- SQLSync는 SQLite 기반 로컬 데이터베이스와 Git Rebase 유사 동기화로 내구 캐시, 인덱스, 제약, 낙관적 변경, 반응형 쿼리를 프런트엔드 스택 안에서 제공하려 함
프런트엔드 캐시가 데이터베이스로 커지는 과정
- 프런트엔드 데이터 관리는 API 응답을 로컬 변수에 저장하는 단순한 캐시에서 시작할 수 있음
- React 같은 선언형 프레임워크는 사용자 상호작용 중 트리를 여러 번 다시 렌더링함
- 렌더마다 API 요청을 보내지 않기 위해
useState와useEffect로 요청 결과나 오류를 컴포넌트 상태에 저장할 수 있음 - 예시는 명확성을 위해 단순화된 형태이며, 실제로는 검증된 API 라이브러리를 쓰는 선택지도 있음
- 캐시는 UI 트리의 더 높은 계층이나 UI 바깥으로 옮겨질 수 있음
- Redux는 상태를 통합하고 시간에 따른 원자적 변경을 조율하는 React 상태 관리 라이브러리임
- Redux 생태계는 API 데이터 캐싱을 관리하는 도구와 패턴으로 확장되어 왔음
- 이런 사용 방식은 캐시 로직을 중앙화하고, 갱신을 조율하며, 컴포넌트 간 캐시 결과를 공유하려는 목적을 가짐
- 캐싱 계층이 커질수록 렌더링 엔진과 사용자 액션에 맞춰 데이터를 효율적으로 다루는 중앙 저장 시스템에 가까워짐
수동 인덱스와 일관성 부담
- 프런트엔드에서도 서버에서 받은 데이터를 ID를 키로 하는 객체에 저장해 빠르게 조회·수정할 수 있음
- REST API를 쓰는 앱에서는 데이터를 배치로 읽고, 필요한 객체를 추가로 보강하는 일이 자주 발생함
- 객체를 ID별로 저장하면 API 결과를 캐시에 병합하기 쉬움
- 이 구조는 ID 기준 생성, 읽기, 수정, 삭제에 최적화됨
- 여러 항목을 훑어야 하는 필터링은 전체 엔트리 검사를 피하기 위해 별도 인덱스를 만들게 함
createdAt의 연/월/일 부분을 기준으로ENTRIES_BY_DATE구조를 만들면 특정 날짜의 엔트리를 빠르게 찾을 수 있음- 대신
CACHE와ENTRIES_BY_DATE사이의 일관성을 계속 유지해야 함 - 날짜 범위 조회에는 여러 번 접근이 필요하며, 날짜순 정렬 배열과 더 복잡한 조회·갱신 로직이 더 나은 구조일 수 있음
- 인덱스가 많아지면 각 인덱스마다 생성, 갱신, 조회 로직이 필요함
- 올바름 검증이 테스트와 코드 리뷰의 부담으로 커짐
- 한 인덱스에서 삭제나 갱신을 빠뜨리면 찾기 어려운 버그가 생길 수 있음
- 새 애플리케이션 기능보다 이 복잡성을 관리하는 인프라에 더 많은 시간이 들어갈 수 있음
- 실제 데이터베이스 인덱스는 단순히 데이터를 다른 형태로 저장하는 것보다 훨씬 복잡함
- 통계 수집, 데이터 버전 관리, 트랜잭션 제어, 잠금, 쿼리 최적화와의 상호작용 같은 요소가 포함됨
낙관적 변경이 추가하는 정합성 문제
- 낙관적 변경은 서버 응답 전에 특정 작업의 효과를 로컬에서 먼저 시뮬레이션함
- UI는 네트워크 지연이 없는 것처럼 즉시 반응할 수 있음
- 서버가 예상과 다른 결정을 내리거나 오류가 발생하면 UI는 변경을 롤백하고 사용자에게 문제 수정을 요구해야 할 수 있음
- 클라이언트가 서버 결과를 잘 예측하고 오류를 클라이언트에서 처리할 수 있으며 로직이 밀접하게 동기화되어 있을 때 강력한 도구가 됨
- 낙관적 업데이트는 보통 네 단계로 흘러감
- UI가 쓰기 작업을 발생시킴
- 서버가 동의할 것이라는 가정으로 로컬 캐시에 변경을 적용하고 UI를 즉시 다시 렌더링함
- 변경 작업을 비동기로 서버에 전송함
- 서버 응답을 로컬 캐시에 병합해 이전 낙관적 변경을 덮어쓰고, 필요하면 UI를 다시 렌더링함
- 서버와의 일관성을 유지하려면 여러 부담이 생김
- 결과를 예측하기 위해 클라이언트와 서버에 로직을 중복해야 함
- 비동기 오류나 서버 불일치를 처리하려면 진행 중인 각 변경을 추적해야 함
- 더 나은 사용자 경험을 위해 낙관적 캐시 부분을 내구성 있게 만들어 앱 재시작 후 변경을 조정해야 할 수도 있음
- 이 과정은 개발 시간과 올바름 검증 비용을 키우며, 데이터 관리가 사용자 가치나 차별화 기능 개발보다 앞서게 될 수 있음
재귀적 캐시 무효화의 복잡성
- 데이터가 많은 앱에서는 같은 정보가 캐시의 여러 위치에 나타남
- 예시 캐시는
projects,tasks,users를 함께 저장함 - 작업 완료 후 프로젝트 진행률, 사용자 할당 작업, 새 작업 정보 같은 여러 부분이 영향을 받을 수 있음
- 예시 캐시는
- 작업 완료 후 캐시를 서버와 맞추려면 여러 왕복이 필요할 수 있음
- 서버에 작업 완료를 알림
- 프로젝트 진행률이 바뀌었으므로 프로젝트를 새로고침함
- 새 작업 할당 여부를 확인함
- 새 할당이 있으면 해당 작업을 조회함
- 더 복잡한 API로 왕복 횟수를 줄일 수는 있지만, API나 클라이언트 로직이 기저 데이터 모델과 결합되는 결과가 남음
- GraphQL은 이 문제에 대한 한 접근이지만 완전한 해결책은 아님
- UI가 각 변경마다 캐시의 어떤 부분이 관련되는지 알아야 하는 구조는 규모가 커질수록 취약해짐
- 데이터 관계와 집계가 로컬 캐시의 여러 부분에 영향을 줄 수 있음
- 엔지니어링 팀이 커지면 문제는 팀 경계를 넘을 수 있고, 큰 소프트웨어 프로젝트의 변경 가능한 전역 변수와 비슷하게 느껴질 수 있음
- 낙관적 변경과 결합되면 클라이언트가 서버 변경을 예측하기 위해 더 많은 백엔드 로직을 복제하게 됨
- 예시에서는 task 1을 user 1에서 제거하고, 전체 작업 수를 이용해 진행률을 새 비율로 계산하려 할 수 있음
- 중첩 변경을 로컬에서 예측하도록 만들수록 클라이언트는 백엔드 스택을 더 많이 중복함
SQLSync가 제안하는 프런트엔드 데이터베이스 스택
- SQLSync는 SQLite 위에 만든 프런트엔드 최적화 데이터베이스 스택임
- 예시 Todo app은 전체 데이터 계층을 Rust 60줄과 컴포넌트에 흩어진 몇 개의 SQL 쿼리로 구현함
- SQLSync는 내구 캐시, SQLite의 인덱스·제약·트리거·쿼리 최적화, 낙관적 변경, 스마트 캐시 무효화, 반응형 쿼리를 제공함
- 로컬 데이터는 하나 이상의 SQLite 데이터베이스에 저장됨
- 인덱스를 쉽게 만들 수 있고 데이터와 자동으로 동기화됨
- 데이터베이스는 백엔드처럼 인덱스를 자동으로 사용해 쿼리를 가속할 수 있음
- SQL은 복잡한 데이터 조회를 표현할 수 있고, triggers, foreign keys, constraints, full-text search 같은 기능도 사용할 수 있음
- 낙관적 변경은 reducer로 처리됨
- 구조는 Redux의 핵심 개념과 유사함
- reducer는 WebAssembly로 컴파일 가능한 어떤 언어로도 작성할 수 있음
- SQLSync는 클라이언트에서 변경을 낙관적으로 실행하고, 서버에서는 전역적으로 일관된 순서로 실행함
- 이후 클라이언트는 Git Rebase와 유사한 작업으로 서버와 동기화함
- 이 아키텍처는 재귀적 캐시 무효화 필요를 없애는 장점이 있음
- 모든 데이터 변경 로직을 클라이언트와 서버에서 공유하기 쉬운 reducer 안에 작성함
- 변경 중 발생한 모든 데이터 변화가 자동으로 보임
- 동기화가 Git Rebase처럼 동작하므로 서버가 클라이언트와 다른 변경을 하더라도 클라이언트는 같은 일관된 결과에 도달하는 보장을 가짐
관련 작업
- Riffle의 “Building data-centric apps with a reactive relational database”는 UI 상태를 포함한 모든 애플리케이션 상태를 하나의 반응형 데이터베이스에 저장하는 아이디어를 다룸
- 반응형 쿼리는 깔끔한 사고 모델을 제공하고 React 같은 선언형 시스템과 잘 맞음
- 클라이언트 앱 개발 문제를 데이터베이스 커뮤니티의 아이디어로 해결함
- 관계형 데이터 모델과 실제 인덱스로 상태를 모델링하는 이점을 다룸
- Instant.db의 Stepan은 브라우저 안의 데이터베이스를 다룬 두 글을 작성함
- Database in the Browser, a Spec
- A Graph-Based Firebase
- 두 글은 프런트엔드와 백엔드 스택의 관계에 더 초점을 두고 비슷한 문제를 다루며, Firebase의 그래프 기반 후속으로 Instant.db가 만들어진 동기를 설명함
- Matt Wonlaw의 CR-SQLite는 SQLite 확장임
- 충돌 없는 복제 데이터 타입(CRDT)과 인과 순서 이벤트 로그를 사용해 데이터를 일관되게 병합함
- 중앙 조정자 없이 피어투피어 앱이 SQLite에 데이터를 저장하고 협업할 수 있게 함
- 브라우저에서 SQLite를 실행하는 예시이기도 함
- Matt Wonlaw는 관련 아이디어도 탐색 중임
- incremental computation
- typed-sql을 통한 SQL 사용성 개선