본문으로 건너뛰기
피드

Linear가 빠른 이유? 브라우저 안에 DB를 넣고 서버를 ‘동기화 대상’으로 밀어낸 설계

frontend 약 14분
vote
0
댓글
북마크

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

  • 1

    Linear UI는 서버 응답이 아니라 브라우저의 IndexedDB와 메모리 상태를 먼저 읽음

  • 2

    이슈 수정은 로컬 MobX observable을 즉시 바꾸고, IndexedDB 트랜잭션 큐에 저장한 뒤 서버로 비동기 전송함

  • 3

    Parcel에서 Rollup, Vite, Rolldown으로 빌드 파이프라인을 바꾸며 전송 코드 50% 감소, 압축 후 30% 감소, Safari 활성 이슈 화면 첫 페인트 59% 개선을 얻음

  • 4

    서비스 워커가 약 1,200개 해시 자산을 백그라운드 캐싱해 이후 탐색과 오프라인 사용을 빠르게 만듦

  • 5

    성능은 엔지니어링만이 아니라 단축키, 커맨드 팔레트, 짧은 애니메이션 같은 제품 설계와 같이 만들어짐

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

  • 전통적인 CRUD 앱은 사용자가 버튼을 누르면 서버 왕복이 끝날 때까지 UI가 기다리는 구조임

    • 클릭 → HTTP 요청 → 서버 DB 조회·수정 → 응답 → 브라우저 리렌더링 흐름이라, 이슈 하나 업데이트에도 대략 300ms가 걸릴 수 있음
    • Linear는 이 루프를 뒤집어서, UI가 읽는 실제 데이터베이스를 브라우저 안에 둠
  • Linear의 핵심은 IndexedDB에 저장된 로컬 데이터와 메모리 상태를 먼저 바꾸는 구조임

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

중요

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

  • 대부분의 앱이 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이 나면 로그인으로 돌리는 방식임

💡

> 사내 툴이나 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개가 각각 다시 그려지는 식임
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 사례는 반대로 ‘네트워크를 사용자 경험에서 지우는 구조’가 얼마나 강한지 보여준다. 특히 업무용 SaaS를 만드는 팀이라면 로컬 우선 모델 전체를 따라 하지 않더라도 낙관적 업데이트, 번들 쪼개기, 애니메이션 제한만으로도 체감 성능을 크게 바꿀 수 있다.

댓글

댓글

댓글을 불러오는 중...

frontend

타입스크립트 7.0, Go 네이티브 포팅으로 빌드가 8~12배 빨라졌다

마이크로소프트가 타입스크립트 7.0을 출시하면서 기존 자바스크립트 기반 구현을 Go로 네이티브 포팅했다. 전체 빌드는 평균 8~12배 빨라졌고, VS코드 빌드는 125.7초에서 10.6초로 줄었으며, 슬랙은 CI 타입 검사 시간을 7.5분에서 1.25분으로 낮췄다.

frontend

타입스크립트 7 공개, 고 포팅으로 빌드가 최대 10배 이상 빨라짐

마이크로소프트가 고(Go)로 네이티브 포팅한 타입스크립트 7을 공개했다. 전체 빌드는 보통 8~12배 빨라지고, VS 코드 코드베이스에서는 125.7초가 10.6초로 줄었으며, 편집기 첫 오류 표시도 17.5초에서 1.3초 미만으로 내려갔다.

frontend

“98% 지원”이 생각보다 별로 안 넉넉한 이유

브라우저 기능 지원률 98%는 얼핏 높아 보이지만, 실제 서비스에서는 2%의 사용자를 깨진 화면 앞에 세우는 숫자일 수 있다는 글이다. 특히 일반 통계와 내 서비스 방문자 분포는 다를 수 있고, 실제 사례로 어떤 사이트는 최신 중첩 CSS 기능 지원 브라우저가 방문자의 약 70%뿐이었다.

frontend

메타, AI가 읽고 쓰는 디자인 시스템 ‘Astryx’ 오픈소스로 공개

메타가 내부 디자인 시스템 Astryx를 오픈소스로 공개했다. React와 StyleX 기반의 150개 이상 접근성 지원 컴포넌트, 디자인 토큰, 테마, 템플릿, CLI를 제공하며, 특히 AI 에이전트가 컴포넌트 구조를 이해하고 UI를 생성·수정할 수 있도록 MCP 서버까지 포함한 점이 핵심이다.

frontend

shadcn/ui, 기본 컴포넌트 라이브러리를 Radix에서 Base UI로 변경

shadcn/ui가 새 프로젝트의 기본 컴포넌트 라이브러리를 Radix가 아니라 Base UI로 바꿨어. Radix 지원은 계속되지만, Base UI가 안정화됐고 주간 다운로드 600만 이상, shadcn/create 신규 프로젝트 선택 비율 2대1이라는 이유로 기본값을 바꾼 거야. 동시에 채팅 UI용 MessageScroller와 관련 컴포넌트, headless 패키지인 @shadcn/react도 공개됐어.