DevTools 설계: AI 어시스턴트의 효율적인 토큰 사용

게시일: 2026년 1월 30일

성능을 위한 AI 어시스턴스를 구축할 때 주요 엔지니어링 과제는 Gemini가 DevTools에서 기록된 성능 트레이스와 원활하게 작동하도록 하는 것이었습니다.

대규모 언어 모델 (LLM)은 한 번에 처리할 수 있는 정보의 양에 엄격한 제한이 있는 '컨텍스트 윈도우' 내에서 작동합니다. 이 용량은 토큰으로 측정됩니다. Gemini 모델의 경우 토큰 하나는 약 4글자 그룹입니다.

성능 트레이스는 수 메가바이트로 구성되는 경우가 많은 대규모 JSON 파일입니다. 원시 트레이스를 전송하면 모델의 컨텍스트 윈도우가 즉시 소진되어 질문을 위한 공간이 남지 않습니다.

성능을 위한 AI 지원을 제공하기 위해 Google은 토큰 사용량을 최소화하면서 LLM에 유용한 데이터의 양을 극대화하는 시스템을 설계해야 했습니다. 이 블로그에서는 Google에서 이 작업을 수행하는 데 사용한 기술을 알아보고 자체 프로젝트에 적용할 수 있습니다.

초기 컨텍스트 맞춤설정

웹사이트 성능을 디버깅하는 것은 복잡한 작업입니다. 개발자는 컨텍스트를 위해 전체 트레이스를 살펴보거나, 핵심 웹 바이탈 및 트레이스의 관련 시간 범위를 집중적으로 살펴보거나, 세부정보로 이동하여 클릭이나 스크롤과 같은 개별 이벤트와 관련 호출 스택에 집중할 수도 있습니다.

디버깅 프로세스를 지원하려면 DevTools의 AI 지원이 이러한 개발자 여정과 일치해야 하며 관련 데이터만 사용하여 개발자 관심사에 맞는 조언을 제공해야 합니다. 따라서 항상 전체 트레이스를 전송하는 대신 디버깅 작업에 따라 데이터를 슬라이스하는 AI 지원을 구축했습니다.

디버깅 작업 AI 지원에 처음 전송된 데이터
성능 추적에 관해 채팅하기 트레이스 요약: 트레이스 및 디버깅 세션의 개요 정보를 포함하는 텍스트 기반 보고서입니다. 페이지 URL, 제한 조건, 주요 실적 측정항목 (LCP, INP, CLS), 사용 가능한 통계 목록, 사용 가능한 경우 CrUX 요약이 포함됩니다.
성능 통계에 관해 채팅하기 트레이스 요약 및 선택한 성능 통계의 이름
트레이스의 작업에 관해 채팅하기 트레이스 요약 및 선택한 작업이 있는 직렬화된 호출 트리
네트워크 요청에 관한 채팅 트레이스 요약, 선택한 요청 키 및 타임스탬프
트레이스 주석 생성 선택한 작업이 있는 직렬화된 호출 트리입니다. 직렬화된 트리에서는 선택된 작업을 식별합니다.

추적 요약은 거의 항상 AI 지원의 기본 모델인 Gemini에 초기 컨텍스트를 제공하기 위해 전송됩니다. AI 생성 주석의 경우 생략됩니다.

AI에 도구 제공

DevTools의 AI 어시스턴스는 에이전트 역할을 합니다. 즉, 개발자의 초기 프롬프트와 공유된 초기 컨텍스트를 기반으로 더 많은 데이터를 자율적으로 쿼리할 수 있습니다. 더 많은 데이터를 쿼리하기 위해 AI 어시스턴트가 호출할 수 있는 사전 정의된 함수 집합을 제공했습니다. 함수 호출 또는 도구 사용이라고 하는 패턴입니다.

앞서 설명한 디버깅 여정을 기반으로 에이전트의 세부 함수 집합을 정의했습니다. 이러한 함수는 인간 개발자가 성능 디버깅에 접근하는 방식과 유사하게 초기 컨텍스트를 기반으로 중요하다고 간주되는 세부사항을 자세히 살펴봅니다. 함수 집합은 다음과 같습니다.

함수 설명
getInsightDetails(name) 특정 성능 통계에 관한 세부정보를 반환합니다 (예: LCP가 플래그된 이유에 관한 세부정보).
getEventByKey(key) 단일 특정 이벤트의 세부 속성을 반환합니다.
getMainThreadTrackSummary(start, end) 하향식, 상향식, 서드 파티 요약을 비롯하여 지정된 경계의 기본 스레드 활동 요약을 반환합니다.
getNetworkTrackSummary(start, end) 지정된 시간 범위의 네트워크 활동 요약을 반환합니다.
getDetailedCallTree(event_key) 성능 트레이스의 특정 기본 스레드 이벤트에 대한 전체 호출 트리를 반환합니다.
getFunctionCode(url, line, col) 리소스의 특정 위치에 정의된 함수의 소스 코드를 반환합니다. 성능 트레이스의 런타임 성능 데이터로 주석이 달려 있습니다.
getResourceContent(url) 페이지에서 사용되는 텍스트 리소스 (예: HTML 또는 CSS)의 콘텐츠를 반환합니다.

데이터 검색을 이러한 함수 호출로 엄격하게 제한함으로써 관련 정보만 잘 정의된 형식으로 컨텍스트 창에 입력되어 토큰 사용이 최적화됩니다.

에이전트 작업의 예

AI 어시스턴트가 함수 호출을 사용하여 추가 정보를 가져오는 실제 사례를 살펴보겠습니다. '이 요청이 느린 이유는 무엇인가요?'라는 초기 프롬프트가 표시된 후 AI 지원은 다음 함수를 점진적으로 호출할 수 있습니다.

  1. getEventByKey: 사용자가 선택한 특정 요청의 자세한 타이밍 분석 (TTFB, 다운로드 시간)을 가져옵니다.
  2. getMainThreadTrackSummary: 요청이 시작되어야 할 때 기본 스레드가 사용 중 (차단됨)인지 확인합니다.
  3. getNetworkTrackSummary: 다른 리소스가 동시에 대역폭을 두고 경쟁했는지 분석합니다.
  4. getInsightDetails: Trace 요약에 이 요청과 관련된 통계가 병목 현상으로 이미 언급되어 있는지 확인합니다.

이러한 호출 결과를 결합하여 AI 지원은 진단을 제공하고 getFunctionCode를 사용하여 코드 개선을 제안하거나 getResourceContent에 따라 리소스 로딩을 최적화하는 등 실행 가능한 단계를 제안할 수 있습니다.

하지만 관련 데이터를 검색하는 것은 문제의 절반에 불과합니다. 세부 데이터를 제공하는 함수를 사용하더라도 이러한 함수에서 반환되는 데이터의 크기가 클 수 있습니다. 다른 예로 getDetailedCallTree는 노드가 수백 개인 트리를 반환할 수 있습니다. 표준 JSON에서는 중첩을 위해 많은 {}가 필요합니다.

따라서 토큰 효율성이 높으면서도 LLM이 이해하고 참조할 수 있을 만큼 구조화된 형식이 필요합니다.

데이터 직렬화

성능 추적의 대부분을 차지하는 호출 트리 예시를 통해 이 문제에 어떻게 접근했는지 자세히 살펴보겠습니다. 참고로 다음 예에서는 호출 스택의 단일 작업을 JSON으로 보여줍니다.

{
  "id": 2,
  "name": "animate",
  "selected": true,
  "duration": 150,
  "selfTime": 20,
  "children": [3, 5, 6, 7, 10, 11, 12]
}

하나의 성능 트레이스에는 다음 스크린샷과 같이 이러한 항목이 수천 개 포함될 수 있습니다. 모든 작은 색상 상자는 이 객체 구조를 사용하여 표현됩니다.

DevTools에서 기록된 성능 트레이스의 호출 스택

이 형식은 DevTools에서 프로그래매틱 방식으로 작업하는 데는 적합하지만 다음과 같은 이유로 LLM에는 낭비적입니다.

  1. 중복 키: "duration", "selfTime", "children"과 같은 문자열이 호출 트리에서 모든 단일 노드에 대해 반복됩니다. 따라서 모델에 전송된 노드가 500개인 트리는 각 키에 대해 500번 토큰을 사용합니다.
  2. 자세한 목록: children를 통해 모든 하위 ID를 개별적으로 나열하면 특히 많은 다운스트림 이벤트를 트리거하는 작업의 경우 엄청난 수의 토큰이 소모됩니다.

실적을 위한 AI 지원에 사용되는 모든 데이터에 대해 토큰 효율적인 형식을 구현하는 것은 단계별 프로세스였습니다.

첫 번째 반복

Google에서는 실적을 위한 AI 지원을 개발할 때 배송 속도를 최적화했습니다. 토큰 최적화에 대한 접근 방식은 기본적이었으며 중괄호와 쉼표에서 원래 JSON을 삭제하여 다음과 같은 형식이 되었습니다.

allUrls = [...]

Node: 1 - update
Selected: false
Duration: 200
Self Time: 50
Children:
   2 - animate

Node: 2 - animate
Selected: true
Duration: 150
Self Time: 20
URL: 0
Children:
   3 - calculatePosition
   5 - applyStyles
   6 - applyStyles
   7 - calculateLayout
   10 - applyStyles
   11 - applyStyles
   12 - applyStyles

Node: 3 - calculatePosition
Selected: false
Duration: 15
Self Time: 2
URL: 0
Children:
   4 - getBoundingClientRect

...

하지만 이 첫 번째 버전은 원시 JSON에 비해 약간만 개선되었습니다. ID와 이름이 있는 노드 하위 요소를 명시적으로 나열하고 각 줄 앞에 설명이 포함된 반복 키 (Node:, Selected:, Duration: 등)를 추가했습니다.

하위 노드 목록 최적화

추가 최적화를 위해 노드 하위 요소의 이름(이전 예의 calculatePosition, applyStyles 등)을 삭제했습니다. AI 지원은 함수 호출을 통해 모든 노드에 액세스할 수 있고 이 정보는 이미 노드 헤드 (Node: 3 - calculatePosition)에 있으므로 이 정보를 반복할 필요가 없습니다. 이를 통해 Children를 간단한 정수 목록으로 축소할 수 있었습니다.

Node: 2 - animate
Selected: true
Duration: 150
Self Time: 20
URL: 0
Children: 3, 5, 6, 7, 10, 11, 12

..

이전보다 크게 개선되었지만 최적화할 여지가 여전히 있었습니다. 이전 예시를 보면 4, 8, 9만 누락된 Children가 거의 순차적임을 알 수 있습니다.

그 이유는 첫 번째 시도에서 깊이 우선 검색 (DFS) 알고리즘을 사용하여 성능 트레이스에서 트리 데이터를 직렬화했기 때문입니다. 이로 인해 형제 노드의 ID가 순차적이지 않아 모든 ID를 개별적으로 나열해야 했습니다.

너비 우선 검색 (BFS)을 사용하여 트리의 색인을 다시 생성하면 순차적 ID를 얻을 수 있어 다른 최적화가 가능하다는 것을 알게 되었습니다. 이제 개별 ID를 나열하는 대신 원래 예의 3-9와 같은 단일 압축 범위로 수백 개의 하위 요소를 나타낼 수 있습니다.

최적화된 Children 목록이 포함된 최종 노드 표기법은 다음과 같습니다.

allUrls = [...]

Node: 2 - animate
Selected: true
Duration: 150
Self Time: 20
URL: 0
Children: 3-9

키 수 줄이기

노드 목록을 최적화한 후 중복 키로 넘어갔습니다. 이전 형식에서 모든 키를 삭제하여 다음과 같은 결과가 나왔습니다.

allUrls = [...]

2;animate;150;20;0;3-10

토큰 효율성은 인정되지만 Gemini가 이 데이터를 이해하는 방법을 알려주는 안내가 필요했습니다. 따라서 Gemini에 호출 트리를 처음 보낼 때 다음 프롬프트를 포함했습니다.

...
Each call frame is presented in the following format:

'id;name;duration;selfTime;urlIndex;childRange;[S]'

Key definitions:

*   id: A unique numerical identifier for the call frame.
*   name: A concise string describing the call frame (e.g., 'Evaluate Script', 'render', 'fetchData').
*   duration: The total execution time of the call frame, including its children.
*   selfTime: The time spent directly within the call frame, excluding its children's execution.
*   urlIndex: Index referencing the "All URLs" list. Empty if no specific script URL is associated.
*   childRange: Specifies the direct children of this node using their IDs. If empty ('' or 'S' at the end), the node has no children. If a single number (e.g., '4'), the node has one child with that ID. If in the format 'firstId-lastId' (e.g., '4-5'), it indicates a consecutive range of child IDs from 'firstId' to 'lastId', inclusive.
*   S: **Optional marker.** The letter 'S' appears at the end of the line **only** for the single call frame selected by the user.

....

이 형식 설명에는 토큰 비용이 발생하지만 전체 대화에 대해 한 번만 지불하는 고정 비용입니다. 비용보다 이전 최적화를 통해 얻은 절감액이 더 큽니다.

결론

AI로 빌드할 때는 토큰 사용량을 최적화하는 것이 중요합니다. 원시 JSON에서 특수 맞춤 형식으로 전환하고, 너비 우선 검색으로 트리의 색인을 다시 생성하고, 도구 호출을 사용하여 필요에 따라 데이터를 가져옴으로써 Chrome DevTools의 AI 지원에서 사용하는 토큰의 양을 크게 줄였습니다.

이러한 최적화는 성능 트레이스에 AI 지원을 사용 설정하기 위한 필수사항이었습니다. 컨텍스트 윈도우가 제한되어 있어 데이터의 양을 처리할 수 없습니다. 하지만 최적화된 형식을 사용하면 노이즈에 압도되지 않고 더 긴 대화 기록을 유지하고 더 정확한 상황 인식 답변을 제공할 수 있는 성능 에이전트를 사용할 수 있습니다.

이러한 기법이 AI를 설계할 때 자체 데이터 구조를 다시 한번 살펴보는 데 도움이 되기를 바랍니다. 웹 애플리케이션에서 AI를 시작하려면 web.dev에서 AI 학습하기를 살펴보세요.