> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aiderx.io/llms.txt
> Use this file to discover all available pages before exploring further.

# 웹 연동 튜토리얼

> 공통 HTML 템플릿으로 배너를 표시하고 클릭 전송과 노출·클릭 집계를 확인하는 선택 실습입니다.

[광고 송출 빠른 시작](https://docs.aiderx.io/development/ko/quick-start.md)에서 만든 배너를 웹페이지에 표시하는 **선택 실습**입니다. HTML 템플릿에 A2 서버 주소와 광고 단위 ID를 입력해 광고를 불러오고, 클릭을 전송한 뒤 Ad Manager에서 집계를 확인합니다.

## 1. 예제 템플릿 준비

빠른 시작에서 HTTP `200` 광고 응답을 받은 환경과 광고 단위 ID를 그대로 사용합니다. 웹 브라우저와 Python 3를 준비하세요. 광고 요청과 클릭 전송에는 액세스 토큰이 필요하지 않습니다.

먼저 예제 파일을 저장할 폴더를 만듭니다.

```bash theme={null}
mkdir -p ~/a2-banner-example
cd ~/a2-banner-example
```

아래 HTML 전체를 복사해 텍스트 편집기에서 **일반 텍스트**로 `~/a2-banner-example/banner-ad.html`에 저장합니다.

> [accordion] HTML 템플릿 보기 및 복사
>
> ```html banner-ad.html theme={null}
> <!doctype html>
> <html lang="en">
>   <head>
>     <meta charset="utf-8" />
>     <meta name="viewport" content="width=device-width, initial-scale=1" />
>     <title>A2 Banner Example</title>
>     <style>
>       * { box-sizing: border-box; }
>       body { margin: 0; background: #f6f8fa; color: #172b4d; font: 16px/1.6 system-ui, sans-serif; }
>       main { max-width: 820px; margin: 40px auto; padding: 28px; background: white; border: 1px solid #dce3eb; border-radius: 12px; }
>       h1 { margin: 0 0 8px; font-size: 28px; }
>       p { margin: 8px 0 20px; }
>       label { display: block; margin-top: 16px; font-weight: 600; }
>       input { display: block; width: 100%; margin-top: 6px; padding: 10px; font: inherit; border: 1px solid #a9b5c6; border-radius: 6px; }
>       button { margin-top: 20px; padding: 10px 22px; border: 0; border-radius: 6px; background: #175cd3; color: white; font: inherit; cursor: pointer; }
>       button:disabled { opacity: .6; cursor: wait; }
>       .hint { color: #52627a; font-size: 14px; }
>       #status { margin: 24px 0; padding: 16px; background: #f1f5fa; border-radius: 6px; }
>       #status p { margin: 0; overflow-wrap: anywhere; }
>       #slot img { display: block; max-width: 100%; height: auto; }
>       #slot a { display: inline-block; position: relative; }
>       .badge { position: absolute; top: 6px; right: 6px; padding: 0 5px; border-radius: 3px; background: #fff; color: #172b4d; font-size: 11px; }
>       details { margin-top: 24px; }
>       summary { cursor: pointer; font-weight: 600; }
>       pre { max-height: 380px; overflow: auto; padding: 16px; background: #10243a; color: #e8eef5; font-size: 13px; }
>       @media (max-width: 600px) { main { margin: 16px; padding: 20px; } }
>     </style>
>   </head>
>   <body>
>     <main>
>       <h1>A2 Banner Example</h1>
>       <p>Enter your A2 server URL and AdUnit ID to display a 300×250 banner.</p>
>       <form id="settings">
>         <label for="base-url">A2 server URL</label>
>         <input id="base-url" type="url" placeholder="&lt;A2_API_URL&gt;" required />
>         <label for="tagid">AdUnit ID</label>
>         <input id="tagid" placeholder="Paste the AdUnit ID from Ad Manager" required pattern="[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" />
>         <button id="load" type="submit">Load ad</button>
>       </form>
>       <p class="hint">Use On Ad Response tracking. Each successful ad response records an impression. Click the banner once to send a click event.</p>
>       <section id="status" aria-live="polite" aria-label="Request status">
>         <p id="request">Ad request: waiting</p>
>         <p id="display">Rendering: waiting</p>
>         <p id="event">Click delivery: waiting</p>
>       </section>
>       <div id="slot"></div>
>       <p class="hint">Verify collected impressions and clicks in Ad Manager's AdUnit Analytics.</p>
>       <details>
>         <summary>Response JSON</summary>
>         <pre id="response">No response yet.</pre>
>       </details>
>     </main>
>     <script>
>       const form = document.querySelector("#settings");
>       const load = document.querySelector("#load");
>       const requestStatus = document.querySelector("#request");
>       const displayStatus = document.querySelector("#display");
>       const eventStatus = document.querySelector("#event");
>       const slot = document.querySelector("#slot");
>       const responseText = document.querySelector("#response");
>
>       function httpUrl(value) {
>         const url = new URL(value);
>         if (!["http:", "https:"].includes(url.protocol)) {
>           throw new Error("Use an HTTP or HTTPS URL");
>         }
>         return url;
>       }
>
>       async function sendClick(baseUrl, adLog) {
>         eventStatus.textContent = "Click delivery: sending";
>         try {
>           const result = await fetch(`${baseUrl}/dmp/v0/log`, {
>             method: "POST",
>             headers: { "Content-Type": "application/json" },
>             body: JSON.stringify({ type: "ad_log", action: "clk", ad_log: adLog }),
>             keepalive: true,
>           });
>           if (!result.ok) throw new Error(`HTTP ${result.status}`);
>           eventStatus.textContent = `Click delivery: HTTP ${result.status} — check metrics`;
>         } catch (error) {
>           eventStatus.textContent = `Click delivery failed: ${error.message}. Check Network and Console before retrying.`;
>         }
>       }
>
>       form.addEventListener("submit", async (event) => {
>         event.preventDefault();
>         load.disabled = true;
>         requestStatus.textContent = "Ad request: loading";
>         displayStatus.textContent = "Rendering: waiting";
>         eventStatus.textContent = "Click delivery: waiting";
>         slot.replaceChildren();
>         responseText.textContent = "Waiting for response...";
>         try {
>           const baseUrl = httpUrl(document.querySelector("#base-url").value.trim()).href.replace(/\/+$/, "");
>           const tagid = document.querySelector("#tagid").value.trim();
>           const result = await fetch(`${baseUrl}/app/v0/ad/${encodeURIComponent(tagid)}`, {
>             method: "POST",
>             headers: { "Content-Type": "application/json" },
>             body: JSON.stringify({ user: { id: "quickstart-user-001" } }),
>           });
>           const text = await result.text();
>           responseText.textContent = text || "(Empty response body)";
>           if (result.status === 204) throw new Error("HTTP 204: no eligible ad. Check the AdUnit, allocation, and line item period.");
>           if (!result.ok) throw new Error(`HTTP ${result.status}`);
>           const response = JSON.parse(text);
>           responseText.textContent = JSON.stringify(response, null, 2);
>           const banner = response.ad?.display?.banner;
>           const adLog = response.ext?.ad_log;
>           if (!banner?.img || !banner.link?.url || !adLog) {
>             throw new Error("Missing banner image, landing URL, or ad_log. Use the Quickstart image banner.");
>           }
>           const imageUrl = httpUrl(banner.img);
>           const landingUrl = httpUrl(banner.link.url);
>           requestStatus.textContent = `Ad request: HTTP ${result.status}`;
>           const image = document.createElement("img");
>           image.width = response.ad.display.w || 300;
>           image.height = response.ad.display.h || 250;
>           image.alt = "A2 Quickstart sample ad";
>           image.onload = () => { displayStatus.textContent = "Rendering: complete"; };
>           image.onerror = () => { displayStatus.textContent = "Rendering: image failed to load. Check the image URL in Response JSON."; };
>           const link = document.createElement("a");
>           link.href = landingUrl.href;
>           link.target = "_blank";
>           link.rel = "noopener noreferrer";
>           link.addEventListener("click", () => { void sendClick(baseUrl, adLog); });
>           link.append(image);
>           if (banner.ext?.show_ad_badge) {
>             const badge = document.createElement("span");
>             badge.className = "badge";
>             badge.textContent = "Ad";
>             link.append(badge);
>           }
>           slot.replaceChildren(link);
>           image.src = imageUrl.href;
>         } catch (error) {
>           requestStatus.textContent = `Ad request failed: ${error.message}`;
>           if (error instanceof TypeError) {
>             requestStatus.textContent += ". Check the server URL, HTTPS, and CORS in the browser's Network and Console tabs.";
>           }
>         } finally {
>           load.disabled = false;
>         }
>       });
>     </script>
>   </body>
> </html>
> ```

파일을 저장한 `~/a2-banner-example` 폴더에서 아래 명령으로 실행합니다. A2 서버와 포트가 겹치지 않도록 `9000`을 사용합니다.

```bash theme={null}
python3 -m http.server 9000 --bind 127.0.0.1
```

[http://localhost:9000/banner-ad.html](http://localhost:9000/banner-ad.html)을 엽니다.

## 2. 광고 요청과 화면 표시

템플릿의 입력란을 채우고 **Load ad**를 한 번 누릅니다. HTML 소스를 수정할 필요는 없습니다.

| 입력란 | 입력할 값 |
| - | - |
| A2 server URL | 빠른 시작에서 `<A2_API_URL>`에 사용한 API 기본 주소를 입력합니다. |
| AdUnit ID | `Quickstart 배너`의 광고 단위 ID입니다. 코드나 광고 항목 ID를 넣지 않습니다. |

`Ad request: HTTP 200`, `Rendering: complete`와 A2 배너가 보이면 화면 표시까지 확인한 것입니다. **Response JSON**을 펼치면 이번 요청의 전체 응답을 확인할 수 있습니다.

이 페이지는 새로운 광고를 요청하므로 클릭에는 **화면에 표시한 광고 응답**의 `ext.ad_log`를 사용합니다. 템플릿이 이를 보관해 자동으로 전송합니다. HTTP `204`이면 [광고 송출 빠른 시작의 확인 순서](https://docs.aiderx.io/development/ko/quick-start.md#광고가-반환되지-않을-때)를 참고하세요.

> [!NOTE]
>
> 이 예제는 빠른 시작에서 설정한 **광고 응답 기준** 노출 추적을 사용합니다. 배너 응답을 받으면 노출이 한 건 더 기록되며, `imp` 이벤트를 별도로 전송하지 않습니다.

## 3. 클릭 이벤트 전송

화면에 표시한 배너를 **한 번** 클릭합니다. 새 탭에 `example.com`이 열리고 실습 페이지에 `Click delivery: HTTP 200 — check metrics`가 표시되는지 확인합니다.

![서버 주소와 광고 단위 ID를 입력한 뒤 HTTP 200 응답과 배너 표시를 확인한 웹 연동 예제](https://mintcdn.com/aiderx/4YOyAVOLclNxMCjG/images/development/web-example.jpg?fit=max\&auto=format\&n=4YOyAVOLclNxMCjG\&q=85\&s=fe978d26bc27d86a9ed95e7ab8ef91b0)

공통 템플릿에서 서버 주소와 광고 단위 ID를 입력해 배너 표시와 클릭 전송을 확인한 화면입니다.

예제의 클릭 핸들러는 해당 광고의 `ad_log`로 다음 본문을 `POST /dmp/v0/log`에 보냅니다. 이미 페이지에서 보냈으므로 터미널에서 다시 보내지 않습니다.

```json 클릭 시 전송하는 본문 theme={null}
{
  "type": "ad_log",
  "action": "clk",
  "ad_log": "<화면에 표시한 광고의 ext.ad_log>"
}
```

서비스에서 클릭을 전송하는 방법은 [광고 이벤트 추적](https://docs.aiderx.io/development/ko/ad-events-and-logs.md)에서 다룹니다. HTTP `200`은 수집·집계 완료를 보장하지 않으므로 다음 단계의 지표까지 확인합니다.

## 4. 노출·클릭 기록 확인

**1. 광고 단위 분석 열기**

Ad Manager의 **분석 > 광고 인벤토리 분석 > 광고 단위 분석**으로 이동합니다.

**2. 실습 광고 단위와 기간 선택**

검색창에 복사한 광고 단위 ID를 넣어 `Quickstart 배너`를 찾습니다. 조회 기간에 실습 날짜를 포함합니다. 콘솔과 비교할 때 날짜는 `Asia/Seoul` 기준으로 맞춥니다.

**3. 기대 횟수와 대조**

아래 표와 노출·클릭 수를 비교합니다. 집계 반영에는 지연이 있을 수 있으므로 나중에 페이지를 다시 불러와 확인합니다. 지표 확인을 위해 광고를 다시 요청하거나 클릭 이벤트를 재전송하지 않습니다.

![광고 단위 분석 상세 화면의 조회 기간과 노출·클릭 수](https://mintcdn.com/aiderx/4YOyAVOLclNxMCjG/images/development/ko/ad-unit-analytics-detail.png?fit=max\&auto=format\&n=4YOyAVOLclNxMCjG\&q=85\&s=7b3b78acf46fd8c5c8d6581125a9756d)

광고 단위 분석 상세의 기본 분석 화면입니다. 조회 기간과 노출·클릭 수를 확인하세요. 수치는 예시입니다.

새로 만든 광고 단위에 이 실습의 요청만 보냈다면 기대값은 다음과 같습니다.

| 실행한 작업 | 노출 | 클릭 |
| - | - | - |
| 터미널에서 배너 응답을 한 번 받습니다. | `+1`을 기록합니다. | 증가하지 않습니다. |
| 웹 페이지에서 배너 응답을 한 번 받습니다. | `+1`을 기록합니다. | 증가하지 않습니다. |
| 웹 페이지의 배너를 한 번 클릭합니다. | 증가하지 않습니다. | `+1`을 기록합니다. |
| 합계 | `2`인지 확인합니다. | `1`인지 확인합니다. |

추가로 요청하거나 페이지를 다시 열어 광고를 불러왔다면 성공한 배너 응답 수만큼 노출 기대값을 늘립니다. 다른 트래픽이 들어왔다면 같은 광고 단위·기간의 전체 요청 기록과 대조합니다.

### 완료 기준

* 배너 응답에 이미지·랜딩 URL과 `ext.ad_log`가 있습니다.
* 웹 페이지에서 배너가 표시되고 클릭하면 랜딩 페이지가 열립니다.
* 클릭 요청과 HTTP 응답을 확인합니다.
* **광고 단위 분석에서 실제 노출·클릭 집계를 확인합니다.**

집계 반영 시간은 A2 환경의 설정에 따라 다를 수 있습니다. 구축 담당자가 안내한 집계 반영 주기가 지난 뒤 분석 페이지를 다시 불러오세요. 반영 주기를 모르면 담당자에게 확인하고, 지표 확인을 위해 광고 요청이나 클릭을 반복하지 않습니다.

광고 단위·날짜·시간대를 맞춰 다시 조회해도 기대한 집계가 확인되지 않으면 실습을 완료한 것으로 판단하지 않습니다. 요청 시각과 시간대, 광고 단위 ID, 응답, 클릭 전송 기록과 조회 조건을 준비해 [검증과 문제 해결](https://docs.aiderx.io/development/ko/integration-verification-and-troubleshooting.md)의 지원 요청 절차에 따라 확인합니다. 개별 이벤트의 수집 상태를 조회하는 기능은 현재 제공되지 않습니다.

## 문제가 생겼을 때

> [accordion] 터미널에서는 성공하지만 웹에서는 실패합니다
>
> 서버 주소를 확인한 뒤 브라우저 Network·Console에서 HTTPS, CORS·사전 요청 오류를 확인합니다. `http://localhost:9000`에서 API를 호출할 수 있어야 하며 `Access-Control-Allow-Origin`이 중복되면 브라우저가 응답을 차단합니다.

> [accordion] 배너 이미지가 표시되지 않습니다
>
> 응답의 이미지 URL에 접속할 수 있는지, 이미지 요청이 차단되지 않았는지 확인합니다. 노출은 응답 생성 시 이미 기록될 수 있습니다.

> [accordion] 클릭 수가 늘지 않습니다
>
> 실습 페이지의 클릭 전송 결과와 조회 기간·광고 단위 ID를 확인합니다. 네트워크 오류나 타임아웃은 처리 실패를 확정하지 않으므로 무조건 재전송하지 않습니다.

> [accordion] 노출·클릭 수가 기대보다 큽니다
>
> 반복 요청, 새로고침 후 광고 불러오기, 여러 번의 클릭이나 다른 트래픽을 확인합니다.

## 다음 단계

[광고 이벤트 추적](https://docs.aiderx.io/development/ko/ad-events-and-logs.md)에서 서비스의 노출·클릭·전환 기록 방법을 확인합니다.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.