CSR 재방문, 빈 화면은 끝.마지막 화면을 즉시 복구하는 3-레이어 툴킷.
FirstTx는 Prepaint, Local-First, Tx 세 레이어를 하나의 툴킷으로 묶었습니다.
CSR 아키텍처는 그대로 두고, 재방문 복원·오프라인 내구성·안전한 낙관적 UI를 한 번에 더하세요.
FirstTx 도입 전
사용자 재방문
브라우저 뒤로 가기나 링크 클릭으로 CSR 앱으로 다시 진입합니다.
빈 화면 노출
JS 번들과 API 응답을 기다리며 1~2초 동안 흰 화면이나 스피너가 보입니다.
JS 로딩 & 마운트
React와 데이터 요청이 끝난 뒤에야 실제 화면이 렌더링됩니다.
FirstTx 도입 후
부트 스니펫
HTML 로드 직후, 작은 부트 스크립트가 한 번 실행됩니다.
마지막 화면 복원
IndexedDB에 저장된 마지막 DOM 스냅샷을 재방문 직후 바로 복원합니다.
Hydration & 동기화
사용자는 복원된 화면을 보고 있는 동안, React 마운트와 데이터 동기화가 백그라운드에서 진행됩니다.
THREE LAYERS · ONE TOOLKIT
Prepaint · Local-First · Tx
필요한 레이어만 골라 도입하세요. 재방문 속도는 Prepaint로, 동기화·낙관적 UI는 Local‑First와 Tx로 해결합니다.
마지막 화면을 DOM 스냅샷으로 저장하고, 재방문 시 React보다 먼저 복원합니다.
- 전체 화면 스냅샷을 IndexedDB에 보관
- 재방문 시 JS 실행 전에 바로 복원
- CSR 구조는 그대로 두고 SSR에 가까운 재방문 경험 제공
IndexedDB를 단일 소스로 두고 서버 동기화를 자동화합니다.
- zod 기반 타입 세이프 모델 정의
- TTL·staleness 메타데이터 기본 제공
- 백그라운드 동기화로 오프라인 친화적 플로우
UI 업데이트를 트랜잭션으로 감싸 낙관적 업데이트를 안전하게 만듭니다.
- 여러 단계를 하나의 트랜잭션으로 실행
- 실패 시 보상(rollback) 로직 자동 실행
- 네트워크 오류에서도 일관된 UI 상태 유지
HOW IT FEELS
앱에서 보이는 동작 방식
SSR은 첫 진입을 빠르게 해주지만, 실제 사용 시간은 재방문·뒤로 가기·탭 이동에 더 많이 쓰입니다. FirstTx는 이 순간들을 빠르게 만들어, 다시 올 때마다 준비된 화면을 보여줍니다.
재방문이 잦은 내부 도구
CRM·어드민·대시보드처럼 목록↔상세를 반복하는 화면에서 매번 새로 로딩하는 대신, 이전 상태를 바로 복원합니다.
작업 중 실수로 새로고침
로컬 모델이 최신 스냅샷을 들고 있어 필터·스크롤·폼 상태가 유지되고, 처음부터 다시 시작할 필요가 없습니다.
낙관적 UI가 실패했을 때
낙관적 업데이트를 트랜잭션으로 묶어 서버에서 거절되면 화면이 깔끔하게 되돌아가고, ‘반쯤만 롤백된 상태’를 피할 수 있습니다.
오프라인·불안정한 네트워크
간단한 동기화 훅 하나로, 오프라인/재연결을 견디면서도 유저의 위치와 컨텍스트를 잃지 않는 데이터 레이어를 만들 수 있습니다.
IndexedDB 스냅샷 → DOM 복원 (4ms)
TTL 초과로 백그라운드 동기화 시작
UI 업데이트와 서버 요청 모두 성공
네트워크 실패 → compensate로 UI 상태 되돌림
Chrome 확장 프로그램 FirstTx DevTools에서 위와 같은 이벤트들을 레이어별 타임라인으로 확인할 수 있습니다.
QUICK START
세 단계로 FirstTx 연결하기
패키지를 설치하고 Vite 플러그인을 켠 뒤, 루트 엔트리를 한 번 감싸기만 하면 됩니다. 처음에는 세 레이어를 모두 쓰거나, Prepaint부터 도입한 뒤 Local-First와 Tx를 나중에 추가해도 괜찮습니다.
pnpm add @firsttx/prepaint @firsttx/local-first @firsttx/tx
# 선택적으로 필요한 레이어만 설치할 수도 있습니다.
pnpm add @firsttx/prepaint
pnpm add @firsttx/prepaint @firsttx/local-first
pnpm add @firsttx/local-first @firsttx/tx1. Vite 플러그인 활성화
import { defineConfig } from "vite";
import { firstTx } from "@firsttx/prepaint/plugin/vite";
export default defineConfig({
plugins: [firstTx()],
});2. 엔트리 포인트 교체
import { createFirstTxRoot } from "@firsttx/prepaint";
import App from "./App";
createFirstTxRoot(
document.getElementById("root")!,
<App />
);Local-First 모델 + 동기화 훅
Local-Firstimport { defineModel, useSyncedModel } from "@firsttx/local-first";
import { z } from "zod";
const CartModel = defineModel("cart", {
schema: z.object({
items: z.array(
z.object({
id: z.string(),
name: z.string(),
qty: z.number(),
}),
),
}),
});
function CartPage() {
const { data: cart } = useSyncedModel(
CartModel,
() => fetch("/api/cart").then((r) => r.json()),
);
if (!cart) return <Skeleton />;
return <div>{cart.items.length} items</div>;
}Tx로 낙관적 업데이트를 트랜잭션으로 감싸기
UI를 먼저 업데이트한 뒤 서버와 동기화하고, 요청이 실패하면 되돌릴 수 있는 트랜잭션 경로를 제공합니다.
import { startTransaction } from "@firsttx/tx";
async function addToCart(item: CartItem) {
const tx = startTransaction();
await tx.run(
() =>
CartModel.patch((draft) => {
draft.items.push(item);
}),
{
compensate: () =>
CartModel.patch((draft) => {
draft.items.pop();
}),
},
);
await tx.run(() =>
fetch("/api/cart", {
method: "POST",
body: JSON.stringify(item),
}),
);
await tx.commit();
}