CSR 앱이 시작되는 동안 마지막 화면을 다시 표시합니다.visual replay·persistent client cache·보상 롤백을 묶은 3-레이어 툴킷.
FirstTx는 Prepaint, Local-First, Tx 세 레이어를 하나의 툴킷으로 묶었습니다.
CSR 아키텍처는 그대로 두고, 측정 가능한 재방문 replay·클라이언트 상태 보존·보상형 낙관적 UI를 더하세요.
FirstTx 도입 전
사용자 재방문
브라우저 뒤로 가기나 링크 클릭으로 CSR 앱으로 다시 진입합니다.
빈 화면 노출
JS 번들과 API 응답을 기다리며 1~2초 동안 흰 화면이나 스피너가 보입니다.
JS 로딩 & 마운트
React와 데이터 요청이 끝난 뒤에야 실제 화면이 렌더링됩니다.
FirstTx 도입 후
부트 스니펫
HTML 로드 직후, 작은 부트 스크립트가 한 번 실행됩니다.
마지막 화면 복원
재방문 부트 구간에 IndexedDB의 마지막 DOM 스냅샷을 다시 표시합니다.
앱 마운트 & 동기화
React 앱이 마운트되고 데이터 재검증은 백그라운드에서 실행할 수 있습니다.
THREE LAYERS · ONE TOOLKIT
Prepaint · Local-First · Tx
필요한 레이어만 골라 도입하세요. 재방문 속도는 Prepaint로, 동기화·낙관적 UI는 Local‑First와 Tx로 해결합니다.
마지막 화면을 DOM 스냅샷으로 저장하고, 재방문 시 React보다 먼저 복원합니다.
- visual snapshot을 IndexedDB에 보관
- 재방문 시 메인 앱 번들보다 먼저 replay
- CSR 부트 구간의 빈 화면 시간 단축
IndexedDB를 persistent client cache로 사용하고 서버 재검증 훅을 제공합니다.
- zod 기반 타입 세이프 모델 정의
- TTL·staleness 메타데이터 기본 제공
- 탭 간 invalidation과 백그라운드 재검증
낙관적 UI 단계를 실행하고 명시적인 보상 작업을 연결합니다.
- 낙관적 단계를 순서대로 실행
- 실패 시 보상(rollback) 로직 자동 실행
- 재시도와 롤백 동작 추적
HOW IT FEELS
앱에서 보이는 동작 방식
SSR은 첫 진입을 빠르게 해주지만, 실제 사용 시간은 재방문·뒤로 가기·탭 이동에 더 많이 쓰입니다. FirstTx는 이 순간들을 빠르게 만들어, 다시 올 때마다 준비된 화면을 보여줍니다.
재방문이 잦은 내부 도구
CRM·어드민·대시보드처럼 목록↔상세를 반복하는 화면에서 매번 새로 로딩하는 대신, 이전 상태를 바로 복원합니다.
작업 중 실수로 새로고침
로컬 모델이 최신 스냅샷을 들고 있어 필터·스크롤·폼 상태가 유지되고, 처음부터 다시 시작할 필요가 없습니다.
낙관적 UI가 실패했을 때
낙관적 업데이트를 트랜잭션으로 묶어 서버에서 거절되면 화면이 깔끔하게 되돌아가고, ‘반쯤만 롤백된 상태’를 피할 수 있습니다.
오프라인·불안정한 네트워크
마지막 클라이언트 snapshot을 보존하고 서버가 다시 연결되면 재검증합니다. 오프라인 쓰기 정책은 앱에서 별도로 정의해야 합니다.
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();
}