<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://glenn-yu.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://glenn-yu.github.io/" rel="alternate" type="text/html" /><updated>2026-05-31T13:48:54+09:00</updated><id>https://glenn-yu.github.io/feed.xml</id><title type="html">Glenn Yu</title><subtitle>모바일, 웹, 서버, 그리고 그 사이의 기술적 가치를 탐구합니다. 만들고, 기록하며 성장합니다.</subtitle><author><name>Glenn Yu</name><email>gwangy.yu@gmail.com</email></author><entry><title type="html">VAST × OpenRTB 완전 해부: 광고 한 편이 0.1초 안에 재생되기까지</title><link href="https://glenn-yu.github.io/posts/vast-openrtb-deep-dive/" rel="alternate" type="text/html" title="VAST × OpenRTB 완전 해부: 광고 한 편이 0.1초 안에 재생되기까지" /><published>2026-05-31T10:00:00+09:00</published><updated>2026-05-31T10:00:00+09:00</updated><id>https://glenn-yu.github.io/posts/vast-openrtb-deep-dive</id><content type="html" xml:base="https://glenn-yu.github.io/posts/vast-openrtb-deep-dive/"><![CDATA[<p>영상 콘텐츠 재생 버튼을 누른 순간, 0.1초 안에 어떤 일이 일어날까요?</p>

<p>전 세계 수백 개 DSP가 동시에 입찰가를 던지고, 낙찰자가 결정되며, 그 광고의 XML이 플레이어로 흘러들어와 파싱되고, 미디어가 다운로드되고, 수십 개의 트래킹 픽셀이 발사됩니다. 이 모든 과정의 뒤에는 IAB Tech Lab이 설계한 두 가지 핵심 표준이 있습니다 — <strong>VAST</strong>와 <strong>OpenRTB</strong>.</p>

<p>이 글은 두 표준의 구조를 단순한 개요가 아니라 <strong>공식 명세 수준의 깊이로</strong> 뜯어보는 기술 레퍼런스입니다. 모든 enum 값과 필드는 다음 1차 소스로 교차 검증했습니다.</p>

<ul>
  <li><a href="https://gwangy.com/docs/vast">gwangy.com/docs/vast</a> — VAST v3.0~v4.2 통합 명세</li>
  <li><a href="https://iabtechlab.com/standards/vast/">IAB Tech Lab — VAST Standards</a></li>
  <li><a href="https://github.com/InteractiveAdvertisingBureau/openrtb2.x/blob/main/2.6.md">IAB Tech Lab — OpenRTB 2.6 Spec</a></li>
  <li><a href="https://github.com/InteractiveAdvertisingBureau/AdCOM/blob/master/AdCOM%20v1.0%20FINAL.md">IAB Tech Lab — AdCOM v1.0 FINAL</a></li>
</ul>

<blockquote>
  <p><strong>Fact-check 노트</strong>: OpenRTB 2.6의 enum(protocol/placement/playbackmethod 등)은 모두 <strong>AdCOM 1.0 사양으로 분리</strong>되어 관리됩니다. 이 글의 모든 enum 표는 AdCOM 1.0 FINAL에서 직접 추출한 값입니다.</p>
</blockquote>

<hr />

<h2 id="1-두-표준의-역할-한눈에">1. 두 표준의 역할 한눈에</h2>

<table>
  <thead>
    <tr>
      <th>표준</th>
      <th>책임</th>
      <th>데이터 형식</th>
      <th>관리 주체</th>
      <th>최신 버전</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>VAST</strong></td>
      <td>광고 소재(영상·배너·트래킹) 명세 → 플레이어 전달</td>
      <td>XML</td>
      <td>IAB Tech Lab Digital Video TWG</td>
      <td>4.3 (2022-12), CTV 2024 (2024-07)</td>
    </tr>
    <tr>
      <td><strong>OpenRTB</strong></td>
      <td>광고 인벤토리 실시간 경매 프로토콜</td>
      <td>JSON</td>
      <td>IAB Tech Lab Programmatic TWG</td>
      <td>2.6 (2024 업데이트 포함), 3.0</td>
    </tr>
    <tr>
      <td><strong>AdCOM</strong></td>
      <td>OpenRTB·OpenDirect 공통 객체·enum 사전</td>
      <td>JSON</td>
      <td>IAB Tech Lab</td>
      <td>1.0 FINAL</td>
    </tr>
    <tr>
      <td><strong>OMID</strong></td>
      <td>광고 가시성·검증 측정 SDK</td>
      <td>Native (iOS/Android/Web)</td>
      <td>IAB Tech Lab</td>
      <td>OM SDK 1.5+</td>
    </tr>
  </tbody>
</table>

<p><strong>한 문장 요약</strong>: OpenRTB는 “어떤 광고를 살까”를 결정하는 시장이고, VAST는 “낙찰된 광고를 어떻게 재생하고 추적할까”를 알려주는 명세서입니다. AdCOM은 둘이 함께 쓰는 공용 사전이고, OMID는 결과를 측정하는 도구입니다.</p>

<hr />

<h2 id="2-vast-상세--광고-소재-전달-명세">2. VAST 상세 — 광고 소재 전달 명세</h2>

<h3 id="21-버전-진화">2.1 버전 진화</h3>

<table>
  <thead>
    <tr>
      <th>버전</th>
      <th>출시 (IAB Tech Lab)</th>
      <th>핵심 변화</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>3.0</td>
      <td>2012-07</td>
      <td>Ad Pod(<code class="language-plaintext highlighter-rouge">sequence</code>), SkipOffset, Companion 강화, ViewableImpression 도입 직전 단계</td>
    </tr>
    <tr>
      <td>4.0</td>
      <td>2016-01</td>
      <td><code class="language-plaintext highlighter-rouge">AdServingId</code> 필수화, <code class="language-plaintext highlighter-rouge">UniversalAdId</code>, <code class="language-plaintext highlighter-rouge">AdVerifications</code>, <code class="language-plaintext highlighter-rouge">ViewableImpression</code>, <code class="language-plaintext highlighter-rouge">Mezzanine</code>(SSAI)</td>
    </tr>
    <tr>
      <td>4.1</td>
      <td>2017-08</td>
      <td>오디오 광고(<code class="language-plaintext highlighter-rouge">adType="audio"</code>), <code class="language-plaintext highlighter-rouge">ClosedCaptionFiles</code>, <code class="language-plaintext highlighter-rouge">ExecutableResource</code> 검증, SSAI 강화</td>
    </tr>
    <tr>
      <td>4.2</td>
      <td>2019-06</td>
      <td><code class="language-plaintext highlighter-rouge">InteractiveCreativeFile</code>(SIMID 공식 채택), 멀티 <code class="language-plaintext highlighter-rouge">UniversalAdId</code>, <code class="language-plaintext highlighter-rouge">BlockedAdCategories</code></td>
    </tr>
    <tr>
      <td>4.3</td>
      <td>2022-12</td>
      <td>CTV 지원 강화, 추가 <code class="language-plaintext highlighter-rouge">ConditionalAd</code> 옵션, 광고 추적 개선</td>
    </tr>
    <tr>
      <td>CTV 2024</td>
      <td>2024-07</td>
      <td>ACIF 지원, DSA 아이콘, 고해상도 크리에이티브</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>사실 확인</strong>: IAB Tech Lab의 공식 <a href="https://iabtechlab.com/standards/vast/">VAST Standards 페이지</a>에 명시된 출시일 기준. 일부 자료는 VAST 4.0을 2016-04 또는 2016-10으로 표기하나, IAB Tech Lab 공식 페이지의 표기를 따랐습니다.</p>
</blockquote>

<hr />

<h3 id="22-xml-최상위-구조">2.2 XML 최상위 구조</h3>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;VAST</span> <span class="na">version=</span><span class="s">"4.2"</span> <span class="na">xmlns:xs=</span><span class="s">"http://www.w3.org/2001/XMLSchema"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;Ad</span> <span class="na">id=</span><span class="s">"12345"</span> <span class="na">sequence=</span><span class="s">"1"</span> <span class="na">adType=</span><span class="s">"video"</span> <span class="na">conditionalAd=</span><span class="s">"false"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;InLine&gt;</span>
      <span class="nt">&lt;AdSystem</span> <span class="na">version=</span><span class="s">"1.0"</span><span class="nt">&gt;</span>My Ad Server<span class="nt">&lt;/AdSystem&gt;</span>
      <span class="nt">&lt;AdServingId&gt;</span>a532d16d-4d7f-4440-bd29-2ec0e693fc80<span class="nt">&lt;/AdServingId&gt;</span>
      <span class="nt">&lt;AdTitle&gt;</span>My Campaign 2026 Q2<span class="nt">&lt;/AdTitle&gt;</span>
      <span class="nt">&lt;Impression</span> <span class="na">id=</span><span class="s">"imp1"</span><span class="nt">&gt;</span><span class="cp">&lt;![CDATA[https://tracker.example.com/imp]]&gt;</span><span class="nt">&lt;/Impression&gt;</span>
      <span class="nt">&lt;Pricing</span> <span class="na">model=</span><span class="s">"CPM"</span> <span class="na">currency=</span><span class="s">"USD"</span><span class="nt">&gt;</span>3.50<span class="nt">&lt;/Pricing&gt;</span>
      <span class="nt">&lt;Creatives&gt;</span>
        ...
      <span class="nt">&lt;/Creatives&gt;</span>
    <span class="nt">&lt;/InLine&gt;</span>
  <span class="nt">&lt;/Ad&gt;</span>
<span class="nt">&lt;/VAST&gt;</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">&lt;Ad&gt;</code> 는 두 가지 형태만 가질 수 있습니다.</p>

<table>
  <thead>
    <tr>
      <th>형태</th>
      <th>의미</th>
      <th>사용 시나리오</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&lt;InLine&gt;</code></td>
      <td>광고 소재를 직접 포함</td>
      <td>자체 광고 서버, 최종 단계 응답</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&lt;Wrapper&gt;</code></td>
      <td>외부 광고 서버 URL로 리다이렉트 (체이닝)</td>
      <td>광고 네트워크 중개, DSP→SSP 변환</td>
    </tr>
  </tbody>
</table>

<p><strong>Wrapper 체인 권고 한계</strong>: 일반적으로 3~5 hop. 깊어질수록 레이턴시·실패율이 누적되며, VAST 4.x에서는 <code class="language-plaintext highlighter-rouge">Wrapper@followAdditionalWrappers="false"</code>로 명시적으로 차단할 수 있습니다.</p>

<hr />

<h3 id="23-ad-속성-전체-v30--v42">2.3 <code class="language-plaintext highlighter-rouge">&lt;Ad&gt;</code> 속성 전체 (v3.0 ~ v4.2)</h3>

<table>
  <thead>
    <tr>
      <th>속성</th>
      <th>도입 버전</th>
      <th>자료형</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">id</code></td>
      <td>3.0</td>
      <td>String</td>
      <td>광고 식별자</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">sequence</code></td>
      <td>3.0</td>
      <td>Integer</td>
      <td>Ad Pod 내 재생 순서 (1부터)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">conditionalAd</code></td>
      <td>4.0</td>
      <td>Boolean</td>
      <td>조건부 노출 광고 여부</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">adType</code></td>
      <td>4.1</td>
      <td>Enum</td>
      <td><code class="language-plaintext highlighter-rouge">video</code> / <code class="language-plaintext highlighter-rouge">audio</code> (기본 <code class="language-plaintext highlighter-rouge">video</code>)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">breakId</code></td>
      <td>4.1</td>
      <td>String</td>
      <td>광고 브레이크 식별자</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">breakIndex</code></td>
      <td>4.1</td>
      <td>Integer</td>
      <td>브레이크 내 광고 인덱스 (1-based)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">breakType</code></td>
      <td>4.1</td>
      <td>Enum</td>
      <td><code class="language-plaintext highlighter-rouge">linear</code> / <code class="language-plaintext highlighter-rouge">nonlinear</code> / <code class="language-plaintext highlighter-rouge">both</code></td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="24-metadata-요소-전체">2.4 Metadata 요소 전체</h3>

<table>
  <thead>
    <tr>
      <th>요소</th>
      <th>도입</th>
      <th>자료형</th>
      <th>필수 위치</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&lt;AdSystem&gt;</code></td>
      <td>3.0</td>
      <td>String + <code class="language-plaintext highlighter-rouge">version</code></td>
      <td>InLine, Wrapper</td>
      <td>광고 시스템 명칭</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&lt;AdServingId&gt;</code></td>
      <td>4.0</td>
      <td>UUID String</td>
      <td><strong>InLine 필수</strong></td>
      <td>광고 세션 전역 고유 ID</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&lt;AdTitle&gt;</code></td>
      <td>3.0</td>
      <td>String</td>
      <td><strong>InLine 필수</strong></td>
      <td>캠페인 제목</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&lt;Description&gt;</code></td>
      <td>3.0</td>
      <td>String</td>
      <td>선택</td>
      <td>광고 상세 설명</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&lt;Advertiser&gt;</code></td>
      <td>3.0</td>
      <td>String</td>
      <td>선택</td>
      <td>광고주명</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&lt;Pricing&gt;</code></td>
      <td>3.0</td>
      <td>Decimal + <code class="language-plaintext highlighter-rouge">model</code>/<code class="language-plaintext highlighter-rouge">currency</code></td>
      <td>선택</td>
      <td>가격(CPM/CPC/CPE/CPV)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&lt;Survey&gt;</code></td>
      <td>3.0</td>
      <td>URL</td>
      <td>선택</td>
      <td>외부 설문 URL</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&lt;Category&gt;</code></td>
      <td>4.0</td>
      <td>String + <code class="language-plaintext highlighter-rouge">authority</code> 필수</td>
      <td>선택</td>
      <td>업종 카테고리</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&lt;UniversalAdId&gt;</code></td>
      <td>4.0</td>
      <td>String + <code class="language-plaintext highlighter-rouge">idRegistry</code></td>
      <td><strong>InLine 필수</strong></td>
      <td>광고 전역 고유 식별자 (Ad-ID, ClearAd 등)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&lt;BlockedAdCategories&gt;</code></td>
      <td>4.2</td>
      <td>Element</td>
      <td>선택</td>
      <td>노출 제한 카테고리 (Wrapper에서 주로 사용)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&lt;Expires&gt;</code></td>
      <td>4.1</td>
      <td>Integer (초)</td>
      <td>선택</td>
      <td>캐시 유효 시간</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&lt;Extensions&gt;</code></td>
      <td>3.0</td>
      <td>Element</td>
      <td>선택</td>
      <td>표준 외 데이터 컨테이너</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="25-linear-광고--mediafile-전체-속성">2.5 Linear 광고 — MediaFile 전체 속성</h3>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;Linear&gt;</span>
  <span class="nt">&lt;Duration&gt;</span>00:00:30<span class="nt">&lt;/Duration&gt;</span>
  <span class="nt">&lt;SkipOffset&gt;</span>00:00:05<span class="nt">&lt;/SkipOffset&gt;</span>
  <span class="nt">&lt;MediaFiles&gt;</span>
    <span class="nt">&lt;MediaFile</span> <span class="na">delivery=</span><span class="s">"progressive"</span> <span class="na">type=</span><span class="s">"video/mp4"</span>
               <span class="na">width=</span><span class="s">"1920"</span> <span class="na">height=</span><span class="s">"1080"</span> <span class="na">bitrate=</span><span class="s">"2000"</span>
               <span class="na">minBitrate=</span><span class="s">"1500"</span> <span class="na">maxBitrate=</span><span class="s">"3000"</span>
               <span class="na">scalable=</span><span class="s">"true"</span> <span class="na">maintainAspectRatio=</span><span class="s">"true"</span>
               <span class="na">codec=</span><span class="s">"avc1.4D401E"</span> <span class="na">fileSize=</span><span class="s">"7500000"</span><span class="nt">&gt;</span>
      <span class="cp">&lt;![CDATA[https://cdn.example.com/ad.mp4]]&gt;</span>
    <span class="nt">&lt;/MediaFile&gt;</span>
    <span class="nt">&lt;Mezzanine</span> <span class="na">type=</span><span class="s">"video/mp4"</span> <span class="na">delivery=</span><span class="s">"progressive"</span>
               <span class="na">width=</span><span class="s">"3840"</span> <span class="na">height=</span><span class="s">"2160"</span> <span class="na">codec=</span><span class="s">"hevc"</span><span class="nt">&gt;</span>
      <span class="cp">&lt;![CDATA[https://cdn.example.com/ad-mezzanine.mp4]]&gt;</span>
    <span class="nt">&lt;/Mezzanine&gt;</span>
    <span class="nt">&lt;InteractiveCreativeFile</span> <span class="na">type=</span><span class="s">"text/html"</span>
                              <span class="na">apiFramework=</span><span class="s">"SIMID"</span>
                              <span class="na">variableDuration=</span><span class="s">"false"</span><span class="nt">&gt;</span>
      <span class="cp">&lt;![CDATA[https://creative.example.com/simid.html]]&gt;</span>
    <span class="nt">&lt;/InteractiveCreativeFile&gt;</span>
    <span class="nt">&lt;ClosedCaptionFiles&gt;</span>
      <span class="nt">&lt;ClosedCaptionFile</span> <span class="na">type=</span><span class="s">"text/vtt"</span> <span class="na">language=</span><span class="s">"ko"</span><span class="nt">&gt;</span>
        <span class="cp">&lt;![CDATA[https://cdn.example.com/captions-ko.vtt]]&gt;</span>
      <span class="nt">&lt;/ClosedCaptionFile&gt;</span>
    <span class="nt">&lt;/ClosedCaptionFiles&gt;</span>
  <span class="nt">&lt;/MediaFiles&gt;</span>
<span class="nt">&lt;/Linear&gt;</span>
</code></pre></div></div>

<p><strong><code class="language-plaintext highlighter-rouge">&lt;MediaFile&gt;</code> 속성 표:</strong></p>

<table>
  <thead>
    <tr>
      <th>속성</th>
      <th>도입</th>
      <th style="text-align: center">필수</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">delivery</code></td>
      <td>3.0</td>
      <td style="text-align: center">✅</td>
      <td><code class="language-plaintext highlighter-rouge">progressive</code> 또는 <code class="language-plaintext highlighter-rouge">streaming</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">type</code></td>
      <td>3.0</td>
      <td style="text-align: center">✅</td>
      <td>MIME 타입 (예: <code class="language-plaintext highlighter-rouge">video/mp4</code>, <code class="language-plaintext highlighter-rouge">video/webm</code>)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">width</code></td>
      <td>3.0</td>
      <td style="text-align: center">✅</td>
      <td>픽셀</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">height</code></td>
      <td>3.0</td>
      <td style="text-align: center">✅</td>
      <td>픽셀</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bitrate</code></td>
      <td>3.0</td>
      <td style="text-align: center">권장</td>
      <td>kbps</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">minBitrate</code> / <code class="language-plaintext highlighter-rouge">maxBitrate</code></td>
      <td>3.0</td>
      <td style="text-align: center">선택</td>
      <td>가변 비트레이트 범위</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">scalable</code></td>
      <td>3.0</td>
      <td style="text-align: center">선택</td>
      <td>확대 허용</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">maintainAspectRatio</code></td>
      <td>3.0</td>
      <td style="text-align: center">선택</td>
      <td>종횡비 유지</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">codec</code></td>
      <td>4.1</td>
      <td style="text-align: center">권장</td>
      <td>RFC 4281/6381 (예: <code class="language-plaintext highlighter-rouge">avc1.4D401E</code>)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">fileSize</code></td>
      <td>4.1</td>
      <td style="text-align: center">권장</td>
      <td>Bytes</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">apiFramework</code></td>
      <td>3.0</td>
      <td style="text-align: center">선택</td>
      <td><code class="language-plaintext highlighter-rouge">VPAID</code>, <code class="language-plaintext highlighter-rouge">SIMID</code>, <code class="language-plaintext highlighter-rouge">MRAID</code> 등</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">mediaType</code></td>
      <td>4.0</td>
      <td style="text-align: center">선택</td>
      <td><code class="language-plaintext highlighter-rouge">2D</code> / <code class="language-plaintext highlighter-rouge">3D</code> / <code class="language-plaintext highlighter-rouge">360</code> (VR 지원)</td>
    </tr>
  </tbody>
</table>

<p><strong>Mezzanine</strong> (v4.0): SSAI(서버 사이드 광고 삽입) 환경에서 트랜스코딩 전 원본 고화질 소재. v4.1에서는 코덱·해상도 권장 사양이 추가되었습니다.</p>

<hr />

<h3 id="26-skipoffset-형식">2.6 SkipOffset 형식</h3>

<table>
  <thead>
    <tr>
      <th>값</th>
      <th>의미</th>
      <th>예</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">HH:MM:SS</code> 또는 <code class="language-plaintext highlighter-rouge">HH:MM:SS.mmm</code></td>
      <td>절대 시간</td>
      <td><code class="language-plaintext highlighter-rouge">00:00:05</code> = 5초 후</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">nn%</code></td>
      <td>광고 길이 대비 비율</td>
      <td><code class="language-plaintext highlighter-rouge">25%</code> = 25% 지점</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="27-trackingevents--전체-enum">2.7 TrackingEvents — 전체 enum</h3>

<table>
  <thead>
    <tr>
      <th>이벤트</th>
      <th>도입</th>
      <th>시점 / 설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">start</code></td>
      <td>3.0</td>
      <td>광고 재생 시작</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">firstQuartile</code></td>
      <td>3.0</td>
      <td>25% 지점</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">midpoint</code></td>
      <td>3.0</td>
      <td>50% 지점</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">thirdQuartile</code></td>
      <td>3.0</td>
      <td>75% 지점</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">complete</code></td>
      <td>3.0</td>
      <td>100% (완료)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">skip</code></td>
      <td>3.0</td>
      <td>시청자 스킵 클릭</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">pause</code></td>
      <td>3.0</td>
      <td>일시 정지</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">resume</code></td>
      <td>3.0</td>
      <td>재개</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">mute</code></td>
      <td>3.0</td>
      <td>음소거</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">unmute</code></td>
      <td>3.0</td>
      <td>음소거 해제</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">rewind</code></td>
      <td>3.0</td>
      <td>되감기</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">fullscreen</code></td>
      <td>3.0</td>
      <td>전체화면 진입 (Deprecated v4.0 — <code class="language-plaintext highlighter-rouge">playerExpand</code> 사용 권장)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">exitFullscreen</code></td>
      <td>3.0</td>
      <td>전체화면 종료 (Deprecated v4.0 — <code class="language-plaintext highlighter-rouge">playerCollapse</code> 사용 권장)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">playerExpand</code></td>
      <td>4.0</td>
      <td>플레이어 확대 (fullscreen 대체)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">playerCollapse</code></td>
      <td>4.0</td>
      <td>플레이어 축소</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">progress</code></td>
      <td>3.0</td>
      <td>임의 시점 (<code class="language-plaintext highlighter-rouge">offset</code> 속성 필수)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">closeLinear</code></td>
      <td>3.0</td>
      <td>종료 버튼 클릭</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">loaded</code></td>
      <td>4.0</td>
      <td>미디어 로드 완료</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">acceptInvitation</code></td>
      <td>3.0</td>
      <td>NonLinear 광고 초대 수락</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">acceptInvitationLinear</code></td>
      <td>3.0</td>
      <td>Linear 광고 초대 수락</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">viewable</code></td>
      <td>4.0</td>
      <td>ViewableImpression 판정 결과</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">notViewable</code></td>
      <td>4.0</td>
      <td>가시성 미충족</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">viewUndetermined</code></td>
      <td>4.0</td>
      <td>판정 불가</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">verificationNotExecuted</code></td>
      <td>4.1</td>
      <td>검증 스크립트 미실행</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">interactiveStart</code></td>
      <td>4.2</td>
      <td>SIMID 인터랙티브 시작</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="28-viewableimpression--mrc-기준">2.8 ViewableImpression — MRC 기준</h3>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;ViewableImpression</span> <span class="na">id=</span><span class="s">"vi1"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;Viewable&gt;</span><span class="cp">&lt;![CDATA[https://t.example.com/viewable]]&gt;</span><span class="nt">&lt;/Viewable&gt;</span>
  <span class="nt">&lt;NotViewable&gt;</span><span class="cp">&lt;![CDATA[https://t.example.com/not-viewable]]&gt;</span><span class="nt">&lt;/NotViewable&gt;</span>
  <span class="nt">&lt;ViewUndetermined&gt;</span><span class="cp">&lt;![CDATA[https://t.example.com/undetermined]]&gt;</span><span class="nt">&lt;/ViewUndetermined&gt;</span>
<span class="nt">&lt;/ViewableImpression&gt;</span>
</code></pre></div></div>

<p><strong>MRC(Media Rating Council) Viewability 기준 — 비디오:</strong></p>

<table>
  <thead>
    <tr>
      <th>기준</th>
      <th>디스플레이</th>
      <th>비디오</th>
      <th>모바일</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>픽셀 노출</td>
      <td>50%</td>
      <td>50%</td>
      <td>50%</td>
    </tr>
    <tr>
      <td>노출 시간</td>
      <td>1초</td>
      <td><strong>2초 연속</strong></td>
      <td>동일</td>
    </tr>
    <tr>
      <td>대형 광고 (242,500px² 이상)</td>
      <td>30% 1초</td>
      <td>-</td>
      <td>-</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="29-adverifications--omid-v40">2.9 AdVerifications &amp; OMID (v4.0+)</h3>

<p>제3자 측정사가 OMID(Open Measurement Interface Definition) SDK를 통해 독립 검증을 수행합니다.</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;AdVerifications&gt;</span>
  <span class="nt">&lt;Verification</span> <span class="na">vendor=</span><span class="s">"doubleverify.com-omid"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;JavaScriptResource</span> <span class="na">apiFramework=</span><span class="s">"omid"</span> <span class="na">browserOptional=</span><span class="s">"true"</span><span class="nt">&gt;</span>
      <span class="cp">&lt;![CDATA[https://cdn.doubleverify.com/dvtp_src.js]]&gt;</span>
    <span class="nt">&lt;/JavaScriptResource&gt;</span>
    <span class="nt">&lt;ExecutableResource</span> <span class="na">apiFramework=</span><span class="s">"omid"</span> <span class="na">type=</span><span class="s">"application/x-javascript"</span><span class="nt">&gt;</span>
      <span class="cp">&lt;![CDATA[https://cdn.doubleverify.com/native.so]]&gt;</span>
    <span class="nt">&lt;/ExecutableResource&gt;</span>
    <span class="nt">&lt;TrackingEvents&gt;</span>
      <span class="nt">&lt;Tracking</span> <span class="na">event=</span><span class="s">"verificationNotExecuted"</span><span class="nt">&gt;</span>
        <span class="cp">&lt;![CDATA[https://t.doubleverify.com/no-exec]]&gt;</span>
      <span class="nt">&lt;/Tracking&gt;</span>
    <span class="nt">&lt;/TrackingEvents&gt;</span>
    <span class="nt">&lt;VerificationParameters&gt;</span><span class="cp">&lt;![CDATA[ctx=1234&amp;cmp=5678]]&gt;</span><span class="nt">&lt;/VerificationParameters&gt;</span>
  <span class="nt">&lt;/Verification&gt;</span>
<span class="nt">&lt;/AdVerifications&gt;</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>요소</th>
      <th>도입</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">vendor</code> 속성</td>
      <td>4.0</td>
      <td>검증 업체 식별자 (필수)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&lt;JavaScriptResource&gt;</code></td>
      <td>4.0</td>
      <td>OMID JS 측정 파일</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&lt;ExecutableResource&gt;</code></td>
      <td>4.1</td>
      <td>네이티브(iOS/Android) 실행 파일</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&lt;VerificationParameters&gt;</code></td>
      <td>4.0</td>
      <td>측정사 전달 파라미터</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">browserOptional</code> 속성</td>
      <td>4.1</td>
      <td><code class="language-plaintext highlighter-rouge">true</code>면 브라우저 없이도 실행 가능</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="210-simid-vs-vpaid--인터랙티브-광고-변천사">2.10 SIMID vs VPAID — 인터랙티브 광고 변천사</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>VPAID 2.0</th>
      <th>SIMID 1.1</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>통신 방식</td>
      <td>JavaScript API (동기 호출)</td>
      <td><code class="language-plaintext highlighter-rouge">window.postMessage</code> (비동기)</td>
    </tr>
    <tr>
      <td>보안 모델</td>
      <td>플레이어와 동일 컨텍스트 (full JS 접근)</td>
      <td>Sandboxed iframe 격리</td>
    </tr>
    <tr>
      <td>표준화 주체</td>
      <td>IAB</td>
      <td>IAB Tech Lab</td>
    </tr>
    <tr>
      <td>VAST 도입</td>
      <td>3.0~</td>
      <td><strong>4.2 공식 채택</strong></td>
    </tr>
    <tr>
      <td>AdCOM apiFramework 값</td>
      <td>1, 2</td>
      <td>8, 9</td>
    </tr>
    <tr>
      <td>현재 권고</td>
      <td><strong>Deprecated</strong> (Google Ads Manager 2022 종료)</td>
      <td><strong>권장 표준</strong></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">variableDuration</code> 지원</td>
      <td>불가</td>
      <td>가능 (v4.2)</td>
    </tr>
  </tbody>
</table>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- SIMID 선언 --&gt;</span>
<span class="nt">&lt;InteractiveCreativeFile</span> <span class="na">type=</span><span class="s">"text/html"</span>
                          <span class="na">apiFramework=</span><span class="s">"SIMID"</span>
                          <span class="na">variableDuration=</span><span class="s">"false"</span><span class="nt">&gt;</span>
  <span class="cp">&lt;![CDATA[https://creative.example.com/simid.html]]&gt;</span>
<span class="nt">&lt;/InteractiveCreativeFile&gt;</span>
</code></pre></div></div>

<hr />

<h3 id="211-icons--adchoices--dsa">2.11 Icons — AdChoices / DSA</h3>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;Icons&gt;</span>
  <span class="nt">&lt;Icon</span> <span class="na">program=</span><span class="s">"AdChoices"</span> <span class="na">width=</span><span class="s">"80"</span> <span class="na">height=</span><span class="s">"15"</span>
        <span class="na">xPosition=</span><span class="s">"right"</span> <span class="na">yPosition=</span><span class="s">"top"</span>
        <span class="na">offset=</span><span class="s">"00:00:00"</span> <span class="na">duration=</span><span class="s">"00:01:00"</span> <span class="na">pxratio=</span><span class="s">"2"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;StaticResource</span> <span class="na">creativeType=</span><span class="s">"image/png"</span><span class="nt">&gt;</span>
      <span class="cp">&lt;![CDATA[https://example.com/adchoices@2x.png]]&gt;</span>
    <span class="nt">&lt;/StaticResource&gt;</span>
    <span class="nt">&lt;IconViewTracking&gt;</span><span class="cp">&lt;![CDATA[https://t.example.com/icon-view]]&gt;</span><span class="nt">&lt;/IconViewTracking&gt;</span>
    <span class="nt">&lt;IconClicks&gt;</span>
      <span class="nt">&lt;IconClickThrough&gt;</span><span class="cp">&lt;![CDATA[https://adchoices.example.com]]&gt;</span><span class="nt">&lt;/IconClickThrough&gt;</span>
      <span class="nt">&lt;IconClickTracking&gt;</span><span class="cp">&lt;![CDATA[https://t.example.com/icon-click]]&gt;</span><span class="nt">&lt;/IconClickTracking&gt;</span>
      <span class="nt">&lt;IconClickFallbackImages&gt;</span>
        <span class="nt">&lt;IconClickFallbackImage</span> <span class="na">width=</span><span class="s">"400"</span> <span class="na">height=</span><span class="s">"150"</span><span class="nt">&gt;</span>
          <span class="nt">&lt;AltText&gt;</span>AdChoices Info<span class="nt">&lt;/AltText&gt;</span>
          <span class="nt">&lt;StaticResource</span> <span class="na">creativeType=</span><span class="s">"image/png"</span><span class="nt">&gt;</span>
            <span class="cp">&lt;![CDATA[https://example.com/fallback.png]]&gt;</span>
          <span class="nt">&lt;/StaticResource&gt;</span>
        <span class="nt">&lt;/IconClickFallbackImage&gt;</span>
      <span class="nt">&lt;/IconClickFallbackImages&gt;</span>
    <span class="nt">&lt;/IconClicks&gt;</span>
  <span class="nt">&lt;/Icon&gt;</span>
<span class="nt">&lt;/Icons&gt;</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>속성</th>
      <th>자료형</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">program</code></td>
      <td>String</td>
      <td>식별자 (예: <code class="language-plaintext highlighter-rouge">AdChoices</code>, <code class="language-plaintext highlighter-rouge">DSA</code>, <code class="language-plaintext highlighter-rouge">EUWebChoices</code>) — <strong>필수</strong></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">width</code> / <code class="language-plaintext highlighter-rouge">height</code></td>
      <td>Integer</td>
      <td>픽셀</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">xPosition</code></td>
      <td><code class="language-plaintext highlighter-rouge">left</code>/<code class="language-plaintext highlighter-rouge">right</code>/숫자(px)</td>
      <td>가로 위치</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">yPosition</code></td>
      <td><code class="language-plaintext highlighter-rouge">top</code>/<code class="language-plaintext highlighter-rouge">bottom</code>/숫자(px)</td>
      <td>세로 위치</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">offset</code></td>
      <td>Time</td>
      <td>노출 시작 시각</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">duration</code></td>
      <td>Time</td>
      <td>노출 지속 시간</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">pxratio</code></td>
      <td>Decimal (4.1+)</td>
      <td>픽셀 밀도 비율</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">apiFramework</code></td>
      <td>String</td>
      <td>인터랙션 프레임워크</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="212-vast-macros--28종-전체-v42-기준">2.12 VAST Macros — 28종 전체 (v4.2 기준)</h3>

<p>VAST 매크로는 <strong><code class="language-plaintext highlighter-rouge">[BRACKET]</code></strong> 형식이며, 플레이어가 트래킹 URL을 호출하기 <strong>직전에 치환</strong>합니다. OpenRTB의 <code class="language-plaintext highlighter-rouge">${DOLLAR}</code> 매크로와 <strong>다른 시스템</strong>입니다.</p>

<p><strong>시간/카운트 매크로:</strong></p>

<table>
  <thead>
    <tr>
      <th>매크로</th>
      <th>자료형</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[TIMESTAMP]</code></td>
      <td>ISO 8601 String</td>
      <td>현재 UTC 시간</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[CONTENTPLAYHEAD]</code></td>
      <td>Time <code class="language-plaintext highlighter-rouge">HH:MM:SS.mmm</code></td>
      <td>콘텐츠 현재 재생 위치</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[MEDIAPLAYHEAD]</code></td>
      <td>Time</td>
      <td>광고 미디어 현재 재생 위치</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[BREAKPOSITION]</code></td>
      <td>Integer</td>
      <td>광고 브레이크 순번 (1=Pre, 2=Mid 등)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[ADPLAYHEAD]</code></td>
      <td>Time</td>
      <td>광고 자체 재생 위치</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[CACHEBUSTING]</code></td>
      <td>8자리 Integer</td>
      <td>캐시 방지 랜덤 값</td>
    </tr>
  </tbody>
</table>

<p><strong>식별 매크로:</strong></p>

<table>
  <thead>
    <tr>
      <th>매크로</th>
      <th>자료형</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[UNIVERSALADID]</code></td>
      <td>String</td>
      <td><code class="language-plaintext highlighter-rouge">&lt;UniversalAdId&gt;</code> 값과 동일</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[ADSERVINGID]</code></td>
      <td>UUID String</td>
      <td><code class="language-plaintext highlighter-rouge">&lt;AdServingId&gt;</code> 값과 동일 (v4.0+)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[ADCATEGORIES]</code></td>
      <td>String</td>
      <td>광고 카테고리 (CSV)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[BLOCKEDADCATEGORIES]</code></td>
      <td>String</td>
      <td>차단된 카테고리 (v4.2)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[PODSEQUENCE]</code></td>
      <td>Integer</td>
      <td>Pod 내 현재 광고 순번 (v4.1)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[ADCOUNT]</code></td>
      <td>Integer</td>
      <td>응답 내 총 광고 수</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[ADTYPE]</code></td>
      <td>String</td>
      <td><code class="language-plaintext highlighter-rouge">video</code> / <code class="language-plaintext highlighter-rouge">audio</code> / <code class="language-plaintext highlighter-rouge">hybrid</code> (v4.1)</td>
    </tr>
  </tbody>
</table>

<p><strong>디바이스/사용자 매크로:</strong></p>

<table>
  <thead>
    <tr>
      <th>매크로</th>
      <th>자료형</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[IFA]</code></td>
      <td>UUID String</td>
      <td>Identifier for Advertising (IDFA/AAID)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[IFATYPE]</code></td>
      <td>String</td>
      <td><code class="language-plaintext highlighter-rouge">idfa</code> / <code class="language-plaintext highlighter-rouge">aaid</code> / <code class="language-plaintext highlighter-rouge">rida</code> / <code class="language-plaintext highlighter-rouge">tifa</code> 등</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[DEVICEUA]</code></td>
      <td>URL-encoded String</td>
      <td>User-Agent 문자열</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[DEVICEIP]</code></td>
      <td>String</td>
      <td>디바이스 IP (보통 서버측만)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[LATLONG]</code></td>
      <td><code class="language-plaintext highlighter-rouge">lat,long</code> String</td>
      <td>위치</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[APPBUNDLE]</code></td>
      <td>String</td>
      <td>앱 번들 ID</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[DOMAIN]</code></td>
      <td>String</td>
      <td>Publisher 도메인</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[PAGEURL]</code></td>
      <td>URL String</td>
      <td>현재 페이지 URL</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[VASTVERSIONS]</code></td>
      <td>Integer Array</td>
      <td>지원 VAST 버전</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[APIFRAMEWORKS]</code></td>
      <td>Integer Array</td>
      <td>지원 API 프레임워크 (AdCOM 값)</td>
    </tr>
  </tbody>
</table>

<p><strong>프라이버시 매크로:</strong></p>

<table>
  <thead>
    <tr>
      <th>매크로</th>
      <th>자료형</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[GDPRCONSENT]</code></td>
      <td>TCF String</td>
      <td>TCF v2.x 동의 문자열</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[USPRIVACY]</code></td>
      <td>String</td>
      <td>CCPA US Privacy String</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[LIMITADTRACKING]</code></td>
      <td>Boolean</td>
      <td>OS 광고 추적 제한 상태</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[REGULATIONS]</code></td>
      <td>String</td>
      <td>적용 규제 (<code class="language-plaintext highlighter-rouge">GDPR</code>, <code class="language-plaintext highlighter-rouge">COPPA</code> 등)</td>
    </tr>
  </tbody>
</table>

<p><strong>이벤트/상태 매크로:</strong></p>

<table>
  <thead>
    <tr>
      <th>매크로</th>
      <th>자료형</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[ERRORCODE]</code></td>
      <td>Integer</td>
      <td><code class="language-plaintext highlighter-rouge">&lt;Error&gt;</code> URL에 <strong>필수</strong></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[REASON]</code></td>
      <td>Integer</td>
      <td>스킵/종료 사유</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[CLICKPOS]</code></td>
      <td><code class="language-plaintext highlighter-rouge">x,y</code> String</td>
      <td>클릭 좌표</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[CLICKTYPE]</code></td>
      <td>String</td>
      <td>클릭 방식 (예: <code class="language-plaintext highlighter-rouge">tap</code>, <code class="language-plaintext highlighter-rouge">key</code>)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[PLAYERSTATE]</code></td>
      <td>String</td>
      <td><code class="language-plaintext highlighter-rouge">fullscreen</code>/<code class="language-plaintext highlighter-rouge">autoplay</code>/<code class="language-plaintext highlighter-rouge">muted</code> (다중 가능)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[PLAYERSIZE]</code></td>
      <td><code class="language-plaintext highlighter-rouge">width,height</code></td>
      <td>플레이어 크기</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[CONTENTID]</code></td>
      <td>String</td>
      <td>콘텐츠 식별자</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[TRANSACTIONID]</code></td>
      <td>UUID String</td>
      <td>트랜잭션 ID</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[INVENTORYSTATE]</code></td>
      <td>String</td>
      <td>인벤토리 상태</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[SERVERSIDE]</code></td>
      <td>Boolean</td>
      <td>SSAI 환경 여부</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[ADCID]</code></td>
      <td>String</td>
      <td>광고 식별자 (DSP 측)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">[ADCAID]</code></td>
      <td>String</td>
      <td>광고 자산 식별자</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>참고</strong>: 매크로의 정식 카탈로그는 별도 라이브 문서로 관리됩니다 → <a href="https://interactiveadvertisingbureau.github.io/vast/vast4macros/vast4-macros-latest.html">vast4-macros-latest</a>. 신규 매크로는 IAB Tech Lab의 Working Group 승인 후 추가됩니다.</p>
</blockquote>

<hr />

<h3 id="213-error-codes--전체-분류">2.13 Error Codes — 전체 분류</h3>

<table>
  <thead>
    <tr>
      <th>코드</th>
      <th>분류</th>
      <th>의미</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>100</td>
      <td>XML</td>
      <td>파싱 오류</td>
    </tr>
    <tr>
      <td>101</td>
      <td>XML</td>
      <td>스키마 검증 실패</td>
    </tr>
    <tr>
      <td>102</td>
      <td>XML</td>
      <td>지원하지 않는 VAST 버전</td>
    </tr>
    <tr>
      <td>200</td>
      <td>Trafficking</td>
      <td>트래픽 정책 위반</td>
    </tr>
    <tr>
      <td>201</td>
      <td>Trafficking</td>
      <td>Linear가 NonLinear로 와야 함</td>
    </tr>
    <tr>
      <td>202</td>
      <td>Trafficking</td>
      <td>NonLinear가 Linear로 와야 함</td>
    </tr>
    <tr>
      <td>203</td>
      <td>Trafficking</td>
      <td>Companion 광고 누락</td>
    </tr>
    <tr>
      <td>300</td>
      <td>Wrapper</td>
      <td>일반 Wrapper 오류</td>
    </tr>
    <tr>
      <td>301</td>
      <td>Wrapper</td>
      <td>호출 타임아웃 / 응답 없음</td>
    </tr>
    <tr>
      <td>302</td>
      <td>Wrapper</td>
      <td>Wrapper 한계 도달 (체인 깊이 초과)</td>
    </tr>
    <tr>
      <td>303</td>
      <td>Wrapper</td>
      <td>NoAd 응답</td>
    </tr>
    <tr>
      <td>304</td>
      <td>Wrapper</td>
      <td>InLine 응답이 광고 미포함</td>
    </tr>
    <tr>
      <td>305</td>
      <td>Wrapper</td>
      <td>VAST 응답이 v4.1 미지원</td>
    </tr>
    <tr>
      <td>400</td>
      <td>Media</td>
      <td>일반 Linear 광고 오류</td>
    </tr>
    <tr>
      <td>401</td>
      <td>Media</td>
      <td>호환되는 MediaFile 미발견</td>
    </tr>
    <tr>
      <td>402</td>
      <td>Media</td>
      <td>MediaFile 다운로드 타임아웃</td>
    </tr>
    <tr>
      <td>403</td>
      <td>Media</td>
      <td>지원 MediaFile 없음</td>
    </tr>
    <tr>
      <td>405</td>
      <td>Media</td>
      <td>표시 가능한 MediaFile 없음</td>
    </tr>
    <tr>
      <td>406</td>
      <td>Media</td>
      <td>Mezzanine 누락 (SSAI)</td>
    </tr>
    <tr>
      <td>407</td>
      <td>Media</td>
      <td>Mezzanine 다운로드 진행 중 (v4.1)</td>
    </tr>
    <tr>
      <td>408</td>
      <td>Media</td>
      <td>조건부 광고 거부</td>
    </tr>
    <tr>
      <td>409</td>
      <td>Media</td>
      <td>인터랙티브 단위 응답 없음</td>
    </tr>
    <tr>
      <td>410</td>
      <td>Media</td>
      <td>Mezzanine 트랜스코딩 중 (v4.1)</td>
    </tr>
    <tr>
      <td>500</td>
      <td>NonLinear</td>
      <td>일반 NonLinear 광고 오류</td>
    </tr>
    <tr>
      <td>501</td>
      <td>NonLinear</td>
      <td>NonLinear 리소스 크기 불일치</td>
    </tr>
    <tr>
      <td>502</td>
      <td>NonLinear</td>
      <td>NonLinear 리소스 가져오기 실패</td>
    </tr>
    <tr>
      <td>503</td>
      <td>NonLinear</td>
      <td>표시 가능한 NonLinear 리소스 없음</td>
    </tr>
    <tr>
      <td>600</td>
      <td>Companion</td>
      <td>일반 Companion 광고 오류</td>
    </tr>
    <tr>
      <td>601</td>
      <td>Companion</td>
      <td>Companion 크기 미지원</td>
    </tr>
    <tr>
      <td>602</td>
      <td>Companion</td>
      <td>Companion 표시 불가 (필수 자원 미수신)</td>
    </tr>
    <tr>
      <td>603</td>
      <td>Companion</td>
      <td>Companion 리소스 가져오기 실패</td>
    </tr>
    <tr>
      <td>604</td>
      <td>Companion</td>
      <td>표시 가능한 Companion 없음</td>
    </tr>
    <tr>
      <td>900</td>
      <td>Undefined</td>
      <td>정의되지 않은 치명적 오류</td>
    </tr>
    <tr>
      <td>901</td>
      <td>VPAID/SIMID</td>
      <td>일반 실행 오류</td>
    </tr>
  </tbody>
</table>

<p><strong><code class="language-plaintext highlighter-rouge">&lt;Error&gt;</code> URL 사용법:</strong></p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;Error&gt;</span><span class="cp">&lt;![CDATA[https://errors.example.com/vast?code=[ERRORCODE]&amp;aid=[ADSERVINGID]]]&gt;</span><span class="nt">&lt;/Error&gt;</span>
</code></pre></div></div>

<hr />

<h2 id="3-openrtb-상세--실시간-입찰-표준">3. OpenRTB 상세 — 실시간 입찰 표준</h2>

<h3 id="31-버전-진화">3.1 버전 진화</h3>

<table>
  <thead>
    <tr>
      <th>버전</th>
      <th>출시</th>
      <th>핵심 변화</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1.0</td>
      <td>2010</td>
      <td>초기 디스플레이 RTB</td>
    </tr>
    <tr>
      <td>2.0</td>
      <td>2012</td>
      <td>비디오(VAST 연동) 공식 지원</td>
    </tr>
    <tr>
      <td>2.3</td>
      <td>2015</td>
      <td>Native 광고</td>
    </tr>
    <tr>
      <td>2.4</td>
      <td>2016</td>
      <td>Multi-Imp 입찰</td>
    </tr>
    <tr>
      <td>2.5</td>
      <td>2016-12</td>
      <td>헤더 비딩, 비디오 배치 타입, IAB New Ad Portfolio</td>
    </tr>
    <tr>
      <td>2.6</td>
      <td>2022-04</td>
      <td><strong>CTV 지원 강화</strong> (Ad Pod, Network/Channel 객체), GPP</td>
    </tr>
    <tr>
      <td>2.6-202303</td>
      <td>2023-03</td>
      <td><code class="language-plaintext highlighter-rouge">placement</code> Deprecated → <code class="language-plaintext highlighter-rouge">plcmt</code>, Pod Deduplication</td>
    </tr>
    <tr>
      <td>3.0</td>
      <td>2017-09</td>
      <td>암호화 서명 입찰 / AdCOM 1.0 분리 / Ads.txt 통합</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>사실 확인</strong>: 3.0이 2017년에 먼저 나왔지만, 산업 표준은 여전히 2.x 계열입니다 (특히 2.5와 2.6). 3.0은 호환성·전환 비용 문제로 광범위하게 채택되지 못했습니다.</p>
</blockquote>

<hr />

<h3 id="32-rtb-흐름-시퀀스">3.2 RTB 흐름 시퀀스</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[1] 사용자가 영상 콘텐츠 재생
[2] Publisher 페이지/앱 → Ad Server (SSP)
[3] SSP가 BidRequest 생성
       │
       ├──→ DSP #1
       ├──→ DSP #2     (병렬 송신, tmax 내 응답 대기)
       ├──→ DSP #3
       └──→ DSP #N
              │
              ↓ BidResponse (price, adm=VAST URL or XML)
       │
[4] SSP: Second-Price Plus Auction → 낙찰자 결정
[5] SSP → 플레이어: 낙찰된 VAST URL/XML 전달
[6] 플레이어 → VAST 파싱 → MediaFile 다운로드 → 재생
[7] 플레이어 → DSP: nurl/burl 호출 (Win/Billing notice)
[8] 플레이어 → 트래킹: Impression, TrackingEvents 발화
</code></pre></div></div>

<p><strong>전체 경매 시한</strong>: <code class="language-plaintext highlighter-rouge">BidRequest.tmax</code> (보통 80~120ms). 이 시간 내에 응답 못 한 DSP는 자동 탈락.</p>

<hr />

<h3 id="33-bidrequest-최상위-객체--핵심-필드">3.3 BidRequest 최상위 객체 — 핵심 필드</h3>

<table>
  <thead>
    <tr>
      <th>필드</th>
      <th>자료형</th>
      <th style="text-align: center">필수</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">id</code></td>
      <td>String</td>
      <td style="text-align: center">✅</td>
      <td>경매 고유 ID</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">imp</code></td>
      <td>Imp[]</td>
      <td style="text-align: center">✅</td>
      <td>광고 슬롯 배열 (최소 1)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">site</code></td>
      <td>Site Object</td>
      <td style="text-align: center">권장</td>
      <td>웹 환경</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">app</code></td>
      <td>App Object</td>
      <td style="text-align: center">권장</td>
      <td>앱 환경</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">dooh</code></td>
      <td>DOOH Object</td>
      <td style="text-align: center">선택</td>
      <td>디지털 옥외광고</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">device</code></td>
      <td>Device Object</td>
      <td style="text-align: center">권장</td>
      <td>디바이스 정보</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">user</code></td>
      <td>User Object</td>
      <td style="text-align: center">권장</td>
      <td>사용자 정보</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">test</code></td>
      <td>Integer</td>
      <td style="text-align: center">기본 0</td>
      <td>테스트 모드 (1=test)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">at</code></td>
      <td>Integer</td>
      <td style="text-align: center">기본 2</td>
      <td>경매 타입: 1=First Price, 2=Second Price Plus, 500+ = 거래소 정의</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">tmax</code></td>
      <td>Integer (ms)</td>
      <td style="text-align: center">-</td>
      <td>최대 응답 대기 시간</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">wseat</code></td>
      <td>String[]</td>
      <td style="text-align: center">-</td>
      <td>허용 buyer seat ID</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bseat</code></td>
      <td>String[]</td>
      <td style="text-align: center">-</td>
      <td>차단 buyer seat ID</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">cur</code></td>
      <td>String[]</td>
      <td style="text-align: center">-</td>
      <td>허용 통화 (ISO-4217)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bcat</code></td>
      <td>String[]</td>
      <td style="text-align: center">-</td>
      <td>차단 광고 카테고리</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">badv</code></td>
      <td>String[]</td>
      <td style="text-align: center">-</td>
      <td>차단 광고주 도메인</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bapp</code></td>
      <td>String[]</td>
      <td style="text-align: center">-</td>
      <td>차단 앱 번들 ID</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">source</code></td>
      <td>Source Object</td>
      <td style="text-align: center">-</td>
      <td>인벤토리 소스 / SupplyChain</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">regs</code></td>
      <td>Regs Object</td>
      <td style="text-align: center">-</td>
      <td>규제 정보 (GDPR, COPPA, GPP)</td>
    </tr>
  </tbody>
</table>

<p><strong><code class="language-plaintext highlighter-rouge">site</code> vs <code class="language-plaintext highlighter-rouge">app</code> vs <code class="language-plaintext highlighter-rouge">dooh</code></strong>: 셋 중 하나만 사용 (mutually exclusive).</p>

<hr />

<h3 id="34-imp-object--핵심-필드">3.4 Imp Object — 핵심 필드</h3>

<table>
  <thead>
    <tr>
      <th>필드</th>
      <th>자료형</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">id</code></td>
      <td>String, 필수</td>
      <td>광고 슬롯 ID (보통 “1”부터)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">banner</code> / <code class="language-plaintext highlighter-rouge">video</code> / <code class="language-plaintext highlighter-rouge">audio</code> / <code class="language-plaintext highlighter-rouge">native</code></td>
      <td>Object</td>
      <td>광고 타입 (혼합 가능)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">pmp</code></td>
      <td>Pmp Object</td>
      <td>Private Marketplace 거래</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">displaymanager</code></td>
      <td>String</td>
      <td>SDK/플레이어 명</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">displaymanagerver</code></td>
      <td>String</td>
      <td>SDK 버전</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">instl</code></td>
      <td>Integer</td>
      <td>1=interstitial/전체화면</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">tagid</code></td>
      <td>String</td>
      <td>광고 태그 식별자</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bidfloor</code></td>
      <td>Float</td>
      <td>최저 입찰가 (CPM)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bidfloorcur</code></td>
      <td>String</td>
      <td>통화 (기본 “USD”)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">secure</code></td>
      <td>Integer</td>
      <td>1=HTTPS 필수</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">rwdd</code></td>
      <td>Integer</td>
      <td>1=보상형 광고</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ssai</code></td>
      <td>Integer</td>
      <td>0=unknown, 1=client-side, 2=hybrid, 3=server-side</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">exp</code></td>
      <td>Integer</td>
      <td>경매 후 노출까지 허용 초</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="35-video-object--핵심-필드-openrtb-26">3.5 Video Object — 핵심 필드 (OpenRTB 2.6)</h3>

<table>
  <thead>
    <tr>
      <th>필드</th>
      <th>자료형</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">mimes</code></td>
      <td>String[], <strong>필수</strong></td>
      <td>지원 MIME (예: <code class="language-plaintext highlighter-rouge">["video/mp4"]</code>)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">minduration</code> / <code class="language-plaintext highlighter-rouge">maxduration</code></td>
      <td>Integer (초)</td>
      <td>광고 길이 범위</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">rqddurs</code></td>
      <td>Integer[]</td>
      <td>정확한 허용 길이 (Live TV용, min/max와 상호배타)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">startdelay</code></td>
      <td>Integer</td>
      <td>광고 위치 (값 표 ↓)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">protocols</code></td>
      <td>Integer[]</td>
      <td>지원 VAST 버전 (AdCOM Creative Subtypes ↓)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">w</code> / <code class="language-plaintext highlighter-rouge">h</code></td>
      <td>Integer</td>
      <td>플레이어 크기 (DIPS)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">placement</code></td>
      <td>Integer</td>
      <td><strong>Deprecated</strong> (2.6-202303), <code class="language-plaintext highlighter-rouge">plcmt</code> 사용</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">plcmt</code></td>
      <td>Integer</td>
      <td>Plcmt Subtypes (값 표 ↓)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">linearity</code></td>
      <td>Integer</td>
      <td>1=Linear, 2=Non-Linear</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">skip</code></td>
      <td>Integer</td>
      <td>1=skippable</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">skipmin</code></td>
      <td>Integer</td>
      <td>스킵 가능 최소 광고 길이</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">skipafter</code></td>
      <td>Integer</td>
      <td>스킵 버튼 노출까지 초</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">minbitrate</code> / <code class="language-plaintext highlighter-rouge">maxbitrate</code></td>
      <td>Integer (kbps)</td>
      <td>비트레이트 범위</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">playbackmethod</code></td>
      <td>Integer[]</td>
      <td>재생 방식 (값 표 ↓)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">playbackend</code></td>
      <td>Integer</td>
      <td>재생 종료 시점 (값 표 ↓)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">delivery</code></td>
      <td>Integer[]</td>
      <td>1=streaming, 2=progressive, 3=download</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">api</code></td>
      <td>Integer[]</td>
      <td>API Frameworks (값 표 ↓)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">companionad</code></td>
      <td>Banner[]</td>
      <td>Companion 광고</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">companiontype</code></td>
      <td>Integer[]</td>
      <td>1=Static, 2=HTML, 3=iframe</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">podid</code></td>
      <td>String</td>
      <td>Ad Pod 식별자</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">podseq</code></td>
      <td>Integer</td>
      <td>Pod 시퀀스: -1=마지막, 0=임의, 1=첫 번째</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">slotinpod</code></td>
      <td>Integer</td>
      <td>Pod 내 슬롯: -1=마지막, 0=임의, 1=첫번째, 2=첫·마지막</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">maxseq</code></td>
      <td>Integer</td>
      <td>Dynamic Ad Pod 최대 광고 수</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">poddur</code></td>
      <td>Integer (초)</td>
      <td>Dynamic Pod 총 시간</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">mincpmpersec</code></td>
      <td>Float</td>
      <td>초당 최소 CPM (Dynamic Pod)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">boxingallowed</code></td>
      <td>Integer</td>
      <td>기본 1, 4:3→16:9 letterboxing 허용</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">pos</code></td>
      <td>Integer</td>
      <td>화면 위치 (Placement Positions)</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="36-adcom-enum--creative-subtypes--videoprotocols">3.6 AdCOM Enum — Creative Subtypes (= <code class="language-plaintext highlighter-rouge">video.protocols</code>)</h3>

<table>
  <thead>
    <tr>
      <th style="text-align: center">값</th>
      <th>정의</th>
      <th style="text-align: center">값</th>
      <th>정의</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center">1</td>
      <td>VAST 1.0</td>
      <td style="text-align: center">9</td>
      <td>DAAST 1.0</td>
    </tr>
    <tr>
      <td style="text-align: center">2</td>
      <td>VAST 2.0</td>
      <td style="text-align: center">10</td>
      <td>DAAST 1.0 Wrapper</td>
    </tr>
    <tr>
      <td style="text-align: center">3</td>
      <td>VAST 3.0</td>
      <td style="text-align: center">11</td>
      <td>VAST 4.1</td>
    </tr>
    <tr>
      <td style="text-align: center">4</td>
      <td>VAST 1.0 Wrapper</td>
      <td style="text-align: center">12</td>
      <td>VAST 4.1 Wrapper</td>
    </tr>
    <tr>
      <td style="text-align: center">5</td>
      <td>VAST 2.0 Wrapper</td>
      <td style="text-align: center">13</td>
      <td>VAST 4.2</td>
    </tr>
    <tr>
      <td style="text-align: center">6</td>
      <td>VAST 3.0 Wrapper</td>
      <td style="text-align: center">14</td>
      <td>VAST 4.2 Wrapper</td>
    </tr>
    <tr>
      <td style="text-align: center">7</td>
      <td>VAST 4.0</td>
      <td style="text-align: center"><strong>15</strong></td>
      <td><strong>VAST 4.3</strong></td>
    </tr>
    <tr>
      <td style="text-align: center">8</td>
      <td>VAST 4.0 Wrapper</td>
      <td style="text-align: center">16</td>
      <td>VAST 4.3 Wrapper</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>⚠️ <strong>주의</strong>: 일부 자료가 <code class="language-plaintext highlighter-rouge">15</code>를 VAST 4.2로 잘못 표기하는 경우가 있습니다. AdCOM 1.0 FINAL 기준 <strong>15 = VAST 4.3</strong>입니다.</p>
</blockquote>

<hr />

<h3 id="37-adcom-enum--plcmt-subtypes--videoplcmt">3.7 AdCOM Enum — Plcmt Subtypes (= <code class="language-plaintext highlighter-rouge">video.plcmt</code>)</h3>

<p>2023년 3월 업데이트로 기존 <code class="language-plaintext highlighter-rouge">placement</code> 필드가 Deprecated되고 <code class="language-plaintext highlighter-rouge">plcmt</code>로 대체되었습니다.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center">값</th>
      <th>정의</th>
      <th>핵심 조건</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center">1</td>
      <td><strong>Instream</strong></td>
      <td>Pre/Mid/Post-roll. 기본 sound-on. 비디오가 페이지의 주요 콘텐츠.</td>
    </tr>
    <tr>
      <td style="text-align: center">2</td>
      <td><strong>Accompanying Content</strong></td>
      <td>영상 콘텐츠 부속 재생. 뷰포트 진입 시 재생 시작.</td>
    </tr>
    <tr>
      <td style="text-align: center">3</td>
      <td><strong>Interstitial</strong></td>
      <td>콘텐츠 없이 단독 재생. 화면 대부분 차지.</td>
    </tr>
    <tr>
      <td style="text-align: center">4</td>
      <td><strong>No Content / Standalone</strong></td>
      <td>콘텐츠 없는 단독 재생 (슬라이드쇼, 네이티브 피드, 스티키 등)</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="38-adcom-enum--api-frameworks--videoapi">3.8 AdCOM Enum — API Frameworks (= <code class="language-plaintext highlighter-rouge">video.api</code>)</h3>

<table>
  <thead>
    <tr>
      <th style="text-align: center">값</th>
      <th>정의</th>
      <th>비고</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center">1</td>
      <td>VPAID 1.0</td>
      <td>Deprecated</td>
    </tr>
    <tr>
      <td style="text-align: center">2</td>
      <td>VPAID 2.0</td>
      <td>Deprecated (2022~)</td>
    </tr>
    <tr>
      <td style="text-align: center">3</td>
      <td>MRAID 1.0</td>
      <td>모바일 리치미디어</td>
    </tr>
    <tr>
      <td style="text-align: center">4</td>
      <td>ORMMA</td>
      <td>Deprecated</td>
    </tr>
    <tr>
      <td style="text-align: center">5</td>
      <td>MRAID 2.0</td>
      <td> </td>
    </tr>
    <tr>
      <td style="text-align: center">6</td>
      <td>MRAID 3.0</td>
      <td>최신 MRAID</td>
    </tr>
    <tr>
      <td style="text-align: center">7</td>
      <td><strong>OMID 1.0</strong></td>
      <td>Open Measurement — 가시성 검증</td>
    </tr>
    <tr>
      <td style="text-align: center">8</td>
      <td><strong>SIMID 1.0</strong></td>
      <td>VPAID 후속</td>
    </tr>
    <tr>
      <td style="text-align: center">9</td>
      <td><strong>SIMID 1.1</strong></td>
      <td>최신 인터랙티브</td>
    </tr>
    <tr>
      <td style="text-align: center">500+</td>
      <td>Vendor-specific</td>
      <td>거래소 정의</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="39-adcom-enum--start-delay-modes--videostartdelay">3.9 AdCOM Enum — Start Delay Modes (= <code class="language-plaintext highlighter-rouge">video.startdelay</code>)</h3>

<table>
  <thead>
    <tr>
      <th style="text-align: center">값</th>
      <th>의미</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center">&gt; 0</td>
      <td>Mid-Roll (값 = 시작 지연 초)</td>
    </tr>
    <tr>
      <td style="text-align: center">0</td>
      <td>Pre-Roll</td>
    </tr>
    <tr>
      <td style="text-align: center">-1</td>
      <td>Generic Mid-Roll</td>
    </tr>
    <tr>
      <td style="text-align: center">-2</td>
      <td>Generic Post-Roll</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="310-adcom-enum--playback-methods--videoplaybackmethod">3.10 AdCOM Enum — Playback Methods (= <code class="language-plaintext highlighter-rouge">video.playbackmethod</code>)</h3>

<table>
  <thead>
    <tr>
      <th style="text-align: center">값</th>
      <th>정의</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center">1</td>
      <td>Page Load 시 자동 재생, <strong>Sound On</strong></td>
    </tr>
    <tr>
      <td style="text-align: center">2</td>
      <td>Page Load 시 자동 재생, Sound Off (기본)</td>
    </tr>
    <tr>
      <td style="text-align: center">3</td>
      <td>클릭 시 재생, Sound On</td>
    </tr>
    <tr>
      <td style="text-align: center">4</td>
      <td>Mouse-Over 시 재생, Sound On</td>
    </tr>
    <tr>
      <td style="text-align: center">5</td>
      <td>Viewport 진입 시 재생, Sound On</td>
    </tr>
    <tr>
      <td style="text-align: center">6</td>
      <td>Viewport 진입 시 재생, Sound Off (기본)</td>
    </tr>
    <tr>
      <td style="text-align: center">7</td>
      <td>Continuous Playback (플레이리스트 자동 연속 재생)</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="311-adcom-enum--playback-cessation-modes--videoplaybackend">3.11 AdCOM Enum — Playback Cessation Modes (= <code class="language-plaintext highlighter-rouge">video.playbackend</code>)</h3>

<table>
  <thead>
    <tr>
      <th style="text-align: center">값</th>
      <th>정의</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center">1</td>
      <td>비디오 완료 또는 사용자 종료 시</td>
    </tr>
    <tr>
      <td style="text-align: center">2</td>
      <td>Viewport 벗어남 또는 사용자 종료 시</td>
    </tr>
    <tr>
      <td style="text-align: center">3</td>
      <td>Viewport 벗어나면 Floating/Slider로 계속, 완료/사용자 종료까지</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="312-adcom-enum--slot-position-in-pod--videoslotinpod">3.12 AdCOM Enum — Slot Position in Pod (= <code class="language-plaintext highlighter-rouge">video.slotinpod</code>)</h3>

<table>
  <thead>
    <tr>
      <th style="text-align: center">값</th>
      <th>정의</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center">-1</td>
      <td>Pod 내 마지막 광고</td>
    </tr>
    <tr>
      <td style="text-align: center">0</td>
      <td>Pod 내 임의 광고</td>
    </tr>
    <tr>
      <td style="text-align: center">1</td>
      <td>Pod 내 첫 번째 광고</td>
    </tr>
    <tr>
      <td style="text-align: center">2</td>
      <td>Pod 내 첫 번째 또는 마지막 광고</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="313-bidrequest-실전-예시-video-openrtb-26-공식-예시-인용">3.13 BidRequest 실전 예시 (Video, OpenRTB 2.6 공식 예시 인용)</h3>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1234567893"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"at"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w">
  </span><span class="nl">"tmax"</span><span class="p">:</span><span class="w"> </span><span class="mi">120</span><span class="p">,</span><span class="w">
  </span><span class="nl">"imp"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="w">
    </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"bidfloor"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.03</span><span class="p">,</span><span class="w">
    </span><span class="nl">"video"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"w"</span><span class="p">:</span><span class="w"> </span><span class="mi">640</span><span class="p">,</span><span class="w">
      </span><span class="nl">"h"</span><span class="p">:</span><span class="w"> </span><span class="mi">480</span><span class="p">,</span><span class="w">
      </span><span class="nl">"pos"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
      </span><span class="nl">"startdelay"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
      </span><span class="nl">"minduration"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span><span class="w">
      </span><span class="nl">"maxduration"</span><span class="p">:</span><span class="w"> </span><span class="mi">30</span><span class="p">,</span><span class="w">
      </span><span class="nl">"maxextended"</span><span class="p">:</span><span class="w"> </span><span class="mi">30</span><span class="p">,</span><span class="w">
      </span><span class="nl">"minbitrate"</span><span class="p">:</span><span class="w"> </span><span class="mi">300</span><span class="p">,</span><span class="w">
      </span><span class="nl">"maxbitrate"</span><span class="p">:</span><span class="w"> </span><span class="mi">1500</span><span class="p">,</span><span class="w">
      </span><span class="nl">"apis"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">],</span><span class="w">
      </span><span class="nl">"protocols"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="mi">3</span><span class="p">],</span><span class="w">
      </span><span class="nl">"mimes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"video/x-flv"</span><span class="p">,</span><span class="w"> </span><span class="s2">"video/mp4"</span><span class="p">,</span><span class="w"> </span><span class="s2">"application/javascript"</span><span class="p">],</span><span class="w">
      </span><span class="nl">"linearity"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
      </span><span class="nl">"boxingallowed"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
      </span><span class="nl">"playbackmethod"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">3</span><span class="p">],</span><span class="w">
      </span><span class="nl">"delivery"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mi">2</span><span class="p">],</span><span class="w">
      </span><span class="nl">"battr"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mi">13</span><span class="p">,</span><span class="w"> </span><span class="mi">14</span><span class="p">],</span><span class="w">
      </span><span class="nl">"companionad"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w">
          </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1234567893-1"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"w"</span><span class="p">:</span><span class="w"> </span><span class="mi">300</span><span class="p">,</span><span class="w"> </span><span class="nl">"h"</span><span class="p">:</span><span class="w"> </span><span class="mi">250</span><span class="p">,</span><span class="w"> </span><span class="nl">"pos"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
          </span><span class="nl">"battr"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mi">13</span><span class="p">,</span><span class="w"> </span><span class="mi">14</span><span class="p">],</span><span class="w">
          </span><span class="nl">"expdir"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="mi">4</span><span class="p">]</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">],</span><span class="w">
      </span><span class="nl">"companiontype"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">]</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}],</span><span class="w">
  </span><span class="nl">"site"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1345135123"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Site ABCD"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"domain"</span><span class="p">:</span><span class="w"> </span><span class="s2">"siteabcd.com"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"page"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://siteabcd.com/page.htm"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"publisher"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pub12345"</span><span class="p">,</span><span class="w"> </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Publisher A"</span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>(출처: OpenRTB 2.6 Spec §6.2.4 Example 4 – Video)</p>

<hr />

<h3 id="314-bid-object--bidresponse-핵심-필드">3.14 Bid Object — BidResponse 핵심 필드</h3>

<table>
  <thead>
    <tr>
      <th>필드</th>
      <th>자료형</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">id</code></td>
      <td>String, <strong>필수</strong></td>
      <td>DSP 측 입찰 ID</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">impid</code></td>
      <td>String, <strong>필수</strong></td>
      <td>응답 대상 <code class="language-plaintext highlighter-rouge">Imp.id</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">price</code></td>
      <td>Float, <strong>필수</strong></td>
      <td>CPM 입찰가</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">nurl</code></td>
      <td>URL</td>
      <td>Win Notice URL (낙찰 시 호출)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">burl</code></td>
      <td>URL</td>
      <td>Billing Notice URL (Billable 판정 시)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">lurl</code></td>
      <td>URL</td>
      <td>Loss Notice URL (낙찰 실패 시)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">adm</code></td>
      <td>String</td>
      <td>광고 markup (VAST XML 또는 URL)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">adid</code></td>
      <td>String</td>
      <td>사전 등록된 광고 ID</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">adomain</code></td>
      <td>String[]</td>
      <td>광고주 도메인</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bundle</code></td>
      <td>String</td>
      <td>앱 번들 ID</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">iurl</code></td>
      <td>URL</td>
      <td>광고 대표 이미지 URL</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">cid</code></td>
      <td>String</td>
      <td>캠페인 ID</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">crid</code></td>
      <td>String</td>
      <td>크리에이티브 ID</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">cat</code></td>
      <td>String[]</td>
      <td>IAB Content Categories</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">attr</code></td>
      <td>Integer[]</td>
      <td>Creative Attributes</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">apis</code></td>
      <td>Integer[]</td>
      <td>지원 API</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">protocol</code></td>
      <td>Integer</td>
      <td>VAST 버전 (AdCOM Creative Subtypes)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">w</code> / <code class="language-plaintext highlighter-rouge">h</code></td>
      <td>Integer</td>
      <td>크리에이티브 크기</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">exp</code></td>
      <td>Integer</td>
      <td>노출 유효 초</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">dur</code></td>
      <td>Integer</td>
      <td>비디오/오디오 길이 (초)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">mtype</code></td>
      <td>Integer</td>
      <td>1=Banner, 2=Video, 3=Audio, 4=Native</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">slotinpod</code></td>
      <td>Integer</td>
      <td>Pod 슬롯 위치</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="315-bidresponse--markup-전달-방식-비교">3.15 BidResponse — Markup 전달 방식 비교</h3>

<table>
  <thead>
    <tr>
      <th>방식</th>
      <th>사용 필드</th>
      <th>장점</th>
      <th>단점</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Markup in Bid</strong></td>
      <td><code class="language-plaintext highlighter-rouge">adm</code></td>
      <td>동시성 ↑, Forfeit 위험 ↓</td>
      <td>대역폭 비용 ↑ (모든 응답에 markup)</td>
    </tr>
    <tr>
      <td><strong>Markup on Win Notice</strong></td>
      <td><code class="language-plaintext highlighter-rouge">nurl</code> (응답 본문에 markup)</td>
      <td>대역폭 절약, 사후 최적화 가능</td>
      <td>Win Notice HTTP 실패 시 Forfeit</td>
    </tr>
  </tbody>
</table>

<p><strong>Forfeit</strong>: 낙찰됐지만 markup 전달 실패로 광고가 안 나가는 경우. 이때도 노출 카운트는 0.</p>

<hr />

<h3 id="316-openrtb-substitution-macros--dollar-형식">3.16 OpenRTB Substitution Macros — <code class="language-plaintext highlighter-rouge">${DOLLAR}</code> 형식</h3>

<p>VAST 매크로(<code class="language-plaintext highlighter-rouge">[BRACKET]</code>)와 <strong>다른 시스템</strong>입니다. 이건 거래소가 win/billing notice URL과 <code class="language-plaintext highlighter-rouge">adm</code> markup에 치환하는 매크로입니다.</p>

<table>
  <thead>
    <tr>
      <th>매크로</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">${AUCTION_ID}</code></td>
      <td>경매 ID (BidRequest.id)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">${AUCTION_BID_ID}</code></td>
      <td>입찰 ID (BidResponse.bidid)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">${AUCTION_IMP_ID}</code></td>
      <td>Imp.id</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">${AUCTION_SEAT_ID}</code></td>
      <td>낙찰 Seat ID</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">${AUCTION_AD_ID}</code></td>
      <td>bid.adid</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">${AUCTION_PRICE}</code></td>
      <td>청산가 (Clearing Price)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">${AUCTION_CURRENCY}</code></td>
      <td>통화</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">${AUCTION_MBR}</code></td>
      <td>Market Bid Ratio = clearance / bid</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">${AUCTION_LOSS}</code></td>
      <td>패찰 사유 코드</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">${AUCTION_MIN_TO_WIN}</code></td>
      <td>낙찰을 위한 최소 입찰가</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">${AUCTION_MULTIPLIER}</code></td>
      <td>노출 총 수량 (DOOH 등)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">${AUCTION_IMP_TS}</code></td>
      <td>노출 시점 (Unix ms)</td>
    </tr>
  </tbody>
</table>

<p><strong>암호화 치환</strong>: <code class="language-plaintext highlighter-rouge">${AUCTION_PRICE:B64}</code> 형식으로 Base64 등 거래소-bidder 간 합의된 알고리즘 적용 가능.</p>

<hr />

<h2 id="4-두-표준의-연동--field-매핑-표">4. 두 표준의 연동 — Field 매핑 표</h2>

<p>DSP가 BidRequest의 어떤 필드를 보고 VAST의 어떤 요소를 만들어야 하는지의 매핑입니다.</p>

<table>
  <thead>
    <tr>
      <th>OpenRTB 필드</th>
      <th>VAST 대응</th>
      <th>비고</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">imp.video.protocols</code></td>
      <td><code class="language-plaintext highlighter-rouge">&lt;VAST version=...&gt;</code></td>
      <td>DSP는 협상된 버전으로 응답</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">imp.video.api</code></td>
      <td><code class="language-plaintext highlighter-rouge">&lt;MediaFile apiFramework=...&gt;</code></td>
      <td>OMID/SIMID 매칭</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">imp.video.mimes</code></td>
      <td><code class="language-plaintext highlighter-rouge">&lt;MediaFile type=...&gt;</code></td>
      <td>MIME 일치</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">imp.video.minduration/maxduration</code></td>
      <td><code class="language-plaintext highlighter-rouge">&lt;Duration&gt;</code></td>
      <td>범위 내</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">imp.video.skip + skipafter</code></td>
      <td><code class="language-plaintext highlighter-rouge">&lt;Linear skipoffset=...&gt;</code></td>
      <td>스킵 정책</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">imp.video.linearity</code></td>
      <td><code class="language-plaintext highlighter-rouge">&lt;Linear&gt;</code> or <code class="language-plaintext highlighter-rouge">&lt;NonLinearAds&gt;</code></td>
      <td>광고 형태</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">imp.video.companionad</code></td>
      <td><code class="language-plaintext highlighter-rouge">&lt;CompanionAds&gt;</code></td>
      <td>동반 광고</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">imp.video.podid + slotinpod</code></td>
      <td><code class="language-plaintext highlighter-rouge">&lt;Ad sequence&gt;</code>, <code class="language-plaintext highlighter-rouge">breakId</code></td>
      <td>Pod 매핑</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bid.adm</code> (URL)</td>
      <td>VAST XML 응답</td>
      <td>플레이어가 GET 호출</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bid.adm</code> (XML 인라인)</td>
      <td>VAST XML 직접</td>
      <td>hop 없음</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bid.price</code></td>
      <td><code class="language-plaintext highlighter-rouge">&lt;Pricing&gt;</code> (선택)</td>
      <td>가격 동기화</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bid.adomain</code></td>
      <td><code class="language-plaintext highlighter-rouge">&lt;Advertiser&gt;</code> (선택)</td>
      <td>광고주 도메인</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bid.crid</code></td>
      <td><code class="language-plaintext highlighter-rouge">&lt;Creative id=...&gt;</code></td>
      <td>크리에이티브 식별</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">regs.gdpr</code> + <code class="language-plaintext highlighter-rouge">user.consent</code></td>
      <td><code class="language-plaintext highlighter-rouge">[GDPRCONSENT]</code> 매크로 치환</td>
      <td>동의 전달</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">regs.us_privacy</code></td>
      <td><code class="language-plaintext highlighter-rouge">[USPRIVACY]</code> 매크로 치환</td>
      <td>CCPA</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">device.ifa</code></td>
      <td><code class="language-plaintext highlighter-rouge">[IFA]</code> 매크로 치환</td>
      <td>광고 ID</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="5-자주-헷갈리는-개념-정리">5. 자주 헷갈리는 개념 정리</h2>

<h3 id="51-impression-vs-viewableimpression-vs-billable-impression">5.1 Impression vs ViewableImpression vs Billable Impression</h3>

<table>
  <thead>
    <tr>
      <th>종류</th>
      <th>발화 시점</th>
      <th>사용 필드</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Impression</strong></td>
      <td>VAST XML 로드 완료 → <code class="language-plaintext highlighter-rouge">&lt;MediaFile&gt;</code> 첫 바이트 전송</td>
      <td>VAST <code class="language-plaintext highlighter-rouge">&lt;Impression&gt;</code></td>
    </tr>
    <tr>
      <td><strong>ViewableImpression</strong></td>
      <td>MRC 기준 충족 (50% 영역, 2초 연속)</td>
      <td>VAST <code class="language-plaintext highlighter-rouge">&lt;ViewableImpression&gt;&lt;Viewable&gt;</code></td>
    </tr>
    <tr>
      <td><strong>Billable Impression</strong></td>
      <td>거래소 정책 기준 (보통 viewable)</td>
      <td>OpenRTB <code class="language-plaintext highlighter-rouge">bid.burl</code></td>
    </tr>
  </tbody>
</table>

<p><strong>핵심</strong>: 셋은 같은 광고의 서로 다른 단계 카운트. 광고주 계약서에 어떤 기준이 청구 대상인지 명시해야 합니다.</p>

<hr />

<h3 id="52-auction-types--first-price-vs-second-price-plus">5.2 Auction Types — First Price vs Second Price Plus</h3>

<table>
  <thead>
    <tr>
      <th>타입</th>
      <th style="text-align: center">OpenRTB <code class="language-plaintext highlighter-rouge">at</code></th>
      <th>청산가 결정</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>First Price</td>
      <td style="text-align: center">1</td>
      <td>낙찰자가 자신의 입찰가 그대로 지불</td>
    </tr>
    <tr>
      <td>Second Price Plus</td>
      <td style="text-align: center">2</td>
      <td>2위 입찰가 + 거래소 정의 증분 (보통 $0.01, 거래소마다 다름)</td>
    </tr>
    <tr>
      <td>Exchange-specific</td>
      <td style="text-align: center">500+</td>
      <td>거래소 정의 (e.g., Soft Floor Auction)</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>사실 확인 정정</strong>: OpenRTB 명세는 “Second Price Plus”라고만 명시할 뿐, <strong>+$0.01은 표준이 아닙니다</strong>. 거래소 정책마다 다르며, 일부는 Bid Floor + 증분, 일부는 2위 + 증분을 사용합니다.</p>
</blockquote>

<p><strong>산업 동향</strong>: Google Ad Manager(2019), Open Bidding 등 주요 SSP가 First Price로 전환. 2026년 현재 First Price가 비디오 시장 다수.</p>

<hr />

<h3 id="53-vast-bracket-vs-openrtb-dollar">5.3 VAST [BRACKET] vs OpenRTB ${DOLLAR}</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>VAST 매크로</th>
      <th>OpenRTB 매크로</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>형식</td>
      <td><code class="language-plaintext highlighter-rouge">[NAME]</code></td>
      <td><code class="language-plaintext highlighter-rouge">${NAME}</code></td>
    </tr>
    <tr>
      <td>치환 주체</td>
      <td>비디오 플레이어</td>
      <td>RTB 거래소</td>
    </tr>
    <tr>
      <td>치환 시점</td>
      <td>트래킹 URL 호출 직전</td>
      <td>win/billing notice 호출 직전</td>
    </tr>
    <tr>
      <td>예시</td>
      <td><code class="language-plaintext highlighter-rouge">[ERRORCODE]</code>, <code class="language-plaintext highlighter-rouge">[CACHEBUSTING]</code>, <code class="language-plaintext highlighter-rouge">[IFA]</code></td>
      <td><code class="language-plaintext highlighter-rouge">${AUCTION_PRICE}</code>, <code class="language-plaintext highlighter-rouge">${AUCTION_ID}</code></td>
    </tr>
    <tr>
      <td>정의 위치</td>
      <td>VAST 4.x 명세</td>
      <td>OpenRTB 2.6 §4.4</td>
    </tr>
  </tbody>
</table>

<p><strong>섞임 주의</strong>: <code class="language-plaintext highlighter-rouge">adm</code> 필드에 VAST URL을 넣을 때 OpenRTB <code class="language-plaintext highlighter-rouge">${AUCTION_PRICE}</code>와 VAST <code class="language-plaintext highlighter-rouge">[CACHEBUSTING]</code>이 함께 들어갈 수 있습니다. 각각 다른 단계에서 치환됩니다.</p>

<hr />

<h3 id="54-ssai-vs-csai">5.4 SSAI vs CSAI</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>CSAI (Client-Side)</th>
      <th>SSAI (Server-Side)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">imp.ssai</code> 값</td>
      <td>1</td>
      <td>3 (또는 2=hybrid)</td>
    </tr>
    <tr>
      <td>광고 삽입 위치</td>
      <td>플레이어가 직접</td>
      <td>서버가 콘텐츠 스트림에 stitching</td>
    </tr>
    <tr>
      <td>광고 차단</td>
      <td>영향 받음 (uBlock 등)</td>
      <td><strong>회피 가능</strong></td>
    </tr>
    <tr>
      <td>트래킹</td>
      <td>클라이언트 발화</td>
      <td>서버 발화 (또는 일부 client-side)</td>
    </tr>
    <tr>
      <td>Mezzanine 필요</td>
      <td>보통 불필요</td>
      <td><strong>필수</strong> (트랜스코딩 원본)</td>
    </tr>
    <tr>
      <td>CTV 채택</td>
      <td>일부</td>
      <td><strong>표준</strong></td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="6-ctv-환경-특이사항">6. CTV 환경 특이사항</h2>

<h3 id="61-ad-pod--연속-광고-묶음">6.1 Ad Pod — 연속 광고 묶음</h3>

<p>전통 TV 광고처럼 한 브레이크에 여러 광고가 연달아 나가는 구조. OpenRTB 2.6에서 본격 표준화.</p>

<p><strong>Static Pod</strong> (사전 정의된 슬롯 수):</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"imp"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1"</span><span class="p">,</span><span class="w"> </span><span class="nl">"video"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"podid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pod-A"</span><span class="p">,</span><span class="w"> </span><span class="nl">"slotinpod"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="err">...</span><span class="p">}},</span><span class="w">
    </span><span class="p">{</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2"</span><span class="p">,</span><span class="w"> </span><span class="nl">"video"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"podid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pod-A"</span><span class="p">,</span><span class="w"> </span><span class="nl">"slotinpod"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="err">...</span><span class="p">}},</span><span class="w">
    </span><span class="p">{</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3"</span><span class="p">,</span><span class="w"> </span><span class="nl">"video"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"podid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pod-A"</span><span class="p">,</span><span class="w"> </span><span class="nl">"slotinpod"</span><span class="p">:</span><span class="w"> </span><span class="mi">-1</span><span class="p">,</span><span class="w"> </span><span class="err">...</span><span class="p">}}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Dynamic Pod</strong> (총 시간만 정해두고 DSP가 자유롭게):</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"imp"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="w">
    </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"video"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"podid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pod-B"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"poddur"</span><span class="p">:</span><span class="w"> </span><span class="mi">90</span><span class="p">,</span><span class="w">
      </span><span class="nl">"maxseq"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span><span class="w">
      </span><span class="nl">"minduration"</span><span class="p">:</span><span class="w"> </span><span class="mi">15</span><span class="p">,</span><span class="w">
      </span><span class="nl">"maxduration"</span><span class="p">:</span><span class="w"> </span><span class="mi">30</span><span class="p">,</span><span class="w">
      </span><span class="nl">"mincpmpersec"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.10</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>VAST 쪽 대응:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;Ad</span> <span class="na">sequence=</span><span class="s">"1"</span> <span class="na">breakId=</span><span class="s">"pod-A"</span> <span class="na">breakIndex=</span><span class="s">"1"</span><span class="nt">&gt;</span>...<span class="nt">&lt;/Ad&gt;</span>
<span class="nt">&lt;Ad</span> <span class="na">sequence=</span><span class="s">"2"</span> <span class="na">breakId=</span><span class="s">"pod-A"</span> <span class="na">breakIndex=</span><span class="s">"2"</span><span class="nt">&gt;</span>...<span class="nt">&lt;/Ad&gt;</span>
</code></pre></div></div>

<hr />

<h3 id="62-ctv-환경-식별">6.2 CTV 환경 식별</h3>

<table>
  <thead>
    <tr>
      <th>OpenRTB 필드</th>
      <th>값</th>
      <th>의미</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">device.devicetype</code></td>
      <td>3</td>
      <td>Connected TV</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">device.devicetype</code></td>
      <td>7</td>
      <td>Set Top Box</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">device.devicetype</code></td>
      <td>6</td>
      <td>Connected Device (general)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">app.bundle</code></td>
      <td>CTV Store ID</td>
      <td>OTT/CTV Store Assigned App ID Guidelines 준수</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">imp.video.plcmt</code></td>
      <td>1</td>
      <td>Instream (CTV는 거의 항상 1)</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="7-프라이버시-프레임워크">7. 프라이버시 프레임워크</h2>

<table>
  <thead>
    <tr>
      <th>규제</th>
      <th>OpenRTB 필드</th>
      <th>VAST 매크로</th>
      <th>비고</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>GDPR (EU)</td>
      <td><code class="language-plaintext highlighter-rouge">regs.gdpr</code>, <code class="language-plaintext highlighter-rouge">user.consent</code> (TCF 2.x)</td>
      <td><code class="language-plaintext highlighter-rouge">[GDPRCONSENT]</code></td>
      <td>IAB Europe TCF</td>
    </tr>
    <tr>
      <td>CCPA (California)</td>
      <td><code class="language-plaintext highlighter-rouge">regs.us_privacy</code></td>
      <td><code class="language-plaintext highlighter-rouge">[USPRIVACY]</code></td>
      <td>US Privacy String v1.0</td>
    </tr>
    <tr>
      <td><strong>GPP</strong> (Global)</td>
      <td><code class="language-plaintext highlighter-rouge">regs.gpp</code>, <code class="language-plaintext highlighter-rouge">regs.gpp_sid</code></td>
      <td>(전용 매크로 부재 - GPP 자체 전달)</td>
      <td>Global Privacy Platform (2022~)</td>
    </tr>
    <tr>
      <td>COPPA (US &lt;13세)</td>
      <td><code class="language-plaintext highlighter-rouge">regs.coppa</code></td>
      <td><code class="language-plaintext highlighter-rouge">[REGULATIONS]</code></td>
      <td>0/1 플래그</td>
    </tr>
    <tr>
      <td>ATT (iOS)</td>
      <td><code class="language-plaintext highlighter-rouge">device.lmt</code>, <code class="language-plaintext highlighter-rouge">device.ifa</code> (제로 UUID)</td>
      <td><code class="language-plaintext highlighter-rouge">[LIMITADTRACKING]</code></td>
      <td>iOS 14.5+</td>
    </tr>
  </tbody>
</table>

<p><strong>GPP</strong>는 GDPR/CCPA/기타 지역 규제 문자열을 통합한 차세대 포맷으로, 2022년 IAB Tech Lab 출시. 향후 표준이 GPP로 수렴할 것으로 예상.</p>

<hr />

<h2 id="8-구현-체크리스트">8. 구현 체크리스트</h2>

<h3 id="81-vast-응답-생성-측-ad-server--dsp">8.1 VAST 응답 생성 측 (Ad Server / DSP)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>☐ &lt;VAST version="..."&gt; — 협상된 버전 사용
☐ &lt;AdServingId&gt; 포함 (v4.0+ 필수, UUID 권장)
☐ &lt;UniversalAdId&gt; 포함 (중복 광고 탐지)
☐ &lt;AdTitle&gt; 포함 (InLine 필수)
☐ &lt;Impression&gt; 최소 1개, [CACHEBUSTING] 매크로 포함
☐ &lt;Error&gt; URL + [ERRORCODE] 매크로
☐ &lt;MediaFile&gt; type/width/height/delivery 모두 명시
☐ 다양한 bitrate 옵션 제공 (ABR)
☐ TrackingEvents: start, 4분위, complete 최소 포함
☐ ViewableImpression 3종 (Viewable/NotViewable/ViewUndetermined)
☐ AdVerifications (OMID) 포함
☐ Companion 광고 1개 이상 권장 (CTV)
☐ Wrapper 사용 시 followAdditionalWrappers, fallbackOnNoAd 명시
</code></pre></div></div>

<h3 id="82-vast-소비-측-video-player--sdk">8.2 VAST 소비 측 (Video Player / SDK)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>☐ Wrapper 체인 hop 제한 (3~5)
☐ tmax / 자체 타임아웃 내 응답 없으면 Error 301 발화
☐ 모든 트래킹 URL에서 매크로 치환 ([CACHEBUSTING] 필수)
☐ &lt;Error&gt; URL 호출 시 [ERRORCODE] 치환
☐ OMID SDK 초기화 (AdVerifications)
☐ ViewableImpression 판정 로직 (MRC 기준)
☐ SkipOffset 후 스킵 버튼 노출
☐ MediaFile 선택: bitrate × 네트워크 × 화면 크기 고려
☐ [PODSEQUENCE]/[PODCOUNT] 등 Pod 매크로 지원 (v4.1+)
</code></pre></div></div>

<h3 id="83-openrtb-bidrequest-송신-측-ssp--exchange">8.3 OpenRTB BidRequest 송신 측 (SSP / Exchange)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>☐ id 고유성 보장
☐ tmax 합리적 설정 (80~120ms 권장)
☐ video.protocols / mimes / api 정확히 명시
☐ video.plcmt 사용 (placement Deprecated)
☐ device.ifa, device.ua 포함 (프라이버시 허용 시)
☐ regs.gdpr / us_privacy / gpp 적용
☐ source.schain (Supply Chain) 포함 (Ads.txt/Sellers.json 검증용)
☐ bcat / badv / bapp 차단 리스트 적용
☐ site/app/dooh 중 정확히 하나
</code></pre></div></div>

<h3 id="84-openrtb-bidresponse-응답-측-dsp">8.4 OpenRTB BidResponse 응답 측 (DSP)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>☐ id, impid, price 필수
☐ adm 또는 nurl 중 하나로 markup 전달
☐ adomain 정확히 (block list 검증용)
☐ crid 일관성 (이전 노출과 동일 ID)
☐ mtype 명시 (2=Video)
☐ apis / protocol 명시
☐ 응답 시간 tmax 내 보장
☐ nurl/burl/lurl URL에 ${AUCTION_PRICE} 등 매크로 포함
</code></pre></div></div>

<hr />

<h2 id="9-자주-발생하는-실전-문제--원인">9. 자주 발생하는 실전 문제 &amp; 원인</h2>

<table>
  <thead>
    <tr>
      <th>증상</th>
      <th>1순위 원인</th>
      <th>해결</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bid.price</code>와 청구 금액 불일치</td>
      <td><code class="language-plaintext highlighter-rouge">${AUCTION_PRICE}</code> 미치환 (markup 그대로)</td>
      <td>nurl/burl, adm 모두 매크로 치환 확인</td>
    </tr>
    <tr>
      <td>Impression 카운트 0</td>
      <td><code class="language-plaintext highlighter-rouge">&lt;Impression&gt;</code> URL 호출 실패 / <code class="language-plaintext highlighter-rouge">[CACHEBUSTING]</code> 미치환</td>
      <td>플레이어 로그 확인, 캐시 헤더 점검</td>
    </tr>
    <tr>
      <td>Viewable rate 비정상 낮음</td>
      <td><code class="language-plaintext highlighter-rouge">&lt;Viewable&gt;</code> URL이 <code class="language-plaintext highlighter-rouge">[VIEWABLE]</code> 매크로 의존</td>
      <td>MRC 기준 판정 로직 구현 확인</td>
    </tr>
    <tr>
      <td>CTV 광고 안 나옴</td>
      <td><code class="language-plaintext highlighter-rouge">protocols</code> 배열에 VAST 4.x 미포함</td>
      <td><code class="language-plaintext highlighter-rouge">[7, 8, 11, 12, 13, 14, 15, 16]</code> 포함</td>
    </tr>
    <tr>
      <td>Error 302 (Wrapper limit)</td>
      <td>Wrapper 체인이 거래소→DSP→3rd party→…로 너무 김</td>
      <td>DSP-거래소 직결 / 체인 단축</td>
    </tr>
    <tr>
      <td>Error 405 (no displayable media)</td>
      <td><code class="language-plaintext highlighter-rouge">mimes</code> mismatch, codec 미지원</td>
      <td>MediaFile 다양화, codec 명시</td>
    </tr>
    <tr>
      <td>Forfeit 발생</td>
      <td><code class="language-plaintext highlighter-rouge">nurl</code> 호출 후 응답 본문 비어있음</td>
      <td>adm 방식으로 전환 또는 nurl 안정성 점검</td>
    </tr>
    <tr>
      <td>iOS에서 IFA 빈 값</td>
      <td>ATT 미동의 / <code class="language-plaintext highlighter-rouge">device.lmt=1</code></td>
      <td>LAT 환경 대비 광고 폴백 준비</td>
    </tr>
    <tr>
      <td>GDPR 영역 노출 0</td>
      <td><code class="language-plaintext highlighter-rouge">user.consent</code> 누락</td>
      <td>TCF 2.x 동의 문자열 전달 확인</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="10-글로서리-glossary">10. 글로서리 (Glossary)</h2>

<table>
  <thead>
    <tr>
      <th>용어</th>
      <th>풀이</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>VAST</strong></td>
      <td>Video Ad Serving Template — 비디오 광고 명세 XML</td>
    </tr>
    <tr>
      <td><strong>VPAID</strong></td>
      <td>Video Player-Ad Interface Definition — 인터랙티브 광고 JS API (Deprecated)</td>
    </tr>
    <tr>
      <td><strong>SIMID</strong></td>
      <td>Secure Interactive Media Interface Definition — VPAID 후속</td>
    </tr>
    <tr>
      <td><strong>OMID</strong></td>
      <td>Open Measurement Interface Definition — 가시성·검증 SDK</td>
    </tr>
    <tr>
      <td><strong>OpenRTB</strong></td>
      <td>Open Real-Time Bidding — 실시간 입찰 프로토콜</td>
    </tr>
    <tr>
      <td><strong>AdCOM</strong></td>
      <td>Advertising Common Object Model — OpenRTB/OpenDirect 공용 사전</td>
    </tr>
    <tr>
      <td><strong>DSP</strong></td>
      <td>Demand-Side Platform — 광고주 측 입찰 시스템</td>
    </tr>
    <tr>
      <td><strong>SSP</strong></td>
      <td>Supply-Side Platform — 매체사 측 인벤토리 관리 시스템</td>
    </tr>
    <tr>
      <td><strong>DMP</strong></td>
      <td>Data Management Platform — 청중 데이터 통합</td>
    </tr>
    <tr>
      <td><strong>PMP</strong></td>
      <td>Private Marketplace — 사전 합의된 1:1 또는 1:다수 거래</td>
    </tr>
    <tr>
      <td><strong>SSAI</strong></td>
      <td>Server-Side Ad Insertion — 서버에서 광고를 콘텐츠에 stitching</td>
    </tr>
    <tr>
      <td><strong>CSAI</strong></td>
      <td>Client-Side Ad Insertion — 플레이어가 직접 광고 호출</td>
    </tr>
    <tr>
      <td><strong>Ad Pod</strong></td>
      <td>한 브레이크에 연속 재생되는 광고 묶음 (CTV 핵심)</td>
    </tr>
    <tr>
      <td><strong>Mezzanine</strong></td>
      <td>SSAI용 원본 고화질 트랜스코딩 소스</td>
    </tr>
    <tr>
      <td><strong>MRC</strong></td>
      <td>Media Rating Council — 가시성 기준 정의 단체</td>
    </tr>
    <tr>
      <td><strong>TCF</strong></td>
      <td>Transparency &amp; Consent Framework — IAB Europe GDPR 동의 표준</td>
    </tr>
    <tr>
      <td><strong>GPP</strong></td>
      <td>Global Privacy Platform — 통합 프라이버시 프레임워크 (2022~)</td>
    </tr>
    <tr>
      <td><strong>CTV</strong></td>
      <td>Connected TV — 스마트 TV, OTT 기기</td>
    </tr>
    <tr>
      <td><strong>DOOH</strong></td>
      <td>Digital Out-Of-Home — 디지털 옥외광고</td>
    </tr>
    <tr>
      <td><strong>CPM/CPC/CPV</strong></td>
      <td>Cost per Mille / Click / View</td>
    </tr>
    <tr>
      <td><strong>Forfeit</strong></td>
      <td>낙찰 후 markup 전달 실패로 노출 불발</td>
    </tr>
    <tr>
      <td><strong>Header Bidding</strong></td>
      <td>페이지 로드 시 SSP/DSP에 사전 입찰 요청 (Prebid.js 등)</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="11-마무리">11. 마무리</h2>

<p>VAST와 OpenRTB는 표면적으로는 각각 XML/JSON, 단독으로 존재하는 표준처럼 보이지만, 실제 광고 생태계에서는 <strong>하나의 흐름</strong>으로 동작합니다. OpenRTB가 0.1초 만에 경매를 끝내면, VAST가 그 결과를 받아 0.5~1초 안에 플레이어로 전달하고, 사용자가 광고를 보는 30초 동안 수십 개의 트래킹 픽셀이 두 표준의 매크로 시스템으로 치환되어 발사됩니다.</p>

<p>두 표준 모두 IAB Tech Lab에서 계속 진화 중입니다. 큰 흐름은:</p>

<ul>
  <li><strong>VAST</strong>: CTV/오디오 지원 강화 (4.3, CTV 2024)</li>
  <li><strong>OpenRTB</strong>: Pod·CTV·Privacy 강화 (2.6-202303, GPP 통합)</li>
  <li><strong>AdCOM</strong>: 두 표준의 공통 사전으로 분리되어 별도 진화</li>
</ul>

<p>업계 표준을 따라가되, 구현 시 <strong>버전 호환성·매크로 치환·enum 정확성</strong> 세 가지에 가장 주의하세요. 경험상 트래킹 데이터 이상의 절반은 매크로 미치환, 사반은 enum 오인(특히 <code class="language-plaintext highlighter-rouge">protocols</code> 값), 나머지는 시간 초과(<code class="language-plaintext highlighter-rouge">tmax</code>)입니다.</p>

<hr />

<p><strong>1차 소스 (Fact-Check 기준)</strong></p>

<ul>
  <li><a href="https://gwangy.com/docs/vast">VAST 통합 명세 — gwangy.com/docs/vast</a></li>
  <li><a href="https://iabtechlab.com/standards/vast/">IAB Tech Lab — VAST Standards</a></li>
  <li><a href="https://github.com/InteractiveAdvertisingBureau/openrtb2.x/blob/main/2.6.md">OpenRTB 2.6 Spec (GitHub)</a></li>
  <li><a href="https://github.com/InteractiveAdvertisingBureau/AdCOM/blob/master/AdCOM%20v1.0%20FINAL.md">AdCOM 1.0 FINAL (GitHub)</a></li>
  <li><a href="https://interactiveadvertisingbureau.github.io/vast/vast4macros/vast4-macros-latest.html">VAST 4.x Macros Latest (Live Doc)</a></li>
  <li><a href="https://iabtechlab.com/standards/open-measurement-sdk/">IAB OMID SDK</a></li>
  <li><a href="https://github.com/InteractiveAdvertisingBureau/Global-Privacy-Platform">IAB GPP Specification</a></li>
  <li><a href="https://github.com/InteractiveAdvertisingBureau/USPrivacy">IAB US Privacy String</a></li>
</ul>]]></content><author><name>Glenn Yu</name><email>gwangy.yu@gmail.com</email></author><category term="AdTech" /><category term="VAST" /><category term="OpenRTB" /><category term="VAST" /><category term="OpenRTB" /><category term="AdCOM" /><category term="RTB" /><category term="IAB" /><category term="Video Ads" /><category term="AdTech" /><category term="Programmatic" /><category term="SIMID" /><category term="VPAID" /><category term="OMID" /><category term="CTV" /><category term="Tracking" /><category term="XML" /><category term="Bidding" /><category term="Header Bidding" /><summary type="html"><![CDATA[영상 콘텐츠 재생 버튼을 누른 순간, 0.1초 안에 어떤 일이 일어날까요?]]></summary></entry><entry><title type="html">Jetpack Compose와 WebView의 위험한 동거: 40개의 삽질 커밋과 단 한 줄의 진짜 해법</title><link href="https://glenn-yu.github.io/posts/jetpack-compose-webview-optimization/" rel="alternate" type="text/html" title="Jetpack Compose와 WebView의 위험한 동거: 40개의 삽질 커밋과 단 한 줄의 진짜 해법" /><published>2026-05-28T11:30:00+09:00</published><updated>2026-05-28T11:30:00+09:00</updated><id>https://glenn-yu.github.io/posts/jetpack-compose-webview-optimization</id><content type="html" xml:base="https://glenn-yu.github.io/posts/jetpack-compose-webview-optimization/"><![CDATA[<p>현대적인 안드로이드 개발 환경에서 Jetpack Compose는 UI 개발의 표준으로 자리 잡았습니다. 하지만 안드로이드 생태계의 ‘오래된 거인’인 <strong>WebView</strong>를 Compose 안으로 끌어들일 때는 이야기가 달라집니다.</p>

<p>이 글은 처음에 “Compose와 WebView는 근본적으로 안 맞는다”고 결론 내렸다가, 코드 이력을 다시 분석한 끝에 <strong>진짜 원인은 따로 있었다</strong>는 것을 깨달은 과정을 정직하게 기록한 것입니다. 40개의 커밋을 쏟아붓고도 못 찾았던, 그러나 단 한 줄이면 끝났던 그 원인 말입니다.</p>

<hr />

<h2 id="1-문제의-발단-멀쩡한-웹페이지가-compose-안에서-무너지다">1. 문제의 발단: 멀쩡한 웹페이지가 Compose 안에서 무너지다</h2>

<p>특정 고사양 웹 게임 페이지와 보상형 이벤트 페이지에서 치명적인 이슈가 보고되었습니다.</p>

<ul>
  <li><strong>현상 1</strong>: 페이지 레이아웃이 통째로 무너지거나, <code class="language-plaintext highlighter-rouge">height: 100vh</code> 기반 요소들이 전부 0px로 찌그러짐.</li>
  <li><strong>현상 2</strong>: 특정 버튼이 수차례 클릭해야 겨우 한 번 반응하거나, 아예 클릭되지 않음.</li>
  <li><strong>현상 3</strong>: 이미지 리소스가 간헐적으로 깨지거나 로딩이 무한 루프에 빠짐.</li>
</ul>

<p>일반적인 정적 HTML 페이지에서는 잘 드러나지 않지만, <strong><code class="language-plaintext highlighter-rouge">vh</code>/<code class="language-plaintext highlighter-rouge">vw</code> 단위를 적극적으로 쓰는 SPA나 고도의 자바스크립트 인터랙션이 포함된 웹 게임</strong>에서 문제가 극대화됐습니다.</p>

<hr />

<h2 id="2-반전-진짜-원인은-compose가-아니라-layoutparams였다">2. 반전: 진짜 원인은 ‘Compose’가 아니라 ‘layoutParams’였다</h2>

<p>처음에는 Compose의 렌더링 파이프라인, 터치 이벤트 선점, 재구성(Recomposition) 등 거창한 구조적 문제를 의심했습니다. 모두 사실이긴 하지만, <strong>가장 직접적인 원인은 어이없을 만큼 단순했습니다.</strong></p>

<h3 id="문제의-코드">문제의 코드</h3>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">AndroidView</span><span class="p">(</span>
    <span class="n">factory</span> <span class="p">=</span> <span class="p">{</span> <span class="n">ctx</span> <span class="p">-&gt;</span>
        <span class="nc">GwangyWebView</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>            <span class="c1">// ← layoutParams 미설정 → 초기 크기 0×0</span>
    <span class="p">},</span>
    <span class="n">update</span> <span class="p">=</span> <span class="p">{</span> <span class="n">wv</span> <span class="p">-&gt;</span>
        <span class="n">wv</span><span class="p">.</span><span class="nf">loadUrl</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>               <span class="c1">// ← 크기가 0인 상태에서 즉시 호출됨</span>
    <span class="p">},</span>
    <span class="n">modifier</span> <span class="p">=</span> <span class="nc">Modifier</span><span class="p">.</span><span class="nf">fillMaxSize</span><span class="p">()</span> <span class="c1">// ← Compose 좌표계에만 적용, 내부 View엔 전달 안 됨</span>
<span class="p">)</span>
</code></pre></div></div>

<h3 id="오동작-메커니즘">오동작 메커니즘</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. factory() 실행 → WebView 생성, layoutParams 없음 (초기 크기 = 0×0)
2. update() 실행 → loadUrl() 호출
3. 브라우저 엔진이 뷰포트를 등록: viewport height = 0px
4. CSS: height: 100vh → 100 × 0px = 0px 렌더링
5. 이후 뷰 크기가 커져도, 이미 vh=0으로 계산된 레이아웃은 그대로 굳어버림
   → vh 기반 요소 전부 0px로 붕괴
</code></pre></div></div>

<h3 id="핵심-modifierfillmaxsize--layoutparams">핵심: <code class="language-plaintext highlighter-rouge">Modifier.fillMaxSize()</code> ≠ <code class="language-plaintext highlighter-rouge">layoutParams</code></h3>

<p>이 버그의 본질은 <strong>두 개의 측정 시스템이 서로 독립적</strong>이라는 사실을 놓친 것입니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Compose 측정 시스템 (Modifier.fillMaxSize)
        │
        ▼
  AndroidView Wrapper (Compose 노드)
        │
        ▼
  View.layoutParams ← factory에서 별도로 설정해야 함 (자동 변환 안 됨!)
        │
        ▼
   실제 View (WebView)
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Modifier.fillMaxSize()</code>는 <strong>Compose 레이어의 측정 공간</strong>을 지정할 뿐, <code class="language-plaintext highlighter-rouge">AndroidView</code>가 내부 View를 만들 때 이 값을 <code class="language-plaintext highlighter-rouge">LayoutParams</code>로 변환해 전달해주지 <strong>않습니다.</strong> <code class="language-plaintext highlighter-rouge">factory</code>에서 명시하지 않으면 내부 View의 <code class="language-plaintext highlighter-rouge">layoutParams</code>는 <code class="language-plaintext highlighter-rouge">WRAP_CONTENT</code>(또는 0×0)로 남습니다.</p>

<h3 id="한-줄이면-끝났다--compose에서도">한 줄이면 끝났다 — Compose에서도</h3>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">AndroidView</span><span class="p">(</span>
    <span class="n">factory</span> <span class="p">=</span> <span class="p">{</span> <span class="n">ctx</span> <span class="p">-&gt;</span>
        <span class="nc">GwangyWebView</span><span class="p">(</span><span class="n">ctx</span><span class="p">).</span><span class="nf">also</span> <span class="p">{</span> <span class="n">wv</span> <span class="p">-&gt;</span>
            <span class="c1">// 이 한 줄만 추가했어도 vh 문제는 해결됐다</span>
            <span class="n">wv</span><span class="p">.</span><span class="n">layoutParams</span> <span class="p">=</span> <span class="nc">ViewGroup</span><span class="p">.</span><span class="nc">LayoutParams</span><span class="p">(</span>
                <span class="nc">ViewGroup</span><span class="p">.</span><span class="nc">LayoutParams</span><span class="p">.</span><span class="nc">MATCH_PARENT</span><span class="p">,</span>
                <span class="nc">ViewGroup</span><span class="p">.</span><span class="nc">LayoutParams</span><span class="p">.</span><span class="nc">MATCH_PARENT</span>
            <span class="p">)</span>
        <span class="p">}</span>
    <span class="p">},</span>
    <span class="o">..</span><span class="p">.</span>
<span class="p">)</span>
</code></pre></div></div>

<p>즉, <strong>“Compose를 버려야만 풀리는 문제”가 아니었습니다.</strong> <code class="language-plaintext highlighter-rouge">factory</code> 블록 안에서 <code class="language-plaintext highlighter-rouge">layoutParams = MATCH_PARENT</code> 한 줄만 추가했어도 <code class="language-plaintext highlighter-rouge">vh</code> 문제는 그대로 해결됐을 겁니다.</p>

<hr />

<h2 id="3-40개-커밋의-삽질-기록">3. 40개 커밋의 삽질 기록</h2>

<p>이 버그를 해결하는 데 <strong>약 40개의 커밋</strong>이 들었습니다. 그리고 솔직히 고백하면, 그 커밋들은 대부분 AI 코딩 어시스턴트가 작성했습니다. 정작 가장 단순한 해결책인 <code class="language-plaintext highlighter-rouge">layoutParams = MATCH_PARENT</code> 한 줄은 그 긴 여정 내내 <strong>단 한 번도 시도되지 않았습니다.</strong></p>

<p>대신 시도된 것들은 이런 것들이었습니다.</p>

<table>
  <thead>
    <tr>
      <th>시도한 접근</th>
      <th style="text-align: center"><code class="language-plaintext highlighter-rouge">MATCH_PARENT</code> 시도?</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>User-Agent에서 <code class="language-plaintext highlighter-rouge">wv</code> 태그 제거 (이미지 차단 의심)</td>
      <td style="text-align: center">❌</td>
    </tr>
    <tr>
      <td>HTTP 에러 로깅 추가 (디버깅)</td>
      <td style="text-align: center">❌</td>
    </tr>
    <tr>
      <td>대용량 이미지(2MB+ PNG) 렌더링 개선</td>
      <td style="text-align: center">❌</td>
    </tr>
    <tr>
      <td>JS 주입으로 DOM 상태 진단</td>
      <td style="text-align: center">❌</td>
    </tr>
    <tr>
      <td>GPU compositing 강제 (<code class="language-plaintext highlighter-rouge">LAYER_TYPE_HARDWARE</code>)</td>
      <td style="text-align: center">❌</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">LAYER_TYPE_HARDWARE</code> 제거, repaint 범위 확장</td>
      <td style="text-align: center">❌</td>
    </tr>
    <tr>
      <td>원격 크롬 디버깅 활성화</td>
      <td style="text-align: center">❌</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">setSupportMultipleWindows(false)</code></td>
      <td style="text-align: center">❌</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">window.open</code> 핸들러 / <code class="language-plaintext highlighter-rouge">window.chrome</code> 폴리필</td>
      <td style="text-align: center">❌</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">color-scheme: light</code> 강제, 다크모드 차단</td>
      <td style="text-align: center">❌</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">mixedContent ALWAYS_ALLOW</code></td>
      <td style="text-align: center">❌</td>
    </tr>
    <tr>
      <td><strong>소프트웨어 렌더링 강제</strong> (<code class="language-plaintext highlighter-rouge">LAYER_TYPE_SOFTWARE</code>)</td>
      <td style="text-align: center">❌</td>
    </tr>
    <tr>
      <td>모든 커스텀 로직 제거, 표준 Chrome UA만 남김</td>
      <td style="text-align: center">❌</td>
    </tr>
    <tr>
      <td><strong>Pure View 전환 → <code class="language-plaintext highlighter-rouge">MATCH_PARENT</code> 첫 등장 → 해결</strong></td>
      <td style="text-align: center">✅</td>
    </tr>
  </tbody>
</table>

<h3 id="왜-40개-커밋-동안-match_parent를-시도하지-않았는가">왜 40개 커밋 동안 <code class="language-plaintext highlighter-rouge">MATCH_PARENT</code>를 시도하지 않았는가</h3>

<p>원인은 하나입니다. <strong><code class="language-plaintext highlighter-rouge">Modifier.fillMaxSize()</code>가 내부 View에도 자동 적용된다고 끝까지 잘못 믿었기 때문입니다.</strong></p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 이 코드를 보고 "WebView 크기는 fillMaxSize()로 이미 잡혔다"고 단정해버렸다</span>
<span class="nc">AndroidView</span><span class="p">(</span>
    <span class="n">factory</span> <span class="p">=</span> <span class="p">{</span> <span class="n">ctx</span> <span class="p">-&gt;</span> <span class="nc">GwangyWebView</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span> <span class="p">},</span>  <span class="c1">// layoutParams 없음</span>
    <span class="n">modifier</span> <span class="p">=</span> <span class="nc">Modifier</span><span class="p">.</span><span class="nf">fillMaxSize</span><span class="p">()</span>          <span class="c1">// Compose 레이어에만 적용됨</span>
<span class="p">)</span>
</code></pre></div></div>

<p>이 하나의 착각 위에서 렌더링 레이어, User-Agent, JS 폴리필, GPU/소프트웨어 렌더링 전환, DOM 진단 스크립트까지 — 전혀 다른 방향만 40번을 파고들었습니다. <strong>잘못된 전제 하나가 얼마나 멀리까지 사람(과 AI)을 끌고 가는지</strong> 보여주는 사례입니다.</p>

<hr />

<h2 id="4-그렇다면-pure-view-전환은-틀린-선택이었나">4. 그렇다면 Pure View 전환은 틀린 선택이었나?</h2>

<p>아닙니다. 결과적으로 옳았습니다. 다만 <strong>이유를 정확히 알아야</strong> 합니다.</p>

<p>Pure View로 전환할 때 사실상 두 가지 변경이 <strong>동시에</strong> 일어났습니다.</p>

<table>
  <thead>
    <tr>
      <th>변경 내용</th>
      <th style="text-align: center">vh 문제 해결</th>
      <th style="text-align: center">터치 문제 해결</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">rootLayout</code>에 <code class="language-plaintext highlighter-rouge">MATCH_PARENT</code> 적용</td>
      <td style="text-align: center">✅ 핵심</td>
      <td style="text-align: center">-</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">webView</code>에 <code class="language-plaintext highlighter-rouge">MATCH_PARENT + weight=1</code> 적용</td>
      <td style="text-align: center">✅ 핵심</td>
      <td style="text-align: center">-</td>
    </tr>
    <tr>
      <td>Compose + <code class="language-plaintext highlighter-rouge">AndroidView</code> 구조 제거</td>
      <td style="text-align: center">-</td>
      <td style="text-align: center">✅</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">vh</code> 문제만 보면 <code class="language-plaintext highlighter-rouge">layoutParams = MATCH_PARENT</code> 한 줄로 Compose에서도 해결 가능했습니다. 하지만 두 변경이 한꺼번에 이뤄졌기에 그동안 “Compose를 버리니 해결됐다”고 결론지었던 것이죠. <strong>정확한 진단은 <code class="language-plaintext highlighter-rouge">vh + layoutParams</code></strong>, 그리고 <strong>Pure View 전환은 vh와 터치 문제를 모두 없앤 올바른 방향</strong>입니다.</p>

<h3 id="a-gwangywebactivitykt-독무대를-마련해주다">A. GwangyWebActivity.kt: 독무대를 마련해주다</h3>

<p><code class="language-plaintext highlighter-rouge">setContentView</code> 시점에 이미 뷰 크기가 <code class="language-plaintext highlighter-rouge">MATCH_PARENT</code>로 확정되므로, <code class="language-plaintext highlighter-rouge">loadUrl()</code> 호출 시 브라우저 엔진이 올바른 viewport height를 등록합니다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// rootLayout: MATCH_PARENT 명시 → WebView가 렌더링되기 전 뷰포트 크기 확정</span>
<span class="kd">val</span> <span class="py">rootLayout</span> <span class="p">=</span> <span class="nc">LinearLayout</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
    <span class="n">orientation</span> <span class="p">=</span> <span class="nc">LinearLayout</span><span class="p">.</span><span class="nc">VERTICAL</span>
    <span class="n">layoutParams</span> <span class="p">=</span> <span class="nc">ViewGroup</span><span class="p">.</span><span class="nc">LayoutParams</span><span class="p">(</span><span class="nc">MATCH_PARENT</span><span class="p">,</span> <span class="nc">MATCH_PARENT</span><span class="p">)</span>  <span class="c1">// ✅</span>
<span class="p">}</span>

<span class="c1">// 네이티브 상단 툴바 (Compose 상단바 대신 시스템 안정성 확보)</span>
<span class="kd">val</span> <span class="py">toolbar</span> <span class="p">=</span> <span class="nc">Toolbar</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
    <span class="n">title</span> <span class="p">=</span> <span class="s">"GwangyBrowser"</span>
    <span class="nf">setNavigationIcon</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">drawable</span><span class="p">.</span><span class="n">ic_back</span><span class="p">)</span>
<span class="p">}</span>

<span class="c1">// webView: MATCH_PARENT + weight=1 → 툴바를 제외한 나머지 공간 전체 점유</span>
<span class="n">webView</span> <span class="p">=</span> <span class="nc">GwangyWebView</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
    <span class="n">layoutParams</span> <span class="p">=</span> <span class="nc">LinearLayout</span><span class="p">.</span><span class="nc">LayoutParams</span><span class="p">(</span><span class="nc">MATCH_PARENT</span><span class="p">,</span> <span class="nc">MATCH_PARENT</span><span class="p">,</span> <span class="mf">1f</span><span class="p">)</span>  <span class="c1">// ✅</span>
<span class="p">}</span>

<span class="n">rootLayout</span><span class="p">.</span><span class="nf">addView</span><span class="p">(</span><span class="n">toolbar</span><span class="p">)</span>
<span class="n">rootLayout</span><span class="p">.</span><span class="nf">addView</span><span class="p">(</span><span class="n">webView</span><span class="p">)</span>

<span class="nf">setContentView</span><span class="p">(</span><span class="n">rootLayout</span><span class="p">)</span>
<span class="n">webView</span><span class="o">?.</span><span class="nf">loadUrl</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>  <span class="c1">// 뷰 크기 확정 후 로딩 시작 → vh CSS 정상 동작</span>
</code></pre></div></div>

<h3 id="b-gwangywebviewkt-브라우저-수준의-최적화-설정">B. GwangyWebView.kt: 브라우저 수준의 최적화 설정</h3>

<p>단순 래핑을 넘어, WebView가 브라우저만큼의 성능을 내도록 세밀하게 튜닝했습니다.</p>

<ul>
  <li><strong>세션 유지의 핵심</strong>: <code class="language-plaintext highlighter-rouge">CookieManager.setAcceptThirdPartyCookies(true)</code>로 서드파티 도메인 간 세션 공유를 허용했습니다. (보상 시스템에서 필수적)</li>
  <li><strong>브라우저급 뷰포트</strong>: <code class="language-plaintext highlighter-rouge">useWideViewPort</code>, <code class="language-plaintext highlighter-rouge">loadWithOverviewMode</code>, <code class="language-plaintext highlighter-rouge">allowFileAccess</code> 등을 조정했습니다.</li>
  <li><strong>User-Agent Customizing</strong>: 일부 서버가 WebView를 봇으로 오인해 차단하는 것을 막기 위해 최신 모바일 크롬 문자열을 주입했습니다.</li>
  <li><strong>디버깅 활성화</strong>: <code class="language-plaintext highlighter-rouge">setWebContentsDebuggingEnabled(true)</code>로 PC 크롬 인스펙터를 1:1로 붙일 수 있게 했습니다.</li>
</ul>

<hr />

<h2 id="5-androidview-인터롭-제대로-쓰기">5. AndroidView 인터롭, 제대로 쓰기</h2>

<p>이번 일을 계기로 <code class="language-plaintext highlighter-rouge">AndroidView</code> 인터롭의 함정을 정리했습니다. WebView뿐 아니라 모든 View 래핑에 공통으로 적용됩니다.</p>

<h3 id="factory-vs-update--생명주기를-이해하라">factory vs update — 생명주기를 이해하라</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>처음 Composition 진입 시:  factory() → View 생성 및 1회 초기화
Recomposition(상태 변화):  update()  → 기존 View 재사용, 상태만 동기화
Composition에서 제거 시:    onRelease() → 리소스 해제
</code></pre></div></div>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ 잘못된 예 — 변하는 값을 factory에서 캡처</span>
<span class="nc">AndroidView</span><span class="p">(</span>
    <span class="n">factory</span> <span class="p">=</span> <span class="p">{</span> <span class="n">ctx</span> <span class="p">-&gt;</span>
        <span class="nc">WebView</span><span class="p">(</span><span class="n">ctx</span><span class="p">).</span><span class="nf">also</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">loadUrl</span><span class="p">(</span><span class="n">url</span><span class="p">)</span> <span class="p">}</span>  <span class="c1">// url이 바뀌어도 factory는 재실행 안 됨!</span>
    <span class="p">}</span>
<span class="p">)</span>

<span class="c1">// ✅ 올바른 예 — 고정 설정은 factory, 변하는 값은 update</span>
<span class="nc">AndroidView</span><span class="p">(</span>
    <span class="n">factory</span> <span class="p">=</span> <span class="p">{</span> <span class="n">ctx</span> <span class="p">-&gt;</span>
        <span class="nc">WebView</span><span class="p">(</span><span class="n">ctx</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
            <span class="n">layoutParams</span> <span class="p">=</span> <span class="nc">ViewGroup</span><span class="p">.</span><span class="nc">LayoutParams</span><span class="p">(</span><span class="nc">MATCH_PARENT</span><span class="p">,</span> <span class="nc">MATCH_PARENT</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">},</span>
    <span class="n">update</span> <span class="p">=</span> <span class="p">{</span> <span class="n">wv</span> <span class="p">-&gt;</span> <span class="k">if</span> <span class="p">(</span><span class="n">wv</span><span class="p">.</span><span class="n">url</span> <span class="p">!=</span> <span class="n">url</span><span class="p">)</span> <span class="n">wv</span><span class="p">.</span><span class="nf">loadUrl</span><span class="p">(</span><span class="n">url</span><span class="p">)</span> <span class="p">}</span>
<span class="p">)</span>
</code></pre></div></div>

<h3 id="인터롭에서-자주-터지는-버그-6가지">인터롭에서 자주 터지는 버그 6가지</h3>

<table>
  <thead>
    <tr>
      <th style="text-align: left">#</th>
      <th style="text-align: left">증상</th>
      <th style="text-align: left">원인</th>
      <th style="text-align: left">해결</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">①</td>
      <td style="text-align: left">View가 안 보임 / vh CSS = 0px</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">factory</code>에서 <code class="language-plaintext highlighter-rouge">layoutParams</code> 미설정</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">MATCH_PARENT</code> 명시</td>
    </tr>
    <tr>
      <td style="text-align: left">②</td>
      <td style="text-align: left">버튼 클릭 누락, 스크롤 충돌</td>
      <td style="text-align: left">Compose가 포인터 이벤트 선점</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Modifier.pointerInteropFilter { true }</code></td>
    </tr>
    <tr>
      <td style="text-align: left">③</td>
      <td style="text-align: left">스크롤 위치 초기화, WebView 리로드</td>
      <td style="text-align: left">Recomposition으로 View 재생성</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">remember { }</code>로 인스턴스 유지</td>
    </tr>
    <tr>
      <td style="text-align: left">④</td>
      <td style="text-align: left">복귀 시 세션/팝업 상태 소실</td>
      <td style="text-align: left">생명주기 주기 불일치</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DisposableEffect</code>로 <code class="language-plaintext highlighter-rouge">onResume/onPause</code> 연결</td>
    </tr>
    <tr>
      <td style="text-align: left">⑤</td>
      <td style="text-align: left">input 탭해도 키보드 안 올라옴</td>
      <td style="text-align: left">Compose <code class="language-plaintext highlighter-rouge">FocusManager</code>가 포커스 가로챔</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">requestFocus()</code> 강제 호출</td>
    </tr>
    <tr>
      <td style="text-align: left">⑥</td>
      <td style="text-align: left">TalkBack이 내부 View를 못 읽음</td>
      <td style="text-align: left">semantics tree 분리</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Modifier.semantics { }</code>로 수동 추가</td>
    </tr>
  </tbody>
</table>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ③ View 인스턴스를 Composition 밖으로 꺼내기</span>
<span class="kd">val</span> <span class="py">webView</span> <span class="p">=</span> <span class="nf">remember</span> <span class="p">{</span>
    <span class="nc">WebView</span><span class="p">(</span><span class="n">context</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="n">layoutParams</span> <span class="p">=</span> <span class="nc">ViewGroup</span><span class="p">.</span><span class="nc">LayoutParams</span><span class="p">(</span><span class="nc">MATCH_PARENT</span><span class="p">,</span> <span class="nc">MATCH_PARENT</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
<span class="c1">// ④ 생명주기 명시 연결</span>
<span class="nc">DisposableEffect</span><span class="p">(</span><span class="nc">Unit</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">webView</span><span class="p">.</span><span class="nf">onResume</span><span class="p">()</span>
    <span class="nf">onDispose</span> <span class="p">{</span> <span class="n">webView</span><span class="p">.</span><span class="nf">onPause</span><span class="p">()</span> <span class="p">}</span>
<span class="p">}</span>
<span class="nc">AndroidView</span><span class="p">(</span><span class="n">factory</span> <span class="p">=</span> <span class="p">{</span> <span class="n">webView</span> <span class="p">})</span>
</code></pre></div></div>

<hr />

<h2 id="6-렌더링-파이프라인-충돌--언제-pure-view가-필수인가">6. 렌더링 파이프라인 충돌 — 언제 Pure View가 ‘필수’인가</h2>

<p>Compose와 View는 <strong>전혀 다른 렌더링 파이프라인</strong>을 가집니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Compose:  Composition → Layout → Draw → 단일 RenderNode 트리로 GPU에 한번에
View:     measure → layout → draw → 각 View가 Canvas에 직접, 일부는 독립 Surface 보유
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">AndroidView</code>로 둘을 섞으면 충돌이 생기는 케이스가 있습니다. WebView의 <code class="language-plaintext highlighter-rouge">vh</code> 버그는 그중 <strong>타이밍 문제</strong>에 속하고, 더 심각한 것은 <strong>독립 Surface를 가진 View</strong>입니다.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">케이스</th>
      <th style="text-align: left">원인</th>
      <th style="text-align: left">증상</th>
      <th style="text-align: center">판단</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>SurfaceView 계열</strong> (VideoView 등)</td>
      <td style="text-align: left">독립 Surface, Z-Order 분리</td>
      <td style="text-align: left">Compose UI가 비디오/카메라 뒤로 사라짐</td>
      <td style="text-align: center"><strong>Pure View</strong> 또는 TextureView</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>GLSurfaceView / 게임엔진</strong></td>
      <td style="text-align: left">독립 GL 렌더 스레드</td>
      <td style="text-align: left">프레임 드랍, 아티팩트, ANR</td>
      <td style="text-align: center"><strong>Pure View 필수</strong></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong><code class="language-plaintext highlighter-rouge">vh</code>/<code class="language-plaintext highlighter-rouge">vw</code> CSS WebView</strong></td>
      <td style="text-align: left">뷰포트 등록 시점 0×0</td>
      <td style="text-align: left">CSS 레이아웃 0px 붕괴</td>
      <td style="text-align: center">Pure View 권장 (또는 <code class="language-plaintext highlighter-rouge">MATCH_PARENT</code>)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>복잡한 JS 인터랙션 WebView</strong></td>
      <td style="text-align: left">터치 이벤트 선점</td>
      <td style="text-align: left">버튼 클릭 무시, 게임 무반응</td>
      <td style="text-align: center">Pure View 권장</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong><code class="language-plaintext highlighter-rouge">LAYER_TYPE_HARDWARE</code> 충돌</strong></td>
      <td style="text-align: left">이중 RenderNode</td>
      <td style="text-align: left">애니메이션 위치 어긋남, 잔상</td>
      <td style="text-align: center">레이어 타입 변경 or Pure View</td>
    </tr>
  </tbody>
</table>

<h3 id="의사결정-흐름도">의사결정 흐름도</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>사용하려는 View가 있다
        │
        ▼
SurfaceView 기반인가? (VideoView, GLSurfaceView, 카메라 HAL)
   예 ──┴── 아니오
   │              │
   ▼              ▼
Pure View      TextureView로 교체 가능한가?
Activity       예 ──┴── 아니오
               │            │
               ▼            ▼
        AndroidView      Pure View
        (TextureView)    Activity
               │
               ▼
        vh/vw CSS 사용 WebView인가?
          예 ──┴── 아니오
          │            │
          ▼            ▼
      Pure View    AndroidView
      Activity     (MATCH_PARENT 필수)
</code></pre></div></div>

<h3 id="우리-앱의-다른-androidview-사용처-점검">우리 앱의 다른 AndroidView 사용처 점검</h3>

<p>이 깨달음으로 앱 전체의 <code class="language-plaintext highlighter-rouge">AndroidView</code> 사용처를 다시 훑었더니, <strong>같은 버그가 한 군데 더</strong> 숨어 있었습니다.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">사용처</th>
      <th style="text-align: left">View 종류</th>
      <th style="text-align: center">layoutParams</th>
      <th style="text-align: center">상태</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">이벤트 보상 WebView</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">WebView</code> (<code class="language-plaintext highlighter-rouge">height:100vh</code> HTML)</td>
      <td style="text-align: center">❌ 미설정</td>
      <td style="text-align: center">🔴 <strong>같은 vh 버그 — 수정</strong></td>
    </tr>
    <tr>
      <td style="text-align: left">커스텀 차트 뷰</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">GwangyChartView</code> (<code class="language-plaintext highlighter-rouge">LAYER_TYPE_HARDWARE</code>)</td>
      <td style="text-align: center">❌</td>
      <td style="text-align: center">⚠️ Compose 애니메이션 추가 전까지는 정상</td>
    </tr>
    <tr>
      <td style="text-align: left">배너 광고</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">AdView</code> (컨테이너 래핑)</td>
      <td style="text-align: center">✅ container <code class="language-plaintext highlighter-rouge">MATCH_PARENT</code></td>
      <td style="text-align: center">✅ 정상 (SDK가 크기 관리)</td>
    </tr>
    <tr>
      <td style="text-align: left">배경 이미지</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">ImageView</code></td>
      <td style="text-align: center">❌</td>
      <td style="text-align: center">✅ 정적 이미지라 타이밍 문제 없음</td>
    </tr>
  </tbody>
</table>

<p>광고 SDK View처럼 <strong>SDK가 직접 크기를 관리하고 <code class="language-plaintext highlighter-rouge">vh</code>를 쓰지 않는 경우</strong>는 문제가 없습니다. 공식 문서도 <code class="language-plaintext highlighter-rouge">AndroidView</code>는 “Compose 대응이 없는 SDK 컴포넌트를 래핑하는 용도”로 권장합니다.</p>

<hr />

<h2 id="7-실제-사례-모음-공개-출처">7. 실제 사례 모음 (공개 출처)</h2>

<p>우리만 겪은 게 아닙니다. 인터롭 함정은 공개적으로도 반복 보고됩니다.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">케이스</th>
      <th style="text-align: left">View</th>
      <th style="text-align: left">근본 원인</th>
      <th style="text-align: left">해결</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">ExoPlayer가 <code class="language-plaintext highlighter-rouge">clip()</code> 무시</td>
      <td style="text-align: left">SurfaceView</td>
      <td style="text-align: left">독립 Surface는 Compose clip 미적용</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">surface_type="texture_view"</code></td>
    </tr>
    <tr>
      <td style="text-align: left">Android TV 흰 화면</td>
      <td style="text-align: left">SurfaceView</td>
      <td style="text-align: left">Compose <code class="language-plaintext highlighter-rouge">Surface</code>의 Offscreen 합성 충돌</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Surface</code> → <code class="language-plaintext highlighter-rouge">Box</code> 교체</td>
    </tr>
    <tr>
      <td style="text-align: left">API 34 동영상 늘어남/잘림</td>
      <td style="text-align: left">SurfaceView</td>
      <td style="text-align: left">Android 14 Surface 동기화 버그</td>
      <td style="text-align: left">Media3 <code class="language-plaintext highlighter-rouge">PlayerSurface</code> 사용</td>
    </tr>
    <tr>
      <td style="text-align: left">WebView 전체화면 안 됨</td>
      <td style="text-align: left">WebView</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">layoutParams</code> 미설정, 뷰포트 0×0</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">factory</code>에서 <code class="language-plaintext highlighter-rouge">MATCH_PARENT</code></td>
    </tr>
    <tr>
      <td style="text-align: left">동영상 재시작</td>
      <td style="text-align: left">ExoPlayer</td>
      <td style="text-align: left">Recomposition 시 <code class="language-plaintext highlighter-rouge">factory</code> 재실행</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">remember</code>로 인스턴스 유지</td>
    </tr>
    <tr>
      <td style="text-align: left">RecyclerView 상태 소실</td>
      <td style="text-align: left">ComposeView</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">ViewCompositionStrategy</code> 미설정</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DisposeOnViewTreeLifecycleDestroyed</code></td>
    </tr>
    <tr>
      <td style="text-align: left">AdMob 메모리 누수</td>
      <td style="text-align: left">AdView</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">onRelease</code>에서 <code class="language-plaintext highlighter-rouge">destroy()</code> 누락</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">onRelease { adView.destroy() }</code></td>
    </tr>
  </tbody>
</table>

<p>특히 비디오/카메라를 Compose에서 다룬다면, 구글은 <code class="language-plaintext highlighter-rouge">SurfaceView</code> 직접 사용 대신 <code class="language-plaintext highlighter-rouge">AndroidExternalSurface</code> / <code class="language-plaintext highlighter-rouge">AndroidEmbeddedExternalSurface</code>(또는 Media3 <code class="language-plaintext highlighter-rouge">PlayerSurface</code>)를 공식 권장합니다.</p>

<p><strong>참고 출처</strong></p>
<ul>
  <li><a href="https://developer.android.com/develop/ui/compose/migrate/interoperability-apis/views-in-compose">Using Views in Compose — Android Developers</a></li>
  <li><a href="https://developer.android.com/media/media3/ui/surface">Surface types (Media3) — Android Developers</a></li>
  <li><a href="https://developer.android.com/develop/ui/views/graphics/hardware-accel">Hardware Acceleration — Android Developers</a></li>
  <li><a href="https://medium.com/@fox.fu.go/webview-not-showing-as-full-screen-in-compose-ui-f2b3e9570ff6">WebView not showing as full screen in Compose UI — Medium</a></li>
  <li><a href="https://medium.com/@art_hacker_/why-your-exoplayer-video-ignores-compose-clipping-and-the-one-line-fix-that-solves-it-727650b7cc7e">Why Your ExoPlayer Video Ignores Compose Clipping — Medium</a></li>
  <li><a href="https://medium.com/androiddevelopers/viewcompositionstrategy-demystefied-276427152f34">ViewCompositionStrategy Demystified — Android Developers Blog</a></li>
  <li><a href="https://github.com/androidx/media/issues/1237">SurfaceView stretched/cropped on API 34 — androidx/media #1237</a></li>
</ul>

<hr />

<h2 id="8-오늘의-교훈">8. 오늘의 교훈</h2>

<p>기술 자체보다 <strong>잘못된 전제 하나가 더 위험하다</strong>는 것을 배웠습니다. “Compose가 WebView와 안 맞는다”는 그럴듯한 가설이 진짜 원인(<code class="language-plaintext highlighter-rouge">layoutParams</code> 미설정)을 40개 커밋 동안 가려버렸으니까요.</p>

<p>정리하면 이렇습니다.</p>

<ol>
  <li><strong><code class="language-plaintext highlighter-rouge">Modifier</code>와 <code class="language-plaintext highlighter-rouge">LayoutParams</code>는 별개의 측정 시스템이다.</strong> <code class="language-plaintext highlighter-rouge">Modifier.fillMaxSize()</code>는 Compose 레이어 전용이며 내부 View에 자동 전달되지 않는다 — <code class="language-plaintext highlighter-rouge">factory</code>에서 <code class="language-plaintext highlighter-rouge">layoutParams = MATCH_PARENT</code>를 직접 설정하라.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">vh</code>/<code class="language-plaintext highlighter-rouge">vw</code> CSS 페이지는 <code class="language-plaintext highlighter-rouge">loadUrl()</code> 전에 뷰 크기가 확정돼 있어야 한다.</strong></li>
  <li><strong>그럼에도, 웹 게임처럼 복잡한 인터랙션·세션이 핵심이라면 Pure View Activity로 분리하는 것이 가장 안정적이다.</strong> <code class="language-plaintext highlighter-rouge">vh</code>와 터치 문제를 한 번에 없애준다.</li>
  <li><strong>막혔을 때는 가설을 의심하라.</strong> 같은 방향으로 39번 더 파기 전에, 가장 단순한 전제부터 검증하는 편이 빠르다.</li>
</ol>

<blockquote>
  <p><strong>💡 한 줄 정리</strong>: 도화지(Compose)와 괴물(WebView)을 같이 그리는 것 자체는 가능하다. 단, 괴물에게 먼저 <strong>자리(<code class="language-plaintext highlighter-rouge">layoutParams = MATCH_PARENT</code>)</strong>를 정확히 내어줘라. 그게 안 되면 전용 운동장(Pure View)으로 보내라.</p>
</blockquote>]]></content><author><name>Glenn Yu</name><email>gwangy.yu@gmail.com</email></author><category term="Android" /><category term="Kotlin" /><category term="Jetpack Compose" /><category term="Android" /><category term="Kotlin" /><category term="Jetpack Compose" /><category term="WebView" /><category term="AndroidView" /><category term="Interop" /><category term="Optimization" /><category term="Performance" /><category term="HybridApp" /><summary type="html"><![CDATA[현대적인 안드로이드 개발 환경에서 Jetpack Compose는 UI 개발의 표준으로 자리 잡았습니다. 하지만 안드로이드 생태계의 ‘오래된 거인’인 WebView를 Compose 안으로 끌어들일 때는 이야기가 달라집니다.]]></summary></entry><entry><title type="html">Swift와 사랑에 빠진 시간, Stanford CS193P 수강기</title><link href="https://glenn-yu.github.io/posts/stanford-cs193p-swift-course-review/" rel="alternate" type="text/html" title="Swift와 사랑에 빠진 시간, Stanford CS193P 수강기" /><published>2026-05-27T12:00:00+09:00</published><updated>2026-05-27T12:00:00+09:00</updated><id>https://glenn-yu.github.io/posts/stanford-cs193p-swift-course-review</id><content type="html" xml:base="https://glenn-yu.github.io/posts/stanford-cs193p-swift-course-review/"><![CDATA[<p>모바일 앱 개발을 하다 보면, 어느 순간 단순히 ‘돌아가는 코드’를 짜는 것을 넘어 플랫폼의 설계 철학과 언어의 깊은 본질에 대한 갈증을 느끼게 됩니다. 저에게는 iOS와 Swift가 그랬습니다.</p>

<p>이 갈증을 해소하기 위해 전 세계 수많은 iOS 개발자들의 ‘바이블’로 통하는 <strong>Stanford University의 CS193P (Developing iOS Apps with Swift)</strong> 강의를 수강했습니다. 폴 헤가티(Paul Hegarty) 교수의 명강의를 따라가며 직접 구현했던 3개의 핵심 프로젝트와, 그 과정에서 배운 기술적 인사이트를 공유하고자 합니다.</p>

<hr />

<h2 id="1-프로젝트-1-calculator--swift와의-첫인상">1. 프로젝트 1: Calculator — Swift와의 첫인상</h2>

<p>가장 먼저 시작한 프로젝트는 기본적인 계산기 앱(Calculator)이었습니다. 겉보기엔 단순하지만, 이 프로젝트의 진짜 목적은 <strong>MVC(Model-View-Controller) 패턴의 엄격한 적용</strong>에 있었습니다.</p>

<h3 id="기술적-포인트">기술적 포인트</h3>
<ul>
  <li><strong>Optional의 이해:</strong> Swift가 다른 언어와 구분되는 가장 큰 특징 중 하나인 <code class="language-plaintext highlighter-rouge">Optional</code> 처리를 확실하게 체득할 수 있었습니다. 값이 없을 수 있는 상황을 안전하게 다루는 법을 배웠습니다.</li>
  <li><strong>Enum을 활용한 로직 분리:</strong> 연산 로직을 <code class="language-plaintext highlighter-rouge">Enum</code>으로 분리하여 코드를 구조화했습니다. 단순히 if-else 문을 나열하는 것이 아니라, 연산자의 성격(단항, 이항 등)에 따라 깔끔하게 분기 처리하는 방법을 익혔습니다.</li>
</ul>

<p><strong>회고:</strong>
“Swift는 안전(Safety)을 최우선으로 하는 언어구나”라는 것을 깊이 실감했습니다. 컴파일러가 잡아주는 에러들을 수정해가며, 앱이 런타임에 죽는 것을 사전에 방지하는 Swift만의 철학을 느낄 수 있었습니다.</p>

<hr />

<h2 id="2-프로젝트-2-concentration--데이터와-메모리-관리">2. 프로젝트 2: Concentration — 데이터와 메모리 관리</h2>

<p>두 번째 프로젝트는 같은 그림을 찾는 카드 맞추기 게임(Concentration)이었습니다. 이 단계에서는 시각적인 재미뿐만 아니라 앱의 뒷단에서 일어나는 효율적인 데이터 구조 설계에 집중했습니다.</p>

<h3 id="기술적-포인트-1">기술적 포인트</h3>
<ul>
  <li><strong>Struct vs Class:</strong> 언제 참조 타입(Class)을 쓰고, 언제 값 타입(Struct)을 써야 하는지 명확한 기준을 세울 수 있었습니다. Swift에서는 Struct가 단순한 데이터 컨테이너 이상의 강력한 역할을 한다는 것을 배웠습니다.</li>
  <li><strong>Property Observer (<code class="language-plaintext highlighter-rouge">didSet</code>):</strong> 변수의 값이 변경될 때마다 UI를 자동으로 업데이트하거나 특정 로직을 실행하도록 하는 Property Observer 패턴의 편리함을 경험했습니다.</li>
</ul>

<p><strong>회고:</strong>
간단한 메모리 게임이지만, 그 뒤에 숨겨진 배열(Array)의 효율적인 관리와 상태 동기화가 얼마나 중요한지 깨달았습니다. 코드를 작성할 때 단순히 로직을 짜는 것을 넘어 메모리와 데이터의 흐름을 상상하게 되었습니다.</p>

<hr />

<h2 id="3-프로젝트-3-playingcard--아름다운-ui의-이면">3. 프로젝트 3: PlayingCard — 아름다운 UI의 이면</h2>

<p>세 번째는 트럼프 카드의 앞면과 뒷면을 커스텀하게 그려보는 프로젝트(PlayingCard)였습니다. 기본적인 UI 컴포넌트를 넘어서, 개발자가 직접 픽셀 단위로 통제하는 법을 배웠습니다.</p>

<h3 id="기술적-포인트-2">기술적 포인트</h3>
<ul>
  <li><strong>커스텀 드로잉과 <code class="language-plaintext highlighter-rouge">@IBDesignable</code>:</strong> <code class="language-plaintext highlighter-rouge">UIView</code>를 상속받아 직접 화면에 그림을 그리는 방식을 학습했습니다. 특히 <code class="language-plaintext highlighter-rouge">@IBDesignable</code>과 <code class="language-plaintext highlighter-rouge">@IBInspectable</code>을 활용해 코드로 작성한 UI가 스토리보드에 실시간으로 반영되는 경험은 무척 인상적이었습니다.</li>
  <li><strong>Core Graphics 활용:</strong> 단순히 이미지를 띄우는 것이 아니라, Core Graphics를 이용해 벡터 기반의 그래픽을 코드로 구현해 내는 방법을 배웠습니다.</li>
</ul>

<p><strong>회고:</strong>
코드로 직접 UI를 한 땀 한 땀 그려보며 느낀 성취감은 대단했습니다. 동시에 부드러운 애니메이션과 세밀한 UI 조정에 필요한 수학적 연산과 제약 사항들에 대해서도 깊이 고민해 보는 계기가 되었습니다.</p>

<hr />

<h2 id="마무리-강의가-나에게-남긴-것">마무리: 강의가 나에게 남긴 것</h2>

<p>CS193P 강의를 수강하며 남긴 레포지토리들(<code class="language-plaintext highlighter-rouge">Calculator</code>, <code class="language-plaintext highlighter-rouge">Concentration</code>, <code class="language-plaintext highlighter-rouge">PlayingCard</code>)은 단순한 실습 결과물을 넘어 제 성장의 기록이 되었습니다.</p>

<p>이 강의는 단지 문법을 알려주는 것을 넘어, Apple이 어떤 철학으로 iOS와 Swift를 설계했는지 이해하게 해 주었습니다. 문서를 꼼꼼히 읽고 스스로 문제를 해결해 나가는 ‘엔지니어링 사고’가 확장되는 귀중한 시간이었습니다.</p>

<p>이제 막 Swift를 시작하시거나, 기존의 지식 위에 기본기를 더욱 탄탄하게 다지고 싶은 동료 개발자분들께 스탠포드 강의를 강력히 추천합니다!</p>

<blockquote>
  <p><em>작성된 코드들은 제 <a href="https://github.com/glenn-yu?tab=repositories">GitHub 레포지토리</a>에서 확인하실 수 있습니다.</em></p>
</blockquote>]]></content><author><name>Glenn Yu</name><email>gwangy.yu@gmail.com</email></author><category term="iOS" /><category term="Swift" /><category term="Retrospective" /><category term="Swift" /><category term="iOS" /><category term="Stanford" /><category term="CS193P" /><category term="Tutorial" /><category term="회고" /><summary type="html"><![CDATA[모바일 앱 개발을 하다 보면, 어느 순간 단순히 ‘돌아가는 코드’를 짜는 것을 넘어 플랫폼의 설계 철학과 언어의 깊은 본질에 대한 갈증을 느끼게 됩니다. 저에게는 iOS와 Swift가 그랬습니다.]]></summary></entry><entry><title type="html">GitHub Pages 로 개인 블로그 만들기 — 0 에서 giscus 까지</title><link href="https://glenn-yu.github.io/posts/github-pages-blog-setup/" rel="alternate" type="text/html" title="GitHub Pages 로 개인 블로그 만들기 — 0 에서 giscus 까지" /><published>2026-05-08T14:00:00+09:00</published><updated>2026-05-08T14:00:00+09:00</updated><id>https://glenn-yu.github.io/posts/github-pages-blog-setup</id><content type="html" xml:base="https://glenn-yu.github.io/posts/github-pages-blog-setup/"><![CDATA[<p>하루 동안 GitHub Pages 로 개인 블로그를 만들었습니다 — 지금 보고 계신 이 사이트입니다.
다음에 또 만들 일이 생길 것 같아서 과정을 기록해 둡니다.</p>

<blockquote>
  <p><strong>결과물</strong>: <a href="https://glenn-yu.github.io">glenn-yu.github.io</a> — Jekyll · 로그캣 부팅 인트로 · ⌘K 팔레트 · 다크/라이트 · giscus 댓글 · 커스텀 404.
<strong>전체 코드</strong>: <a href="https://github.com/glenn-yu/glenn-yu.github.io">glenn-yu/glenn-yu.github.io</a></p>
</blockquote>

<h2 id="왜-github-pages-인가">왜 GitHub Pages 인가</h2>

<p>선택지는 많습니다 — Vercel, Netlify, Cloudflare Pages 등.
그중 GitHub Pages 를 고른 이유는 단순합니다.</p>

<ul>
  <li><strong>레포 = 사이트</strong> — <code class="language-plaintext highlighter-rouge">username.github.io</code> 레포를 만들면 그게 그대로 사이트 URL.</li>
  <li><strong>Jekyll 자동 빌드</strong> — 마크다운만 push 하면 GitHub 가 알아서 빌드. CI 설정 필요 없음.</li>
  <li><strong>무료 + HTTPS 자동</strong>.</li>
  <li><strong>버전 관리가 곧 발행 이력</strong> — <code class="language-plaintext highlighter-rouge">git log</code> 가 변경사항 기록.</li>
</ul>

<p>단점이라면 “GitHub 화이트리스트된 Jekyll 플러그인만 쓸 수 있다” 정도. 일반 블로그로는 충분합니다.</p>

<h2 id="5분-컷--한-줄로-사이트-띄우기">5분 컷 — 한 줄로 사이트 띄우기</h2>

<p>가장 빠른 코스부터 가봅시다.</p>

<ol>
  <li>GitHub 에서 새 레포: 이름은 정확히 <code class="language-plaintext highlighter-rouge">&lt;본인-username&gt;.github.io</code>, Public.</li>
  <li>클론하고 <code class="language-plaintext highlighter-rouge">index.html</code> 하나 만들어 push:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/&lt;본인&gt;/&lt;본인&gt;.github.io.git
<span class="nb">cd</span> &lt;본인&gt;.github.io
<span class="nb">echo</span> <span class="s2">"&lt;h1&gt;Hello, world&lt;/h1&gt;"</span> <span class="o">&gt;</span> index.html
git add index.html
git commit <span class="nt">-m</span> <span class="s2">"init"</span>
git push origin main
</code></pre></div></div>

<ol>
  <li>1~2분 후 <code class="language-plaintext highlighter-rouge">https://&lt;본인&gt;.github.io</code> 접속.</li>
</ol>

<p>여기서 끝내도 됩니다. 정적 HTML/CSS/JS 만 잘 짜도 사이트 하나 만드는 데 충분해요.
하지만 글이 늘어나면 레이아웃·메타·목록 처리가 귀찮아지니, 다음 단계로 넘어갑니다.</p>

<h2 id="jekyll-로-본격-블로그-만들기">Jekyll 로 본격 블로그 만들기</h2>

<p>블로그 운영에는 Jekyll 이 편합니다. 마크다운 파일을 추가하면 곧 글이 됩니다.</p>

<p><strong>최소 구성</strong>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.
├── _config.yml          # 사이트 설정
├── _layouts/
│   ├── default.html     # 모든 페이지 공통 레이아웃
│   └── post.html        # 블로그 글 레이아웃
├── _includes/
│   ├── head.html
│   ├── header.html
│   └── footer.html
├── _posts/
│   └── YYYY-MM-DD-제목.md
├── assets/css/main.css
├── index.html           # 홈
└── about.md             # 소개
</code></pre></div></div>

<p><strong><code class="language-plaintext highlighter-rouge">_config.yml</code></strong> 예시:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">title</span><span class="pi">:</span> <span class="s">Glenn Yu</span>
<span class="na">tagline</span><span class="pi">:</span> <span class="s">Mobile · Adtech Engineer</span>
<span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://glenn-yu.github.io"</span>

<span class="na">author</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">Glenn Yu</span>
  <span class="na">email</span><span class="pi">:</span> <span class="s">gwangy.yu@gmail.com</span>
  <span class="na">github</span><span class="pi">:</span> <span class="s">glenn-yu</span>

<span class="na">markdown</span><span class="pi">:</span> <span class="s">kramdown</span>
<span class="na">permalink</span><span class="pi">:</span> <span class="s">/posts/:title/</span>
<span class="na">timezone</span><span class="pi">:</span> <span class="s">Asia/Seoul</span>

<span class="na">plugins</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">jekyll-feed</span>
  <span class="pi">-</span> <span class="s">jekyll-sitemap</span>
</code></pre></div></div>

<p><strong>Gemfile</strong> (로컬 미리보기 안 할 거면 없어도 됨):</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">source</span> <span class="s2">"https://rubygems.org"</span>
<span class="n">gem</span> <span class="s2">"github-pages"</span><span class="p">,</span> <span class="ss">group: :jekyll_plugins</span>
<span class="n">group</span> <span class="ss">:jekyll_plugins</span> <span class="k">do</span>
  <span class="n">gem</span> <span class="s2">"jekyll-feed"</span>
  <span class="n">gem</span> <span class="s2">"jekyll-sitemap"</span>
<span class="k">end</span>
</code></pre></div></div>

<p><strong>글 한 편</strong> (<code class="language-plaintext highlighter-rouge">_posts/2026-05-08-hello.md</code>):</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">안녕하세요"</span>
<span class="na">date</span><span class="pi">:</span> <span class="s">2026-05-08 14:00:00 +0900</span>
<span class="na">tags</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">intro</span><span class="pi">]</span>
<span class="nn">---</span>

본문...
</code></pre></div></div>

<p>push 하면 <code class="language-plaintext highlighter-rouge">/posts/안녕하세요/</code> 로 자동 발행됩니다.</p>

<h2 id="한국어-친화-셋업">한국어 친화 셋업</h2>

<p>기본 시스템 폰트로도 작동하지만, 한국어 가독성에 진심이라면 <a href="https://github.com/orioncactus/pretendard">Pretendard</a> 추천입니다.
CDN 한 줄로 끝납니다.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span>
      <span class="na">href=</span><span class="s">"https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable.min.css"</span><span class="nt">&gt;</span>
</code></pre></div></div>

<p>CSS 폰트 스택:</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">:root</span> <span class="p">{</span>
  <span class="py">--font-sans</span><span class="p">:</span> <span class="s1">"Pretendard Variable"</span><span class="p">,</span> <span class="n">Pretendard</span><span class="p">,</span> <span class="n">-apple-system</span><span class="p">,</span>
               <span class="n">BlinkMacSystemFont</span><span class="p">,</span> <span class="s1">"Apple SD Gothic Neo"</span><span class="p">,</span>
               <span class="s1">"Segoe UI"</span><span class="p">,</span> <span class="n">Roboto</span><span class="p">,</span> <span class="nb">sans-serif</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">body</span> <span class="p">{</span> <span class="nl">font-family</span><span class="p">:</span> <span class="n">var</span><span class="p">(</span><span class="n">--font-sans</span><span class="p">);</span> <span class="p">}</span>
</code></pre></div></div>

<h2 id="다크라이트-자동--수동-토글">다크/라이트 자동 + 수동 토글</h2>

<p><code class="language-plaintext highlighter-rouge">prefers-color-scheme</code> 미디어 쿼리만 쓰면 시스템 설정에 따라 자동 전환됩니다.</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">:root</span> <span class="p">{</span> <span class="py">--bg</span><span class="p">:</span> <span class="m">#ffffff</span><span class="p">;</span> <span class="py">--fg</span><span class="p">:</span> <span class="m">#111418</span><span class="p">;</span> <span class="p">}</span>
<span class="k">@media</span> <span class="p">(</span><span class="n">prefers-color-scheme</span><span class="p">:</span> <span class="n">dark</span><span class="p">)</span> <span class="p">{</span>
  <span class="nd">:root</span> <span class="p">{</span> <span class="py">--bg</span><span class="p">:</span> <span class="m">#0e1116</span><span class="p">;</span> <span class="py">--fg</span><span class="p">:</span> <span class="m">#e6edf3</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>수동 토글까지 원하면 <code class="language-plaintext highlighter-rouge">data-theme</code> 속성으로 오버라이드를 추가합니다.</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">:root</span><span class="o">[</span><span class="nt">data-theme</span><span class="o">=</span><span class="s1">"light"</span><span class="o">]</span> <span class="p">{</span> <span class="py">--bg</span><span class="p">:</span> <span class="m">#ffffff</span><span class="p">;</span> <span class="p">}</span>
<span class="nd">:root</span><span class="o">[</span><span class="nt">data-theme</span><span class="o">=</span><span class="s1">"dark"</span><span class="o">]</span>  <span class="p">{</span> <span class="py">--bg</span><span class="p">:</span> <span class="m">#0e1116</span><span class="p">;</span> <span class="p">}</span>
</code></pre></div></div>

<p>JS 로 토글하면서 <code class="language-plaintext highlighter-rouge">localStorage</code> 에 저장하면 페이지 이동에도 유지됩니다.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">toggleTheme</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">var</span> <span class="nx">cur</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">.</span><span class="nx">getAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">data-theme</span><span class="dl">'</span><span class="p">);</span>
  <span class="kd">var</span> <span class="nx">next</span> <span class="o">=</span> <span class="nx">cur</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">dark</span><span class="dl">'</span> <span class="p">?</span> <span class="dl">'</span><span class="s1">light</span><span class="dl">'</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">dark</span><span class="dl">'</span><span class="p">;</span>
  <span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">data-theme</span><span class="dl">'</span><span class="p">,</span> <span class="nx">next</span><span class="p">);</span>
  <span class="nx">localStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="dl">'</span><span class="s1">theme</span><span class="dl">'</span><span class="p">,</span> <span class="nx">next</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>페인트 깜빡임을 막으려면 <code class="language-plaintext highlighter-rouge">&lt;head&gt;</code> 에서 동기 스크립트로 미리 적용해 두세요.</p>

<h2 id="색다른-한-끗--로그캣-부팅-인트로">색다른 한 끗 — 로그캣 부팅 인트로</h2>

<p>여기부터는 취향 영역입니다.
저는 안드로이드 개발자 정체성을 살리고 싶어서, 사이트 첫 진입 시 <code class="language-plaintext highlighter-rouge">adb logcat</code> 같은 텍스트가 흐르는 인트로를 넣었습니다.</p>

<p>핵심 아이디어:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">sessionStorage</code> 로 <strong>세션당 1회만</strong> 표시</li>
  <li>클릭 / 키 입력으로 즉시 skip</li>
  <li>텍스트는 한 줄씩 <code class="language-plaintext highlighter-rouge">setTimeout</code> 으로 출력</li>
  <li>페이지 본문은 <code class="language-plaintext highlighter-rouge">boot-pending</code> 클래스로 잠시 숨김 → 인트로 끝나면 해제</li>
</ul>

<p>요지만 옮기면:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">runBoot</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">sessionStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="dl">'</span><span class="s1">boot-seen</span><span class="dl">'</span><span class="p">)</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">1</span><span class="dl">'</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
  <span class="c1">// overlay 생성, 줄 단위로 setTimeout 출력</span>
  <span class="c1">// 끝나면 sessionStorage.setItem('boot-seen', '1')</span>
<span class="p">}</span>
</code></pre></div></div>

<p><a href="https://github.com/glenn-yu/glenn-yu.github.io/blob/main/assets/js/site.js">전체 구현 보기</a>.</p>

<h2 id="k-커맨드-팔레트">⌘K 커맨드 팔레트</h2>

<p>Linear / VSCode 의 그 팔레트입니다. <code class="language-plaintext highlighter-rouge">Cmd+K</code> 누르면 페이지·글·명령을 한 번에 검색.</p>

<p>페이지·글 데이터는 <code class="language-plaintext highlighter-rouge">_layouts/default.html</code> 에서 Liquid 로 주입합니다:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script&gt;</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">SITE_DATA</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">pages</span><span class="p">:</span> <span class="p">[</span>
    <span class="p">{</span> <span class="na">title</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Home</span><span class="dl">"</span><span class="p">,</span>  <span class="na">url</span><span class="p">:</span> <span class="dl">"</span><span class="s2">{{ '/' | relative_url }}</span><span class="dl">"</span> <span class="p">},</span>
    <span class="p">{</span> <span class="na">title</span><span class="p">:</span> <span class="dl">"</span><span class="s2">About</span><span class="dl">"</span><span class="p">,</span> <span class="na">url</span><span class="p">:</span> <span class="dl">"</span><span class="s2">{{ '/about/' | relative_url }}</span><span class="dl">"</span> <span class="p">}</span>
  <span class="p">],</span>
  <span class="na">posts</span><span class="p">:</span> <span class="p">[</span>
    <span class="p">{</span><span class="o">%-</span> <span class="k">for</span> <span class="nx">p</span> <span class="k">in</span> <span class="nx">site</span><span class="p">.</span><span class="nx">posts</span> <span class="o">-%</span><span class="p">}</span>
      <span class="p">{</span> <span class="na">title</span><span class="p">:</span> <span class="p">{{</span> <span class="nx">p</span><span class="p">.</span><span class="nx">title</span> <span class="o">|</span> <span class="nx">jsonify</span> <span class="p">}},</span>
        <span class="na">url</span><span class="p">:</span> <span class="p">{{</span> <span class="nx">p</span><span class="p">.</span><span class="nx">url</span> <span class="o">|</span> <span class="nx">relative_url</span> <span class="o">|</span> <span class="nx">jsonify</span> <span class="p">}}</span> <span class="p">}</span>
      <span class="p">{</span><span class="o">%-</span> <span class="nx">unless</span> <span class="nx">forloop</span><span class="p">.</span><span class="nx">last</span> <span class="o">-%</span><span class="p">},{</span><span class="o">%-</span> <span class="nx">endunless</span> <span class="o">-%</span><span class="p">}</span>
    <span class="p">{</span><span class="o">%-</span> <span class="nx">endfor</span> <span class="o">-%</span><span class="p">}</span>
  <span class="p">]</span>
<span class="p">};</span>
<span class="nt">&lt;/script&gt;</span>
</code></pre></div></div>

<p>JS 쪽에서는 <code class="language-plaintext highlighter-rouge">Cmd+K</code> 핸들링 + 간단한 퍼지 매칭 + 화살표 키 네비게이션만 있으면 됩니다.</p>

<h2 id="seo--공유-기본--파비콘--og-이미지">SEO · 공유 기본 — 파비콘 + OG 이미지</h2>

<p>여기는 건너뛰면 안 됩니다.
카카오톡·슬랙·트위터 어디든 링크 미리보기가 안 뜨면 사이트가 빈약해 보여요.</p>

<p><strong>파비콘</strong> (SVG 추천 — 모던 브라우저 모두 지원):</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"icon"</span> <span class="na">type=</span><span class="s">"image/svg+xml"</span> <span class="na">href=</span><span class="s">"/assets/favicon.svg"</span><span class="nt">&gt;</span>
<span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"icon"</span> <span class="na">type=</span><span class="s">"image/png"</span> <span class="na">sizes=</span><span class="s">"64x64"</span> <span class="na">href=</span><span class="s">"/assets/favicon.png"</span><span class="nt">&gt;</span>
<span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"apple-touch-icon"</span> <span class="na">href=</span><span class="s">"/assets/apple-touch-icon.png"</span><span class="nt">&gt;</span>
</code></pre></div></div>

<p><strong>Open Graph / Twitter Card 메타</strong>:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;meta</span> <span class="na">property=</span><span class="s">"og:type"</span> <span class="na">content=</span><span class="s">"website"</span><span class="nt">&gt;</span>
<span class="nt">&lt;meta</span> <span class="na">property=</span><span class="s">"og:title"</span> <span class="na">content=</span><span class="s">"{{ site.title }}"</span><span class="nt">&gt;</span>
<span class="nt">&lt;meta</span> <span class="na">property=</span><span class="s">"og:description"</span> <span class="na">content=</span><span class="s">"{{ site.description }}"</span><span class="nt">&gt;</span>
<span class="nt">&lt;meta</span> <span class="na">property=</span><span class="s">"og:image"</span> <span class="na">content=</span><span class="s">"{{ '/assets/og-image.png' | absolute_url }}"</span><span class="nt">&gt;</span>
<span class="nt">&lt;meta</span> <span class="na">name=</span><span class="s">"twitter:card"</span> <span class="na">content=</span><span class="s">"summary_large_image"</span><span class="nt">&gt;</span>
<span class="nt">&lt;meta</span> <span class="na">name=</span><span class="s">"twitter:image"</span> <span class="na">content=</span><span class="s">"{{ '/assets/og-image.png' | absolute_url }}"</span><span class="nt">&gt;</span>
</code></pre></div></div>

<p>OG 이미지는 1200×630 권장이지만 1200×1200 도 대부분 플랫폼이 받아줍니다.
디자이너 없으면 SVG 로 직접 그리고 macOS 의 <code class="language-plaintext highlighter-rouge">qlmanage</code> 로 PNG 변환 가능:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>qlmanage <span class="nt">-t</span> <span class="nt">-s</span> 1200 <span class="nt">-o</span> <span class="nb">.</span> og-image.svg
<span class="nb">mv </span>og-image.svg.png og-image.png
</code></pre></div></div>

<h2 id="포스트-ux-폴리시">포스트 UX 폴리시</h2>

<p><strong>읽기 시간</strong> — kramdown 의 <code class="language-plaintext highlighter-rouge">size</code> 필터로 글자 수 세고 분 단위 환산. 한국어 기준 ~500자/분:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{%-</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">chars</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">content</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">strip_html</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">size</span><span class="w"> </span><span class="p">-%}</span>
<span class="p">{%-</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">rt</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">chars</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">divided_by</span><span class="p">:</span><span class="w"> </span><span class="mi">500</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">plus</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="p">-%}</span>
📖 <span class="p">{{</span><span class="w"> </span><span class="nv">rt</span><span class="w"> </span><span class="p">}}</span>분 읽기
</code></pre></div></div>

<p><strong>코드 복사 버튼</strong> — <code class="language-plaintext highlighter-rouge">&lt;pre&gt;</code> 마다 버튼 주입:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="dl">'</span><span class="s1">.post-content pre</span><span class="dl">'</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">pre</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">var</span> <span class="nx">btn</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">button</span><span class="dl">'</span><span class="p">);</span>
  <span class="nx">btn</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">Copy</span><span class="dl">'</span><span class="p">;</span>
  <span class="nx">btn</span><span class="p">.</span><span class="nx">onclick</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">code</span> <span class="o">=</span> <span class="p">(</span><span class="nx">pre</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">code</span><span class="dl">'</span><span class="p">)</span> <span class="o">||</span> <span class="nx">pre</span><span class="p">).</span><span class="nx">textContent</span><span class="p">;</span>
    <span class="nb">navigator</span><span class="p">.</span><span class="nx">clipboard</span><span class="p">.</span><span class="nx">writeText</span><span class="p">(</span><span class="nx">code</span><span class="p">);</span>
    <span class="nx">btn</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">Copied!</span><span class="dl">'</span><span class="p">;</span>
    <span class="nx">setTimeout</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span> <span class="nx">btn</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">Copy</span><span class="dl">'</span><span class="p">;</span> <span class="p">},</span> <span class="mi">1500</span><span class="p">);</span>
  <span class="p">};</span>
  <span class="nx">pre</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">btn</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<p><strong>헤딩 앵커</strong> — kramdown 은 <code class="language-plaintext highlighter-rouge">auto_ids</code> 가 기본 활성이라 <code class="language-plaintext highlighter-rouge">h2</code> / <code class="language-plaintext highlighter-rouge">h3</code> 에 자동으로 <code class="language-plaintext highlighter-rouge">id</code> 가 붙습니다.
JS 로 호버 시 보이는 <code class="language-plaintext highlighter-rouge">#</code> 링크만 추가하면 됩니다.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="dl">'</span><span class="s1">.post-content h2[id], .post-content h3[id]</span><span class="dl">'</span><span class="p">)</span>
  <span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">h</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">a</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">a</span><span class="dl">'</span><span class="p">);</span>
    <span class="nx">a</span><span class="p">.</span><span class="nx">className</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">heading-anchor</span><span class="dl">'</span><span class="p">;</span>
    <span class="nx">a</span><span class="p">.</span><span class="nx">href</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">#</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">h</span><span class="p">.</span><span class="nx">id</span><span class="p">;</span>
    <span class="nx">a</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">#</span><span class="dl">'</span><span class="p">;</span>
    <span class="nx">h</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">a</span><span class="p">);</span>
  <span class="p">});</span>
</code></pre></div></div>

<h2 id="giscus-댓글">giscus 댓글</h2>

<p>Disqus 대신 GitHub Discussions 기반 <a href="https://giscus.app">giscus</a> 를 썼습니다.
광고 없고, 댓글이 곧 디스커션이라 통계 보기도 편합니다.</p>

<p><strong>셋업 순서</strong>:</p>

<ol>
  <li>레포에 Discussions 활성화:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gh api <span class="nt">-X</span> PATCH repos/&lt;유저&gt;/&lt;레포&gt; <span class="nt">-f</span> <span class="nv">has_discussions</span><span class="o">=</span><span class="nb">true</span>
</code></pre></div>    </div>
  </li>
  <li><a href="https://github.com/apps/giscus">github.com/apps/giscus</a> 에서 giscus 앱을 레포에 설치.</li>
  <li><a href="https://giscus.app">giscus.app</a> 에서 설정 후 스크립트 카피.</li>
</ol>

<p><strong>스크립트</strong> (<code class="language-plaintext highlighter-rouge">_includes/giscus.html</code> 같은 곳에 두고 post 레이아웃에서 include):</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://giscus.app/client.js"</span>
        <span class="na">data-repo=</span><span class="s">"&lt;유저&gt;/&lt;레포&gt;"</span>
        <span class="na">data-repo-id=</span><span class="s">"..."</span>
        <span class="na">data-category=</span><span class="s">"Announcements"</span>
        <span class="na">data-category-id=</span><span class="s">"..."</span>
        <span class="na">data-mapping=</span><span class="s">"pathname"</span>
        <span class="na">data-theme=</span><span class="s">"preferred_color_scheme"</span>
        <span class="na">data-lang=</span><span class="s">"ko"</span>
        <span class="na">crossorigin=</span><span class="s">"anonymous"</span>
        <span class="na">async</span><span class="nt">&gt;</span>
<span class="nt">&lt;/script&gt;</span>
</code></pre></div></div>

<p>수동 테마 토글이 있으면 giscus iframe 한테도 알려줘야 합니다:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">iframe</span><span class="p">.</span><span class="nx">contentWindow</span><span class="p">.</span><span class="nx">postMessage</span><span class="p">(</span>
  <span class="p">{</span> <span class="na">giscus</span><span class="p">:</span> <span class="p">{</span> <span class="na">setConfig</span><span class="p">:</span> <span class="p">{</span> <span class="na">theme</span><span class="p">:</span> <span class="dl">'</span><span class="s1">dark</span><span class="dl">'</span> <span class="p">}</span> <span class="p">}</span> <span class="p">},</span>
  <span class="dl">'</span><span class="s1">https://giscus.app</span><span class="dl">'</span>
<span class="p">);</span>
</code></pre></div></div>

<h2 id="404-페이지에도-정체성을">404 페이지에도 정체성을</h2>

<p><code class="language-plaintext highlighter-rouge">/404.html</code> 만들어 두면 GitHub Pages 가 안 잡히는 경로마다 자동으로 띄워줍니다.
저는 안드로이드 크래시 화면 미러링한 logcat 스타일로 만들었습니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FATAL EXCEPTION: main
Process: io.glenn.site, PID: 404
com.gwangy.site.RouteNotFoundException: no route matched "/asdf"
    at com.gwangy.site.Router.resolve(Router.kt:42)
    ...
</code></pre></div></div>

<p>JS 로 잘못 입력된 경로를 동적으로 박아 넣어서 어디서 길을 잃었는지 보여줍니다.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">bad-path</span><span class="dl">'</span><span class="p">).</span><span class="nx">textContent</span>
  <span class="o">=</span> <span class="dl">'</span><span class="s1">"</span><span class="dl">'</span> <span class="o">+</span> <span class="p">(</span><span class="nx">location</span><span class="p">.</span><span class="nx">pathname</span> <span class="o">+</span> <span class="nx">location</span><span class="p">.</span><span class="nx">search</span><span class="p">).</span><span class="nx">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">80</span><span class="p">)</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">"</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>

<h2 id="정리하며">정리하며</h2>

<p>하루 만에 만든 셋업이지만 글 쓸 환경으로는 충분합니다.
정리하면 이렇습니다.</p>

<p><strong>필수</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">username.github.io</code> 레포 + <code class="language-plaintext highlighter-rouge">index.html</code></li>
  <li>Jekyll layouts / includes / posts</li>
  <li>다크 · 라이트 + 한국어 폰트</li>
  <li>파비콘 + OG 이미지</li>
</ul>

<p><strong>있으면 좋은</strong></p>

<ul>
  <li>정체성을 드러내는 한 끗 (부팅 인트로 같은)</li>
  <li>⌘K 팔레트</li>
  <li>읽기 시간 + 코드 복사 + 헤딩 앵커</li>
  <li>giscus 댓글</li>
  <li>커스텀 404</li>
</ul>

<p>다음은 사이트를 만지는 게 아니라 <strong>글을 쓰는 것</strong>입니다.
만든 도구를 실제로 쓰면서 부족한 부분을 발견하는 게 다음 사이클이에요.</p>

<hr />

<p>읽어주셔서 감사합니다.
질문 / 제보 / 잘못된 부분은 아래 댓글이나 <a href="https://github.com/glenn-yu/glenn-yu.github.io/issues">이슈</a> 로 부탁드립니다 🙇‍♂️</p>]]></content><author><name>Glenn Yu</name><email>gwangy.yu@gmail.com</email></author><category term="github-pages" /><category term="jekyll" /><category term="blog" /><category term="setup" /><summary type="html"><![CDATA[username.github.io 레포 한 줄 push 부터 logcat 부팅 인트로 · Cmd+K 팔레트 · giscus 댓글 · 커스텀 404 까지. 하루 만에 만든 개인 블로그 셋업 정리.]]></summary></entry><entry><title type="html">Hello, World — 블로그를 시작하며</title><link href="https://glenn-yu.github.io/posts/hello-world/" rel="alternate" type="text/html" title="Hello, World — 블로그를 시작하며" /><published>2026-05-07T21:00:00+09:00</published><updated>2026-05-07T21:00:00+09:00</updated><id>https://glenn-yu.github.io/posts/hello-world</id><content type="html" xml:base="https://glenn-yu.github.io/posts/hello-world/"><![CDATA[<h2 id="시작하며">시작하며</h2>

<p>GitHub Pages 와 Jekyll 을 이용해 블로그를 만들었습니다.
앞으로 다음과 같은 주제로 글을 정리해 나갈 예정입니다.</p>

<ul>
  <li><strong>Android / Kotlin</strong> — Jetpack Compose, Coroutines, 아키텍처 패턴</li>
  <li><strong>모바일 광고 SDK</strong> — SSP / Mediation / Bridge 구현</li>
  <li><strong>크로스플랫폼</strong> — React Native · Flutter Bridge 작업기</li>
  <li><strong>Tooling</strong> — Gradle, GitHub Actions, 자동화</li>
</ul>

<h2 id="왜-jekyll-인가">왜 Jekyll 인가</h2>

<ul>
  <li><strong>GitHub Pages 가 자동 빌드</strong> — <code class="language-plaintext highlighter-rouge">git push</code> 만 하면 끝</li>
  <li><strong>마크다운 기반</strong> — 글 쓰기에 집중</li>
  <li><strong>유연한 커스터마이징</strong> — Liquid 템플릿 + Sass</li>
</ul>

<h2 id="다음-글-예고">다음 글 예고</h2>

<p>다음 글에서는 이 블로그를 만들면서 한 결정들과
한국어 타이포그래피를 위한 Pretendard 적용기를 다뤄볼 예정입니다.</p>

<hr />

<p>읽어주셔서 감사합니다 🙇‍♂️</p>]]></content><author><name>Glenn Yu</name><email>gwangy.yu@gmail.com</email></author><category term="blog" /><category term="intro" /><summary type="html"><![CDATA[GitHub Pages + Jekyll 로 블로그를 시작합니다. 앞으로 Android, Kotlin, 모바일 광고 SDK 관련 글을 정리해 나갈 예정입니다.]]></summary></entry></feed>