# Linear는 어떻게 이렇게 빠른가? 기술적 분석

> Clean Markdown view of GeekNews topic #30276. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=30276](https://news.hada.io/topic?id=30276)
- GeekNews Markdown: [https://news.hada.io/topic/30276.md](https://news.hada.io/topic/30276.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-06-08T12:35:37+09:00
- Updated: 2026-06-08T12:35:37+09:00
- Original source: [performance.dev](https://performance.dev/how-is-linear-so-fast-a-technical-breakdown)
- Points: 3
- Comments: 1

## Topic Body

- **Linear**는 이슈 관리 작업을 브라우저 내 데이터베이스와 로컬 우선 동기화로 처리해, 이슈 업데이트가 몇 밀리초 안에 UI에 반영되는 생산성 도구
- UI가 읽는 실제 데이터베이스는 **IndexedDB**에 있고, 변경은 로컬에 먼저 적용된 뒤 서버로 비동기 전송되고 WebSocket으로 델타를 다시 배포하는 구조
- 첫 로드는 적은 JavaScript·CSS 전송, 공격적 코드 분할, **modulepreload**, 서비스 워커 프리캐시, 인라인 앱 셸로 네트워크 대기를 줄이는 로딩 전략
- 동기화 엔진은 IndexedDB 데이터를 **MobX** 객체 풀로 수화하고, 변경을 트랜잭션 큐에 저장하며, 필드 단위 관찰 가능 상태로 필요한 셀만 다시 렌더링하는 구현
- 빠른 체감 속도는 키보드 중심 입력, 전역 명령 팔레트, GPU 친화적 애니메이션, 짧은 전환 시간까지 합쳐진 **시스템 설계**의 결과

---

### 브라우저 안의 데이터베이스
- 전통적인 CRUD 웹앱은 사용자의 클릭 뒤 HTTP 요청, 서버의 데이터베이스 조회, 응답 수신, 브라우저 리페인트를 거치며 수백 밀리초 동안 스피너·스켈레톤·멈춘 UI가 발생
- Linear는 UI가 읽는 실제 데이터베이스를 브라우저의 IndexedDB에 두고, 변경은 로컬에 먼저 적용한 뒤 서버로 비동기 전송하며, 서버는 WebSocket으로 다른 클라이언트에 델타를 브로드캐스트
- 빠른 웹앱에서 가장 큰 병목은 네트워크이며, 클라이언트와 서버 사이의 데이터 전송은 수백 밀리초 비용을 발생
- Linear의 핵심 흐름은 네트워크 요청을 사용자에게 보이지 않게 만들고, 가능한 로딩 상태를 없애는 방식

```js
// Linear
issue.title = "Faster app launch";
issue.save();
```

- `issue.title = "Faster app launch"`는 메모리 내 데이터 저장소를 갱신하고, Linear의 경우 MobX observable을 사용
- `issue.save();`는 동기화 엔진이 배치 처리해 서버로 플러시할 트랜잭션을 큐에 넣는 동작
- UI는 로컬 메모리 변경을 기준으로 동기적으로 다시 렌더링되며, 데이터 동기화는 백그라운드에서 진행되므로 스피너가 필요 없음
- [Tuomas](https://x.com/artman)는 2024년 컨퍼런스에서 Linear에서 처음 작성한 코드가 동기화 엔진이었다고 말했으며, 스타트업에서 일반적이지 않은 접근이었다는 표현 사용
- 대부분의 앱은 Linear처럼 자체 동기화 엔진을 만들 필요가 없으며, [TanStack Query](https://tanstack.com/query/latest)와 [SWR](https://swr.vercel.app/)의 낙관적 업데이트만으로도 상당히 가까운 체감 속도 구현 가능
- 낙관적 요청은 불필요한 스피너 제거, 즉시 상태 업데이트, 백그라운드 검증, 필요 시 롤백을 통해 높은 개선 효과 제공
- UI 반응성은 네트워크 지연에 의존하지 않아야 하며, 사용자가 느끼는 속도는 서버 응답 속도보다 인터페이스 반응 속도에 의해 결정
- ## Linear의 스택 엿보기
  - Linear는 React, TypeScript, MobX, Postgres, CDN 같은 단순한 스택 위에 구축
  - 프런트엔드는 React와 `react-dom`, MobX, TypeScript, Rolldown-Vite와 `plugin-react-oxc`, ProseMirror와 `y-prosemirror`, Radix UI primitives, Emotion과 StyleX, Comlink, `idb`, `graphql-request`, Sentry, Inter Variable 사용
  - 백엔드는 Node.js와 TypeScript, Cloud SQL 위 PostgreSQL, Memorystore Redis, turbopuffer, GCP의 Kubernetes, Cloudflare Workers 사용
  - 데스크톱 클라이언트는 Electron 기반이며, 모바일은 iOS용 Swift와 Kotlin으로 별도 전체 재구현
  - 마케팅 사이트는 Next.js, styled-components, 인라인 SVG sprite 사용
  - Linear는 클라이언트 사이드 렌더링을 유지하며, 올바른 아키텍처와 디자인이 있으면 CSR도 즉각적으로 느껴질 수 있다는 사례
  - 앱 전체를 클라이언트 사이드로 유지하면 서버·클라이언트 구분, `window` 접근 가능 여부, 캐시 헤더 설정 같은 복잡성을 줄이는 단순한 정신 모델 확보

### 첫 로드를 즉각적으로 느끼게 만들기
- 생산성 도구에서 실제 작업을 시작하기까지 걸리는 시간은 중요한 세부 요소
- 클라이언트 사이드 앱의 초기 로드는 `index.html` 요청, JavaScript와 CSS 요청, 인증 처리, 앱 표시를 위한 API 요청 순서로 느려질 수 있음
- ## Linear의 번들러 흐름: Parcel, Rollup, Vite, Rolldown
  - 즉각적인 체감 속도는 런타임 이전인 빌드 타임에서 시작하며, 빠른 로드를 위해 전송하는 JavaScript와 CSS 양을 줄이는 작업이 중요
  - Linear는 빌드 파이프라인을 Parcel → Rollup → Vite → Rolldown 순서로 다시 작성했으며, 각 이전은 JavaScript·CSS 양 감소와 개발자 경험 개선을 목표로 함
  - Linear 블로그 기준 개선 수치
  - 전송 코드 50% 감소
  - 압축 후 크기 30% 감소
  - 콜드 캐시 페이지 로드 10~30% 개선
  - Safari에서 active-issues 뷰의 Time-to-first-paint 59% 감소
  - 메모리 사용량 70~80% 감소
  - 개선의 상당 부분은 최신 브라우저만 대상으로 삼는 결정, 더 나은 dead-code elimination, 공격적 코드 분할의 조합에서 발생
  - 레거시 지원 중단은 polyfill, ES5 트랜스파일, `nomodule` fallback 제거로 이어지는 큰 이점
  - Linear는 최적화 이후에도 약 21MB의 minified JavaScript를 전송하지만, 이를 수백 개의 라우트 수준 청크로 공격적으로 분할해 필요할 때 가져오는 방식
  - 핵심은 특정 번들러 선택이 아니라 레거시 브라우저 제거, 네이티브 ESM 전환, 적극적 코드 분할
  - 이런 단계들이 누적되며 Linear의 첫 로드 JavaScript는 대략 절반으로 줄고, 빌드 시간은 한 자릿수 차원이 아니라 한 규모 단위로 감소
- ## 초기 로드 이후 프리로드
  - JavaScript를 작은 청크로 나누면 각 청크가 다른 청크를 import하는 폭포형 로드 문제가 생김
  - Linear는 JavaScript 실행 전 브라우저가 전체 목록을 보고 요청을 병렬로 시작하게 만들어, entry script가 첫 `import`에 도달할 때 관련 청크가 이미 캐시에 들어가도록 구성
  - `&lt;head&gt;`의 `modulepreload`와 entry script의 `crossorigin` 값을 맞춰, 브라우저가 preload와 import를 별도 리소스로 취급하지 않고 캐시된 fetch를 재사용
  - 콜드 로드 타임라인은 순차 폭포에서 단일 병렬 배치로 바뀌며, 네트워크 작업은 여전히 존재하지만 한꺼번에 수행
  - 사용자가 로그인 페이지에 처음 도달한 시점에 백그라운드에서 이 작업을 수행하고, 몇 초 뒤 전체 앱을 캐시에 저장해 즉시 제공 가능
- ## 더 빠른 속도와 오프라인 기능을 위한 서비스 워커
  - 사용자가 아직 방문하지 않은 뷰의 라우트 수준 청크는 서비스 워커가 백그라운드에서 캐시
  - 서비스 워커는 소스에 내장된 precache manifest를 갖고 있으며, 약 1,200개의 해시된 asset이 라우트 청크, 아이콘, 폰트를 포괄
  - 로그인 화면에 도달한 뒤 몇 초 안에 전체 앱이 캐시에 들어가는 구조
  - 이후 탐색은 네트워크를 완전히 건너뛰고, 서비스 워커가 HTTP 캐시를 거치지 않고 자체 캐시에서 직접 응답
  - 로컬 우선 동기화 엔진과 IndexedDB에 이미 저장된 사용자 데이터가 결합되면 Linear는 오프라인에서도 사용 가능
  - 이슈 읽기, 새 이슈 생성, 제목과 설명 편집, 상태 변경 지원
  - 모든 작업은 로컬 트랜잭션 저장소에 큐잉되고, 연결이 돌아오면 플러시
  - `modulepreload`는 지금 필요한 것을 병렬로 가져와 브라우저가 직렬 import chain에서 막히지 않게 하는 장치
  - 서비스 워커는 다음에 필요할 것을 준비하는 장치
  - Linear의 빠른 로딩 단계는 가능한 많은 코드 제거, 작은 조각으로 분할, 백그라운드 프리캐시이며, 목표는 네트워크 요청을 빠르게 만들거나 완전히 제거하는 것
- ## Vendor 번들 구성
  - Linear가 사용하는 각 패키지는 별도 청크로 나뉘고 독립적으로 캐시
  - 전통적인 `vendor.js`는 의존성 하나만 업데이트해도 전체 의존성 그래프 캐시가 무효화
  - Linear의 청크 분할은 단일 대형 파일 대신 세밀한 vendor 캐싱을 만들며, 특정 의존성 업데이트 시 해당 청크 하나만 무효화되고 나머지는 캐시에 유지
- ## 큰 폰트 파일 로딩
  - 잘못된 폰트 로딩은 잠시 보이지 않는 텍스트, 실제 폰트 교체 시 레이아웃 시프트, preload 불일치로 인한 중복 fetch를 만들 수 있음
  - Linear는 `&lt;head&gt;`에서 Inter Variable 폰트를 preload하고, `static.linear.app`에 preconnect
  ```html
  <link rel="preload"
        href="https://static.linear.app/fonts/InterVariable.woff2?v=4.1"
        as="font" type="font/woff2" crossorigin="anonymous">
  &lt;link rel="preconnect" href="https://static.linear.app" crossorigin&gt;
  ```
  - Variable font는 100~900 전체 weight 축을 단일 woff2로 처리해 weight별 요청 제거
  - `font-display: swap`은 fallback stack을 즉시 렌더링하고, Inter 로드 완료 후 교체
  - preload 태그의 `crossorigin="anonymous"`는 CSS가 이후 같은 폰트를 참조할 때 브라우저가 캐시된 리소스를 재사용하게 만드는 핵심 설정
  - `crossorigin`이 없으면 preload와 CSS 참조의 CORS 모드가 달라져 브라우저가 폰트를 다시 가져오는 문제 발생
- ## 인라인 앱 셸
  - Linear는 `&lt;head&gt;` 안에 로딩 상태를 그리기에 충분한 CSS를 인라인으로 넣어 외부 스타일시트 요청 없이 앱 셸을 표시
  - 인라인 JavaScript는 초기 경험에 필요한 분기를 즉시 수행
  - Electron과 Linear user agent를 감지해 `electron` 클래스 추가
  - `localStorage.ApplicationStore`가 없으면 `logged-out` 클래스 추가
  - `localStorage.splashScreenConfig`에서 사이드바 배경, 사이드바 폭, 다크 모드 같은 shell token 복원
  - 사용자가 데스크톱 앱에서 링크를 열도록 설정한 경우 사이드바 폭을 `8px`로 조정
  - 첫 JavaScript 번들이 네트워크에서 도착하기 전에 로딩 화면은 로그인 여부에 맞게 테마, 크기, 위치가 이미 맞춰진 상태
  - 사용자가 URL 입력 뒤 Enter를 누르는 즉시 앱이 준비된 것처럼 느끼게 만드는 가장 빠른 방법은 초기 `index.html` 응답에 앱 셸을 내려보내는 방식
- ## 먼저 렌더링하고 나중에 인증
  - 일반적인 인증 흐름은 HTML fetch, 번들 로드, 세션 검증, 사용자 fetch, 워크스페이스 fetch, 렌더 순서로 진행되며, 사용자가 무언가를 보기까지 1~3초가 걸릴 수 있음
  - Linear는 인증도 변경 처리와 같은 방식으로 다루며, 정상 경로를 가정하고 백그라운드에서 검증
  - 대부분의 CRUD 앱은 실제 세션을 HttpOnly 쿠키에 두고, 프런트엔드가 시작 중 로그인 여부를 알 수 있도록 JavaScript에서 읽을 수 있는 별도 쿠키나 `/me` 요청을 추가
  - Linear의 인라인 부트 스크립트는 병렬 인증 신호 대신 `localStorage.ApplicationStore` 존재 여부만 확인
  ```js
  if (localStorage.getItem("ApplicationStore") === null) {
    document.documentElement.classList.add("logged-out");
  }
  ```
  - `ApplicationStore`가 있으면 사용자가 이 브라우저에서 Linear를 사용한 적이 있고, 워크스페이스 데이터가 IndexedDB에 이미 있는 상태
  - 값이 없으면 렌더링할 데이터가 없으므로 shell이 logged-out 레이아웃으로 전환되고 로그인 흐름이 이어짐
  - 실제 세션 토큰은 쿠키에 있으며, 번들은 세션 상태를 미리 판단하지 않음
  - WebSocket handshake, sync delta, HTTP 호출 중 하나가 만료된 세션으로 401을 받으면 클라이언트가 로그인으로 리디렉션
  - 전체 패턴은 로컬 데이터를 신뢰해 즉시 렌더링하고, 서버를 정확성의 출처로 두며, 양쪽을 비동기로 조정하는 구조

### 동기화 엔진
- Linear의 속도는 서버를 UI의 source of truth가 아니라 sync target으로 보는 결정에서 출발
- 속도는 단일 요소가 아니라 세 가지 축이 맞물린 결과
- ## 1. 데이터가 이미 있음
  - 앱 부팅 시 서버에서 워크스페이스를 가져오지 않고, IndexedDB에서 메모리 내 MobX 객체 풀로 수화
  - UI의 모든 쿼리는 객체 풀을 먼저 향하며, 이슈가 이미 사용자 기기에 있으므로 “loading issues” 상태가 없음
  - Linear는 확장 과정에서 JavaScript 번들과 비슷한 원리로 동기화 엔진의 데이터를 청크화
  - 가장 무거운 두 테이블인 Issue와 Comment는 한꺼번에 가져오지 않고 필요할 때 lazy-hydrate
  - 이 방식은 데이터 수준 코드 분할이며, 시작 비용이 워크스페이스 크기가 아니라 워크스페이스 구조를 따라가게 만듦
  - 10,000개 이슈가 있는 워크스페이스도 100개 이슈 워크스페이스와 거의 비슷한 속도로 부팅
  - 프로젝트로 들어가면 이슈가 이미 있고, assignee로 필터링하면 인덱스가 이미 구축된 상태
- ## 2. 변경은 네트워크를 기다리지 않음
  - 이슈 상태를 바꾸면 세 가지가 거의 동시에 발생
  - MobX observable 업데이트로 UI에 변경 반영
  - IndexedDB의 내구성 있는 트랜잭션 큐에 변경 기록
  - 서버 전송 큐에 변경 추가
  - 이 시점에서 네트워크는 아직 사용되지 않음
  - 사용자는 자신의 변경을 보기 위해 기다리지 않으며, 재시도, 롤백, reload across durability는 모두 백그라운드에서 처리
  - 서버가 거부하면 observable이 되돌아가고 짧은 flicker가 발생하지만, 대부분의 잘못된 변경은 트랜잭션 생성 전에 잡힘
  - Linear의 흐름은 로컬 변경에서 시작하고 서버를 허가 단계가 아니라 확인 단계로 취급
- ## 3. 하나의 델타, 하나의 셀
  - 서버가 사용자의 변경이나 다른 사람의 변경을 확인하면, 무엇이 이동했는지 나타내는 작은 JSON envelope가 클라이언트로 돌아옴
  - 클라이언트는 해당 MobX observable에 값을 쓰는 방식으로 변경 적용
  - Linear의 모든 모델 속성은 각각 observable이며, 해당 속성을 읽는 모든 컴포넌트는 `observer()`로 감싸짐
  - MobX는 어떤 컴포넌트가 어떤 필드에 의존하는지 정확히 알 수 있음
  - 한 이슈의 한 필드 변경은 그 필드를 읽는 컴포넌트만 다시 렌더링하며, 부모 목록이나 사이드바 전체를 다시 렌더링하지 않음
  - 50개 이슈 업데이트는 목록 전체 리렌더링이 아니라 50개 셀 리렌더링
  - 10명이 동시에 편집하는 바쁜 워크스페이스에서도 업데이트 수신 비용은 화면에 있는 전체 항목이 아니라 실제로 바뀐 항목에 맞춰 증가
- ## 세 가지가 맞물리는 이유
  - 로컬 데이터베이스만 있고 낙관적 쓰기가 없으면 저장 시 여전히 스피너 발생
  - 낙관적 쓰기만 있고 세밀한 observable이 없으면 모든 업데이트에서 버벅임 발생
  - 세밀한 observable만 있고 로컬 데이터베이스가 없으면 초기 로드에서 여전히 대기 발생
  - Linear의 속도는 단일 계층의 속성이 아니라 시스템 전체의 속성
  - 번들러와 로더 셸은 첫 paint를 빠르게 느끼게 만들고, 동기화 엔진은 사용 시작 이후에도 빠른 느낌을 유지

### 속도를 위한 디자인
- 속도는 엔지니어링 문제이면서 디자인 문제
- 가장 빠른 액션 경로가 마우스, 세 개의 메뉴, 클릭을 요구하면 사용자는 내부 엔진 속도와 무관하게 그 단계를 비용으로 지불
- Linear 속도의 또 다른 축은 키보드를 탐색과 작업 완료의 주요 도구로 통합한 점
- 모든 일반적인 작업에는 shortcut이 있고, command palette는 한 번의 키 입력으로 열리며, right-click menu는 커스텀으로 구축
- ## 모든 액션에는 shortcut이 있음
  - 단일 문자는 포커스된 이슈를 편집하고, 두 글자 조합은 탐색에 사용되며, modifier는 전역 동작에 사용
  - Linear의 초기 단계부터 shortcut은 기반 요소였고, 동기화 엔진은 어떤 액션도 언제든 수행할 수 있도록 설계된 부분이 있음
  - UI 곳곳에서 shortcut이 보이며, 가장 자주 쓰는 shortcut은 단일 문자
  - 초보자를 배제하지 않기 위해 모든 액션은 마우스로도 수행 가능
- ## Command palette는 항상 한 번의 키 입력 거리
  - `⌘ k`는 Linear의 거의 모든 액션을 검색할 수 있는 command palette를 열음
  - 검색 대상은 이슈, 프로젝트, label, 상태 변경, 탐색, 이슈 생성, 설정, 테마 토글 등
  - command palette는 서버가 아니라 로컬 MobX 객체 풀을 검색하므로 매우 빠름
  - 전체 앱은 단일 pane에서 접근 가능하며, 탐색, 이슈 생성, 상태 변경이 모두 검색을 통해 수행
  - command palette는 현재 작업 맥락에 맞춰 적응하고, 각 view의 핵심 액션과 shortcut을 가르치는 방식으로 동작
  - 빠른 앱에는 뛰어난 엔지니어링과 디자인이 모두 필요하며, 엔지니어링 속도는 단일 상호작용을 빠르게 만들고 디자인 속도는 상호작용까지의 경로를 짧게 만듦
  - 하루 종일 쓰는 도구에서는 shortcut과 2초짜리 마우스 경로의 차이가 모든 액션에서 누적

### 애니메이션
- 나쁜 애니메이션은 초기 로드, 업데이트, 데이터베이스 쿼리 최적화로 줄인 밀리초를 마지막 단계에서 다시 낭비할 수 있음
- 500ms짜리 height animation 같은 요소는 사용자가 기다리지 않게 하려는 노력을 무너뜨릴 수 있음
- ## 애니메이션해야 할 속성은 몇 가지뿐
  - 브라우저의 property change는 렌더링 파이프라인상의 위치에 따라 세 계층의 비용을 가짐
  - composited property인 `transform`과 `opacity`는 GPU로 작업을 넘기고 main thread와 독립적으로 실행
  - paint-triggering property인 `color`, `background-color`, `border-color`, `fill`은 layout을 건너뛰지만 pixel redraw를 발생
  - layout-triggering property인 `width`, `height`, `top`, `left`, `margin`, `padding`은 이후 모든 요소의 위치를 다시 계산하게 만들며 애니메이션 대상이 아니어야 함
  ```css
  /* Linear 방식 */
  .row:hover {
    background-color: var(--color-bg-hover);
    transition: background-color 0.12s;
  }
  .icon-arrow {
    transform: translateX(0);
    transition: transform 0.15s;
  }
  ```
  - `margin-left`를 애니메이션하면 hover된 row 아래 모든 row의 layout이 transition 전체 200ms 동안 매 frame 재계산
  - 긴 이슈 목록에서는 이 차이가 부드러운 화면과 jank를 가르는 요소
  - Linear의 애니메이션 속성은 대부분 `transform`과 `opacity` 같은 composited property이며, 때때로 `background-color`와 `border-color` 사용
- ## 절제할 때를 알아야 함
  - 매일 사용하는 도구에서는 마케팅 사이트에서 보기 좋은 애니메이션이 작업을 방해할 수 있음
  - 잘못된 위치의 작은 hover delay도 사용자가 알아차리는 지점이 될 수 있음
  - Linear의 많은 애니메이션은 origin을 참조하기 때문에 동작이 효과적
  - status popover는 status pill에서 scale out하고, agent panel은 toggle에서 slide in
  - 이런 motion은 장식용 fade가 아니라 새 요소가 어디에서 왔는지 알려주는 공간적 역할 수행
- ## 짧고 즉각적인 duration 유지
  ```css
  --speed-highlightFadeIn: 0s;
  --speed-highlightFadeOut: .15s;
  --speed-quickTransition: .1s;
  --speed-regularTransition: .25s;
  --speed-slowTransition: .35s;
  ```
  - 많은 design system은 기본 duration을 필요 이상으로 길게 둠
  - Material의 standard duration은 200ms이고, iOS spring은 350ms에 가까움
  - Linear의 기본값은 업계 관행보다 짧은 쪽에 위치
  - Linear는 enter와 exit에 비대칭 timing을 사용
  - hover highlight, popover, agent panel은 호출될 때 즉시 나타나고, 닫을 때 150ms 동안 fade out
  - agent window는 즉시 나타나고 macOS와 비슷하게 fade out

### Linear가 빠른 방식
- Linear의 성능은 단일 비밀이나 한 가지 기술이 아니라 올바른 수백 개의 결정이 누적된 결과
- 접근 방식의 많은 부분은 단순하며, Next, TanStack, 화려한 framework 없이도 사용자에게 맞는 아키텍처를 초기에 정하고 유지한 결과
- 서버는 UI의 source of truth가 아니라 sync target으로 동작
- 데이터베이스는 브라우저 안에 있고, 변경은 로컬에 먼저 적용된 뒤 백그라운드에서 조정
- 첫 로드는 더 적은 코드를 더 많은 조각으로 전송하고, 서비스 워커는 사용자가 로그인 페이지에 있는 동안 나머지를 precache
- 인증은 로컬 상태를 기반으로 정상 경로를 가정하고 나중에 검증
- 동기화 엔진은 IndexedDB에서 per-property MobX observable로 수화하므로, 50개 이슈 업데이트가 목록 전체 리렌더링이 아니라 50개 셀 리렌더링으로 처리
- 입력 모델은 keyboard-first이며, 모든 일반 작업에는 shortcut과 전역 command palette가 있음
- 애니메이션은 GPU 친화적 속성에 머물고, layout-triggering property는 애니메이션하지 않음
- 어려운 부분은 구현 자체보다 코드베이스가 성숙하고 확장되며 새로운 제약을 만나는 동안 수년간 세부 품질에 집중하는 태도

## Comments



### Comment 59118

- Author: neo
- Created: 2026-06-08T12:35:38+09:00
- Points: 1

###### [Hacker News 의견들](https://news.ycombinator.com/item?id=48437609) 
- 애플리케이션에 이런 경험을 넣고 싶다면 **Zero**([https://zero.rocicorp.dev/](<https://zero.rocicorp.dev/>))를 보면 좋음  
  라이브 데모: [https://gigabugs.rocicorp.dev/](<https://gigabugs.rocicorp.dev/>)  
  대안 목록도 여기 있음: [https://zero.rocicorp.dev/docs/when-to-use#alternatives](<https://zero.rocicorp.dev/docs/when-to-use#alternatives>)  
  내부 동작이 궁금하면 Replicache 설계 문서도 참고할 만함: [https://doc.replicache.dev/concepts/how-it-works](<https://doc.replicache.dev/concepts/how-it-works>)  
  Replicache는 Zero의 전신이고, 핵심 프로토콜은 여전히 같은 방식으로 동작함
  - **Zero**는 정말 추천할 만함. 추상화가 훌륭하고 세심하게 만든 소프트웨어임  
    데이터를 클라이언트에 동기화해 두는 데서 오는 명확한 성능 이점뿐 아니라, React 코드가 얼마나 단순해지는지도 놀라웠음. 동기화 엔진이 있으면 대부분의 클라이언트 상태가 사라지고, 컴포넌트 코드 대부분을 동기적으로 생각할 수 있게 됨
  - 한동안 **Zero**를 써왔음. 처음에는 Linear식 동기화 엔진을 내부에서 만들려고 했는데 Zero를 발견했음  
    전담 팀을 꾸리지 않고 얻을 수 있는 것 중에는 아마 가장 가까운 선택지임
  - Zero 사용자로서, 데이터베이스가 바뀌면 UI가 몇 밀리초 안에 즉시 갱신되는 **사용자 경험**을 원할 때 딱 맞는 도구임

- Linear가 빠르다는 얘기를 늘 들었지만, 실제로 매일 써보니 열정이 식었음. **검색은 꽤 느리고**, UI는 종종 둔탁하며 보기에는 좋지만, “Pulse”는 작은 규모에서도 소음의 홍수 같음  
  필요한 것을 찾기 어려워서 결국 전부 즐겨찾기에 넣게 됨. 초기 Trello가 프로젝트 추적 경험으로는 압도적으로 최고였음
  - 회의나 허들 중에 티켓을 띄우려 할 때, 로딩인지 캐싱인지 모를 작업을 터무니없이 오래 기다리며 어색하게 바라보게 되는 게 정말 싫음

- 작년에 누군가 **Linear 동기화 엔진**을 역공학해서 GitHub에 올리고 멋진 설명도 붙였음  
  [https://github.com/wzhudev/reverse-linear-sync-engine/blob/m...](<https://github.com/wzhudev/reverse-linear-sync-engine/blob/main/SUMMARY.md>)
  - 당시에도 논의됐음: Reverse engineering of Linear's sync engine - [https://news.ycombinator.com/item?id=44123131](<https://news.ycombinator.com/item?id=44123131>) - 2025년 5월, 댓글 33개

- 이런 **로컬 우선 동기화 웹 앱**은 정말 흥미롭고 유용할 수 있지만, 전제는 다소 틀렸다고 봄  
  “Linear에서 이슈를 업데이트하는 데 몇 밀리초면 충분하다. 전통적인 CRUD 앱은 같은 작업에 약 300ms가 걸린다”, “클라이언트와 서버 사이에 오가는 모든 데이터는 수백 밀리초를 비용으로 치른다”는 식의 전제임  
  HTTP 클라이언트와 서버 사이 왕복 시간이 빛의 속도 때문에 커지는 문제 자체는 해결할 수 없지만, 백엔드를 사용자 가까이에 두고 빠르게 만들 수는 있음  
  예를 들어 대부분 사용자에게 약 10ms 왕복 시간 안에 있는 웹 앱 백엔드를 운영하고, 백엔드도 약 10ms 안에 응답을 렌더링하게 하는 건 충분히 가능함. 즉 전통적인 CRUD 앱도 같은 작업을 300ms가 아니라 **30ms 정도**로 만들 수 있음
  - 300ms를 빠르다고 말하는 걸 보면서 이상한가 싶었는데, 기억하는 한 **TTFB 30ms**가 오랫동안 목표였음  
    Linear가 백엔드에서 정당한 이유로 시간이 더 걸려 프런트엔드의 도움이 필요할 수는 있지만, 그걸 일반화할 수는 없음. JavaScript 한 조각마다 자체 비용도 따름
  - “대부분 사용자에게 약 10ms 왕복 시간 안에 웹 앱 백엔드를 운영하는 게 가능하다”는 말은 이상함. us-east-1에서 10ms 미만인 AWS 리전은 사실상 us-east-2뿐임: [https://www.cloudping.co/](<https://www.cloudping.co/>)  
    us-west-1은 60ms, eu-centra-1은 100ms, 아시아는 200ms 떨어져 있음. 이것도 데이터센터 간 트래픽이고, 실제 공용 인터넷에서 가정용 회선까지의 지연 시간은 훨씬 나쁨  
    데이터베이스는 정확히 한 리전에 있어야 함. 어디에 두든 지구상 사용자 대다수는 거기서 100ms 이상 떨어지게 됨  
    엔드포인트가 어디 있든 상관없는 이유는, 엔드포인트가 데이터를 읽고 쓰려면 데이터베이스와 통신해야 하기 때문임. 데이터를 사용자 가까이에 복제하려는 순간, 결국 **로컬 우선 동기화 데이터베이스**를 소유하게 됨  
    직접 만들든 기성품을 쓰든, 그 복제 데이터베이스는 클라이언트 측 동기화와 같은 문제를 모두 갖게 되고, 여전히 상당한 네트워크 지연도 남음. 물리는 피해갈 수 없어서, 대부분 사용자에게 0.25초 커밋을 주거나 최종 일관성, 즉 동기화를 택하는 수밖에 없음
  - 사용자들이 서로 꽤 가까운 곳에 있거나, 슬프지만 흔하듯 미국 사용자만 빠르면 되고 나머지는 신경 쓰지 않는 경우에만 가능함  
    물론 전 세계 CDN 엣지 네트워크 같은 곳에 “중간 백엔드”를 둘 수는 있지만, 그 시점에는 “중간 백엔드”를 클라이언트에 두는 이 방식과 같은 복잡도 비용을 치르게 됨
  - 클라이언트에 **중간 백엔드**를 두고, 아직 처리되지 않은 변경을 로컬 저장소에 쓰고, 백그라운드 워커가 필요한 재시도와 함께 백엔드로 보내게 할 수 있음  
    최악의 경우 백그라운드 워커가 업데이트 실패 메시지를 내보내고 UI 스레드가 받아서 보여주면 됨. 성공 경로는 계속 번개처럼 빠르게 유지됨
  - 이런 엣지 백엔드들이 모두 공유해야 하는 **데이터베이스**가 있을 때도 가능한가?

- **최종 일관성 데이터베이스**를 작성하는 건 어렵고, Linear의 사용 사례에는 괜찮을 수 있지만 내 업데이트가 서버, 즉 팀에 도달했는지 모르는 건 문제가 됨  
  예전에 참여한 다른 프로젝트들에서 동기화 지연이 셀 수 없는 문제를 만들었기 때문에 항상 동기식 해법을 택함. 화려한 기능은 꼭 필요할 때만 꺼내고, 차라리 서버를 엄청 빠르게 최적화한 뒤 사용자가 네트워크 지연을 감수하게 두겠음
  - Linear에서 불일치를 겪은 적은 있지만, Jira는 쓰레기장이라서 어쩔 수 없긴 함

- 회사에서 Linear를 쓰고 있음. 소수派인 건 알지만 **사용자 경험**이 정말 힘듦. 빠르다고도 부르기 어려움  
  페이지 자체는 기술적으로 그럭저럭 빨리 로드되지만, 절반쯤은 데이터 로딩이 아직 진행 중이라는 시각적 표시 없이 페이지의 숫자들이 바뀌는 걸 보게 됨
  - Linear에서 자주 겪는 문제는 반복적인 쓰기가 가끔 서로를 덮어쓴다는 것임. 입력하고, 잠깐 생각하려고 멈추고, 더 입력하면, Linear가 데이터를 첫 입력 상태로 되돌림  
    이 정도로 나빠서 Linear에서는 한 문장짜리 설명으로 이슈만 만들고, 자세한 내용은 GitHub로 가서 채움. Linear가 잘하고 빠른 건 딱 그 정도임
  - Linear는 자신이 없애려 했던 대상, 즉 **복잡한 도구**가 되어버렸음  
    안타깝지만 회사가 살아남고 상위 시장으로 올라가려면 사실상 그 길밖에 없음
  - Chrome을 쓰지 않아서 그 영향인지는 모르겠지만, Linear 페이지는 자주 멈추거나 첫 로드에 수초가 걸림  
    “빠르다”는 단어는 쓰지 않겠음. 애초에 로드하는 데 30초가 걸리는데, 이슈 업데이트가 300ms에서 “몇” 밀리초로 줄어드는 건 별로 중요하지 않음
  - 예전 직장에서는 괴상한 UX 때문에 Linear에서 Jira로 바꿨음. 의미를 알기 어려운 아이콘이 많고, 발견 가능성이 낮고, 페이지의 내용이 왜 바뀌는지 표시도 거의 없었음
  - 정말 끔찍함. 항목에 **마감일**을 추가하는 방법을 동료에게 물어봐야 했는데, 탐색 패널 안에 숨겨져 있었음  
    Jira보다는 낫지만, 그건 기준이 아주 낮음

- 멋지다. 개발 중인 브라우저 게임과 엔진에도 비슷한 걸 넣어서 첫 로드 이후에는 **로딩 상태**를 완전히 없앨 수 있을지도 모르겠음. 내 것은 서버 없는 완전 클라이언트 측 정적 에셋 구조임  
  이 게임의 성능에 집착해 왔음. 지난 주말 전에는 M1 MacBook Pro에서 동시 플레이어 128명을 시뮬레이션하면서 경로 탐색, 무거운 전략 로직, 렌더링을 모두 뷰포트 안에서 처리하고 120fps를 유지하는 데 애먹었고, 아주 가끔 프레임 드롭이 나면서 프레임 시간이 약 4ms였음  
  주말 동안 강하게 성능 작업을 했고 이제는 동시 플레이어 **2048명**을 밀리초 미만 프레임 시간으로 시뮬레이션할 수 있음. 렌더링과 모든 로직, 절차적 생성까지 포함한 수치임  
  또 11.2배 CPU 스로틀링을 걸어 저사양 모바일 기기 수준을 시뮬레이션했을 때도 동시 플레이어 256~512명에서 약 5ms 프레임 시간으로 안정적인 60fps가 나옴. 지금 주요 병목은 약간의 로직 문제와 저사양 기기에서 개선해야 할 시작/부팅 시간이고, Linear에서 배울 점이 있을 것 같음

- Linear가 사실 꽤 느리다고 느꼈음. 한동안 탭을 열어두면 **CPU 100%** 로 도는 주도 있었음
  - Firefox에서는 메모리도 많이 쓰는 것 같음. Linear 탭을 몇 개 이상 동시에 열어둘 수가 없음

- 흥미롭긴 함. 솔직히 Linear를 “빠르다”고 생각해본 적은 없음. 대부분의 웹 앱처럼 지연이 있는 편이었지만, JIRA와 비교하면 물론 광속임  
  Linear 자체는 훌륭하고, **JIRA 고문** 이후에는 정말 신선함. 낙관적 라우트와 “빠름”을 말하려면 먼저 Gmail부터 이야기해야 하지 않을까 싶음

- 속도의 답은 **미리 불러오기**임. 기본적으로 초기화 시점에 클라이언트 데이터베이스를 다운로드하고 캐시 무효화 전략을 두는 방식임  
  이 패러다임의 데이터 동기화 측면을 수행하려고 starfx를 만들었음: [https://starfx.bower.sh/learn#data-loading-strategy-stale-wh...](<https://starfx.bower.sh/learn#data-loading-strategy-stale-while-revalidate>)
