Tech_News

Linear는 왜 그렇게 빠른가 — 로컬 우선 동기화 엔진의 실체

TeEm0 2026. 6. 8. 22:34

Linear는 왜 그렇게 빠른가 — 로컬 우선 동기화 엔진의 실체

왜 지금 이 주제인가

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 같은 기성 동기화 엔진부터 검토. 충돌 처리와 스키마 마이그레이션 비용을 처음부터 견적에 넣어라.

참고 자료

주: Linear 내부 구현의 충돌 처리·저장소 세부는 공개 정보가 제한적이므로, 위 공식 블로그와 발표 자료로 교차 확인하길 권한다.

사진: Microsoft Copilot / Unsplash

728x90