왜 지금 이 주제인가
Linear 써본 사람은 다들 한 번쯤 "어, 이거 왜 이렇게 빨라?"라고 느낀다. 이슈를 만들고 상태를 바꾸고 필터를 거는데 로딩 스피너가 거의 안 보인다. 네트워크 탭을 열어보면 클릭할 때마다 API를 때리는 게 아니라, UI가 먼저 반응하고 서버 통신은 뒤에서 조용히 처리된다.
이게 단순한 "캐싱 잘했네" 수준이 아니다. Linear는 로컬 우선(local-first) 동기화 엔진이라는 아키텍처를 정면으로 채택했다. 클라이언트가 데이터의 1차 소유자처럼 동작하고, 서버는 동기화 허브 역할을 한다. 요즘 사내 툴이나 대시보드 만들 때 "왜 이렇게 굼뜨지"를 고민하는 사람이라면, Linear가 택한 트레이드오프를 이해해 두는 게 큰 도움이 된다.
핵심은 이거다. 대부분의 웹앱이 느린 이유는 네트워크가 사용자 클릭과 화면 갱신 사이에 끼어 있기 때문이다. Linear는 그 네트워크를 임계 경로(critical path)에서 빼버렸다.
핵심: 동작 원리
전통적인 SPA의 데이터 흐름은 대충 이렇다.
클릭 → API 요청 → DB 쓰기 → 응답 → 상태 갱신 → 리렌더
└────────── 이 구간 동안 사용자는 기다림 ──────────┘
Linear 같은 로컬 우선 모델은 순서가 뒤집힌다.
클릭 → 로컬 스토어 즉시 갱신 → 리렌더 (여기서 사용자 체감 끝)
└→ 백그라운드로 서버에 변경분 전송
└→ 서버가 다른 클라이언트에 브로드캐스트
1) 로컬에 전체 모델을 들고 있는다
Linear는 사용자가 접근 가능한 이슈/프로젝트/팀 데이터를 브라우저 로컬(IndexedDB로 보인다)에 적재해 둔다. 그래서 필터링, 정렬, 검색 같은 작업이 서버 왕복 없이 메모리/로컬에서 처리된다. 필터를 바꿀 때 즉각 반응하는 이유가 이거다. 서버에 "이 조건으로 다시 쿼리해줘"라고 묻지 않는다.
2) 옵티미스틱 업데이트 + 동기화 큐
상태를 바꾸면 로컬 모델을 먼저 고치고 화면을 갱신한다. 그 변경(mutation)은 동기화 큐에 쌓여 백그라운드로 전송된다. 네트워크가 끊겨도 큐에 남아 있다가 복구되면 흘려보낸다. 오프라인에서도 작업이 되는 이유다.
// 개념적 의사코드 (실제 구현 아님)
function updateIssueStatus(issueId, status) {
localStore.apply({ issueId, status }); // 1. 즉시 반영
rerender(); // 2. UI 갱신
syncQueue.push({ type: 'update', issueId, status }); // 3. 큐잉
}
// 동기화 워커가 큐를 비우며 서버로 전송하고,
// 서버 ACK / 충돌 응답을 받아 로컬을 보정
3) 델타 동기화와 실시간 전파
전체를 다시 받지 않고 마지막 동기화 시점 이후의 변경분(델타)만 주고받는다. 다른 사람이 이슈를 바꾸면 WebSocket으로 변경이 흘러와 내 로컬 모델에 머지된다. 그래서 협업 중에도 화면이 자연스럽게 갱신된다.
4) 충돌 처리
여러 클라이언트가 같은 필드를 동시에 고치면 충돌이 난다. Linear는 이 부분을 자체 동기화 엔진으로 다룬다고 알려져 있는데, 구체적으로 LWW(Last-Write-Wins)인지 CRDT 기반인지 필드 단위 머지인지는 공개 정보가 제한적이라 공식 자료 확인이 필요하다. 다만 "필드 단위로 잘게 쪼개 머지하면 충돌 범위가 줄어든다"는 일반 원리는 분명하다.
실무 관점: 도입할 때 진짜 따져야 할 것들
여기가 핵심이다. 로컬 우선이 멋있어 보인다고 아무 프로젝트에 끌어오면 망한다. 내가 사내 툴 몇 개 다뤄보며 느낀 트레이드오프를 정리한다.
이게 잘 맞는 경우
- 데이터 총량이 클라이언트에 들어갈 수 있을 때. Linear는 "한 사람이 접근하는 이슈 수"가 수만~수십만 단위라 로컬에 올릴 수 있다. 수억 row를 다루는 분석 대시보드엔 이 모델이 안 맞는다.
- 같은 데이터를 반복해서 읽고 쓰는 워크플로. 이슈 트래커, 노트앱, 칸반처럼 사용자가 한 데이터셋을 계속 만지는 도구.
- 오프라인/저지연이 진짜 가치인 경우. 그냥 "빠르면 좋지" 정도면 React Query 캐싱으로 충분할 때가 많다.
흔한 함정
- 권한/필터링을 클라이언트로 미루는 실수. 로컬에 데이터를 올린다고 권한 검사를 클라이언트에서만 하면 보안 사고다. 서버는 여전히 "이 유저가 받을 수 있는 데이터만" 내려줘야 한다. 로컬 우선은 UX 최적화지, 보안 모델이 아니다.
- 초기 동기화 비용. 첫 로그인 때 데이터를 다 끌어오는 부트스트랩 구간이 무겁다. 이걸 백그라운드 점진 로딩으로 어떻게 자르냐가 체감 품질을 좌우한다.
- 스키마 마이그레이션 지옥. 서버 DB만 바꾸면 끝이 아니다. 사용자 브라우저마다 옛 버전의 로컬 스키마가 살아 있다. 클라이언트 마이그레이션 전략(버전 태깅, 강제 리싱크)을 처음부터 설계 안 하면 나중에 크게 운다.
- 충돌 처리를 직접 짜는 비용. 옵티미스틱 업데이트 롤백, 서버 거부 시 UI 복구, 머지 로직… 이거 제대로 만들려면 사람과 시간이 든다. "빨라 보이려고" 시작했다가 동기화 엔진 자체가 제품이 되어버린다.
대안 — 풀 셀프빌드 말고도 길은 있다
- 가벼운 옵티미스틱 캐싱: TanStack Query(React Query)의 optimistic update + 무효화. 대부분의 CRUD 앱은 이 정도로 충분히 빠르다. 동기화 엔진 직접 안 짜도 된다.
- 로컬 우선 프레임워크: ElectricSQL, Replicache, Zero, PowerSync, RxDB 같은 솔루션이 동기화/충돌 처리를 대신 해준다. Linear 흉내 내고 싶으면 바닥부터 짜지 말고 이쪽을 먼저 검토하는 게 현실적이다.
- CRDT 기반: 협업 편집(텍스트, 동시 수정)이 핵심이면 Yjs/Automerge. 단, 이슈 트래커 같은 구조화 데이터엔 과할 수 있다.
한 줄 조언: "Linear처럼 만들자"가 아니라 "우리 데이터 모델이 로컬에 들어가나? 충돌 빈도가 높나?"부터 답하라. 그 답이 애매하면 동기화 엔진은 오버엔지니어링이다.
정리
Linear가 빠른 이유는 마법이 아니라 네트워크를 사용자 클릭의 임계 경로에서 제거했기 때문이다. 로컬에 데이터를 들고, 옵티미스틱하게 먼저 그리고, 동기화는 백그라운드로 밀어버리는 구조다.
- 써야 할 사람: 사용자가 한정된 데이터셋을 반복적으로 만지는 협업 툴(이슈 트래커, 노트, 칸반)을 만드는 팀.
- 피해야 할 경우: 데이터가 로컬에 안 들어갈 만큼 크거나, 단순 CRUD라 React Query 캐싱으로 충분한 경우.
- 현실적 시작: 바닥부터 짜지 말고 Replicache·ElectricSQL·Zero 같은 기성 동기화 엔진부터 검토. 충돌 처리와 스키마 마이그레이션 비용을 처음부터 견적에 넣어라.
참고 자료
- How's Linear so fast? A technical breakdown (performance.dev)
- Linear 공식 블로그 — Scaling the Linear Sync Engine
- Local-First Web Development 자료 모음
- Ink & Switch — Local-first software (개념 원전)
- TanStack Query — Optimistic Updates 문서
- ElectricSQL · Replicache · Zero
주: Linear 내부 구현의 충돌 처리·저장소 세부는 공개 정보가 제한적이므로, 위 공식 블로그와 발표 자료로 교차 확인하길 권한다.
'Tech_News' 카테고리의 다른 글
| SQLite에서 UUID를 기본 키로 쓰면 안 되는 이유: 클러스터드 인덱스와 B-tree 재균형 비용 (0) | 2026.06.07 |
|---|---|
| pg_durable: PostgreSQL 안에서 돌아가는 durable execution, 실무에서 쓸만한가 (1) | 2026.06.07 |
| Java 25 가상 스레드, 진짜로 1만 개를 띄워봤다 - 실무 도입 전 알아야 할 것들 (0) | 2026.06.07 |
| Elixir 1.20, 점진적 타입 시스템이 들어왔다 — 동적 언어에 타입을 붙인다는 것의 실체 (0) | 2026.06.04 |
| github.dev에서 한 번 클릭으로 GitHub 토큰이 털린다 - VSCode 웹뷰 버그 뜯어보기 (0) | 2026.06.04 |
