#Frontend

로컬 DB를 화면의 기준으로 삼는다는 것: hearth를 직접 만든 이유

로컬 DB를 화면의 기준으로 삼는다는 것: hearth를 직접 만든 이유
01

Summary

스피너 없는 초고속 웹앱의 비결, '오프라인 퍼스트' 라이브러리 직접 구현하기

채널톡 팀이 기존 오픈소스를 거치지 않고 직접 로컬 DB 동기화 엔진 'hearth'를 만든 여정

네트워크 상태와 무관하게 즉각적으로 로딩되는 '오프라인 퍼스트' 구조는 뛰어난 사용자 경험을 자랑하지만 직접 구축하기는 매우 까다롭습니다. 채널톡 프론트엔드 팀은 무거운 상용 라이브러리 대신 자체 API 서버에 최적화된 Dexie 기반의 경량 동기화 엔진 'hearth'를 완성했습니다. LWW 규칙의 함정, 낙관적 업데이트 방어, 캐시 축출 버그 등을 실전에서 극복하며 다듬어 나간 엔지니어링 스토리를 공유합니다.

  • 01화면이 서버 응답 대신 로컬 DB를 진실의 원천으로 바라보게 하여 스피너 없는 UX 구현
  • 02사내 프로젝트에서 검증된 코드를 도메인 무지(domain-agnostic) 형태의 npm 패키지로 분리
  • 03LWW(Last-Writer-Wins) 기반 타임스탬프 비교를 통한 로컬과 실시간 소켓 간 동시 수정 충돌 해소
  • 04수동 플래그 대신 행 단위 추적을 활용한 안정적인 낙관적 업데이트 및 컬럼 단위 쓰기 보호
  • 05동기화 판단 근거와 실제 데이터의 수명을 일치시켜 복제 사각지대 및 캐시 유령 데이터 해결

RECOMMENDATION

IndexedDB를 활용한 로컬 캐싱 고도화, 실시간 동기화 상태 관리, 또는 브라우저 캐시 정합성 설계로 머리를 싸매고 있는 시니어 프론트엔드 개발자분들에게 적극 권장합니다.

The Problem

네트워크 상태와 무관하게 빠른 속도로 동작하는 오프라인 퍼스트(Local-First) 아키텍처를 도입하려 했으나, 기존의 오픈소스 동기화 엔진(WatermelonDB, RxDB 등)은 자체 백엔드 API와의 연동 시 불필요한 추상화 계층이 발생하여 비효율적이었습니다. 또한, 채널톡의 AI 비서 Drive 기능 등 여러 제품군에서 진입 시마다 스피너가 발생하고 오프라인 동작이 불가한 문제를 공통적으로 겪고 있었습니다.

The Solution

사내에서 이미 검증되었던 동기화 코드를 추상화하여 Dexie(IndexedDB 래퍼) 기반의 도메인 독립적 사내 라이브러리인 'hearth'를 개발했습니다. hearth는 core 계층(LWW 비교 훅, 필수 캐시 eviction 정책, 전체 생애주기 plugin)과 sync 계층으로 논리적 구조를 분리하고, 서버 동기화는 별도의 pull/push 로직을 설계하여 해결했습니다.

The Result

Drive 기능에 도입하여 화면 진입 시 스피너를 완전히 제거하고 지연 없는 극강의 UX와 오프라인 읽기/쓰기를 성공적으로 구현했습니다. 복잡한 로컬 동기화 로직을 라이브러리 내부로 회수하여 개발자가 수동으로 동기화 로직을 다시 작성하지 않고도 여러 제품군에 공통 엔진으로 손쉽게 이식할 수 있는 환경을 마련했습니다.

Trade-off

LWW(Last-Writer-Wins) 규칙으로 인해 데이터 정합성 보장의 책임이 서버로 확장되면서 서버 측 타임스탬프 미갱신 시 변경 사항이 영구히 덮어씌워지는 버그가 발생할 수 있습니다. 또한, 과거 동기화 영수증 기록과 실제 로컬 캐시 데이터의 수명이 일치하지 않아 데이터 유실이 감지되는 설계 결함이 발생하기도 했으며, 네트워크 요청 자체를 줄이는 완전한 Delta Sync 구현은 아직 과제로 남아 있습니다.

03

Key Concepts

Concept · 01

오프라인 퍼스트 (Local-First)

사용자의 데이터 조작을 원격 서버가 아닌 로컬 디바이스의 데이터베이스에 우선적으로 기록하고, UI 역시 로컬 데이터를 바탕으로 즉각 변경한 뒤 서버와의 동기화는 백그라운드에서 지연 처리하는 아키텍처 패턴입니다.

  • 채널톡의 Drive 서비스에 도입하여 접속 스피너를 제거하고 지하철 등 오프라인 환경에서도 원활하게 동작하도록 구성함
  • 로컬 DB를 우선 렌더링하고 서버와 로컬을 백그라운드에서 조용히 맞추는 방식으로 흐름을 전환함
Concept · 02

LWW (Last-Writer-Wins)

분산 데이터 처리 중 동일한 항목에 여러 쓰기 요청이 겹칠 때, 가장 늦은 타임스탬프나 최종 순서 번호를 가진 데이터가 최종적으로 유효하다고 판정하는 가장 직관적이고 널리 쓰이는 충돌 해결 메커니즘입니다.

  • hearth 엔진에서 충돌을 조정할 때 서버 기준의 최신 타임스탬프 시각을 비교하여 높은 쪽만 반영하게 함
  • 서버 응답에서 타임스탬프를 누락했을 때 로컬의 LWW 규칙이 이를 옛 데이터로 판단하여 화면에 신규 데이터가 보이지 않던 정합성 이슈를 해결하는 근거가 됨
Concept · 03

낙관적 쓰기 보호 (Optimistic Write Protection)

서버가 성공 응답을 주기 전에 화면에 먼저 반영하는 낙관적 업데이트 도중, 사용자 수정 이전 시점에 생성된 서버 응답이 뒤늦게 오며 사용자의 입력을 덮어써 버리는 레이스 컨디션 오류를 방지하는 장치입니다.

  • 글로벌 수동 상태 플래그 대신 개별 행(row)에 서버 확인 여부 필드를 부여해 타이밍 버그가 생기지 않도록 함
  • 로컬에서 수정했지만 아직 서버 확인을 받지 못한 컬럼(필드) 단위의 변경 사항을 추적해 과거 서버 데이터의 역습을 완벽하게 방어함