성능 개선 스프린트의 내부 이야기입니다. Claude가 직접 만든 벤치마크, Slack 스레드마다 돌아간 개선 루프, 3,000건에 달하는 변경 사항을 안전하게 배포할 수 있었던 안전장치를 소개합니다.
올해 8월, 저희는 2주간의 스프린트로 claude.ai와 Claude 데스크톱 앱의 핵심 사용자 경험을 약 3배 빠르게 만들었습니다. 사용자들이 느리다고 계속 말해 왔고, 그 말이 맞았습니다. 작업은 모두 하나의 Slack 채널에서 진행했고, 모든 스레드에 Claude가 함께했습니다.
저희는 전체 사용자 활동의 95%를 차지하는 네 가지 사용 흐름에 집중했습니다. 75번째 백분위수 기준으로, claude.ai를 새로 불러왔을 때 입력 가능한 페이지가 뜨기까지 걸리는 시간은 3.1초에서 0.55초로 줄었습니다. 새 Claude Code 세션을 시작하는 시간은 0.8초에서 0.3초로, Claude Cowork 클라우드 세션을 불러오는 시간은 2.6초에서 0.73초로 단축됐습니다. 전체적으로 따지면 사용자가 매일 기다리는 시간이 수만 시간 줄어드는 셈입니다.

이번 작업에는 Claude Tag(베타)를 사용했고, 모델은 Opus 5.5와 비슷한 수준의 내부 연구용 모델이었습니다. Claude는 병목을 찾고, 벤치마크를 만들고, 개선 사항을 배포하고, 배포할 때마다 상태를 지켜봤습니다. 저희는 목표를 정하고, 트레이드오프를 판단하고, 모든 변경을 승인하는 방식으로 방향을 잡았습니다. 이렇게 해서 3,000건이 넘는 변경 사항을 병합하는 동안 고객에게 영향을 준 장애나 롤백은 한 번도 없었습니다. 이 글에서는 무엇을 배포했는지, 어떻게 측정했는지, 그리고 이를 안전하게 해내기 위해 Claude와 함께 만든 개선 루프를 소개합니다.
스프린트에 앞서 저희는 Slack 채널을 만들고 다음과 같은 상시 지침을 남겼습니다.
@Claude 당신의 역할은 claude.ai 웹사이트와 데스크톱 앱의 성능과 관련된 모든 일을 원활하게 이끄는 것입니다. 구체적으로는 배포 시 성능 회귀가 없는지 모니터링하고, 기존 텔레메트리가 정확하고 충분한지 평가하고, 잘 정리된 관측 대시보드를 관리하고, 발견된 문제와 쉽게 개선할 수 있는 부분을 먼저 찾아 해결하고, 성능 프로젝트 후보를 제안하고, 사람 팀원들과 소통하는 일을 맡습니다. […]
이 채널의 최종 목표는 당신이 가능한 한 자율적으로 일하게 되는 것입니다. 다만 아직은 그렇게 할 수 없다는 점도 알고 있습니다.
저희는 Claude에게 Datadog MCP 서버로 사용 데이터를 분석해 달라고 요청했습니다. Claude는 영향이 가장 큰 사용 흐름 네 가지를 골라냈습니다. 앱 실행, 대화 시작, 기존 대화 불러오기, 메시지 전송입니다. 웹과 데스크톱, 그리고 여러 제품에 걸쳐 보면 이 흐름은 모두 13개의 개별 측정 항목이 됐습니다. 기준선을 잡기 위해 항목들을 직접 비교할 수 있을 때까지 계측 코드를 추가했습니다. 모든 측정은 사용자 상호작용에서 시작해 결과가 화면에 렌더링되면 끝나도록 했고, 클라이언트 작업과 서버 작업도 구분했습니다.
스프린트는 특정 사용 흐름을 겨냥한 프로젝트 약 20개를 직접 골라 목록으로 만들면서 시작했습니다. Claude가 프로젝트별 효과를 밀리초 단위로 추정했고, 이 추정치를 합산해 스프린트 목표를 정했습니다. 꽤 규모가 큰 프로젝트도 있었지만, 대부분 2주 안에 해낼 수 있으리라 봤습니다.
13개 목표 중 12개를 사흘째에 달성했습니다.
계획했던 프로젝트가 예상보다 일찍 마무리된 것입니다. 실행 속도를 높이기 위해 정적 컴포저를 HTML에 미리 넣어, React가 초기화되는 동안에도 사용자가 입력할 수 있게 했습니다. 또 V8 코드 캐시를 미리 컴파일해 두어 데스크톱 셸의 메인 프로세스가 처음부터 다시 컴파일하지 않게 했습니다. 화면 전환을 빠르게 하기 위해서는 대화를 오가는 동안 컴포저를 마운트된 상태로 유지하고, 사용자가 세션 위에 마우스를 올리면 해당 세션을 미리 불러오고, 사이드바 리렌더링을 90% 줄였습니다.
저희는 Claude가 직접 개선 기회를 찾아 새로운 작업을 제안할 여지도 남겨 뒀습니다. 이렇게 시작된 작업들은 금세 독립된 대형 프로젝트로 커졌고, 처음 세운 목표를 훌쩍 넘어섰습니다. 그래서 목표를 새로 정하고 측정할 대상을 더 찾아 나섰습니다.
@Claude 처음 프로젝트 목록에 있던 거의 모든 항목에 투자했고, 그 밖의 것도 많이 했어요. 이제 목록을 새로 정리해 봅시다 […] 아직 안 살펴본 곳은 어디일까요? 힐 클라이밍으로 개선할 수 있는 부분은요? 지금 가장 기회가 큰 곳은 어디인가요? […] 엉뚱한 아이디어도 환영합니다
처음부터 저희는 배포 주기보다 빠르게 반복 개선하고 싶었습니다. Claude는 몇 시간씩, 심지어 밤새도록 비동기로 작업할 수 있으니, 실제 사용자 데이터가 쌓이기를 기다리지 않고도 프로토타입을 검증하게 하고 싶었습니다. 그래서 실험실 환경에서 성능을 측정할 다른 방법을 찾기 시작했습니다.
첫 단서는 Sam이 찾았습니다.

11분 뒤에는 스레드 다섯 개가 돌아가고 있었고, 각각 명령어 수, V8 호출 횟수, React 커밋, 스타일 재계산, DOM 변경이라는 서로 다른 지표를 맡고 있었습니다.
저희는 새 벤치마크를 만들 때마다 어느 정도 의심의 눈으로 봤습니다. 벤치마크에는 두 가지 역할이 있었습니다. 하나는 Claude가 실험실에서 개선할 수 있는 지표가 되는 것, 다른 하나는 CI에서 숫자가 내려가기만 하는 안전장치가 되는 것입니다. 벤치마크가 불안정하거나 실제 사용자 지연과 상관관계가 없으면, Claude가 엉뚱한 언덕을 오르지 않도록 폐기했습니다.
@Claude 이 지표들을 각각 힐 클라이밍했을 때 실제로 측정 가능한 wall clock 성능 향상으로 이어진다는 걸 증명해 주세요. 증명하지 못하는 후보는 벤치마크를 빼겠습니다
사용자가 체감하는 것은 wall-clock 시간이지만, 이 값은 노이즈가 많고 밀리초 단위는 너무 불안정해서 CI 게이트로 쓸 수 없습니다. 명령어 수는 결과가 결정적이라 매력적이었지만, 그래도 wall-clock 시간과 실제로 같이 움직이는지는 Claude가 증명해야 했습니다.
그래서 Claude에게 두 개의 핫 패스에서 명령어 수를 줄여 보라고 했습니다. 하나는 대화의 메시지 트리를 조립하는 루틴이고, 다른 하나는 Claude Code 출력에서 상태 표시줄을 찾아내는 스캐너입니다. Claude가 Valgrind로 두 경로를 프로파일링해 보니, 첫 번째 경로 명령어의 4분의 1이 메가모픽 딕셔너리 조회였고, 같은 메시지 ID를 세 번이나 따로 조회하고 있었습니다.
한 시간 뒤 Claude는 두 경로의 명령어를 각각 48%, 31% 줄였고, wall-clock 시간은 78%, 44% 줄었습니다. 저희는 새 래칫 두 개를 체크인했습니다. 이후로 이 경로의 명령어 수를 늘리는 PR은 CI에서 실패하고, 매일 실행되는 작업이 명령어 수가 줄어들 때마다 상한선을 낮춥니다.

여기서 이번 스프린트의 핵심 교훈을 얻었습니다. Claude와 일할 때는 무언가를 측정하는 순간 그것이 해결 가능한 문제가 됩니다.
예전에는 측정이 0단계였습니다. 지표를 추가하고, 데이터가 쌓이길 기다리고, 그제야 문제를 이해하기 시작했습니다. Claude와 함께하면 측정은 개선의 첫걸음입니다. 넘어서야 할 숫자가 생기는 순간 Claude는 최적화를 시작할 수 있습니다. 그래서 저희가 할 수 있는 가장 효과적인 일은 측정할 대상을 더 많이 찾아내는 것이었습니다.
이 모든 작업은 같은 Slack 채널에서 이뤄졌고, 모든 스레드에서 여러 엔지니어와 Claude가 함께 아이디어를 주고받았습니다. 그러면서 스프린트는 다음과 같은 루프로 자리 잡았습니다.

예를 들어 어떤 팀원이 페이지가 로드된 뒤에 사이드바 항목이 하나둘 튀어나오는 화면 녹화를 공유했습니다. 채팅 항목과 Cowork 항목이 서로 다른 시점에 나타나서 페이지가 어색하게 느껴졌습니다. 기존 모니터링으로는 이 문제를 전혀 잡아내지 못했습니다. 그나마 가까운 지표가 Cumulative Layout Shift였지만, 이동 한 번당 점수가 0.008 정도라서 '좋음' 기준인 0.1에 한참 못 미쳤습니다.
Issac은 기반이 되는 Layout Instability API를 직접 활용하자는 아이디어를 냈습니다. Claude는 각 layout-shift 항목의 sources를 이름이 붙은 영역(예: 사이드바, 트랜스크립트)과 단계(예: 첫 페인트 이전, 입력 가능 이후)에 매핑하는 텔레메트리 이벤트를 만들었습니다. 그리고 사이드바에 데이터가 채워진 상태로 페이지를 열고, 사이드바 데이터를 첫 페인트 이후까지 붙잡아 둔 다음, 이름 붙은 어느 영역에서든 레이아웃 이동이 생기면 실패하는 통합 테스트를 추가했습니다. Claude는 이 테스트를 수정 효과를 증명하는 벤치마크로 썼습니다. main 브랜치에서는 20번 중 20번 실패했고, 수정이 담긴 PR에서는 20번 중 20번 통과했습니다.
이벤트를 배포하고 나서 Claude가 실사용 데이터를 살펴보니, 웹 페이지 로드의 31%에서 페이지를 쓸 수 있게 된 뒤 사용자가 아무것도 하지 않았는데도 화면의 무언가가 움직이고 있었습니다. Claude는 원인을 하나씩 짚어 나갔습니다. 늦게 도착하는 헤더 행, 사용자 이름이 로드되면 옆으로 밀려나는 캐럿, 스크롤바가 나타나면서 위치가 바뀌는 목록 같은 것들이었습니다. Claude는 가장 큰 원인들을 한꺼번에 고쳤고, 그것들이 사라지자 다음 원인 묶음을 찾아냈습니다.
이것은 스레드 하나의 이야기였습니다. 스프린트 기간에는 동시에 150개가 넘는 스레드를 운영했습니다.
루프가 스레드 하나에서 돌아가자, 더 많은 스레드에서 돌리는 일은 스레드를 새로 여는 것만으로 충분했습니다. Claude는 처음 요청이 처리됐다고 스레드를 닫지 않고 계속 이어 갔습니다. 스레드 하나에서 최적화 PR이 쉰 개, 많게는 백 개까지 올라왔습니다. 갈수록 새 스레드를 여는 쪽도 저희가 아니라 Claude였습니다. 별도 조사나 야간 작업 중에 스스로 찾아낸 기회를 파고들기 위해서였습니다. 채널에 있던 엔지니어 Shelley는 "[이 모델은] 숫자 악마예요"라고 말했습니다.
측정할 때마다 개선할 거리가 나왔습니다. Claude가 React 훅을 전수 조사해 보니 컴포저의 입력 경로에 훅 6,900개와 스토어 구독 900개가 있었고, 키를 누를 때마다 전부 리렌더링되고 있었습니다. 스타일 재계산을 세어 보니 :root:has() 셀렉터 하나가 DOM이 바뀔 때마다 24밀리초를 더하고 있었습니다. 첫 페인트 이후의 코드 경로를 추적해서는 남아 있던 location.reload() 때문에 하루 50만 건의 보이지 않는 리로드가 발생하고 있다는 것을 발견했는데, 기존 로드 지표로는 전혀 보이지 않던 문제였습니다. 유휴 상태 탭의 프로파일러 샘플을 읽고는 똑같은 캐시 스냅샷이 1분에 두 번씩 IndexedDB로 복제되고 있으며, 이 작업이 전부 메인 스레드에서 실행된다는 사실도 알아냈습니다.
스레드가 어디로 이어질지는 대개 알 수 없었습니다. CPU 끊김을 전반적으로 점검하던 중 Claude는 완성된 코드 블록에 하이라이트를 적용할 때 페이지가 1초가량 멈출 수 있다는 점을 발견했습니다. 실험실에서 파고든 끝에 찾아낸 범인은 em dash였습니다. 응답의 마크다운에 em dash나 둥근 따옴표 같은 Latin-1 범위 밖의 문자가 하나라도 있으면 V8이 문자열 전체를 UTF-16으로 저장하는데, 그 탓에 구문 강조용 정규식이 모두 느린 2바이트 경로를 타게 됩니다. Claude는 하이라이트를 적용하기 전에 각 코드 블록을 1바이트 문자열로 복사하는 20줄짜리 변경으로 이 문제를 해결했습니다.

둘째 주에는 하루 단위로 결과를 정리해 공유하기도 벅찰 정도였습니다. 가장 바쁜 날에는 200건이 넘는 변경이 반영됐습니다. Claude는 계속 새 벤치마크를 제안했고, PR 세 개 중 하나꼴로 텔레메트리나 안전장치가 추가로 들어갔으며, 새 계측 도구가 생길 때마다 더 많은 기회가 담긴 스레드가 늘어났습니다.
한 채널에서 일하니 모든 것이 투명하게 공개됐습니다. 서로의 스레드를 오가며 결정을 토론하고 성과를 함께 기뻐했습니다. 소문이 퍼지면서 다른 팀들도 자신들의 변경을 이 채널에 가져와 성능 리뷰를 받기 시작했습니다. 도입된 안전장치와 Claude 스킬 덕분에 새 프로젝트도 은연중에 더 성능을 고려한 방식으로 작성됐습니다.
저희는 이런 속도에 대비해 두었습니다. 손대는 거의 모든 곳이 핫 패스(첫 페인트, 컴포저, 트랜스크립트)였기 때문에, 안전 장치를 처음부터 갖춰 놓았습니다. 모든 PR은 자동 리뷰를 거치고 사람의 승인을 최소 한 번 받았습니다. 최적화에 앞서 항상 단위 테스트를 먼저 작성했고, 사용자에게 문제를 일으킬 수 있는 변경은 수명이 짧은 기능 플래그 뒤에서 배포했습니다.
플래그가 쌓이기 시작하자 롤아웃과 정리를 조율하기 위한 스레드를 열었습니다. Claude는 모든 플래그를 킬 스위치와 점진적 램프 중 하나로 분류하고, 안전해지는 즉시 하나씩 걷어 냈습니다. 2주 동안 도입한 플래그는 200개에 가까웠고, 그중 절반 넘게가 스프린트가 끝날 무렵 이미 정리됐습니다.
또한 변화가 빠른 코드베이스에서는 성능 개선 효과가 시간이 지나며 사라지기 쉽고, Anthropic에서는 코드가 빠르게 배포된다는 점도 알고 있었습니다. 그래서 프로젝트가 성과를 입증하면 그 성과를 지킬 방법에도 투자했습니다. 예를 들어 정적 컴포저는 구조상 깨지기 쉽습니다. 페이지의 HTML 사본을 사용자에게 거의 즉시 보여 주고, 그 위에 React가 곧바로 화면을 그리게 하기 때문입니다.
React 렌더링 결과가 1픽셀만 어긋나도 이 방식의 효과는 사라집니다. 그래서 Claude는 수십 개의 안전장치를 만들었습니다.
실험실에서 모든 문제를 잡을 수는 없었기에, 가장 오래된 안전장치인 점진적 롤아웃도 활용했습니다. 위험도가 높은 변경은 먼저 직원들에게, 그다음 사용자의 1%에게, 마지막으로 전체에 배포했습니다. 정적 컴포저를 사내에 배포하고 네 시간 뒤, 한 팀원이 어떤 지표로도 잡히지 않던 레이아웃 이동 화면 녹화를 공유했습니다. 새 탭에서 claude.ai를 열면 컴포저가 아래로 툭 떨어지는데, 저희 코드가 원인이 아니었습니다.

Claude는 어떻게 했는지 이 문제를 Chrome의 추측 로딩(speculative loading)에서 발생하는 예외적인 경우까지 추적해 냈습니다. 사용자가 주소창에 URL을 입력하는 동안 Chrome은 현재 탭 높이에 맞춰 백그라운드에서 페이지를 미리 렌더링합니다. 그런데 조직이 관리하는 브라우저에서는 푸터 때문에 새 탭 페이지가 약간 더 짧습니다. 사용자가 Enter를 누르면 claude.ai의 첫 프레임은 이 약간 짧은 레이아웃으로 표시되고, Chrome은 0.1초쯤 뒤에 크기를 조정합니다. Claude는 크기가 조정되는 동안에도 레이아웃을 고정했고, 저희는 이 프리렌더 흐름을 재현하는 테스트를 추가했습니다.
이 루프는 성과를 냈지만 완전히 자율적으로 돌아가지는 않았습니다. 속도를 유지하면서 안전하게 올바른 방향으로 이끄는 것은 저희 몫이었고, 여기에는 세 가지 역할이 있었습니다.
야심. Claude는 기본적으로 작업 범위에 신중합니다. 발견한 문제는 티켓으로 남기고, 실현 가능성에는 단서를 달고, 예상 소요도 넉넉하게 잡습니다. 하지만 저희는 안전장치를 믿고 있었습니다. 특히 초반에는 Claude가 더 과감하게 움직이도록 독려하는 일이 많았습니다.

세워 둔 목표에 도달하기 시작하자 스레드의 속도가 느려지는 게 눈에 띄었습니다. Sam은 스레드를 돌며 같은 메시지를 남겼습니다. "계속 줄여 나갑시다. 목표가 끝이 아닙니다. 다음은 뭐죠? 야심 차게 가 봅시다."
안목. 모든 스레드에는 이름이 명시된 사람 담당자가 있었고, Claude는 사용자가 체감할 수 있는 변경이면 전후 스크린샷이나 녹화 영상을 붙여 담당자가 판단하게 했습니다. 표는 셀 단위로 채워야 할까, 아니면 행이 완성될 때까지 기다려야 할까? 로딩 스켈레톤은 즉시 보여 줘야 할까, 0.5초 뒤에 보여 줘야 할까? 스트리밍 텍스트에 단어 단위 페이드 효과를 넣을 때 들어가는 프레임 예산의 5분의 1은 그만한 가치가 있을까? Claude는 밀리초를 깎을 방법을 찾았고, 저희는 트레이드오프를 따져 봤습니다.
방향. 저희는 각 스레드를 벤치마크 하나 또는 사용 흐름 하나에 집중하도록 일부러 좁게 유지했고, Claude에게는 그 범위 안에서만 개선점을 찾게 했습니다. 스레드를 못을 찾아다니는 망치 150개라고 생각했습니다. 저희가 내린 판단 대부분은 작업 순서와 사용자 영향에 관한 것이었습니다. 어떤 화면을 우선할지, 서로 겹치는 스레드를 어떻게 합칠지, 효과가 줄어드는 스레드는 언제 닫을지를 정했습니다. 900줄짜리 PR 하나에는 이런 한 줄짜리 답이 달렸습니다. "이건 닫겠습니다. 전송당 2ms를 아끼자고 이 빌드 플러그인을 유지보수하는 복잡성을 감수할 가치는 없어요."
사이드 퀘스트 하나가 이 모든 요소가 함께 돌아가는 모습을 보여 줍니다. 실시간 구문 강조에 쓰이는 정규식의 최적화를 입증하기 위해, Claude는 실험실에서 긴 답변이 스트리밍되는 화면 녹화를 첨부했습니다. 화면 구석에는 페이지 안에서 애니메이션 프레임 타임스탬프로 계산한 프레임 레이트 표시를 추가해 두었습니다.

측정 방법과 목표 의식이 갖춰지자 Claude는 곧바로 작업에 들어갔습니다. 화면에 그려지는 프레임 하나당 예산은 8.33밀리초였고, Claude는 긴 답변을 프레임 단위로 하나씩 돌려 보며 시간을 재서 느린 부분을 찾았습니다. 완성된 블록을 메모이제이션해 청크마다 반복되던 O(message length) 작업을 없앴고, 길어지는 코드 펜스의 토큰화 로직을 워커로 옮겼으며, 표는 셀 단위로 나타나게 했습니다.
이 스레드 하나에서만 PR을 60개 가까이 반영했습니다. 긴 답변이 메인 스레드를 막는 시간은 예전의 약 750밀리초에서 총 약 200밀리초로 줄었고, CPU 사용량은 3분의 1 수준이 됐으며, 120Hz MacBook에서 처음부터 끝까지 120fps를 유지했습니다. 이 120Hz 테스트 환경 자체도 야간 작업으로 만들어, Claude가 성능 회귀를 감시하게 했습니다.

스프린트를 시작할 때만 해도 스트리밍 중 프레임 사이의 밀리초까지 힐 클라이밍하겠다는 계획은 없었습니다. 하지만 그 시간도 셀 수 있었고⟧, 저희가 셀 수 있는 것이라면 Claude가 끌어올릴 수 있었습니다.
현재 claude.ai와 데스크톱 앱은 8월 초보다 약 3배 빠르며, 래칫이 이 속도를 계속 지켜 줄 것입니다. 하지만 아직 끝이 아닙니다. 95번째 백분위수, 다른 사용 흐름, 매우 긴 대화에는 여전히 개선할 여지가 있습니다. 별도의 글에서는 스프린트 중 업스트림까지 거슬러 올라가게 된 사이드 퀘스트 몇 가지도 소개할 예정입니다. Electron, Chromium, Node.js 등에 기여한 내용입니다.
결과를 사내에 공유했을 때 Issac이 가장 정확하게 표현했습니다. "6개월 전만 해도 이게 가능하다고 누가 말했다면 믿지 않았을 거예요." 저희는 앞으로도 이런 방식으로, 한 번에 스레드 하나씩, 어떤 규모에서든 일해 나갈 생각입니다. 채널은 지금도 계속 돌아가고 있습니다.
기여: Alfred Xing, Anthony Morris, Benjamin Pasero, Chase McCoy, Joshua N., Luke Deen Taylor, Marius Schulz, Shelley Vohr. 더 야심 차게 도전하라고 격려해 준 Boris Cherny에게 특별히 감사드립니다.