ChatGPT 웹을 역공학하다: OpenAI는 10억 사용자를 위해 어떻게 만들었나

TMT

원문: Dennis Brotzky, "Reverse Engineering ChatGPT Web: How OpenAI Built for a Billion Users", https://performance.dev/chatgpt (2026-07-02 게시)

이 글이 다루는 것

새 탭을 열고 chatgpt.com에 접속해 무언가를 물어보면, 계정도 로그인도 로딩 스피너도 없이 페이지가 즉시 반응하고 답변이 곧바로 흘러나옵니다. 그 경험을 약 10억 명이 쓰고 있습니다. 겉보기에는 단순한 인터페이스인데 파고들면 전혀 단순하지 않습니다. 저자는 페이지 소스, 번들 코드, 네트워크 요청을 며칠에 걸쳐 뜯어보며 이 앱이 어떻게 만들어졌는지 재구성했습니다.

목차는 다음과 같습니다.

  • 제약 조건에서 출발하기
  • Next.js에서 React Router로 옮긴 과정
  • 첫 페인트까지 가장 빠른 길
  • 10억 사용자 규모의 CSS
  • 컴포넌트는 바퀴를 다시 발명하지 않는다
  • 모든 답변은 렌더링 문제다
  • 이제 타이핑할 수 있습니까?
  • 전부 기능 플래그 뒤에 두기
  • 10억 명의 낯선 사람을 들여보내기
  • 첫 토큰까지 가장 빠른 길
  • ChatGPT와 Claude의 차이

시작하기 전에 전제를 밝혀 둡니다. 저자는 OpenAI 직원이 아니고 소스 코드를 본 적도 없습니다. 저자가 앞서 쓴 Linear·Conductor 분석과 다른 점은, OpenAI가 ChatGPT 웹에 대해 공개한 자료가 사실상 없다는 것입니다. 그래서 이 글은 역사 정리와 조사, 기술적 해석이 섞여 있습니다. 근거는 chatgpt.com 자체의 HTML·JavaScript·CSS·네트워크 요청이며, 여기에 여기저기 흩어진 발표·트윗·채용 페이지를 보탰습니다.

새로 접속한 chatgpt.com에서 프롬프트를 한 번 입력한 상황입니다. 누구나 겪는 흔한 경험입니다.


제약 조건에서 출발하기

저자가 웹 앱 구조를 설계할 때 가장 중요하게 보는 두 가지는 목표와 제약입니다. 이 둘이 이후의 모든 결정을 결정합니다. 고객이 누구인지, 초기 로딩이 얼마나 중요한지, 쓰려면 계정이 필요한지, 검색 엔진에 노출돼야 하는지, 예산은 얼마인지, 개발자 경험은 얼마나 중요한지, 이미 있는 구조가 있는지 같은 질문들입니다.

밖에서 보기에 ChatGPT의 목표는 분명합니다. 전 세계가 AI를 쓸 때 찾아오는 사이트가 되겠다는 것입니다. 그래서 어느 나라의 어떤 기기에서든 누구나 접근할 수 있어야 한다는 점이 대단히 중요합니다. 연결이 불안정한 안드로이드 폰으로 ChatGPT를 처음 접한 사람부터, 맥북 프로에서 Pro 요금제를 쓰는 헤비 유저까지 모두를 받아내야 합니다.

이런 제약을 세워 두면 선택 가능한 구조가 크게 줄어듭니다. 일반적인 클라이언트 사이드 렌더링은 탈락합니다. 의미 있는 화면이 나오기 전까지 자바스크립트를 내려받는 동안 빈 페이지를 보고 있어야 하기 때문입니다. Linear 같은 앱은 사용자가 한 번 로그인해 하루 종일 앱 안에서 지내니 CSR로도 문제가 없지만, OpenAI의 목표에는 맞지 않습니다.

이 경우 서버 사이드 렌더링이 더 낫지만 공짜는 아닙니다. 요청마다 서버가 데이터를 가져와 HTML을 만들어 돌려줘야 하니 정적 자산을 서빙하는 것보다 운영 비용이 올라갑니다. 개발자 입장에서도 머릿속으로 따라가기가 훨씬 어렵습니다. 앱이 서버와 브라우저라는 두 환경에 동시에 존재하게 되고, 어떤 API가 어디서 쓸 수 있는지를 계속 신경 써야 합니다. 하이드레이션 불일치를 만나거나, 서버에서 실수로 window를 참조하거나, 캐싱과 렌더링 동작을 디버깅하는 데 시간을 쓰게 됩니다. 실패 지점이 더 많은 복잡한 구조입니다. 하지만 목표가 가장 빠른 첫 로딩, 검색 엔진이 읽을 수 있는 진짜 HTML, 처음 방문한 사람에게 좋은 경험이라면 그 대가를 치를 만합니다.

저자가 파악한 현재 스택은 대략 이렇습니다.

프런트엔드
  React 19 + react-dom              (UI 런타임, 스트리밍 서버 렌더)
  React Router 7 (framework mode)   (라우팅, loader, 스트리밍 SSR)
  TypeScript                        (언어)
  TanStack Query                    (클라이언트 쪽 서버 상태)
  Tailwind CSS                      (유틸리티 스타일링 + 디자인 토큰)
  Radix UI primitives               (메뉴, 팝오버, 셀렉트, 토스트)
  ProseMirror                       (입력창 composer)
  CodeMirror 6                      (답변 코드 블록 + 캔버스 에디터)
  Motion                            (애니메이션)
  silk-hq                           (모바일에서 네이티브 느낌의 시트)
  KaTeX                             (수식, 폰트는 필요할 때만 로드)
  Mapbox GL                         (답변 안의 지도)
  시스템 폰트 스택                    (UI 텍스트에 웹폰트 없음)

빌드와 배포
  Vite                              (번들링, 라우트 단위 JS·CSS 청크)
  Cloudflare                        (CDN, 캐시, 봇 방어)
  자체 도메인 자산                    (전부 chatgpt.com/cdn/assets)

실험과 관측
  Statsig                           (플래그 + 실험, 서버에서 평가)
  Datadog RUM                       (실사용자 모니터링)

실시간
  fetch 위의 SSE                     (토큰 스트리밍)
  LiveKit + WebRTC                  (음성 모드)
  WebAssembly                       (오디오 처리, 구문 문법)

여기서 가장 눈에 띄는 것은 자체 프레임워크나 자체 컴포넌트가 아니라 시중에 나와 있는 라이브러리를 거의 그대로 쓴다는 점입니다. Gemini의 소스를 본 적이 있다면 구글 내부 기술로 가득한 전혀 다른 물건이라는 것을 알 것입니다. ChatGPT는 단순하고 표준적입니다. 10억 명 규모로 확장하는 앱을 만들 때 이 점은 대단히 중요합니다.

Next.js에서 React Router로 옮긴 과정

ChatGPT는 2022년 11월 30일에 Pages Router를 쓰는 Next.js 12 앱으로 출시됐습니다. OpenAI 내부에서 ChatGPT는 기대치가 낮은 "리서치 프리뷰"였고, 파인튜닝한 GPT-3.5를 감싸 대중의 피드백을 받아 보려는 간단한 래퍼였습니다. 결과가 어땠는지는 모두 아는 대로입니다.

Next.js 시절의 유물 중 저자가 가장 좋아하는 것은 웨이백 머신에 아직 남아 있는 라우트 매니페스트입니다. 초기 웹 앱이 얼마나 단순했는지, 그리고 같은 빌드 안에 사내용 codespace·workspace 도구가 함께 실려 있던 흔적까지 볼 수 있습니다. MVP를 급히 내보내는 과정에서 딸려 나온 부산물로 보입니다.

// 2022년 12월 출시 빌드의 _buildManifest.js
sortedPages: [
  "/",
  "/_app",
  "/_error",
  "/auth/error",
  "/auth/login",
  "/chat",
  "/codespace",
  "/error",
  "/workspace/[[...tid]]"
]

그 뒤 이어진 Next.js App Router 시대 내내 chatgpt.com은 App Router를 채택하지 않았습니다. Pages Router로 약 21개월을 버티다가 Next.js 자체를 걷어냈습니다. 저자도 예전 프로젝트에서 같은 경로를 밟았다고 합니다.

이 이전 작업은 발표된 것이 아니라 외부에 들켰습니다. 2024년 8월 27일 Tibor Blaho가 chatgpt.com이 일부 사용자에게 Remix 빌드를 실험적으로 서빙하는 것을 발견했습니다. 9월 4일에는 널리 배포되어 Remix 공동 제작자인 Ryan Florence가 "새 Remix 앱 등장, chatgpt.com"이라고 트윗했고, 다음 날 Wes Bos가 유튜브에서 번들을 뜯어봤습니다.

Wes가 찾아낸 내용이 흥미로운 지점입니다. 이 앱의 철학을 설명해 주기 때문입니다. Remix 시절의 앱은 실제 서버 인프라를 돌리고 있었습니다. Express 서버가 라우트 loader를 실행하고 약 7천 줄에 달하는 JSON을 window.__remixContext로 직렬화했습니다. 그런데 서버에서 실제 UI는 거의 렌더링하지 않았습니다. 셸, 프리로드 링크 몇 개, 테마 스크립트가 전부였습니다. Remix action은 어디에도 없었고, 변경 작업은 라이브러리의 내장 action 대신 자체 API를 통했습니다. Remix는 사실상 클라이언트 앱을 감싸는 라우터이자 데이터 파이프이자 하이드레이션 셸이었던 셈입니다. Ryan Florence는 나중에 ChatGPT가 Remix 앱이며 "데이터 로딩 대부분을 React Router의 loader 대신 TanStack Query로 처리한다"고 확인했습니다. Vite를 만든 Evan You는 커뮤니티의 해석을 한 줄로 정리했습니다. "여러분 중 많은 분들도 그냥 SPA면 충분할 겁니다."

여기까지는 Next.js Pages Router로 MVP를 낸 뒤 SPA에 가까운 구성의 Remix로 옮겼다는 이야기입니다. 그런데 뒷이야기가 더 있습니다.

2024년 11월 Remix v2가 React Router v7로 다시 합쳐지자 ChatGPT도 따라갔습니다. 지금은 어느 페이지의 소스를 봐도 이렇게 나옵니다.

window.__reactRouterContext = {
  "basename": "/",
  "ssr": true,
  "isSpaMode": false,
  "routeDiscovery": {
    "mode": "lazy",
    "manifestPath": "/__manifest"
   },
  // ...
};

ssr: true입니다. Vite 기반으로 스트리밍과 서버 렌더링을 모두 갖춘 React Router 7 framework mode입니다. 저자가 흥미롭게 본 변화는 이것입니다. Remix 시절에는 빈 셸만 내려줬는데, 지금은 로그아웃 상태의 경험 전체를 진짜 HTML로 서버 렌더링합니다. SSR 쪽으로 오히려 더 깊이 들어간 것입니다.

라우터 안에 숨어 있는 또 하나의 특이한 점이 있습니다. 클라이언트 매니페스트에는 354개의 라우트가 실려 있는데, 그중 실제 채팅 앱은 13개 정도뿐입니다. 나머지는 전부 마케팅·랜딩 페이지이고, 같은 코드베이스·같은 라우터·같은 배포에 함께 들어 있습니다. 라우트 이름은 React Router의 파일 규칙을 그대로 따릅니다.

routes/_conversation._index             채팅
routes/_conversation.c.$conversationId  대화 하나
routes/($lang).codex.pricing            마케팅
routes/($lang).business._index          마케팅
routes/($lang).atlas.get-started        마케팅

대부분의 회사는 마케팅 사이트를 앱과 분리합니다. Linear도 마케팅을 앱과 떨어진 Next.js에서 돌립니다. 저자가 파악한 바로는 OpenAI는 마케팅을 제품 앱 안에 함께 배포합니다. 랜딩 페이지가 디자인 시스템, 플래그 시스템, 라우터를 제품과 공유한다는 뜻입니다. 캠페인 페이지에서 채팅으로 넘어갈 때 페이지를 새로 받는 것이 아니라 클라이언트 사이드 내비게이션으로 이동합니다. 꽤 멋진 구성입니다.

chatgpt.com 페이지 소스에서 reactRouterContext 소스의 ssr 값이 true로 설정되어 있는 모습입니다.

chatgpt.com 페이지 소스에서 reactRouterContext 소스의 ssr 값이 true로 설정되어 있는 모습입니다.

첫 페인트까지 가장 빠른 길

저자가 측정한 로그아웃 상태의 문서는 압축 후 84KB로 내려오고, 밴쿠버에 있는 노트북까지 첫 바이트가 도착하는 데 약 50~65ms가 걸립니다. 가까운 Cloudflare 엣지가 서빙합니다. 이 한 번의 응답 안에 동작하는 앱 셸을 그리는 데 필요한 모든 것이 담겨 있습니다. 사이드바, "오늘은 어떤 일정이 있나요?" 인사말, 입력창을 포함한 약 30KB의 실제 마크업과 그것을 그릴 스타일입니다. 하이드레이션이 끝난 뒤 페이지 전체의 DOM 노드는 548개로 대단히 가볍습니다. 이 화면은 오직 하나의 목표, 곧 채팅 입력창에 집중하고 있습니다.

다만 문서에서 마크업과 DOM은 가장 재미없는 부분입니다. 성능 작업은 head에 들어 있습니다. 번들을 요청하기도 전에 인라인 스크립트가 먼저 돕니다. 첫 번째는 흔한 테마 선택 스크립트입니다.

// <head>에 인라인, 첫 페인트 전에 실행
!function(){try{
  var d=document.documentElement, c=d.classList;
  c.remove('light','dark');
  var e=localStorage.getItem('theme');
  if('system'===e||(!e&&true)){
    var t='(prefers-color-scheme: dark)', m=window.matchMedia(t);
    if(m.media!==t||m.matches){ d.style.colorScheme='dark'; c.add('dark') }
    else { d.style.colorScheme='light'; c.add('light') }
  } else if(e){ c.add(e||'') }
  if(e==='light'||e==='dark') d.style.colorScheme=e
}catch(e){}}();

Linear 분석 글에서 다뤘던 것과 똑같은 패턴입니다. localStorage에 저장된 테마를 읽어 페인트 전에 html 요소에 적용해서, 엉뚱한 테마가 한 번 번쩍이는 일을 없앱니다.

바로 다음에 오는 것이 ChatGPT의 목표를 잘 보여 주는, 저자가 특히 좋아하는 부분입니다.

window.__oai_logHTML?window.__oai_logHTML()
  :window.__oai_SSR_HTML=window.__oai_SSR_HTML||Date.now();
requestAnimationFrame(function(){
  window.__oai_logTTI?window.__oai_logTTI()
    :window.__oai_SSR_TTI=window.__oai_SSR_TTI||Date.now()
});

HTML이 실행을 시작하는 시점과 그 직후의 첫 프레임에 모든 사용자마다 타임스탬프를 찍어 실사용자 모니터링으로 보냅니다. 성능이 우선순위라는 것이 분명히 드러납니다. 첫 바이트부터 상호작용 가능 시점까지 로딩 경로 전체를 측정하고 있습니다. 측정하지 않는 것은 개선할 수 없습니다.

문서 응답 자체도 스트리밍됩니다. React Router를 통한 React 19 스트리밍 SSR입니다. 서버가 셸을 즉시 내보내고, 데이터가 준비되는 대로 Suspense 경계가 채워지며, 작은 인라인 스크립트가 그 조각들을 제자리에 끼워 넣습니다. 라우트 loader 데이터는 별도의 ReadableStream으로 HTML과 병렬로 도착합니다. 그리고 그 loader 데이터 안에는 클라이언트가 무엇을 미리 준비해 둘지에 대한 서버의 결정이 들어 있습니다. shouldPrefetchModels, shouldPrefetchHistory, shouldPrefetchStarredConversations 같은 값들입니다. chatgpt.com에서는 서버가 클라이언트의 동작을 상당 부분 지시합니다. 사용자별 프리페치 계획을 서버가 클라이언트에 건네주는 것입니다. 클라이언트는 서버가 운전하는 얇은 껍데기처럼 느껴집니다.

생각해 보면 이 용도에 아주 잘 맞는 접근입니다. 생성형 UI를 믿는다면 이것이 그 방향의 첫걸음입니다. 채팅 인터페이스 자체도 같은 방식입니다. 답변이 토큰, 마크다운, HTML, 그 밖의 위젯 신호를 통해 무엇을 보여 줄지 지시합니다. HTML 문서 전체가 그 구조를 그대로 반영합니다.

인증도 "먼저 그리고 나중에 확인하는" 방식을 따릅니다. 성능이 목표라면 이것이 최선이라고 저자는 거듭 강조합니다. 로그아웃 상태의 방문자는 인증 확인 때문에 막히는 일이 없습니다. 대신 백엔드 전체에 익명용 표면이 나란히 존재합니다. /backend-anon/me, /backend-anon/models, /backend-anon/conversation 같은 것들입니다. 익명 방문자도 실제 사용자 ID를 받으므로 계정 없이도 요청 제한과 실험이 동작합니다. 새 사용자와 입력창 사이에 "로그인했습니까?"를 묻는 왕복이 끼어들지 않습니다. 앞서 이야기한 제약 조건과 정확히 맞물립니다.

여기까지의 모든 설계는 최대한 많은 사람이 브라우저에 chatgpt.com을 입력하고 곧바로 AI를 쓰게 하려는 것입니다. 서버 사이드 렌더링된 문서에 매우 빠른 상호작용 가능 시점, 그 위에 인증 벽도 계정 생성도 없습니다. 가입 퍼널에서 가장 크게 이탈하는 지점 하나를 아예 없앤 것입니다.

자바스크립트를 완전히 끈 상태에서 chatgpt.com이 서버에서 렌더링한 껍데기 화면(셸)을 불러오는 모습입니다.

자바스크립트를 완전히 끈 상태에서 chatgpt.com이 서버에서 렌더링한 껍데기 화면(셸)을 불러오는 모습입니다.

10억 사용자 규모의 CSS

ChatGPT는 세계에서 가장 큰 Tailwind 앱 중 하나입니다. Adam Wathan이 2023년 3월에 Tailwind 쇼케이스에 추가했고, 2024년 OpenAI가 프레임워크를 버릴 때도 Tailwind는 살아남았습니다. Wes Bos는 Syntax에서 Tanner Linsley에게 "그 이전 작업에서 목이 안 날아간 건 TanStack Query랑 Tailwind뿐"이라고 농담했습니다.

Tailwind의 장점 하나는 styled-components 같은 CSS-in-JS보다 SSR이 훨씬 간단하다는 것입니다. 게다가 스타일을 스크립트가 아니라 스타일시트로 서빙하니 그만큼의 오버헤드를 덜어냅니다.

아무 요소나 열어 보면 익숙한 클래스가 보이는데, 한 가지가 다릅니다.

<div class="bg-token-main-surface-primary border-token-border-light
            flex h-svh w-screen flex-col @container/thread">

token-* 클래스들이 디자인 시스템입니다. 유틸리티 아래에 의미 기반 CSS 변수 계층이 깔려 있습니다. 라이트 모드, 다크 모드, 사용자가 고르는 채팅 테마가 모두 변수 값을 갈아 끼우는 방식으로 동작하고, 컴포넌트를 다시 렌더링하지 않습니다. 이 역시 styled-components 같은 도구에 비해 Tailwind가 유리한 지점입니다.

head의 두 번째 인라인 스크립트는 localStorage에서 저장된 채팅 테마를 읽어 페인트 전에 html 요소의 data 속성으로 설정합니다. 다크 모드 스크립트와 같은 수법입니다.

CSS도 자바스크립트처럼 라우트 크기 단위로 쪼개져 나갑니다. 큰 루트 스타일시트 하나와 그보다 큰 대화용 스타일시트가 있고, 그 아래로는 훨씬 잘게 나뉩니다. code-block.css, cot-message.css, global-modals.css가 각각 해당 기능과 함께 지연 로드됩니다. 스타일을 그것에 의존하는 컴포넌트나 라우트 기준으로 아주 잘 쪼개 놓았습니다. 그래서 첫 페이지를 열 때는 꼭 필요한 것만 기다리면 됩니다.

chatgpt.com을 띄우면서 불러오는 여러 CSS 파일입니다.

chatgpt.com을 띄우면서 불러오는 여러 CSS 파일입니다.

CSS와 디자인에서 저자가 가장 좋아하는 결정은 하지 않은 결정입니다. 웹폰트가 없습니다. UI 텍스트는 플랫폼 자체의 폰트 스택으로 그려집니다. 애플 기기에서는 SF Pro, 윈도우에서는 Segoe UI, 안드로이드에서는 Roboto입니다. 페이지가 요청하는 폰트 파일은 브랜드 요소용 semibold woff2 하나뿐이고, KaTeX 수식 폰트는 답변에 수식이 들어 있을 때만 내려옵니다. 느린 회선을 쓰는 수억 명이 사용자층에 포함된다면, 가장 빠른 폰트는 이미 기기에 설치된 폰트입니다.

font-family: -apple-system-body, ui-sans-serif, -apple-system, "system-ui", "Segoe UI", Helvetica, "Apple Color Emoji", Arial, "sans-serif", "Segoe UI Emoji", "Segoe UI Symbol"

CSS에서 얻을 가장 큰 교훈은 이 모든 것이 상당히 표준적이라는 점입니다. 여러 색 구성과 프리미티브를 위한 테마 변수 집합은 크지만, 전반적으로는 흔한 Tailwind 패턴과 코드 분할에 의존합니다. 단순하고 효과적이고 빠릅니다.

컴포넌트는 바퀴를 다시 발명하지 않는다

DOM을 열면 data-radix 속성이 곳곳에 있습니다. 메뉴, 셀렉트, 토스트, 스크롤 영역, 팝오버가 그렇습니다. ChatGPT는 누구나 사이드 프로젝트에서 쓰는 것과 똑같은 접근성 프리미티브를 씁니다. 바로 그것이 요점입니다. Radix는 Linear와 Conductor도 쓰고 있어서, 저자가 분석한 세 앱 모두 Radix였습니다.

ChatGPT의 드롭다운을 만드는 데 기본 재료로 쓰인 Radix 드롭다운입니다.

ChatGPT의 드롭다운을 만드는 데 기본 재료로 쓰인 Radix 드롭다운입니다.

입력창은 실제로 복잡도를 투자한 곳입니다. 단순한 textarea처럼 보이지만 완전한 ProseMirror 에디터입니다. 서버는 시각적으로 똑같은 정적 자리표시자를 렌더링하고, 번들이 도착하면 ProseMirror가 그 위로 하이드레이션합니다. OpenAI는 이 계층을 충분히 중요하게 여겨 ProseMirror와 CodeMirror를 모두 만든 Marijn Haverbeke를 후원하고 있습니다. 그가 만든 CodeMirror는 캔버스의 코드 편집도 돌립니다.

ChatGPT 입력창의 리치 텍스트 편집기로 쓰인 ProseMirror입니다.

ChatGPT 입력창의 리치 텍스트 편집기로 쓰인 ProseMirror입니다.

리치 텍스트 에디터를 만들어 본 적이 있다면 서식, 커서 위치, 위젯, 멘션 처리가 얼마나 괴로운지 알 것입니다. ProseMirror와 그 위에 얹은 TipTap 같은 라이브러리는 그 고생을 크게 줄여 줍니다.

전반적으로 디자인 시스템은 매우 미니멀합니다. 애니메이션이 거의 없고, 드롭다운은 즉시 열리고, 색상은 단순하고, 형태도 복잡하지 않습니다. 처음의 제약 조건으로 되돌아가는 이야기입니다. 모두가 쓰는 앱을 만들려면 UI를 최대한 단순하게 유지해야 합니다. 저자는 이 단순함이 꽤 아름답다고 말합니다. 형태보다 기능에 확실히 집중한 결과입니다.

Image

성능 쪽에서 저자가 직관에 어긋난다고 느낀 점이 있습니다. 메시지 목록이 가상화되어 있지 않습니다. 대화 안의 모든 메시지가 DOM에 그대로 남아 있습니다. 저자의 해석은 이렇습니다. ChatGPT 대화는 대체로 짧고, 페이지 내 찾기가 10억 명에게 동작해야 하며, 가상화는 실질적인 복잡도와 접근성 비용을 함께 데려옵니다. 여기서도 더 넓은 사용자층을 기준으로 최적화했다는 것이 분명합니다. 평균 대화 길이에 관한 데이터가 이 결정을 뒷받침하고 있을 것입니다.

데이터 페칭에 관한 짧은 여담

클라이언트 데이터는 TanStack Query로 돌아가는데, 최근에 들어온 것이 아닙니다. Remix 이전 작업으로 모두가 떠들썩할 때 Tanner Linsley는 Syntax에서 "그들은 한참 전부터 쓰고 있었다"고 밝혔고, 이전 후에도 그대로 유지했습니다. 통합 수준은 루트에 프로바이더 하나 얹는 정도를 넘어섭니다. 서빙되는 문서는 서버 데이터가 하이드레이션될 자리로 window.__REACT_QUERY_CACHE__ 전역을 초기화하고, 그 바로 옆에 Promise.withResolvers를 위한 세 줄짜리 폴리필이 놓여 있습니다.

저자는 TanStack Query와 Vercel의 SWR 같은 라이브러리를 아주 좋아합니다. 낙관적 요청, 서버 사이드 렌더링, 캐시 처리를 갖춘 클라이언트 페칭을 세팅하는 가장 간단한 방법이 되었기 때문입니다. ChatGPT는 서버 사이드 렌더링까지 포함해 이 기능을 전부 활용하고 있고, 아주 잘 돌아갑니다.

모든 답변은 렌더링 문제다

입력창은 사람이 타이핑하는 곳이지만 ChatGPT가 실제로 사는 곳은 답변입니다. 그리고 답변을 그리는 일은 보기보다 어렵습니다. 스트리밍 채팅 인터페이스를 만들어 본 사람은 이게 얼마나 어려운지 압니다. 네트워크 탭을 열면 답변은 토큰의 흐름입니다. 동시에 DOM을 열어 보면 토큰이 도착할 때마다 문서가 허물어지고 다시 세워집니다. 청크 처리, 스크롤, 리렌더링과 관련된 예외 상황이 워낙 많아서 어렵습니다.

도착하는 토큰은 각각 마크다운의 한 조각이고, 앱은 커져 가는 메시지를 제자리에서 다시 파싱하고 다시 렌더링합니다. 문제는 스트리밍 도중의 마크다운이 거의 항상 미완성이라는 점입니다. 아직 닫히지 않은 코드 펜스, 세 줄만 온 표, 짝을 기다리는 볼드 표시 같은 것들입니다. 이것을 잘못 파싱하면 파서가 매 프레임마다 해석을 바꾸면서 레이아웃이 깨집니다. 그런데도 ChatGPT는 처음부터 끝까지 매끄럽습니다.

저자가 특히 좋아하는 세부는 코드 블록의 실체입니다. 스타일을 입힌 <pre>처럼 보이지만 실제로는 완전한 CodeMirror 에디터입니다. ProseMirror를 만든 사람이 만든 라이브러리이고, 캔버스를 돌리는 것과 같은 라이브러리입니다. 아무 답변의 코드 블록이나 검사해 보면 DOM에 .cm-editor.cm-content가 있고, 구문 강조와 복사 버튼이 연결되어 있습니다.

Image

수식도 같은 정성을 받습니다. 수식은 KaTeX로 렌더링되는데, 하나가 두 번 실려 나갑니다. 눈에 보이는 시각적 버전과, 그 아래에 숨어 있는 MathML <math> 트리가 있습니다. 이 두 번째 사본이 사려 깊은 부분입니다. 스크린 리더가 수식을 읽어 줄 수 있게 하고, 수식 스크린샷이 아니라 진짜 수식으로 선택해 복사할 수 있게 해 줍니다. 대부분의 앱이 그냥 평면 이미지로 렌더링하고 지나갈 지점인데 건너뛰지 않았습니다.

Image

앱 전체의 패턴과 맞아떨어집니다. 답변 주변의 모든 것은 의도적으로 지루하고 주류이며 남이 만든 것을 재사용합니다. 진짜 엔지니어링을 쏟아붓는 곳은 답변 자체 하나입니다. 응답이 곧 가치의 전부인 제품이라면, 그 응답의 렌더링이 제품 그 자체이기 때문입니다.

이제 타이핑할 수 있습니까?

로그아웃 상태에서 chatgpt.com을 처음 열면 백 개가 넘는 자바스크립트 청크를 받아 옵니다. 문서가 modulepreload하는 것은 핵심 파일 14개뿐입니다. 엔트리, React 벤더 청크, 그리고 매칭된 라우트의 모듈들입니다. 나머지는 전부 딱 하나의 질문 뒤로 밀립니다. 이제 타이핑할 수 있는가?

이것이 그 질문이라고 단정할 수 있는 근거는 코드에 있습니다. 서버의 부트스트랩 설정에는 이름이 그대로 deferStartupImportsUntilComposerTTFI인 플래그가 있습니다. 입력창이 상호작용 가능해질 때까지 시작 시점 import를 미루라는 뜻입니다. 부팅 순서 전체가 사람이 타이핑을 시작할 수 있는 순간을 중심으로 짜여 있습니다.

목표를 처음부터 완전히 이해하면 엔지니어링 결정이 얼마나 빨리 나오는지를 보여 주는 또 하나의 예입니다. 모든 것이 사람들이 ChatGPT에 타이핑할 수 있게 하는 데 맞춰져 있습니다. 앞서 봤듯 상호작용 가능 시점은 그들이 계속 감시하는 대단히 중요한 지표이고, 이 지표의 목적은 누군가 chatgpt.com에 왔을 때 입력창 타이핑이 막히지 않게 하는 것입니다.

라우트 청크도 한 가지를 더 알려 줍니다. 대부분의 파일명은 익명 해시인데, 대화 화면만은 이름을 갖고 나갑니다. conversation-small입니다. 채팅의 핵심을 덜어낸 버전이고 라우트 매니페스트에서 수백 번 참조됩니다. 반면 코드 블록 렌더링이나 사고 과정 보기 같은 조각은 답변이 필요할 때만 각자의 청크로 도착합니다. 처음 온 방문자가 내려받는 채팅은 의도적으로 작게 만든 버전이고, 나머지는 필요할 때 옵니다. 필요한 것만 로드하게 만드는 영리한 기법입니다.

청크 그래프에서 또 하나 눈에 띄는 점은 거의 전부가 자체 도메인이라는 것입니다. 모든 청크, 스타일시트, 폰트가 Cloudflare 뒤의 chatgpt.com/cdn/assets에서 30일 캐시 헤더와 함께 옵니다. 서드파티 CDN 도메인이 없다는 것은 추가 DNS 조회도, 추가 TLS 핸드셰이크도 없다는 뜻입니다. 네트워크는 적이고, 새 오리진 하나하나가 또 다른 적입니다.

저자는 도메인 헤더나 도메인 설정을 잘못 잡아 핸드셰이크 요청이 하나 더 생기고 그래서 네트워크 요청 시간이 두 배가 되는 사례를 자주 본다고 합니다. 고치기는 아주 쉬운데 놀랄 만큼 많은 팀이 틀립니다. CORS 문제, OPTIONS 헤더, 불필요한 핸드셰이크는 언제나 피해야 합니다.

마지막으로 없는 것 하나가 눈에 띕니다. 서비스 워커가 없습니다. Linear는 약 1,200개의 자산을 미리 캐시해 오프라인에서도 앱이 뜹니다. ChatGPT는 표준 HTTP 캐시 외에는 아무것도 캐시하지 않습니다. 저자는 의도된 선택으로 읽습니다. 그들은 끊임없이 배포하고, 저쪽에 모델이 없으면 오프라인 채팅 앱은 아무 소용이 없으며, 오래된 서비스 워커는 그것을 고친 배포보다 오래 살아남을 수 있는 몇 안 되는 버그입니다. 제품이 곧 네트워크 대화라면, 오프라인 우선 설계는 가치가 아니라 복잡도만 사 오는 셈입니다.

전부 기능 플래그 뒤에 두기

서빙되는 HTML 안에는 377KB짜리 JSON을 담은 스크립트 태그가 묻혀 있습니다. 클라이언트 부트스트랩입니다. 인증 상태, 로케일, 지역, 그리고 실험 상태 전체의 스냅숏이 들어 있고, 이 스냅숏은 요청마다 익명 ID와 지역을 기준으로 서버에서 계산됩니다. 저자가 확인한 날에는 기능 게이트 556개, 동적 설정 144개, 실험 레이어 192개가 모든 방문자의 문서에 인라인되어 있었습니다.

"statsigPayload": {
  "feature_gates": {
    "3479398748": {
      "name": "3479398748",
      "rule_id": "6cYbYFM2vjVEPNwAxdQvEB:100.00:3",
      "value": true
    }
    // ... 약 555개 더
  },
  "dynamic_configs": { },
  "layer_configs": { }
}

여기서 짚어 볼 결정이 세 가지 있습니다.

첫째, 플래그를 서버에서 평가해 인라인합니다. 순진하게 붙이면 부팅 시점에 플래그 서비스에서 값을 받아 와야 하고, 그러면 렌더링을 네트워크에 걸어 두거나 기본값으로 먼저 그린 뒤 플래그가 도착할 때 화면이 번쩍이는 것 중 하나를 골라야 합니다. OpenAI는 그 요청 자체를 없앴습니다. React가 하이드레이션할 시점에 모든 게이트의 답이 이미 나와 있습니다. 가장 좋은 네트워크 요청은 아예 보내지 않는 요청입니다.

둘째, 게이트 이름을 해시로 바꿔 놓았습니다. 읽을 수 있는 이름 대신 3479398748 같은 값이 옵니다. ChatGPT의 프런트엔드를 캐내 미출시 기능을 찾아내는 유출자 생태계가 통째로 존재하는데, 읽을 수 있는 플래그 이름은 그들에게 로드맵을 그대로 건네주는 것이었습니다. 이 글을 쓴 저자도 그 부류에 포함됩니다.

셋째, 실험 트래픽이 자체 도메인을 씁니다. 이벤트 로깅이 Statsig의 엔드포인트가 아니라 자기 도메인의 chatgpt.com/ces/v1/로 흐릅니다. Statsig는 과거에 광고 차단기를 피하려고 featuregates.org 같은 도메인을 돌려 가며 썼습니다. 자기 오리진을 거치게 하면, 모든 결정을 좌우하는 실험 데이터에 "광고 차단기를 쓰는 모든 사람"이라는 모양의 사각지대가 생기지 않습니다.

저자가 가장 좋아하는 플래그는 로딩 전략 자체를 겨눈 것들입니다. deferStartupImportsUntilComposerTTFI 옆에 promoteCssstripModulepreloadImports가 나란히 있습니다. 이름이 하는 일을 그대로 말해 주는 서버 쪽 스위치입니다. 문서가 자기 CSS와 자바스크립트를 어떻게 로드할지를 바꿉니다. 두 변형을 모두 배포해 놓고 수백만 사용자가 자기 로딩 시간으로 투표하게 합니다.

이 시스템의 규모는 과장하기 어렵습니다. Statsig 사이트에 실린 OpenAI 인용은 플래그 뒤에서 600개가 넘는 기능을 출시했다고 언급하고, 데이터 엔지니어링 팀은 "수억 명의 사용자를 대상으로 수백 건의 실험"을 한다고 표현합니다. Codex 출시 때 한 엔지니어는 출시 당일 밤을 모놀리스를 배포하고 나서 "플래그를 켜는" 일로 설명했습니다. 그리고 2025년 9월, OpenAI는 11억 달러에 Statsig를 인수하고 창업자 Vijaye Raji를 애플리케이션 부문 CTO로 앉혀 ChatGPT와 Codex의 엔지니어링을 맡겼습니다. 기능 플래그 도구가 마음에 들어서 회사를 사고 그 팀에 제품을 맡긴 것입니다. 실험이 이들의 개발 방식에서 얼마나 중심인지를 이보다 크게 말하는 방법은 떠오르지 않습니다.

Image

10억 명의 낯선 사람을 들여보내기

지금까지의 모든 절이 같은 것을 칭찬했습니다. 계정 없이 그냥 타이핑을 시작하면 된다는 점입니다. 그 열린 문이 전략의 핵심인데, 아직 이야기하지 않은 대가가 있습니다. 지구상의 누구나 로그인 없이 최전선 모델에 공짜로 질문할 수 있다면, 지구상의 누구나 그것에 스크립트를 붙일 수도 있습니다. 무료 익명 추론은 봇을 끌어당기는 자석이고, 악성 요청 하나하나가 실제 GPU 비용으로 빠져나갑니다. 그러니 진짜 중요한 질문은 ChatGPT가 왜 계정 없이 들여보내는가가 아니라, 그것을 하루 수십억 번씩 하면서 어떻게 살아남는가입니다.

답은 거의 완전히 눈에 보이지 않는 보안 계층입니다. 보이지 않는 것이 바로 요점입니다. 메시지 하나를 보내기 전에 이미 배경에서 두 가지가 벌어집니다. Cloudflare가 작업 증명 챌린지를 돌립니다. 페이지를 열면 바로 발생하는 cdn-cgi/challenge-platform 요청들이 그것이고, 모든 클라이언트가 진짜 브라우저임을 증명하느라 CPU를 조금 태우게 만듭니다. 그리고 OpenAI가 Sentinel이라고 부르는 자체 남용 방지 시스템이 샌드박스 iframe 안에서 부팅합니다. sentinel/frame.html이며 별도로 버전이 관리되는 sentinel/sdk.js를 갖고 있습니다. 보호 대상 앱과 격리된 상태에서 클라이언트를 지문 채취할 수 있게 하려는 구성입니다.

앞 절에서 이것의 앞면을 이미 봤습니다. 메시지를 보내기 전에 앱이 발생시키는 chat-requirements의 prepare·finalize 핸드셰이크입니다. 그 용도가 바로 이것입니다. 사용자가 아직 타이핑하는 동안 남용 검사를 미리 통과해 둬서, 엔터를 누를 시점에는 문 앞의 경비원이 이미 신분증을 확인해 둔 상태가 됩니다. 이 앱의 다른 모든 결정과 같은 수법입니다. 비싼 작업은 사용자가 이미 하고 있는 다른 일 뒤에 숨겨 미리 해 두고, 사용자가 진짜 신경 쓰는 것은 즉각적으로 느껴지게 만듭니다. 봇 방어도 인증 확인과 프리페치처럼 선불로 치러집니다.

깊이도 상당합니다. chatgpt.com의 Cloudflare 챌린지를 복호화한 연구자들은, 사람이라고 판단하기 전에 앱 자체의 React Router 상태를 포함해 실행 중인 페이지의 수십 가지 속성을 들여다본다는 것을 확인했습니다. 이것이 반갑든 걱정스럽든, 낯선 사람과 그의 첫 무료 토큰 사이에 얼마나 많은 기계 장치가 놓여 있는지, 그리고 그 낯선 사람이 그 존재를 단 1초도 느끼지 않게 하려고 팀이 얼마나 애쓰는지를 잘 보여 줍니다.

// 한 글자도 타이핑하기 전에 배경에서 발생
GET   /cdn-cgi/challenge-platform/...        Cloudflare 작업 증명
GET   /backend-api/sentinel/sdk.js           남용 방지 SDK
      sentinel/frame.html                    샌드박스 iframe에 로드
POST  /backend-anon/sentinel/chat-requirements/prepare
POST  /backend-anon/sentinel/chat-requirements/finalize

"로그인 없이 그냥 타이핑하면 된다"의 아무도 보지 못하는 나머지 절반입니다. 열린 문은 보안이 없다는 뜻이 아닙니다. 소비자 웹에서 가장 공격적인 봇 방어 스택 위에 마찰 없는 문을 달아 놓고, 그 비용이 스크립트에만 떨어지고 사람에게는 절대 떨어지지 않게 조정한 것입니다. Anthropic은 같은 문제를 열린 문을 아예 만들지 않는 방식으로 해결합니다. OpenAI는 문을 열어 두고 경비원을 숨기는 쪽을 택했고, 이 절의 거의 모든 내용은 사용자가 그 경비원의 존재를 눈치채지 못하게 하려고 존재합니다.

첫 토큰까지 가장 빠른 길

위의 모든 것은 단 하나의 상호작용을 위해 존재합니다. 엔터를 누르면 토큰이 나타나는 것입니다. 로그아웃 사용자 기준으로 네트워크 관점에서 무슨 일이 일어나는지 보겠습니다.

사용자가 아직 페이지를 읽고 있는 동안, 앱은 이미 /backend-anon/conversation/init을 호출하고 남용 방지 계층인 Sentinel의 chat-requirements 검사를 돌려 둡니다. 무엇을 제출하기도 전입니다. 엔터를 누르면 짧은 prepare 호출로 요건이 아직 충족되는지 확인하고, 그다음 메시지가 나갑니다.

POST /backend-anon/f/conversation
content-type: application/json

200 OK
content-type: text/event-stream

답변은 POST fetch 위의 서버 전송 이벤트(SSE) 스트림입니다. 2022년부터 써 온 것과 같은 패턴이고, 토큰은 이미 그려져 있는 셸 안으로 도착하는 대로 렌더링됩니다. 전송 방식은 의도적으로 지루합니다. 선불로 치러 둔 남용 검사부터 스트리밍 셸까지, 구조 전체가 사용자가 마침내 엔터를 눌렀을 때 기다릴 대상이 모델뿐이도록 배치되어 있습니다.

요청 바디까지도 사려 깊습니다. client_contextual_info 객체에 뷰포트 크기, 픽셀 비율, 다크 모드 상태, 페이지 로드 후 경과 초가 담깁니다. 백엔드가 자기가 그려 넣을 캔버스를 알게 하거나, 검토용 주요 데이터를 더 남기려는 것입니다.

ChatGPT와 Claude의 차이

ChatGPT 웹 앱의 선택과 Claude의 선택을 나란히 놓고 보면 두 회사의 초점이 분명해집니다. chatgpt.com을 열면 계정 없이 몇 초 안에 질문을 던지게 되고, 그 무료 익명 답변은 OpenAI에 실제 GPU 비용으로 청구됩니다. OpenAI는 이를 광고로 상쇄하기 시작했습니다. claude.ai를 열면 한 글자도 입력하기 전에 로그인 벽과 온보딩을 만납니다. Anthropic은 기업 시장을 골랐고, 그 선택이 OpenAI의 소비자 쪽 승부수가 그들의 엔지니어링 선택으로 퍼져 나간 것과 똑같은 방식으로 Anthropic의 엔지니어링 선택으로 퍼져 나갑니다.

초점이 문제를 얼마나 단순하게 만드는지는 인정할 만합니다. Anthropic의 선택은 더 쉬운 문제를 사 옵니다. 모든 사용자가 인증되어 있고, 남용 표면이 더 작고, 10억 명의 낯선 사람을 위한 무료 놀이터를 서버 렌더링할 일이 없습니다. 스택에도 그대로 드러납니다. claude.ai는 CDN에서 바로 서빙되는 클라이언트 사이드 렌더링 단일 페이지 앱입니다. 어차피 모든 사용자가 로그인해 있다면 완벽하게 합리적인 선택이고, Linear가 클라이언트 사이드 렌더링을 유지하는 것과 같은 논리입니다. OpenAI의 선택은 엄청난 복잡도를 삼키는 대가를 치릅니다. 봇 방어, 챌린지 흐름, 익명 요청 제한, backend-anon이라는 별도 백엔드 API 표면 전체가 그것입니다. 낯선 사람의 첫 질문에서 마찰을 단 1그램도 남기지 않으려는 것입니다. 어느 쪽도 틀리지 않았습니다. 다만 두 회사의 전략을 각자의 네트워크 탭에서 그대로 읽어 낼 수 있습니다.

ChatGPT 웹은 어떻게 만들어졌나

전체 모양은 대략 이렇습니다. 제약은 알 수 없는 기기를 쓰는 완전히 새로운 사용자입니다. 그래서 앱은 완성된 셸을 서버에서 렌더링해 엣지에서 스트리밍하며 첫 바이트를 100ms 안에 보냅니다. 프레임워크는 framework mode의 React Router 7이고, 2024년에 Next.js를 떠나 Remix로 갔다가 병합을 그대로 타고 도착한 결과입니다. 스타일링은 디자인 토큰 계층 위에서 컴파일되는 Tailwind v4이고 라우트 단위로 쪼개지며 웹폰트가 없습니다. 컴포넌트는 Radix 프리미티브에 ProseMirror 입력창입니다. 클라이언트 데이터는 TanStack Query이고 서버가 초기값을 심어 줍니다. 부팅은 콘텐츠 해시가 붙은 160개 청크를 타이핑 가능 시점 기준으로 배치한 것입니다. 페이지가 자기 자신을 로드하는 방식까지 포함해 모든 결정이 서버에서 평가되는 556개 플래그 중 하나 뒤에 놓여 있습니다. 그 결과가 모델이 할 말이 생긴 그 순간부터 흘러나오기 시작하는 답변입니다.

저자가 가장 인상 깊게 본 것은 이 모든 것이 가장 좋은 의미에서 표준적이라는 점입니다. Linear의 속도는 첫날부터 만들기 시작한 자체 동기화 엔진에서 나옵니다. Conductor의 속도는 네이티브 셸과 로컬 데이터베이스에서 나옵니다. ChatGPT의 속도는 프런트엔드에서 가장 주류인 스택에, 모든 기본값을 하나하나 의심하고 측정하는 태도에서 나옵니다. 플래그는 서버에서 평가하고, 테마 스크립트는 인라인하고, 청크 이름은 해시로 감추고, 나머지는 전부 입력창 뒤로 미루고, 첫 바이트부터 측정합니다.

웹을 만드는 사람이라면 chatgpt.com에서 개발자 도구를 열고 둘러보길 권합니다. 인터넷의 누구에게든 훌륭한 React를 배포하는 법을 배우는 무료 수업입니다.

Edit this page