
Google이 발표한 새로운 Web 표준 초안――WebMCP. 실제로는 어떨까? IO Extended에서 구현 및 발표하며 알게 된 점
요약
Google이 제안한 새로운 Web 표준 초안인 WebMCP의 개념과 구현 방식을 소개합니다. WebMCP는 웹 페이지의 기능을 AI 에이전트가 사용할 수 있는 도구(tool)로 선언하고 연결하는 메커니즘을 다룹니다.
핵심 포인트
- WebMCP는 웹 페이지 기능을 AI 에이전트용 도구로 선언하는 W3C 드래프트 사양임
- JavaScript를 이용한 명령형 등록과 HTML 폼을 이용한 선언형 등록 방식 지원
- 기존 MCP(Model Context Protocol)와 유사한 인터페이스를 웹 환경으로 확장
- 선언형 방식 사용 시 HTML 요소로부터 JSON Schema가 자동으로 구성됨
안녕하세요.
평소에는 tanahiro2010이라는 이름으로 활동하고 있습니다. GDG Greater Kwansai에 소속되어 있습니다.
Google I/O Extended Osaka 2026의 핸즈온 「WebMCP를 만들어서 AI 에이전트로부터 호출해 보자!」에서 전반부 발표를 담당했습니다.
이것은 핸즈온 내용을 Qiita용으로 정리한 것입니다.
사용한 코드랩은 여기입니다.
대상은 MCP는 알고 있지만 WebMCP는 들어본 적이 없는 사람.
그리고 "Google이 내놓은 새로운 사양, 실제로는 어떨까?"를 알고 싶은 사람입니다.
WebMCP를 알고 계신가요?
갑작스럽지만, WebMCP를 알고 계신가요?
저는 핸즈온 자료를 만들기 전까지 존재조차 몰랐습니다.
이름을 보고 MCP의 Web 버전일 것이라고 멋대로 생각했습니다.
실제로 사양을 읽고 구현해 보니, 생각했던 것보다 훨씬 개성이 강한 사양이었습니다.
이 기사에서는 그 내용과 실제로 직접 움직여보며 알게 된 점을 써 내려가겠습니다.
애초에 MCP란 무엇인가?
WebMCP 이야기를 하기 전에, MCP(Model Context Protocol)를 복습해 두겠습니다.
MCP는 AI Agent에 외부 도구(tool)를 연결하기 위한 공통 인터페이스입니다.
흐름은 다음과 같습니다.
포인트는, Agent가 구동되는 동안 계속 tool이 등록된 상태로 유지된다는 점입니다.
로컬 파일 조작이든, 사내 API를 호출하는 도구든, Agent를 종료하지 않는 한 계속 호출할 수 있습니다.
이번에 만든 webmcp-bridge-mcp도 stdio로 대기하는 일반적인 MCP Server입니다.
Antigravity CLI와 같은 클라이언트 입장에서 보면 "흔히 볼 수 있는 MCP Server 중 하나"일 뿐입니다.
특수한 것은 이 다음에 나오는 WebMCP 측입니다.
WebMCP란 무엇인가
WebMCP는 W3C Web Machine Learning Community Group이 공개하고 있는 드래프트(draft) 사양입니다.
2026년 2월 드래프트로, 아직 제안 단계인 사양입니다.
한마디로 말하자면 다음과 같습니다.
Web 페이지의 기능을 Agent용 tool로서 페이지 스스로가 선언하는 메커니즘
등록 방법은 두 가지가 있습니다.
명령형 (Imperative)
JavaScript에서 직접 tool을 등록하는 방식입니다.
await document.modelContext.registerTool({
name: "reserve_hotel",
description: "Reserve a hotel",
...
name / description / inputSchema / execute라는 형태는 MCP의 tool 정의와 거의 동일합니다.
MCP를 다뤄본 적이 있는 사람이라면 보는 순간 "아, 저것과 같은 형태구나"라고 느낄 것입니다.
선언형 (Declarative)
기존의 <form>에 속성을 추가하는 것만으로 tool화하는 방식입니다.
<form toolname="search_hotels" tooldescription="Search hotels">
<input name="city" toolparamdescription="City to search hotels in" required />
<button type="submit">Search</button>
...
toolname / tooldescription을 가진 폼이 발견되면, <input>의 name이나 required, toolparamdescription(없을 경우 관련 <label>의 텍스트)으로부터 JSON Schema가 자동으로 구성됩니다.
재미있는 점은 여기서부터인데, 선언형은 내부적으로 명령형과 동일한 registerTool()로 정규화됩니다.
API를 두 개 별도로 가지는 것이 아니라, 선언형은 명령형의 신택스 슈거(syntactic sugar)로서 구현되어 있다는 구조입니다.
개인적으로 이 설계가 솔직히 마음에 듭니다.
전송 결과는 사양대로 SubmitEvent#respondWith()로 받습니다.
form.addEventListener("submit", (event) => {
event.preventDefault();
if (event.agentInvoked) {
...
event.agentInvoked를 통해 "인간이 클릭했는지, Agent가 전송했는지"를 판정할 수 있습니다.
사소해 보이지만, 이 부분이 나중에 큰 차이를 만듭니다.
MCP와 WebMCP, 이름은 비슷하지만
이름도 API 형태도 비슷해서, 처음에는 "MCP의 Web 버전이겠구나" 정도의 인식이었습니다.
하지만 실제로 읽어 내려가 보니, 세 가지 축에서 성격이 확연히 달랐습니다.
| 항목 | MCP | WebMCP |
|---|---|---|
| 대상 | AI Agent 전반 | 사양상 브라우저 내장 Agent 중심 |
| ... |
체감상 가장 크게 다가오는 것은 세션 기간의 차이입니다.
MCP의 tool은 Agent가 실행 중이라면 계속 호출할 수 있습니다.
하지만 WebMCP의 tool은 "해당 페이지가 지금 열려 있는가"에 완전히 종속됩니다.
탭을 전환하는 순간 해당 tool은 보이지 않게 되고, 다시 돌아오면 다시 보이게 됩니다.
"탭의 생존 기간 = tool의 생존 기간"이라는 발상은 MCP만 알고 있는 상태에서 읽었을 때 상당히 신선했습니다.
2026년 8월 시점에서 어디까지 작동하는가
집필 시점(2026년 8월) 기준으로 WebMCP는 아직 제안 단계의 드래프트(Draft)입니다.
Chrome Docs에서도 "향후 기능"으로 소개되고 있는 상태였습니다.
- Origin Trial은 Chrome 149부터 참여 가능
- 로컬에서 테스트만 하려면
chrome://flags/#enable-webmcp-testing사용 - API는 향후 변경될 가능성이 있다고 명시되어 있음
즉, "핸즈온(Hands-on) 회장의 모든 참가자 브라우저에서 네이티브 구현이 활성화되어 있다"는 전제를 세울 수 없습니다.
여기서 이번에 만든 것이 등장합니다.
만든 것――WebMCP Bridge
핸즈온의 요구사항과 WebMCP의 사양 사이에는 그대로 따라 했을 때 간극이 있었습니다.
- 사양의 가정: WebMCP는 브라우저 내장 Agent를 대상
- 핸즈온의 현실: 참가자는 Antigravity CLI와 같은 기존 MCP 대응 Agent로부터 호출하고 싶어 함
그렇다면, 다리 역할을 하는 것을 만들자.
그렇게 생각하여 Chrome Extension과 MCP Server를 세트로 직접 제작했습니다.
- MCP Server: https://github.com/tanahiro2010/webmcp-bridge-mcp
- Chrome Extension: https://github.com/tanahiro2010/webmcp-bridge-extension
여기서 한 가지 고집한 부분이 있습니다.
WebMCP 사양이 정의하는 API (document.modelContext / annotated <form>)는 있는 그대로 탐지하고 실행만 하도록 한다.
독자적인 프로토콜로 사양을 덮어쓰는 방식은 취하지 않았습니다.
이유는 단순합니다. 핸즈온에서 배우는 사양과 실제로 동작하는 것의 사양이 어긋나 있다면, 애초에 핸즈온을 진행할 의미가 없다고 생각했기 때문입니다.
Bridge의 전체 구조
구성 방식은 다음과 같습니다.
MCP Server(webmcp-bridge-mcp)는 스스로 DOM에 전혀 관여하지 않습니다.
철저히 Extension 사이의 Bridge / Registry / Router 역할에 집중하며, DOM 조작은 전부 Extension 측으로 넘깁니다.
사소한 역할 분담이지만, 책임이 명확하여 구현할 때 망설임이 적었습니다.
WebSocket은 ws://127.0.0.1:58787에 바인드(bind)되어 있습니다.
8787이 아닌 58787을 사용한 이유는 wrangler dev (Cloudflare Workers)의 기본 포트와 충돌했기 때문입니다.
Workers 개발을 병행하고 있으면, Extension이 이쪽이 아닌 wrangler 측으로 접속을 시도하여 계속 연결되지 않는 사소한 버그를 먼저 겪었습니다.
Extension 측은 MV3(Manifest V3)로, 2층 구조로 되어 있습니다.
main world의 injected.ts가 필요한 이유는, 페이지의 document.modelContext에 접근하려면 main world에서 실행되어야 하기 때문입니다.
isolated world(일반적인 content script)에서는 직접 접근할 수 없습니다.
document.modelContext가 아직 브라우저에 네이티브로 구현되어 있지 않은 경우에는, injected.ts가 registerTool / getTools
/ executeTool
/ toolchange
이벤트의 최소한의 폴리필 (polyfill)을 제공합니다.
네이티브 구현이 있는 경우에는 아무것도 하지 않습니다.
"네이티브 구현이 있다면 조용히 양보한다"라는 설계로 해두면, 네이티브 구현이 롤아웃 (rollout) 되어도 코드를 크게 바꾸지 않아도 될 것입니다.
Agent에서 보이는 tool
Agent에서 보이는 tool은 다음 6가지입니다.
| tool | 설명 |
|---|---|
webmcp_get_status | Extension의 접속 상태, 알려진 탭 수, 활성 탭 ID를 반환 |
webmcp_list_tabs | Extension이 잡고 있는 WebMCP 대응 탭 목록을 반환 |
webmcp_discover_tools | 지정된 탭의 WebMCP tool을 검출 |
webmcp_call_tool | 지정된 탭의 tool을 실행 |
webmcp_submit_tool | 인간의 전송 대기를 Agent 측에서 확정 |
webmcp_ping | Extension과의 통신 확인 |
입출력은 다음과 같습니다.
// webmcp_discover_tools 의 input
{ "tabId": 123, "forceRefresh": true }
// output
...
// webmcp_call_tool 의 input
{ "toolId": "reserve_hotel", "args": { "city": "Osaka" } }
// output
...
조금 특이한 것이 webmcp_submit_tool입니다.
toolautosubmit이 없는 선언형 폼 (declarative form)은, 사양상 "전송 버튼에 포커스하는 것만으로 멈추고, 인간이 내용을 확인하여 수동으로 전송한다"는 것을 전제로 하고 있습니다.
이는 WebMCP 사양이 의도하고 있는 안전 장치입니다.
webmcp_submit_tool은 이것을 Agent 측에서 명시적으로 덮어쓰기 위한 tool입니다.
사양이 의도하는 인간의 확인을 우회하는 것이라고 이해한 상태에서 사용해 주길 바란다는 주의 사항을 README에도 적어 두었습니다.
실기에서 돌려봤더니, 지뢰투성이였다
여기서부터가 이 기사에서 가장 쓰고 싶었던 부분입니다.
문서를 읽는 것만으로는 눈치채지 못했던 것들이, Chrome for Testing에서 실제로 구동해 보니 하나둘씩 튀어나왔습니다.
getTools()는 사실 비동기였다
Chrome 개발자 문서의 요약을 읽었을 때의 인상으로는 동기 함수처럼 보였습니다.
하지만 실기(Chrome for Testing 150)에서 확인해 보니, document.modelContext.getTools()는 Promise<ModelContextTool[]>를 반환했습니다.
게다가 executeTool()도 tool 이름인 문자열이 아니라, getTools()로 얻은 tool 객체 그 자체를 요구합니다.
문자열을 전달하면 TypeError가 발생합니다.
문서 요약만 보고 구현했다면 평범하게 빠질 수 있는 함정이었습니다.
file://에서는 왜인지 핸드셰이크 (handshake)가 영원히 끝나지 않는다
content.ts와 injected.ts 사이의 postMessage에서는 targetOrigin에 window.location.origin을 사용하고 있었습니다.
file://로 연 페이지에서는 왜인지 이것이 문자열 `
그 경우, 이 Extension 자체의 registerTool() 호출은 '중복'으로 간주되어 실패하지만, 이는 정상적인 흐름(normal case)으로 처리하고 있습니다.
findAnnotatedFormByName()을 통해 DOM을 직접 확인하고 source: "declarative"라고 판정하기 때문에, 어느 쪽이 등록했는지와 관계없이 올바르게 보고할 수 있습니다.
네이티브로 합성된 inputSchema가 이 시점의 빌드에서는 빈 값({ type: "object", properties: {} })을 반환하는 경우도 있었습니다.
이는 아마도 브라우저 측의 구현 상황에 따른 것이며, Extension의 버그는 아닙니다.
tool의 실행 결과가 왜인지 문자열로 반환되는 문제
WebMCP 사양의 execute는 본래 '에이전트용 문자열 요약(string summary)'을 반환하도록 상정되어 있습니다.
그렇기 때문에 페이지 측에서 { ok: true, city }와 같은 객체를 반환하더라도, 브라우저의 네이티브 구현이 이를 JSON 문자열로 변환하여 반환하는 것을 실기(real device)에서 확인했습니다.
Extension과 MCP Server 모두 result를 일절 가공하지 않고 그대로 통과(pass-through)시킵니다.
따라서 MCP 클라이언트 측에서 '문자열인지 구조화된 데이터(structured data)인지'를 판정해야 합니다.
Service Worker가 멋대로 잠드는 문제
MV3의 Service Worker는 유휴(idle) 상태가 되면 서스펜드(suspend)됩니다.
그동안 WebSocket 연결도 끊깁니다.
chrome.tabs 이벤트 등이 발생하면 자동으로 깨어나며 재연결 로직이 작동합니다.
하지만 '기동 직후에 아무런 탭 조작이 없는' 상태가 지속되면, 한동안 extensionConnected: false 상태로 남아 있는 경우가 있었습니다.
webmcp_get_status / webmcp_ping이 false를 반환하는 경우에는 대상 탭을 무언가 조작(전환, 새로고침 등)하면 복구됩니다.
핸드셰이크(Handshake)보다 먼저 tool 등록이 끝나서 오버레이가 영원히 나오지 않는 문제
선언형 전용(declarative-only, 명령형 JS를 전혀 포함하지 않는) 페이지에서, content.ts와의 핸드셰이크가 끝나기 전에 tool 등록이 완료되어 버려 오버레이가 영원히 나타나지 않는 경합 상태(race condition)가 있었습니다.
injected.ts는 페이지 내의 <form toolname tooldescription>을 MutationObserver로 발견하는 즉시 바로 등록합니다.
따라서 명령형 스크립트 실행을 거치지 않는 가벼운 선언형 페이지일수록 이 경합이 일어나기 쉽습니다.
이전 구현은 'manifest를 보냈다/보내려고 했다'는 사실을 핸드셰이크 완료 전에 기록해 버리고 있었습니다.
그 결과, 핸드셰이크가 완료된 후의 재전송 요청을 '차이 없음(no difference)'으로 오판하여 묵묵히 무시(swallow)해 버렸습니다.
reportManifestIfChanged()를 '채널이 확립될 때까지는 기록도 전송도 일절 수행하지 않도록' 수정했습니다.
의도적으로 나쁜 순서(tool 등록 → 지연된 핸드셰이크)를 재현하는 테스트를 작성하여, 수정 전에는 재현되고 수정 후에는 해소됨을 확인했습니다.
버그 그 자체보다, '일부러 나쁜 순서를 재현하는 테스트를 작성하여 고쳤음을 증명하는' 방식이 은근히 가장 효과적이었다고 느낀 포인트였습니다.
사람이 클릭할 때까지 영원히 반환되지 않는 tool
사양대로 toolautosubmit이 없는 폼은 전송 버튼에 포커스를 맞추는 단계에서 멈춥니다.
사람이 내용을 확인하고 수동으로 전송할 것을 상정하고 있습니다.
적어도 일부 네이티브 구현은 이 '수동 대기' 시간 동안 executeTool() 자체를 블록(block)한 상태로 유지합니다.
이 Extension의 polyfill처럼 즉시 { pending: true, ... }를 반환하는 동작과는 다릅니다.
사람이 조작하지 않는 자동화 환경에서 toolautosubmit이 없는 tool을 webmcp_call_tool 하면, 타임아웃이 발생할 때까지 응답이 돌아오지 않습니다.
호출하기 전에 webmcp_discover_tools의 결과에서 requiresUserGesture: true로 되어 있지 않은지 확인할 필요가 있습니다.
테스트는 브라우저 없음과 브라우저 있음의 2단계 구성
검증한 시나리오는 다음 4가지 패턴입니다.
-
명령형 전용 페이지 (
<form> -
선언형 전용 페이지 (
<form>호출 없음) - 선언형 전용 페이지 (registerTool()호출 없음) - 명령형/선언형이 혼재된 페이지 -
특정 tool의 실행이 다른 tool의 유무를 변화시키는 패턴 (
unlock을 호출하면 새로운 tool이 동적으로 나타나고,lock으로 사라짐)
Chrome for Testing 위에서 실제로 Extension을 로드하여 모든 패턴이 PASS됨을 확인했습니다.
네 번째 패턴은 toolchange 이벤트와 MutationObserver를 통한 동적 감지 확인도 겸하고 있습니다.
webmcp_discover_tools는 기본적으로 캐시를 반환하므로, 이러한 동적 변화를 확인하려면 forceRefresh: true가 필요합니다.
Chrome for Testing(Playwright에 포함된 Chromium)을 사용한 이유는, Chrome 137 이후 정식 빌드의 Google Chrome이 자동화 목적의 --load-extension 플래그를 폐지했기 때문입니다.
수동으로 chrome://extensions에서 로드하는 경우에는 일반 Chrome을 사용해도 문제없습니다.
하지만 자동 테스트에서 확장 기능을 로드하고 싶다면 Chrome for Testing이나 Chromium이 필요합니다.
MCP Server 측은 FakeExtension이라는 모의(Mock) 클래스를 사용하고 있습니다.
Extension과 동일한 WebSocket 프로토콜로 통신하는 모의 객체로, 브라우저 없이도 연결 상태, discover_tools의 캐시/forceRefresh, tool 호출의 병렬 실행, 연결 해제 시의 클린업(Cleanup)까지 검증할 수 있습니다.
브라우저를 실행하지 않는 만큼 빠르게 돌릴 수 있으므로, 브릿지(Bridge) 로직은 이쪽에서 담당하고, 실제 DOM 조작을 동반하는 검증은 Extension 측의 Playwright 테스트에서 담당하도록 역할을 분담했습니다.
보안 관련 주의사항 (프로토타입 전제)
- main world (
injected.ts)와 isolated world (content.ts) 사이의postMessage에는 페이지 로드 시마다 발행하는 랜덤한 channel id를 단 한 번의 핸드셰이크(Handshake)로 공유하며, 이후 모든 메시지에 이를 부착합니다. 이는 무관한 페이지 스크립트가 가짜 manifest나 실행 결과를 보낼 수 없도록 하기 위함입니다. - MCP Server 측의 WebSocket 서버는
127.0.0.1에만 바인드(Bind)되므로, 동일 머신 이외에서는 접속할 수 없습니다.
개인 이용 및 프로토타입 용도를 전제로 하고 있어, 토큰 인증 등의 추가적인 인가(Authorization)는 수행하지 않습니다.
"핸즈온(Hands-on)으로 구동하기에는 충분하다"는 수준에서 멈추었으며, 실제 운영을 상정한 인가 설계는 하지 않았습니다.
그럼에도 불구하고, 장점은 확실히 있다
억지로 만들어낸 측면이 있다고는 해도, 실제로 만져보며 느낀 장점은 있습니다.
- 추측이 줄어듦: Agent가 화면의 버튼이나 입력창의 의미를 상상할 필요가 없습니다. 사이트 측에서 tool로서 의도를 선언할 수 있습니다.
- 실행 시간이 단축됨: 클릭이나 입력을 여러 단계로 재현할 필요 없이, tool call로서 직접 처리할 수 있습니다.
- 눈으로 확인 가능: 브라우저 위에서 동작하므로, 결과를 사람이 그 자리에서 바로 볼 수 있습니다.
하지만 개인적으로는, 필요 없다고 생각했다
여기서부터는 완전히 개인적인 감상입니다.
솔직히 말해서, 구현하여 핸즈온으로 사용해 보며 가장 강하게 든 생각은 "솔직히, 필요 없지 않나?"였습니다.
이유는 몇 가지 있습니다.
우선, 브라우저 내장 Agent를 사용할 동기가 아직 약합니다.
정확히 말하면 저 자신도 Chrome에 내장 Agent가 있다는 사실 자체를 이번 검증을 하기 전까지는 몰랐습니다.
아마 비슷한 분들이 꽤 많을 것입니다.
그리고 WebMCP는 페이지를 열고 있는 동안에만 사용할 수 있다는 제약이 있습니다.
이 점이 은근히 답답합니다.
탭을 전환하는 순간 tool이 사라지는 것은, 익숙한 MCP의 감각으로 볼 때 조금 불편했습니다.
실제로 사용해 보니 Agent를 통한 브라우저 조작과 WebMCP를 통한 조작 사이에서, 체감되는 토큰 사용량이나 실행 속도에 그리 큰 차이를 느끼지 못했습니다.
Claude Code나 Codex와 같은 기존 Agent를 항상 띄워두는 사람들에게는, 굳이 브라우저 내장 Agent를 열어가며 사용할 매력이 적다고 느꼈습니다.
애초에 초보자의 시선입니다만, 타겟 선정(Targeting)이 약간 어긋난 것이 아닌가 하는 생각도 들었습니다.
「Agent로부터의 오작동이 줄어든다」는 이점 자체는 매력적입니다.
하지만 그것을 실현하는 장소가 「브라우저 내장 Agent (Browser-built-in Agent)」라는 그리 알려지지 않은 존재로 한정되어 있다는 점이 솔직히 아쉬운 부분입니다.
다만, 꽂히는 사람에게는 확실히 꽂힌다
그렇다고는 해도, 브라우저 내장 Agent를 일상적으로 활용하고 있는 계층에게는 「추측이 줄어든다」, 「실행 시간이 짧다」는 두 가지 점이 분명 강력하게 작용할 것입니다.
제가 이번에 그만큼 매력을 느끼지 못했던 것은, 단순히 저의 사용 방식이 브라우저 내장 Agent를 전제로 하고 있지 않았기 때문이라고 생각합니다.
그럼에도 이 기사를 공개한 이유
지금까지 이렇게 냉정한 글을 써놓고는 왜 이런 내용으로 공개했는지 의문이 드실 수도 있습니다.
이유는 단순합니다.
모처럼 Extension과 MCP Server를 만들었는데, 핸즈온(Hands-on) 당일에만 사용되고 끝나는 것은 너무 아깝다고 생각했기 때문입니다.
사양으로서의 WebMCP에 대해 회의적인 부분은 있으면서도, 직접 구현하고 손을 움직여 보았기에 깨달을 수 있었던 기술적 지견(비동기 API (Asynchronous API), Origin 문제, 경쟁 상태 (Race Condition) 해결 방법 등)은 그 자체만으로도 누군가에게 도움이 될 것이라고 믿습니다.
그리고, 모두가 Extension이나 MCP를 사용해 주신다면 저는 행복할 것 같습니다.
요약
WebMCP는 「웹 페이지 자체가 Agent를 위해 Tool을 선언한다」는 방향성 측면에서 흥미로운 사양입니다.
다만 2026년 8월 시점에서는 아직 초안(Draft) 단계이며, 타겟 또한 브라우저 내장 Agent로 한정되어 있습니다.
따라서 기존의 MCP 대응 Agent를 적극적으로 활용하고 있는 사람에게는, 현시점에서 그다지 매력적이지 않다는 것이 개인적인 결론입니다.
한편, 구현하면서 알게 된 비동기 API, Origin 문제, 경쟁 상태와 같은 난관들은 향후 네이티브 구현이 확산되는 과정에서도 참고가 될 것입니다.
만든 결과물과 링크는 다음과 같습니다.
- Codelab: https://learn.gdgs.jp/webmcp-agent/
- WebMCP Bridge MCP: https://github.com/tanahiro2010/webmcp-bridge-mcp
- WebMCP Bridge Extension: https://github.com/tanahiro2010/webmcp-bridge-extension
- connpass: https://gdgkwansai.connpass.com/event/391029
GitHub이나 질문 등 언제든 환영합니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기