For the complete documentation index, see llms.txt. Markdown versions of documentation pages are available by appending .md to the page URL.
기본 탐색 메뉴
Theme

브라우저 주석 기능 확장

브라우저 주석에 사용할 웹사이트의 선택 대상, 컨텍스트, 컨트롤을 맞춤 설정하는 방법

Browser Annotation API를 사용하면 웹사이트에서 사용자가 선택할 수 있는 대상과 피드백에 함께 담을 컨텍스트를 지정할 수 있습니다. 사용자가 ChatGPT에 주석을 보내기 전에 변경 사항을 미리 볼 때 사용할 컨트롤도 설정할 수 있습니다.

ChatGPT Enterprise 및 ChatGPT Edu 워크스페이스에서는 관리자가 워크스페이스 정책에서 사이트 주석 맞춤 설정을 허용해야 합니다.

브라우저 주석은 사이트 코드를 변경하지 않아도 사용할 수 있습니다. 사용자는 페이지 일부를 선택하고 의견을 작성한 뒤 컨텍스트에 담아 Codex나 ChatGPT Work로 보낼 수 있습니다.

개발자는 Browser Annotation API로 애플리케이션에 맞는 컨텍스트나 컨트롤을 제공할 수 있습니다. 예를 들어 디자인 시스템 미리보기에서 컴포넌트 베리언트별 미리보기를 주석에 첨부하면 개발자가 웹사이트의 컴포넌트를 어떻게 수정해야 할지 파악할 수 있습니다.

Browser Annotation API를 이해하거나 웹사이트에 주석 기능을 추가하는 데 도움이 필요하면 Annotations Extensibility 플러그인을 설치하세요.

Annotations Extensibility 플러그인 설치

직접 해 보기

이 가이드에서 Browser Annotation API가 어떻게 작동하는지 확인하세요.

  1. ChatGPT의 앱 내 브라우저에서 이 페이지를 여세요.
  2. 이 카드에 표시되는 추천 프롬프트를 열어 보세요.
  3. 주석 모드로 전환한 다음 아래 표나 코드 예제를 선택해 미리 정의된 레이아웃과 테마를 바꿔 보세요.
  4. 페이지에서 원하는 항목에 주석을 달아 기본 동작도 확인하세요.

ChatGPT 브라우저에서 열기

맞춤 설정할 기능 선택하기

웹사이트에 필요한 기능에 맞춰 연동 방법을 선택하세요.

목표 연동 방법
카드나 여러 요소를 묶어 하나의 객체로 선택하기 선택 대상
사용자가 구절이나 문장을 선택하도록 하기 텍스트 선택 컨테이너
선택한 대상에 컨텍스트 추가하기 선택 메타데이터
사이트 버튼으로 추천 의견이 담긴 주석 열기 주석 요청
사이트 UI에서 특정 구절에 대한 피드백 요청하기 텍스트 범위 요청
주석이 열릴 때 고급 컨트롤 표시하기 편집기 기본 설정
애플리케이션 속성 변경을 미리 보거나 사용자의 선택 수집하기 사용자 지정 컨트롤
캔버스 안에 그린 객체를 개별적으로 선택하기 주석 서피스
사이트에서 주석 모드 켜기 또는 끄기 주석 모드 제어

이 가이드는 DevDay 2026에서 출시된 ChatGPT 데스크톱 앱과 그 이후 버전을 대상으로 합니다. 앱 내 브라우저에서는 HTTPS나 로컬호스트처럼 보안 컨텍스트에 있는 최상위 페이지에서 document.oai.annotation을 통해 JavaScript API를 사용할 수 있습니다. API가 활성화되어 있으면 브라우저가 페이지 스크립트를 실행하기 전에 API를 설치합니다. 이전 버전이나 API를 지원하지 않는 브라우저에서도 사이트가 작동하도록 각 메서드의 지원 여부를 확인한 뒤, 필요한 DOM 요소가 생성되면 연동 기능을 초기화하세요. 준비 완료 이벤트나 폴링은 필요하지 않습니다.

API 메서드는 동기적으로 반환되며 등록 핸들은 즉시 사용할 수 있습니다. 브라우저는 그 이후에 주석 편집기 로딩을 완료할 수 있습니다. 서피스의 hitTest 콜백은 프로미스를 반환할 수 있습니다.

선택 대상 맞춤 설정하기

기본적으로 주석 모드는 페이지 DOM에서 요소를 선택하며 텍스트, 이미지, 컨트롤 등을 우선합니다. 더 큰 객체를 선택할 수 있게 하려면 해당 객체를 포함하는 영역에 oai-annotation-container를 지정하고, 선택 대상으로 삼을 하위 요소에 oai-annotatable을 지정하세요.

다음 예제에서는 차트 카드를 하나의 객체로 선택할 수 있습니다.

<section oai-annotation-container>
  <article id="chart-card" oai-annotatable="Q3 Revenue">
    <h2>Q3 Revenue</h2>
    <p>$120,000 this quarter</p>
    <button type="button">More info</button>
  </article>
</section>

카드 안 어디에 마우스 포인터를 놓아도 카드 전체가 강조 표시됩니다. oai-annotatable에 값을 지정하면 사용자와 모델에 표시할 객체 이름을 정할 수 있습니다. 주변 객체와 구분하기 쉬운 이름을 사용하거나 값을 생략하세요.

컨테이너는 이 선택 규칙이 적용되는 영역을 정의합니다. oai-annotatable 속성만 지정하면 선택 동작은 바뀌지 않습니다. 컨테이너 안에서는 브라우저가 마우스 포인터 아래의 요소를 포함하는 대상 중 선택 대상으로 표시된 가장 가까운 요소를 선택합니다. 컨테이너가 중첩된 경우 가장 가까운 컨테이너의 규칙을 따릅니다. 컨테이너 안에서 선택 대상으로 표시되지 않은 영역을 선택하면 웹페이지 주석이 생성됩니다. 모든 컨테이너 바깥에서는 기본 동작이 유지됩니다.

텍스트 선택 활성화하기

사용자가 주석 모드에서 드래그해 텍스트를 선택할 수 있도록 해당 영역에 oai-annotation-container-text를 추가하세요. 이 속성은 값을 지정할 필요가 없습니다.

<article oai-annotation-container-text>
  <h2>Design guidelines</h2>
  <p>Use consistent spacing between related components.</p>
  <p>Leave more space between separate groups.</p>
</article>

텍스트를 선택하고 드래그를 마치면 선택 범위와 컨텍스트가 담긴 텍스트 주석 편집기가 열립니다. 텍스트를 선택하지 않고 클릭하면 주석이 생성되지 않습니다. Escape 키를 누르거나 제스처를 취소하면 선택이 취소됩니다.

제스처를 시작하는 위치에서 가장 가까운 텍스트 선택 컨테이너 또는 DOM 선택 컨테이너가 초기 동작을 결정합니다. 텍스트 컨테이너에서는 oai-annotatable 표시로 요소를 선택하지 않습니다. 내부에 oai-annotation-container를 중첩하면 해당 영역에서 다시 요소를 선택할 수 있고, 텍스트 컨테이너를 중첩하면 다시 텍스트를 선택할 수 있습니다. 한 요소에 두 속성이 모두 있으면 텍스트 선택이 우선합니다.

텍스트 선택은 페이지의 일반 선택 규칙을 따르며 시작 컨테이너 바깥까지 이어질 수 있습니다. 컨테이너 경계에서 선택 범위가 잘리지 않으며, 컨테이너를 지정해도 iframe 안에서 선택이 활성화되지는 않습니다. 텍스트 필드에서는 계속 텍스트를 선택할 수 있지만 주석 모드에서는 일반 페이지 클릭과 컨트롤의 기본 동작이 차단됩니다. 명시적으로 request()를 호출할 때의 동작은 그대로 유지됩니다.

텍스트 선택 컨테이너는 사이트 주석 기능과 해당 사이트의 설정이 모두 활성화된 상태에서 주석 모드를 사용할 때만 적용됩니다.

선택 대상에 컨텍스트 추가하기

선택 대상으로 표시한 요소에 oai-annotation-metadata를 추가하면 페이지에 표시되지 않는 정보도 컨텍스트에 포함할 수 있습니다. 예를 들어 가상의 연락처 행에 이메일 주소를 포함해 이메일 초안 작성 요청에 활용할 수 있습니다.

<section oai-annotation-container>
  <div
    id="contact-row"
    oai-annotatable="Alex Morgan"
    oai-annotation-metadata='{"email":"alex@example.com"}'
  >
    <div>Alex Morgan</div>
    <div>Project lead</div>
  </div>
</section>

두 줄 중 어느 줄을 선택해도 행 전체가 선택됩니다. 메타데이터는 주석에 표시되고 대화에도 주석과 함께 전달됩니다. 사용자와 모델 모두에게 공유하려는 정보만 포함하세요. 실제로 이메일을 보내려면 별도로 연결된 이메일 도구가 필요합니다.

다음 제한을 충족하는 작은 단일 계층 JSON 객체를 사용하세요.

  • 속성은 최대 6개이며 값은 문자열, 유한한 수, 불리언 또는 null이어야 합니다.
  • 키는 최대 64자, 문자열 값은 최대 256자입니다.
  • 직렬화한 객체의 크기는 최대 2,048바이트입니다.

키는 ASCII 영문자로 시작해야 하며 ASCII 영문자, 숫자, 공백, 밑줄, 하이픈만 포함할 수 있습니다. 단어 사이는 공백 한 칸으로 구분하세요. 중첩 객체와 배열은 지원되지 않습니다. 브라우저는 유효하지 않은 메타데이터를 무시합니다.

request() 또는 서피스의 hitTest 결과로도 메타데이터를 제공할 수 있습니다.

웹사이트에서 주석 열기

버튼을 누르는 등 사용자가 상호작용하는 시점에 document.oai.annotation.request(target, options)를 직접 호출하세요. 대상은 현재 문서에 연결되어 페이지의 보이는 영역 안에 있는 HTML 요소이거나 DOM Range일 수 있습니다. 대상에 주석 속성이 없어도 됩니다. 가상 객체 ID는 지원되지 않습니다.

사이트에서 시작하는 요청에는 사용자의 허가가 필요할 수 있습니다. 사용자는 사이트 도구 > 주석 기능에서 차단한 주석 기능을 다시 활성화할 수 있습니다.

다음 예제는 선택 속성이 없는 단락에 주석을 엽니다. 두 요소가 모두 생성된 뒤 스크립트를 실행하세요.

<p id="delivery-summary">
  Standard delivery takes three to five business days.
</p>
<button id="discuss-delivery" type="button" hidden>Explain more</button>
const summary = /** @type {HTMLElement} */ (
  document.getElementById("delivery-summary")
);
const button = /** @type {HTMLButtonElement} */ (
  document.getElementById("discuss-delivery")
);

button.hidden = typeof document.oai?.annotation?.request !== "function";

button.addEventListener("click", () => {
  const annotation = document.oai?.annotation;
  if (typeof annotation?.request !== "function") return;

  annotation.request(summary, {
    initialComment: "Explain this delivery estimate.",
  });
});

자세히 설명하기를 선택하면 브라우저에 해당 단락을 선택하고 편집 가능한 의견이 담긴 주석을 열도록 요청합니다. 사용자는 의견을 수정하고 주석을 저장한 뒤 메시지와 함께 보낼 수 있습니다. 주석을 여는 것만으로는 ChatGPT에 메시지가 전송되지 않습니다. 메시지는 사용자만 보낼 수 있습니다.

사용자 상호작용이 활성 상태일 때 호출하세요. 먼저 네트워크 요청이 완료되기를 기다리면 사용자 상호작용이 더 이상 활성 상태가 아닐 수 있습니다.

선택 사항인 두 번째 인수는 다음 필드를 지원합니다.

옵션 동작
mode "advanced"를 지정하면 요소의 고급 컨트롤이 열리고, "default"를 지정하면 편집기가 기본 동작을 따릅니다. 기본값은 "default"입니다. "default"에서도 사용자 지정 컨트롤이 열릴 수 있습니다. 편집기 기본 설정을 참고하세요. 텍스트 범위는 "default"만 지원합니다.
enterAnnotationMode true로 설정하면 주석 모드로 전환하고 주석을 취소하거나 보낸 뒤에도 이 모드를 유지합니다.
omitScreenshot true로 설정하면 이 주석에서 자동 스크린샷을 생략합니다. 기본값은 false이며 이 경우 스크린샷 기능은 평소처럼 작동합니다. 요소 요청과 텍스트 범위 요청에 모두 적용됩니다.
metadata 유효하고 비어 있지 않은 메타데이터를 지정하면 이번 요청에서 대상의 HTML 메타데이터를 대체합니다. 대상이 텍스트 범위이거나 섀도 DOM 안에 있는 요소이면 이 옵션은 무시됩니다.
initialComment 편집 가능한 의견을 제공합니다. 길이는 최대 240 UTF-16 코드 단위입니다.

request()는 불리언 필드 accepted가 있는 객체를 반환합니다. 브라우저가 요청을 수신하고 유효성을 확인했는지 판단하려면 결과 객체 자체가 아닌 result.accepted 값을 확인하세요. 이 값만으로는 편집기가 열렸는지, 사용자가 주석을 저장하거나 보냈는지 알 수 없습니다.

브라우저는 편집기를 로드하는 동안 수락한 요청 하나를 보류할 수 있습니다. 대기 중인 요청이 있거나 편집기가 열려 있거나 ChatGPT가 브라우저를 제어하는 동안에는 추가 요청을 거부할 수 있습니다.

텍스트 범위에 대한 주석 요청하기

DOM Range를 전달하면 브라우저의 현재 텍스트 선택을 바꾸지 않고 특정 구절에 대한 피드백을 요청할 수 있습니다. 다음 예제에서는 강조 표시된 구절만 선택합니다. oai-annotation-container-text 속성은 필요하지 않습니다.

<p>
  Your trial includes full access for <mark id="draft-passage">14 days</mark>.
</p>
<button id="discuss-passage" type="button" hidden>
  Ask about highlighted text
</button>

두 요소가 모두 생성된 뒤 다음 스크립트를 실행하세요.

const passage = /** @type {HTMLElement} */ (
  document.getElementById("draft-passage")
);
const button = /** @type {HTMLButtonElement} */ (
  document.getElementById("discuss-passage")
);

button.hidden = typeof document.oai?.annotation?.request !== "function";

button.addEventListener("click", () => {
  const annotation = document.oai?.annotation;
  if (typeof annotation?.request !== "function") return;

  const range = document.createRange();
  range.selectNodeContents(passage);
  annotation.request(range, {
    initialComment: "Explain when this trial ends.",
  });
});

범위에는 현재 문서에서 표시되는 텍스트가 한 글자 이상 있어야 하며, 선택 범위의 적어도 일부가 페이지의 보이는 영역에 있어야 합니다. 범위의 최대 길이는 20,000 UTF-16 코드 단위입니다. 시작점과 끝점이 같은 범위, 공백만 있는 텍스트, 선택한 텍스트가 숨겨진 경우, 닫힌 섀도 루트 안의 범위는 지원되지 않습니다.

텍스트 범위 요청에는 기본 텍스트 편집기만 사용됩니다. mode: "advanced"를 지정한 요청은 거부되며 요청 메타데이터는 무시됩니다. 브라우저는 편집기를 열기 직전에 범위를 다시 확인하므로, 수락된 요청이 편집기를 기다리는 동안에도 대상 텍스트를 유지하세요. 이와 별개로 텍스트 선택 컨테이너를 사용하면 사용자가 주석 모드에서 드래그해 텍스트를 선택할 수 있습니다.

편집기의 기본 모드 선택하기

브라우저의 선택 UI에서 주석을 열 때 고급 컨트롤을 바로 표시하려면 페이지의 <head>에 다음 태그를 추가하세요.

<meta name="oai-annotation-editor-default-mode" content="advanced" />

사이트 UI에서 주석을 열 때는 { mode: "advanced" }를 request()에 전달하세요. 편집기의 기본 동작을 사용하려면 mode: "default"를 지정하거나 이 옵션을 생략하세요. 페이지의 메타 설정은 이 요청 옵션을 재정의하지 않습니다. 기본 모드에서도 의견 입력란만 표시되는 것은 아닙니다. 사용자 지정 컨트롤에 시작값을 제안했거나 currentValue를 생략했다면 컨트롤 편집기가 열릴 수 있습니다.

수동 조정, 접기, Option 키를 누른 채 클릭 기능은 Codex 또는 로컬호스트에서만 사용할 수 있습니다. ChatGPT에서 호스팅된 사이트를 열면 조정 버튼이나 접기 버튼 없이 등록된 사용자 지정 컨트롤이 자동으로 표시됩니다. 이 경우에도 페이지에서 고급 모드를 요청할 수 있습니다.

사용자 지정 컨트롤 추가하기

registerControls()를 사용하면 하나 이상의 DOM 요소에 주석 컨트롤을 연결할 수 있습니다. 컨트롤을 통해 간격 토큰과 같은 애플리케이션 속성의 변경을 미리 보거나, 이메일 어조처럼 요청에 포함할 항목을 사용자가 선택하도록 할 수 있습니다.

공유 간격 토큰 미리보기

이 예제의 두 카드는 같은 CSS 속성을 사용합니다. 컨트롤을 카드에 직접 등록하세요. 선택 속성은 필요하지 않습니다.

<style>
  #component-preview {
    --card-padding: 16px;
  }

  .preview-card {
    padding: var(--card-padding);
    border: 1px solid #d1d5db;
  }
</style>

<section id="component-preview">
  <article class="preview-card">Profile card</article>
  <article class="preview-card">Summary card</article>
</section>

미리보기를 생성한 뒤 다음 스크립트를 실행하세요.

const preview = /** @type {HTMLElement} */ (
  document.getElementById("component-preview")
);
const annotation = document.oai?.annotation;

/** @param {CustomEvent<{ callback: string; value: number | string }>} event */
function previewSpacing(event) {
  const { callback, value } = event.detail;
  if (callback === "setCardPadding") {
    preview.style.setProperty("--card-padding", `${value}px`);
  }
}

/** @type {{ dispose(): void } | undefined} */
let registration;
if (typeof annotation?.registerControls === "function") {
  preview.addEventListener("oaiannotationcontrolchange", previewSpacing);
  registration = annotation.registerControls({
    targets: preview.querySelectorAll(".preview-card"),
    controlsHeading: "Card spacing",
    controlsMode: "replace",
    controls: [
      {
        type: "range",
        label: "Card padding (pixels)",
        callback: "setCardPadding",
        reference: "--card-padding",
        min: 8,
        max: 32,
        step: 4,
        currentValue: 16,
      },
    ],
  });
}

function disposeAnnotationControls() {
  preview.style.removeProperty("--card-padding");
  registration?.dispose();
  preview.removeEventListener("oaiannotationcontrolchange", previewSpacing);
}

카드 하나에 주석을 달고 카드 안쪽 여백(픽셀) 값을 16에서 24로 변경하세요. ChatGPT에서 호스팅된 사이트를 열면 컨트롤이 자동으로 표시됩니다. Codex나 로컬호스트에서는 필요하면 조정을 선택하세요. 두 카드가 모두 업데이트됩니다. 주석에는 레이블, 참조, 이전 값과 새 값이 기록됩니다. 미리보기를 지우면 원래 여백이 복원됩니다. 컴포넌트를 제거할 때는 disposeAnnotationControls()를 호출하세요.

미리보기 없이 선택 항목 수집하기

다음 예제에서는 일반 단락에 이메일 어조를 선택하는 컨트롤을 제공합니다. 선택 속성이나 메타데이터는 필요하지 않습니다. 단락이 생성된 뒤 스크립트를 실행하세요.

<p id="email-draft">Draft a follow-up email about the project timeline.</p>
const draft = /** @type {HTMLElement} */ (
  document.getElementById("email-draft")
);
const registration = document.oai?.annotation?.registerControls?.({
  targets: draft,
  controlsHeading: "Email options",
  controlsMode: "replace",
  controls: [
    {
      type: "select",
      label: "Email tone",
      callback: "emailTone",
      options: [
        { label: "Professional", value: "professional" },
        { label: "Friendly", value: "friendly" },
        { label: "Direct", value: "direct" },
      ],
      defaultValue: "professional",
    },
  ],
});

이 컨트롤은 페이지 변경 사항을 미리 보여 주지 않으므로 이벤트 핸들러가 필요하지 않습니다. currentValue를 생략하면 사용자가 초기 옵션을 그대로 두더라도 브라우저는 선택된 어조를 주석에 포함합니다. 단락을 제거할 때는 registration?.dispose()를 호출하세요.

선택형 컨트롤의 미리보기 콜백은 option.value를 받습니다. 예를 들면 "professional"입니다. 주석 기록과 ChatGPT에는 이전 옵션과 새로 선택한 옵션 모두 사용자에게 표시되는 option.label이 전달됩니다. 예를 들면 "Professional"입니다. 각 선택 항목의 의미가 드러나는 레이블을 사용하세요. value의 내부 ID는 선택 항목의 텍스트로 전송되지 않습니다.

컨트롤과 시작값 설정하기

controlsMode: "replace"를 지정하면 등록된 대상에 사용자 지정 컨트롤만 표시되고, "extend"를 지정하면 기본 제공 컨트롤과 함께 표시됩니다. 선택 사항인 controlsHeading으로 패널 제목을 지정할 수 있습니다. 브라우저는 제목의 앞뒤 공백을 제거하며 길이는 1~80자여야 합니다. 제목을 생략하면 패널에 요소의 HTML 태그가 표시됩니다. 제목은 ChatGPT에 보내는 컨텍스트에 포함되지 않습니다.

한 번에 등록할 수 있는 컨트롤은 최대 12개입니다. 각 컨트롤에는 type, 사용자에게 표시할 label, callback 식별자가 필요합니다.

컨트롤 이름은 무엇을 변경하는지 알 수 있게 지으세요. ‘따뜻한 아이보리색’이나 ‘짙은 숲색’처럼 현재 값을 가리키는 이름 대신 ‘페이지 배경’, ‘브랜드 기본 색상’, ‘강조 색상’처럼 속성이나 디자인에서의 역할을 나타내는 레이블을 사용하세요. 값이 바뀌어도 레이블의 의미는 유지되어야 합니다. 기존 값은 currentValue에, 그 값에 해당하는 토큰이나 속성은 reference에 지정하세요.

다음 컨트롤 정의에서는 레이블과 색상 값을 별도로 지정합니다.

const pageBackgroundControl = {
  type: "color",
  label: "Page background",
  callback: "setPageBackground",
  reference: "--color-background",
  currentValue: "#f6f3ec",
};

지원되는 컨트롤 유형은 다음과 같습니다.

유형 값 추가 필드
color "#2563eb"와 같은 16진수 색상 없음
range 숫자 min, max, step
select 문자열 options, 즉 { label, value } 객체의 배열
toggle 불리언 없음

callback은 JavaScript 함수가 아니라 문자열 식별자입니다. 한 등록 내에서 고유해야 하며 ASCII 영문자로 시작해야 합니다. ASCII 영문자, 숫자, 밑줄, 하이픈을 사용할 수 있습니다. 선택 사항인 reference는 변경 대상 속성을 식별하며 주석에 레이블 및 값과 함께 포함됩니다.

currentValue에는 저장하지 않은 편집 내용까지 반영한 속성의 유효한 현재 값을 지정하세요. 미리보기 상태나 입력 중인 값은 기준값으로 사용하지 마세요. 브라우저는 기준값을 사용해 변경 전후를 비교하고 값을 초기화합니다. 애플리케이션 상태가 바뀌면 registration.update({ controls })로 컨트롤의 currentValue 값을 갱신하세요. 갱신한 값은 이후 주석에 적용되며 기존 주석에는 생성 당시의 값이 유지됩니다.

시작값을 제안하려면 defaultValue를 지정하세요. 컨트롤의 초기 상태에서는 이 값이 currentValue보다 우선합니다. 두 값 모두 생략하면 색상 컨트롤은 흰색, 범위 컨트롤은 최솟값, 선택형 컨트롤은 첫 번째 옵션, 토글은 false로 시작합니다.

제안 값은 일반 편집 초안과 별도로 관리하세요. 업데이트에서 예외가 발생하거나 요청이 실패하거나 accepted: false가 반환되면 적용하려던 제안을 버리고 이전 컨트롤과 제안 상태를 복원하세요. 요청이 수락되면 제안을 유지하세요. 브라우저가 컨트롤 상태를 기록하기 전에 요청을 대기열에 넣을 수 있습니다.

컨트롤 및 등록 유효성 검사하기

브라우저는 등록하거나 업데이트할 때 다음 제약 조건을 검사합니다.

필드 또는 리소스 제약 조건
컨트롤 등록당 최대 12개이며 각 callback 식별자는 고유해야 합니다. 해당 컨트롤 유형에 정의된 필드만 사용하세요.
레이블 및 제목 컨트롤 레이블, 옵션 레이블, controlsHeading은 앞뒤 공백을 제거한 뒤에도 비어 있지 않아야 하며 최대 80 UTF-16 코드 단위까지 허용됩니다. 컨트롤 텍스트에는 제어 문자나 텍스트 방향을 바꾸는 문자를 포함할 수 없습니다.
식별자 callback은 앞뒤 공백을 제거한 뒤 최대 80 UTF-16 코드 단위여야 하며 위에서 설명한 형식을 따라야 합니다. reference의 길이는 1~80자입니다. ASCII 영문자, 숫자, _ . / : @ $ # - 기호를 사용할 수 있으며 공백은 허용되지 않습니다.
선택 옵션 옵션은 1~12개여야 합니다. 각 value는 서로 다른 문자열이어야 하며 최대 길이는 512 UTF-16 코드 단위입니다. currentValue와 defaultValue를 지정할 경우 각각 옵션 중 하나의 값과 일치해야 합니다.
범위 값 min, max, currentValue, defaultValue는 −10,000 이상 10,000 이하의 유한한 수여야 합니다. min < max여야 하며, step은 0.001 이상 10,000 이하이고 max - min을 초과할 수 없습니다. 시작값은 범위 안에 있어야 하지만 step 간격에 맞출 필요는 없습니다.
색상 및 토글 색상은 # 뒤에 16진수 3자리, 4자리, 6자리 또는 8자리를 사용해야 합니다. 토글 값은 true 또는 false여야 합니다.
대상 등록 시 대상은 1~128개이며 모두 현재 문서의 요소여야 합니다. 업데이트할 때 targets: []를 전달하면 연결을 해제할 수 있습니다. 문서당 컨트롤 등록은 최대 64개, 등록과 대상 간 연결은 최대 1,024개까지 지원됩니다.
직렬화된 컨트롤 controls, controlsHeading, controlsMode를 포함한 JSON 페이로드는 16,384 UTF-16 코드 단위를 초과할 수 없습니다. 함수, 심벌, BigInt를 제외하고 JSON으로 인코딩할 수 있는 데이터를 사용하세요.

registerControls()와 registration.update()는 정의나 대상이 유효하지 않거나 한도를 초과하면 동기적으로 예외를 던질 수 있습니다. dispose() 호출 후 업데이트해도 예외가 발생합니다. 업데이트가 거부되면 기존 등록과 그 안의 컨트롤 및 대상이 유지됩니다. 호출 지점에서 오류를 처리해 일반 편집 기능을 계속 사용할 수 있게 하세요. 한도를 넘지 않도록 직접 생성한 등록을 재사용하거나 해제하세요.

미리보기 및 초기화 처리하기

oaiannotationcontrolchange 이벤트는 선택된 요소에서 상위 요소로 버블링됩니다. 이벤트의 detail에는 callback, value, action이 포함됩니다.

액션 전달된 값을 적용한 결과
preview 요청한 변경 사항을 표시합니다.
preview-original 비교할 수 있도록 원래 상태를 잠시 표시합니다.
reset 미리보기를 지우면 원래 상태를 복원합니다.

간격 예제처럼 모든 액션에서 전달된 값을 적용하세요. 핸들러가 변경 사항을 되돌릴 수 있고 반복 호출해도 안전하게 작동하도록 구현하세요. 미리보기 이벤트는 영구적인 변경을 요청하지 않습니다. 저장은 애플리케이션의 일반 저장 플로우에서 처리하세요. 다른 편집 내용이 미리보기를 덮어쓰지 않도록 일반 편집 초안, 입력 중인 값, 미리보기를 별도로 관리하세요. 사용자가 같은 설정을 편집하면 해당 미리보기를 교체하고 이후 주석에 사용할 기준값을 갱신하세요.

업데이트와 정리에 사용할 등록 핸들을 보관하세요. update()에는 targets, controls, controlsHeading, controlsMode를 원하는 조합으로 전달할 수 있습니다. 생략한 필드는 이전 값을 유지합니다. 예를 들어 컴포넌트의 DOM 요소를 교체할 때는 registration.update({ targets: newElement })를 사용하세요. 대상 컬렉션은 등록 시점에 존재하는 요소를 담으며 나중에 선택자와 일치하는 요소를 자동으로 추적하지 않습니다.

targets: []를 전달하면 등록과 대상의 연결을 해제하고, controls: []를 전달하면 등록된 컨트롤을 비울 수 있습니다. 연동 기능을 제거할 때는 dispose()를 호출하고 이벤트 리스너를 제거하세요. 정리할 때 일반 렌더링 상태도 복원해야 합니다. dispose()는 컨트롤 등록만 해제하며 미리보기에서 적용한 변경 사항까지 되돌리지는 않습니다. 간격 예제에서는 인라인으로 덮어쓴 속성을 제거해 원래 CSS 값을 복원합니다. 두 메서드 모두 동기적으로 종료되며 반환값은 없습니다. 업데이트는 이후 선택에 적용됩니다. 저장된 주석에는 당시 기록된 컨트롤과 대상이 유지됩니다.

컨트롤 이벤트의 대상은 주석에 기록된 요소입니다. 등록 대상을 업데이트해도 기존 주석의 이벤트 대상은 바뀌지 않습니다. 제거된 요소에서도 초기화 이벤트가 발생할 수 있으며, 이때 이전 부모 요소에 연결된 리스너는 이벤트를 받지 못합니다.

캔버스 객체를 선택할 수 있게 하기

주석 서피스를 사용하면 애플리케이션에서 캔버스 안에 그린 객체를 개별적으로 식별할 수 있습니다. registerSurface()로 호스트 요소를 등록하고 hitTest 함수를 제공하세요. 이 함수는 고정된 id가 있는 객체를 반환하고, 빈 공간에서는 null을 반환해야 합니다.

호스트는 보안 컨텍스트의 최상위 문서에 연결된 HTML 요소여야 하며 섀도 DOM 바깥에 있어야 합니다. iframe 안에서의 서피스 등록은 지원되지 않습니다.

선택한 객체 식별하기

다음 예제는 매출을 나타내는 막대를 그리고, hitTest와 고정된 객체 ID만으로 막대를 선택할 수 있게 합니다. 스크립트를 캔버스 뒤에 배치하세요.

<canvas id="revenue-canvas" width="480" height="240">
  Revenue this quarter: $120,000.
</canvas>
const canvas = /** @type {HTMLCanvasElement} */ (
  document.getElementById("revenue-canvas")
);
const context = /** @type {CanvasRenderingContext2D} */ (
  canvas.getContext("2d")
);
const bar = { x: 40, y: 60, width: 320, height: 100 };

context.fillStyle = "#2563eb";
context.fillRect(bar.x, bar.y, bar.width, bar.height);

const surface = document.oai?.annotation?.registerSurface?.({
  element: canvas,
  hitTest({ clientX, clientY }) {
    const bounds = canvas.getBoundingClientRect();
    const scaleX = bounds.width / canvas.width;
    const scaleY = bounds.height / canvas.height;
    const rect = {
      x: bounds.left + bar.x * scaleX,
      y: bounds.top + bar.y * scaleY,
      width: bar.width * scaleX,
      height: bar.height * scaleY,
    };

    if (
      clientX < rect.x ||
      clientX > rect.x + rect.width ||
      clientY < rect.y ||
      clientY > rect.y + rect.height
    ) {
      return null;
    }

    return { id: "revenue-this-quarter" };
  },
});

위 코드 예제에서 주석 모드로 막대를 선택하면 revenue-this-quarter라는 ID로 식별됩니다. 같은 서피스 안에서는 객체의 ID를 일정하게 유지하세요. 예제는 빈 공간에 대해 null을 반환합니다.

hitTest는 프로미스를 반환할 수 있습니다. 새 작업으로 대체된 이전 작업을 취소할 수 있도록 AbortSignal을 signal로 전달받습니다. 브라우저는 최대 250밀리초 동안 기다린 뒤 DOM 선택으로 전환합니다. 오류가 발생하거나 결과가 유효하지 않을 때도 DOM 선택으로 전환합니다. 히트 테스트를 정상적으로 마쳤지만 해당 위치에 객체가 없으면 명시적으로 null을 반환하세요.

작업을 취소해도 해당 프로미스는 반드시 이행되거나 거부되어야 합니다. 브라우저는 한 번에 하나의 hitTest 콜백만 실행합니다. 취소되거나 시간이 초과된 후에도 프로미스가 이행되거나 거부될 때까지 다음 콜백을 실행하지 않습니다. 워커가 객체 선택을 처리한다면 워커 작업을 중단할 때 대기 중인 프로미스도 이행하거나 거부하세요. 취소된 워커의 응답을 그대로 버리면 이후 캔버스 객체 선택이 차단될 수 있습니다.

객체를 이동하거나 확대·축소 배율을 변경한 뒤에는 surface?.invalidate()를 호출하고, 연동 기능을 제거할 때는 surface?.dispose()를 호출하세요. 두 메서드 모두 동기적으로 종료되며 반환값은 없습니다.

객체 컨텍스트 추가하기(선택 사항)

객체를 식별하는 데는 id만 있으면 됩니다. 표시 이름, 의미상 역할, 화면에 보이지 않는 컨텍스트 또는 선택 영역의 경계를 포함하려면 앞의 hitTest 콜백에서 객체를 찾았을 때의 반환문을 다음과 같이 바꾸세요.

return {
  id: "revenue-this-quarter",
  name: "Revenue this quarter",
  role: "chart-bar",
  metadata: { metricId: "quarterly-revenue" },
  rect,
};

선택 사항인 name은 사용자에게 표시되며, role은 객체의 역할을 간략히 설명합니다. 메타데이터는 화면에 보이지 않는 컨텍스트를 추가할 때만 포함하세요. 선택 사항인 rect는 페이지의 보이는 영역을 기준으로 CSS 픽셀 좌표를 사용하며, 이는 clientX 및 clientY의 좌표계와 일치합니다. 앞의 객체 선택 예제에서는 이미 이 사각형을 계산합니다. 장면 좌표를 변환할 때는 배율, 화면 이동, 확대·축소를 반영하세요.

선택 상태 표시하기(선택 사항)

선택 사항인 renderSelection 콜백을 사용하면 마우스 포인터가 객체 위에 있거나 객체가 선택되었을 때 원하는 방식으로 상태를 표시할 수 있습니다. 이 콜백에 선택 사항인 컨텍스트 필드는 필요하지 않습니다.

위 캔버스에 다음 함수를 정의하고 renderSelection: renderRevenueSelection을 registerSurface() 옵션에 추가하세요.

/** @param {{ hoveredId: string | null; selectedId: string | null }} selection */
function renderRevenueSelection({ hoveredId, selectedId }) {
  context.clearRect(0, 0, canvas.width, canvas.height);
  context.fillStyle = "#2563eb";
  context.fillRect(bar.x, bar.y, bar.width, bar.height);

  if (
    hoveredId === "revenue-this-quarter" ||
    selectedId === "revenue-this-quarter"
  ) {
    context.strokeStyle = "#111827";
    context.lineWidth = 3;
    context.strokeRect(bar.x, bar.y, bar.width, bar.height);
  }
}

마우스 포인터가 막대 위에 있거나 막대가 선택된 동안에는 이 콜백이 막대에 윤곽선을 그립니다. 두 ID가 모두 null이면 윤곽선 없이 막대를 다시 그립니다.

캔버스 객체에 컨트롤 추가하기

서피스의 DOM 요소에 사용자 지정 컨트롤을 등록하세요. 캔버스에 그린 객체를 API에서는 가상 객체라고 합니다. 가상 객체에는 사용자 지정 컨트롤이 사용되며 기본 제공 CSS 및 텍스트 컨트롤은 적용되지 않습니다.

객체가 여러 개라면 renderSelection을 사용해 selectedId가 바뀔 때 호스트의 컨트롤을 업데이트하세요. 업데이트는 브라우저가 주석 상태를 기록하기 전에 이루어져야 합니다. 컨트롤 이벤트에는 detail.virtualTarget: { surfaceId, targetId }가 포함됩니다. 이벤트를 처리할 객체는 현재 선택 상태 대신 이벤트에 기록된 식별 정보와 callback으로 결정하세요. targetId는 반환 객체의 id와 일치합니다. 이 객체는 hitTest가 반환하며, surfaceId는 브라우저에 등록된 서피스를 식별합니다. 일반 DOM 이벤트에는 virtualTarget이 포함되지 않습니다.

다른 객체를 선택한 뒤에도 미리보기, 비교, 초기화 이벤트에는 원래 객체의 식별 정보가 유지됩니다. 저장된 주석을 다시 열 때는 히트 테스트를 다시 실행하지 않으므로 기록된 객체나 메타데이터가 바뀌지 않습니다.

주석 모드 제어하기

toggle()로 모드 전환을 요청하고 isActive()로 실제 적용된 상태를 읽으세요. 문서의 oaiannotationmodechange 이벤트를 사용하면 UI를 현재 모드와 동기화할 수 있습니다. 이전 버전이나 API를 지원하지 않는 브라우저에 대비해 두 메서드의 지원 여부를 확인하세요. 다음 버튼을 추가하고 버튼이 생성된 뒤 스크립트를 실행하세요.

<button id="toggle-annotations" type="button" hidden>
  Enter annotation mode
</button>
const button = /** @type {HTMLButtonElement} */ (
  document.getElementById("toggle-annotations")
);
const annotation = document.oai?.annotation;

/** @param {boolean} active */
function renderMode(active) {
  button.textContent = active
    ? "Exit annotation mode"
    : "Enter annotation mode";
}

/** @param {CustomEvent<{ active: boolean }>} event */
const onModeChange = (event) => renderMode(event.detail.active);
const onClick = () => annotation?.toggle(!annotation.isActive());

if (
  typeof annotation?.toggle === "function" &&
  typeof annotation?.isActive === "function"
) {
  document.addEventListener("oaiannotationmodechange", onModeChange);
  button.addEventListener("click", onClick);
  renderMode(annotation.isActive());
  button.hidden = false;
}

function cleanupAnnotationButton() {
  document.removeEventListener("oaiannotationmodechange", onModeChange);
  button.removeEventListener("click", onClick);
  button.hidden = true;
}

toggle()는 현재 모드를 반전합니다. 모드를 켜려면 true를, 끄려면 false를 전달하세요. 같은 불리언 값으로 반복해서 요청해도 결과는 같습니다. true를 전달하면 활성 상태인 편집기나 대기 중인 활성화 요청을 유지합니다. false를 전달하면 대기 중인 활성화를 취소하고 일반 종료 플로우를 따릅니다.

사용자 상호작용이 활성 상태일 때 toggle()를 호출하세요. 동기적으로 반환되는 { accepted }는 요청 수락 여부를 나타내며, 실제로 모드가 전환되었는지는 보장하지 않습니다. 브라우저가 전환 조건을 검사한 결과에 따라 모드가 바뀌지 않을 수도 있습니다. 예제처럼 isActive()와 이벤트에서 실제 상태를 읽으세요.

isActive()는 사용자 제스처 없이 호출할 수 있습니다. 브라우저는 상태를 갱신한 뒤 oaiannotationmodechange 이벤트를 전달합니다. 이 이벤트의 event.detail.active는 불리언입니다. 브라우저는 초기 이벤트를 보내지 않으며 현재 상태와 같은 값을 명시적으로 요청한 경우에도 중복 이벤트를 보내지 않습니다. 따라서 게터로 상태를 읽어 UI를 초기화하세요. 주석 모드가 활성화된 상태에서 접근 권한이 취소되면 마지막 이벤트에 active: false가 전달되며, 보관해 둔 게터는 false를 반환합니다.

주석 모드는 페이지 클릭을 가로챕니다. 사이트의 종료 버튼을 비롯한 페이지 컨트롤을 사용하려면 스페이스바를 누른 채 클릭하세요. 브라우저의 주석 UI에서도 모드를 종료할 수 있습니다. 스페이스바만 누르고 있으면 isActive()는 계속 참을 반환합니다. 이 API는 oai-annotation-ignore를 비롯해 클릭을 그대로 통과시키는 속성을 지원하지 않습니다.

주석 모드를 종료하면 편집기가 닫힙니다. 저장한 주석은 유지되며 전송되거나 Composer로 이동하지 않습니다. 컴포넌트를 제거할 때는 cleanupAnnotationButton()을 호출하세요. 미리 보관한 API 참조를 사용하므로 네임스페이스가 사라진 뒤에도 정리 함수에서 리스너를 제거할 수 있습니다.

연동 기능 테스트하기

데스크톱 앱의 앱 내 브라우저에서 웹사이트를 열고 추가한 기능을 테스트하세요.

  1. 주석 모드로 전환해 객체와 텍스트를 선택하세요. 강조 표시, 이름, 범위, 메타데이터가 의도한 대상과 일치하는지 확인하세요.
  2. 사이트 버튼으로 주석을 여세요. 선택한 요소나 텍스트 범위, 초기 의견, 편집기 모드를 확인하세요.
  3. 사용자 지정 컨트롤의 값을 변경해 원래 상태와 비교한 뒤 미리보기를 지우세요. 애플리케이션이 원래 상태를 복원하는지 확인하세요.
  4. 캔버스에서는 빈 공간 선택, 크기 조정, 장면 변경을 각각 테스트하세요. 다른 객체를 선택한 뒤에도 컨트롤 이벤트가 주석에 기록된 대상을 업데이트하는지 확인하세요. 비동기 히트 테스트를 취소한 뒤에도 객체를 계속 선택할 수 있는지 확인하세요.
  5. 주석을 저장한 뒤 Composer의 첨부 파일 미리보기에서 다시 열어 편집하세요. 기존 컨트롤과 대상이 유지되는지, 주석을 제거하면 미리보기도 모두 지워지는지 확인하세요.
  6. 주석을 메시지와 함께 보내세요. ChatGPT가 선택한 콘텐츠, 메타데이터, 요청한 값을 받는지 확인하세요. 여기에는 선택형 컨트롤에 표시된 레이블과 currentValue를 생략한 컨트롤에서 변경하지 않은 선택 항목도 포함됩니다.
  7. API가 없는 브라우저에서 사이트를 열어 일반적인 상호작용이 정상적으로 작동하는지 확인하세요.

ChatGPT가 웹사이트에서 수행할 수 있는 작업을 제공하려면 사이트 도구(WebMCP)를 추가하세요. 주석을 사용하면 사용자가 선택한 대상과 피드백이 대화에 포함됩니다. 사이트 도구를 사용하면 에이전트가 이 컨텍스트를 바탕으로 애플리케이션의 기존 기능을 실행할 수 있습니다.