---
title: "Linear가 빠른 이유? 브라우저 안에 DB를 넣고 서버를 ‘동기화 대상’으로 밀어낸 설계"
published: 2026-06-07T19:01:37.000Z
canonical: https://jeff.news/article/3848
---
# Linear가 빠른 이유? 브라우저 안에 DB를 넣고 서버를 ‘동기화 대상’으로 밀어낸 설계

Linear의 속도는 특정 프레임워크나 마법 같은 최적화가 아니라, 브라우저 로컬 DB, 낙관적 업데이트, 세밀한 MobX 반응성, 공격적인 코드 스플리팅, 서비스 워커 캐싱, 키보드 중심 UX가 쌓인 결과다. 전통적인 CRUD 앱이 이슈 업데이트에 약 300ms를 쓰는 동안 Linear는 사용자가 체감하기 전에 로컬 상태를 먼저 바꾸고 서버와는 나중에 맞춘다.

## Linear의 속도는 ‘서버가 빨라서’가 아니라 ‘사용자가 서버를 기다리지 않게 해서’ 나옴

- 전통적인 CRUD 앱은 사용자가 버튼을 누르면 서버 왕복이 끝날 때까지 UI가 기다리는 구조임
  - 클릭 → HTTP 요청 → 서버 DB 조회·수정 → 응답 → 브라우저 리렌더링 흐름이라, 이슈 하나 업데이트에도 대략 300ms가 걸릴 수 있음
  - Linear는 이 루프를 뒤집어서, UI가 읽는 실제 데이터베이스를 브라우저 안에 둠

- Linear의 핵심은 IndexedDB에 저장된 로컬 데이터와 메모리 상태를 먼저 바꾸는 구조임
  - 사용자가 이슈 제목을 바꾸면 `issue.title = "Faster app launch"`처럼 메모리의 MobX observable이 즉시 바뀜
  - `issue.save()`는 서버 응답을 기다리는 호출이 아니라, 동기화 엔진에 트랜잭션을 큐잉하는 동작에 가까움
  - UI는 이미 로컬 상태를 보고 다시 그려졌기 때문에 스피너가 낄 자리가 없음

> [!IMPORTANT]
> 이 글의 한 줄 요약은 이거임. 사용자가 느끼는 속도는 서버 응답 시간이 아니라 인터페이스가 얼마나 빨리 반응하느냐로 결정됨.

- 대부분의 앱이 Linear식 동기화 엔진을 직접 만들 필요는 없지만, 낙관적 업데이트는 바로 써먹을 수 있음
  - TanStack Query나 SWR의 optimistic mutation만 잘 써도 ‘저장 중’ 스피너를 상당히 줄일 수 있음
  - 대부분의 요청은 성공하므로 먼저 UI를 바꾸고, 실패했을 때만 롤백하는 쪽이 체감상 훨씬 빠름
  - 글쓴이는 ‘네트워크는 적’이라고 반복하는데, 과장은 좀 있어도 웹앱 성능 감각으로는 꽤 맞는 말임

## 첫 로드를 빠르게 보이게 만드는 빌드와 캐싱 전략

- Linear의 스택은 의외로 화려하지 않음
  - 프론트엔드는 React, TypeScript, MobX, IndexedDB wrapper, GraphQL transport, Radix UI, ProseMirror, Yjs 등을 사용함
  - 백엔드는 Node.js, TypeScript, PostgreSQL, Redis, Kubernetes on GCP, Cloudflare Workers 정도로 정리됨
  - 눈에 띄는 건 Next.js나 React Server Components 같은 유행 프레임워크 없이 클라이언트 사이드 렌더링(CSR)을 고수한다는 점임

- 대신 빌드 파이프라인은 계속 갈아엎음
  - Parcel → Rollup → Vite → Rolldown 순서로 옮겨왔고, 목표는 늘 전송하는 JavaScript와 CSS를 줄이는 것이었음
  - Linear가 공개한 수치 기준으로 전송 코드 50% 감소, 압축 후 크기 30% 감소, 콜드 캐시 페이지 로드 10~30% 개선이 있었음
  - Safari의 active issues 화면에서는 time-to-first-paint가 59% 줄었고, 메모리 사용량은 70~80% 감소했다고 함

- 레거시 브라우저 지원을 줄인 것도 큰 선택임
  - `esnext` 타깃, 네이티브 ESM, 폴리필 제거, ES5 트랜스파일 제거, 공격적인 dead-code elimination이 조합됨
  - 전체 minified JavaScript는 여전히 약 21MB로 크지만, 수백 개의 라우트 단위 청크로 쪼개서 필요한 순간에 가져오게 만듦
  - 각 npm 패키지를 별도 vendor 청크로 나누면, 의존성 하나가 바뀌어도 전체 `vendor.js`를 다시 받지 않아도 됨

- 코드 스플리팅은 잘못하면 네트워크 폭포가 되는데, Linear는 modulepreload로 이걸 눌러버림
  - 브라우저가 엔트리 스크립트를 파싱한 뒤 import를 보고 다음 청크를 받기 시작하면 매 단계마다 왕복 시간이 붙음
  - Linear는 HTML의 `<head>`에 주요 청크를 `rel=modulepreload`로 박아두고, 브라우저가 처음부터 병렬로 가져오게 함
  - `crossorigin`을 preload와 entry script에 맞춰 캐시 재사용이 깨지지 않게 하는 디테일도 챙김

- 서비스 워커는 ‘지금 필요한 것’이 아니라 ‘곧 필요할 것’을 미리 깔아둠
  - Linear의 서비스 워커는 약 1,200개 해시 자산이 들어간 precache manifest를 갖고 있고, 라우트 청크·아이콘·폰트 등을 백그라운드에서 가져옴
  - 사용자가 로그인 화면에 도착한 뒤 몇 초 안에 앱 대부분이 캐시에 들어가고, 이후 탐색은 네트워크를 거의 타지 않음
  - IndexedDB에 데이터가 있고 트랜잭션 큐가 있으니, 오프라인에서도 이슈 읽기·생성·수정·상태 변경이 가능함

## 앱이 뜨는 척이 아니라, 진짜로 바로 쓸 수 있게 보이게 함

- Linear는 초기 HTML 안에 앱 셸을 그릴 만큼의 CSS와 JavaScript를 인라인으로 넣음
  - 외부 CSS를 기다리지 않고 배경, 사이드바 폭, 테두리, 다크 모드 같은 기본 레이아웃을 먼저 그림
  - `localStorage.splashScreenConfig`를 읽어 사용자가 마지막으로 보던 사이드바 색상, 폭, 테마를 첫 페인트 전에 적용함
  - 번들이 오기 전부터 로그인 상태에 맞는 껍데기가 보이니, 사용자는 앱이 바로 준비된 것처럼 느낌

- 인증도 ‘먼저 검증하고 렌더링’이 아니라 ‘보여줄 게 있으면 먼저 보여주고 나중에 검증’에 가까움
  - 일반 앱은 HTML → 번들 → 세션 검증 → 사용자 조회 → 워크스페이스 조회 → 렌더링 순서로 가면서 1~3초를 잃기 쉬움
  - Linear는 `localStorage.ApplicationStore`가 있으면 이전에 이 브라우저에서 쓴 데이터가 IndexedDB에 있다고 보고 먼저 렌더링함
  - 실제 세션 토큰은 쿠키에 있고, 이후 WebSocket handshake나 HTTP 요청에서 401이 나면 로그인으로 돌리는 방식임

> [!TIP]
> 사내 툴이나 SaaS를 만들 때도 ‘세션이 완벽히 검증되기 전에는 아무것도 못 보여준다’는 습관을 의심해볼 만함. 이미 로컬에 보여줄 데이터가 있다면 먼저 보여주고, 서버 검증 실패를 나중에 처리하는 선택지가 있음.

## 동기화 엔진은 세 가지가 같이 맞물릴 때 빨라짐

- 첫째, 데이터가 이미 브라우저에 있음
  - 앱 부팅 시 서버에서 워크스페이스 전체를 새로 가져오지 않고, IndexedDB에서 MobX 객체 풀로 hydrate함
  - UI의 쿼리는 서버가 아니라 이 메모리 풀을 먼저 봄
  - 무거운 `Issue`, `Comment` 테이블은 필요할 때 lazy hydrate해서, 10,000개 이슈 워크스페이스도 100개짜리처럼 부팅 비용을 낮추는 방향으로 설계됨

- 둘째, 변경은 네트워크를 기다리지 않음
  - 이슈 상태를 바꾸면 MobX observable이 먼저 바뀌고, durable transaction queue가 IndexedDB에 기록되고, 서버 전송은 그다음에 큐잉됨
  - 서버가 거부하면 롤백과 짧은 flicker가 생길 수 있지만, 대부분의 잘못된 변경은 트랜잭션 생성 전에 클라이언트에서 걸러짐
  - 서버는 사용자의 클릭에 허락을 내려주는 존재가 아니라, 변경을 확인하고 다른 클라이언트에 전파하는 대상에 가까움

- 셋째, 서버에서 돌아온 delta는 딱 필요한 셀만 바꿈
  - 서버 확인이나 다른 사용자의 변경은 작은 JSON envelope로 들어오고, 클라이언트는 해당 MobX observable에만 적용함
  - 모델의 각 속성이 observable이고, 컴포넌트가 `observer()`로 감싸져 있어 어떤 필드를 읽었는지 추적됨
  - 이슈 50개가 바뀌면 리스트 전체가 다시 그려지는 게 아니라 관련 셀 50개가 각각 다시 그려지는 식임

```mermaid
sequenceDiagram
    participant 사용자
    participant 로컬상태 as 브라우저 메모리
    participant 로컬DB as IndexedDB
    participant 동기화 as 동기화 엔진
    participant 서버
    사용자->>로컬상태: 이슈 상태 변경
    로컬상태->>사용자: UI 즉시 갱신
    로컬상태->>로컬DB: 트랜잭션 큐 저장
    로컬DB->>동기화: 변경 대기열 전달
    동기화->>서버: 배치 전송
    서버-->>동기화: delta 확인
    동기화-->>로컬상태: 필요한 observable만 갱신
```

## 빠른 앱은 코드만 빠른 게 아니라 조작 경로도 짧음

- Linear는 키보드를 보조 수단이 아니라 기본 입력 모델로 봄
  - 자주 쓰는 동작은 단일 문자 단축키로 처리하고, 이동은 두 글자 조합, 전역 동작은 modifier와 함께 묶음
  - 마우스로도 모든 작업을 할 수 있지만, 숙련자는 거의 키보드만으로 이슈 생성·이동·상태 변경·검색을 처리할 수 있음
  - 엔지니어링이 한 번의 상호작용을 빠르게 만든다면, 설계는 그 상호작용까지 가는 길 자체를 줄임

- 커맨드 팔레트는 로컬 데이터 위에서 돌기 때문에 빠름
  - `⌘ K`로 이슈, 프로젝트, 라벨, 상태 변경, 설정, 테마 전환 등을 검색하고 실행함
  - 서버 검색이 아니라 로컬 MobX 객체 풀을 검색하므로 반응이 즉각적임
  - 같은 프리미티브를 네비게이션, 생성, 상태 변경, 설정에 반복해서 쓰기 때문에 사용자는 하나의 패턴만 익히면 됨

- 애니메이션도 성능의 일부로 제한함
  - Linear는 주로 `transform`, `opacity`처럼 GPU 합성으로 처리되는 속성을 애니메이션함
  - `width`, `height`, `margin`, `padding`, `top`, `left` 같은 레이아웃 트리거 속성은 피함
  - 기본 전환 시간도 짧게 잡혀 있고, hover나 popover는 나타날 때 즉시 반응하고 사라질 때만 150ms 정도로 부드럽게 빠지는 식임

- 결론은 ‘한 방 최적화’가 아니라 수년간 같은 방향으로 쌓은 선택들임
  - 서버를 UI의 진실 공급원이 아니라 동기화 대상으로 둠
  - 데이터는 브라우저에 두고, 변경은 로컬에서 먼저 적용함
  - 번들은 작게 쪼개고, 필요한 청크는 병렬 preload하고, 나머지는 서비스 워커가 캐싱함
  - 렌더링은 필드 단위로 최소화하고, 조작은 키보드와 커맨드 팔레트로 짧게 만들고, 애니메이션은 레이아웃을 건드리지 않음

---

## 기술 맥락

- Linear가 고른 핵심 선택은 서버 중심 CRUD가 아니라 local-first 동기화예요. 왜냐하면 생산성 도구에서 사용자가 가장 자주 느끼는 병목은 데이터베이스 쿼리 속도보다 클릭 뒤에 UI가 멈추는 순간이거든요.

- IndexedDB와 MobX 조합은 단순 캐시보다 더 깊은 선택이에요. IndexedDB는 새로고침과 오프라인 상황에서도 데이터를 남기고, MobX는 어떤 화면 조각이 어떤 필드를 읽었는지 추적해서 변경 비용을 작게 만들기 때문이에요.

- 이 구조의 대안은 서버에서 항상 최신 데이터를 받아오는 전통적인 방식이에요. 구현은 더 단순하지만 네트워크 왕복마다 스피너가 생기고, 협업이 많은 워크스페이스에서는 작은 변경도 큰 리렌더로 번질 수 있어요.

- 빌드 최적화도 같은 철학으로 이어져요. Linear는 모든 코드를 한 번에 빨리 받으려 하기보다, 필요한 청크는 modulepreload로 병렬화하고 나머지는 서비스 워커가 뒤에서 채우게 해요. 사용자가 기다리는 경로에서 네트워크를 최대한 치우는 거죠.

- 이 방식은 아무 앱에나 공짜로 붙는 패턴은 아니에요. 충돌 처리, 롤백, 권한 검증, 오프라인 큐 같은 복잡도가 생기니까요. 그래도 하루 종일 켜두는 협업 SaaS라면 그 복잡도를 감수할 이유가 충분해요.

## 핵심 포인트

- Linear UI는 서버 응답이 아니라 브라우저의 IndexedDB와 메모리 상태를 먼저 읽음
- 이슈 수정은 로컬 MobX observable을 즉시 바꾸고, IndexedDB 트랜잭션 큐에 저장한 뒤 서버로 비동기 전송함
- Parcel에서 Rollup, Vite, Rolldown으로 빌드 파이프라인을 바꾸며 전송 코드 50% 감소, 압축 후 30% 감소, Safari 활성 이슈 화면 첫 페인트 59% 개선을 얻음
- 서비스 워커가 약 1,200개 해시 자산을 백그라운드 캐싱해 이후 탐색과 오프라인 사용을 빠르게 만듦
- 성능은 엔지니어링만이 아니라 단축키, 커맨드 팔레트, 짧은 애니메이션 같은 제품 설계와 같이 만들어짐

## 인사이트

요즘 웹앱 성능 논의가 서버 컴포넌트나 엣지 런타임으로 쉽게 흐르는데, Linear 사례는 반대로 ‘네트워크를 사용자 경험에서 지우는 구조’가 얼마나 강한지 보여준다. 특히 업무용 SaaS를 만드는 팀이라면 로컬 우선 모델 전체를 따라 하지 않더라도 낙관적 업데이트, 번들 쪼개기, 애니메이션 제한만으로도 체감 성능을 크게 바꿀 수 있다.
