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

# 네이티브 광고

> Flutter 앱에서 네이티브 광고를 구현하는 방법을 안내합니다.

## 개요

네이티브 광고는 앱의 콘텐츠와 자연스럽게 어우러지도록 커스터마이징할 수 있는 광고입니다. `AdropNativeAd`와 `AdropNativeAdView`를 사용하여 앱의 UI에 맞게 광고를 구성할 수 있습니다.

### 주요 특징

* 앱 디자인에 맞춤 레이아웃 구성 가능
* 헤드라인, 본문, CTA 버튼, 프로필 등 다양한 요소 제공
* 이미지 및 HTML 크리에이티브 지원
* 커스텀 클릭 핸들링 지원
* 백필 광고 지원

<Note>
  개발 환경에서는 테스트 유닛 ID를 사용하세요: `PUBLIC_TEST_UNIT_ID_NATIVE`
</Note>

***

## AdropNativeAd

### 생성자

```dart theme={null}
AdropNativeAd({
  required String unitId,
  bool useCustomClick = false,
  AdropAdChoicesPosition preferredAdChoicesPosition = AdropAdChoicesPosition.topRight,
  AdropNativeListener? listener,
})
```

**파라미터**

| 파라미터                         | 타입                       | 필수 | 설명                                                                      |
| ---------------------------- | ------------------------ | -- | ----------------------------------------------------------------------- |
| `unitId`                     | `String`                 | Y  | 애드컨트롤 콘솔에서 생성한 유닛 ID                                                    |
| `useCustomClick`             | `bool`                   | N  | 커스텀 클릭 핸들링 사용 여부 (기본값: `false`)                                         |
| `preferredAdChoicesPosition` | `AdropAdChoicesPosition` | N  | 백필 네이티브 광고의 AdChoices 마크 표시 위치 (기본값: `AdropAdChoicesPosition.topRight`) |
| `listener`                   | `AdropNativeListener`    | N  | 광고 이벤트 리스너                                                              |

### 속성

| 속성               | 타입                      | 설명                 |
| ---------------- | ----------------------- | ------------------ |
| `isLoaded`       | `bool`                  | 광고 로드 완료 여부        |
| `unitId`         | `String`                | 광고 유닛 ID           |
| `creativeId`     | `String`                | 크리에이티브 ID          |
| `txId`           | `String`                | 트랜잭션 ID            |
| `campaignId`     | `String`                | 캠페인 ID             |
| `destinationURL` | `String`                | 목적지 URL            |
| `properties`     | `AdropNativeProperties` | 네이티브 광고 속성         |
| `creativeSize`   | `CreativeSize`          | 크리에이티브 크기          |
| `isBackfilled`   | `bool`                  | 백필 광고 여부           |
| `browserTarget`  | `BrowserTarget?`        | 브라우저 타겟 (외부 또는 내부) |

### 메서드

| 메서드      | 반환 타입          | 설명        |
| -------- | -------------- | --------- |
| `load()` | `Future<void>` | 광고를 로드합니다 |

<Note>
  다른 광고 타입(`AdropInterstitialAd`, `AdropRewardedAd`, `AdropPopupAd`)과 달리 `AdropNativeAd`는 `dispose()` 메서드를 제공하지 않습니다.
  새로운 광고를 로드하려면 새 `AdropNativeAd` 인스턴스를 생성하세요. 이전 인스턴스의 리소스는 가비지 컬렉터에 의해 자동으로 해제됩니다.
</Note>

***

## AdropNativeAdView

네이티브 광고를 화면에 표시하는 위젯입니다.

### 생성자

```dart theme={null}
AdropNativeAdView({
  required AdropNativeAd? ad,
  required Widget child,
})
```

**파라미터**

| 파라미터    | 타입               | 필수 | 설명                |
| ------- | ---------------- | -- | ----------------- |
| `ad`    | `AdropNativeAd?` | Y  | 로드된 네이티브 광고 객체    |
| `child` | `Widget`         | Y  | 광고 콘텐츠를 표시할 자식 위젯 |

***

## AdropNativeProperties

네이티브 광고의 콘텐츠 속성입니다.

### 속성

| 속성               | 타입                    | 설명              |
| ---------------- | --------------------- | --------------- |
| `headline`       | `String?`             | 광고 제목           |
| `body`           | `String?`             | 광고 본문           |
| `creative`       | `String?`             | HTML 크리에이티브 콘텐츠 |
| `asset`          | `String?`             | 이미지 에셋 URL      |
| `destinationURL` | `String?`             | 클릭 시 이동할 URL    |
| `callToAction`   | `String?`             | CTA 버튼 텍스트      |
| `profile`        | `AdropNativeProfile?` | 광고주 프로필 정보      |
| `extra`          | `Map<String, String>` | 추가 커스텀 필드       |
| `isBackfilled`   | `bool`                | 백필 광고 여부        |

### AdropNativeProfile

| 속성            | 타입        | 설명             |
| ------------- | --------- | -------------- |
| `displayName` | `String?` | 광고주 이름         |
| `displayLogo` | `String?` | 광고주 로고 이미지 URL |

***

## 기본 사용법

```dart theme={null}
import 'package:flutter/material.dart';
import 'package:adrop_ads_flutter/adrop_ads_flutter.dart';

class NativeAdExample extends StatefulWidget {
  const NativeAdExample({super.key});

  @override
  State<NativeAdExample> createState() => _NativeAdExampleState();
}

class _NativeAdExampleState extends State<NativeAdExample> {
  bool isLoaded = false;
  AdropNativeAd? nativeAd;

  @override
  void initState() {
    super.initState();
    _createNativeAd();
  }

  void _createNativeAd() {
    nativeAd = AdropNativeAd(
      unitId: 'YOUR_UNIT_ID',
      listener: AdropNativeListener(
        onAdReceived: (ad) {
          debugPrint('네이티브 광고 수신 성공: ${ad.creativeId}');
          setState(() {
            isLoaded = true;
          });
        },
        onAdClicked: (ad) {
          debugPrint('네이티브 광고 클릭: ${ad.creativeId}');
        },
        onAdImpression: (ad) {
          debugPrint('네이티브 광고 노출: ${ad.creativeId}');
        },
        onAdFailedToReceive: (ad, errorCode) {
          debugPrint('네이티브 광고 수신 실패: $errorCode');
          setState(() {
            isLoaded = false;
          });
        },
      ),
    );

    // 광고 로드
    nativeAd?.load();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('네이티브 광고 예제')),
      body: SingleChildScrollView(
        child: Column(
          children: [
            // 메인 콘텐츠
            const Padding(
              padding: EdgeInsets.all(16),
              child: Text('메인 콘텐츠'),
            ),
            // 네이티브 광고
            if (isLoaded) _buildNativeAdView(),
          ],
        ),
      ),
    );
  }

  Widget _buildNativeAdView() {
    return AdropNativeAdView(
      ad: nativeAd,
      child: Container(
        padding: const EdgeInsets.all(16),
        margin: const EdgeInsets.all(16),
        decoration: BoxDecoration(
          border: Border.all(color: Colors.grey),
          borderRadius: BorderRadius.circular(8),
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 광고주 프로필
            if (nativeAd?.properties.profile != null)
              Row(
                children: [
                  if (nativeAd?.properties.profile?.displayLogo != null)
                    Image.network(
                      nativeAd!.properties.profile!.displayLogo!,
                      width: 24,
                      height: 24,
                    ),
                  const SizedBox(width: 8),
                  Text(nativeAd?.properties.profile?.displayName ?? ''),
                ],
              ),
            const SizedBox(height: 8),
            // 헤드라인
            if (nativeAd?.properties.headline != null)
              Text(
                nativeAd!.properties.headline!,
                style: const TextStyle(
                  fontSize: 18,
                  fontWeight: FontWeight.bold,
                ),
              ),
            const SizedBox(height: 4),
            // 본문
            if (nativeAd?.properties.body != null)
              Text(nativeAd!.properties.body!),
            const SizedBox(height: 8),
            // 이미지 에셋
            if (nativeAd?.properties.asset != null)
              Image.network(
                nativeAd!.properties.asset!,
                width: double.infinity,
                fit: BoxFit.cover,
              ),
            const SizedBox(height: 8),
            // CTA 버튼
            if (nativeAd?.properties.callToAction != null)
              ElevatedButton(
                onPressed: () {},
                child: Text(nativeAd!.properties.callToAction!),
              ),
          ],
        ),
      ),
    );
  }
}
```

***

## AdropNativeListener

네이티브 광고 이벤트를 처리하는 리스너입니다.

### 콜백 함수

```dart theme={null}
AdropNativeListener(
  onAdReceived: (AdropNativeAd ad) {
    // 광고 수신 성공
  },
  onAdClicked: (AdropNativeAd ad) {
    // 광고 클릭
  },
  onAdImpression: (AdropNativeAd ad) {
    // 광고 노출
  },
  onAdFailedToReceive: (AdropNativeAd ad, AdropErrorCode errorCode) {
    // 광고 수신 실패
  },
  onAdVideoStart: (AdropNativeAd ad) {
    // 동영상 광고 재생 시작
  },
  onAdVideoEnd: (AdropNativeAd ad) {
    // 동영상 광고 재생 종료
  },
)
```

### 콜백 설명

| 콜백                    | 설명                |
| --------------------- | ----------------- |
| `onAdReceived`        | 광고 수신 성공 시 호출     |
| `onAdClicked`         | 광고 클릭 시 호출        |
| `onAdImpression`      | 광고 노출 시 호출        |
| `onAdFailedToReceive` | 광고 수신 실패 시 호출     |
| `onAdVideoStart`      | 동영상 광고 재생 시작 시 호출 |
| `onAdVideoEnd`        | 동영상 광고 재생 종료 시 호출 |

***

## 커스텀 클릭 핸들링

동영상 크리에이티브나 커스텀 클릭 동작이 필요한 경우 `useCustomClick`을 사용합니다.

```dart theme={null}
nativeAd = AdropNativeAd(
  unitId: 'YOUR_UNIT_ID',
  useCustomClick: true, // 커스텀 클릭 활성화
  listener: AdropNativeListener(
    onAdReceived: (ad) {
      setState(() {
        isLoaded = true;
      });
    },
    onAdClicked: (ad) {
      // 커스텀 클릭 동작 처리
      debugPrint('광고 클릭됨: ${ad.destinationURL}');
    },
  ),
);
```

`useCustomClick`이 `true`인 경우, 자식 위젯의 클릭 이벤트가 광고 클릭으로 처리됩니다.

***

## HTML 크리에이티브 표시

네이티브 광고에 HTML 크리에이티브가 포함된 경우 WebView를 사용하여 표시할 수 있습니다.

<Warning>
  **네이티브 광고의 영상 크리에이티브**는 반드시 `properties.creative`(HTML 페이로드)를 WebView로 렌더링해야 합니다. `properties.asset`을 `video_player` 등 자체 플레이어에 직접 전달하면 SDK의 영상 트래킹 파이프라인을 거치지 않게 되어 **VTR이 측정되지 않고** `onAdVideoStart` / `onAdVideoEnd` 콜백도 호출되지 않습니다. [영상 트래킹과 VTR](#영상-트래킹과-vtr) 섹션을 참고하세요.
</Warning>

```dart theme={null}
import 'package:webview_flutter/webview_flutter.dart';

class NativeWithWebView extends StatefulWidget {
  const NativeWithWebView({super.key});

  @override
  State<NativeWithWebView> createState() => _NativeWithWebViewState();
}

class _NativeWithWebViewState extends State<NativeWithWebView> {
  bool isLoaded = false;
  AdropNativeAd? nativeAd;
  late final WebViewController webViewController;

  @override
  void initState() {
    super.initState();

    // WebView 컨트롤러 초기화
    webViewController = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted);

    _createNativeAd();
  }

  void _createNativeAd() {
    nativeAd = AdropNativeAd(
      unitId: 'YOUR_UNIT_ID',
      useCustomClick: true,
      listener: AdropNativeListener(
        onAdReceived: (ad) {
          // HTML 크리에이티브 로드
          if (ad.properties.creative != null) {
            webViewController.loadHtmlString(ad.properties.creative!);
          }
          setState(() {
            isLoaded = true;
          });
        },
      ),
    );

    nativeAd?.load();
  }

  @override
  Widget build(BuildContext context) {
    if (!isLoaded) return const SizedBox.shrink();

    return AdropNativeAdView(
      ad: nativeAd,
      child: Container(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 프로필
            Row(
              children: [
                if (nativeAd?.properties.profile?.displayLogo != null)
                  Image.network(
                    nativeAd!.properties.profile!.displayLogo!,
                    width: 24,
                    height: 24,
                  ),
                const SizedBox(width: 8),
                Text(nativeAd?.properties.profile?.displayName ?? ''),
              ],
            ),
            const SizedBox(height: 8),
            // 헤드라인
            if (nativeAd?.properties.headline != null)
              Text(
                nativeAd!.properties.headline!,
                style: const TextStyle(
                  fontSize: 18,
                  fontWeight: FontWeight.bold,
                ),
              ),
            const SizedBox(height: 8),
            // HTML 크리에이티브
            SizedBox(
              width: MediaQuery.of(context).size.width,
              height: 300,
              child: WebViewWidget(controller: webViewController),
            ),
            // CTA 버튼
            if (nativeAd?.properties.callToAction != null)
              Padding(
                padding: const EdgeInsets.only(top: 8),
                child: ElevatedButton(
                  onPressed: () {},
                  child: Text(nativeAd!.properties.callToAction!),
                ),
              ),
          ],
        ),
      ),
    );
  }
}
```

<Note>
  HTML 크리에이티브를 사용하려면 `webview_flutter` 패키지를 추가해야 합니다.

  ```bash theme={null}
  flutter pub add webview_flutter
  ```
</Note>

***

## 백필 광고 처리

네이티브 광고가 백필 광고인 경우 `isBackfilled` 속성으로 확인하고 처리할 수 있습니다.

```dart theme={null}
Widget _buildCreativeView() {
  if (nativeAd?.isBackfilled == true) {
    // 백필 광고: 이미지 에셋 사용
    return Image.network(
      nativeAd?.properties.asset ?? '',
      width: double.infinity,
      fit: BoxFit.cover,
      loadingBuilder: (context, child, loadingProgress) {
        if (loadingProgress == null) return child;
        return const Center(child: CircularProgressIndicator());
      },
      errorBuilder: (context, error, stackTrace) {
        return const Icon(Icons.error);
      },
    );
  } else if (nativeAd?.properties.creative != null) {
    // 직광고: HTML 크리에이티브 사용
    return SizedBox(
      width: double.infinity,
      height: 300,
      child: WebViewWidget(controller: webViewController),
    );
  } else {
    return const SizedBox.shrink();
  }
}
```

### AdChoices 위치 조정

백필 네이티브 광고에서 AdChoices 마크가 표시될 코너를 지정할 수 있습니다. `AdropNativeAd` 생성자에서 `preferredAdChoicesPosition`을 설정하세요.

```dart theme={null}
final nativeAd = AdropNativeAd(
  unitId: 'YOUR_UNIT_ID',
  preferredAdChoicesPosition: AdropAdChoicesPosition.bottomRight,
  listener: AdropNativeListener(
    onAdReceived: (ad) => debugPrint('수신 완료'),
    onAdFailedToReceive: (ad, errorCode) => debugPrint('실패: $errorCode'),
  ),
);

await nativeAd.load();
```

| 값                                    | 설명          |
| ------------------------------------ | ----------- |
| `AdropAdChoicesPosition.topLeft`     | 좌측 상단       |
| `AdropAdChoicesPosition.topRight`    | 우측 상단 (기본값) |
| `AdropAdChoicesPosition.bottomLeft`  | 좌측 하단       |
| `AdropAdChoicesPosition.bottomRight` | 우측 하단       |

<Note>
  이 설정은 백필 네트워크에 전달되는 선호 위치 힌트로, 백필 네이티브 광고에만 적용됩니다 — 직광고에는 영향이 없습니다. 백필 네트워크가 정책상 다른 위치로 표시할 수 있습니다.
</Note>

***

## 영상 트래킹과 VTR

Adrop은 네이티브 광고의 영상 지표 — VTR(영상 완수율), `onAdVideoStart`, `onAdVideoEnd` — 를 SDK 미디어 컨테이너 안에서 영상이 렌더링될 때에만 수집합니다. Flutter에서는 [HTML 크리에이티브 표시](#html-크리에이티브-표시) 섹션처럼 `properties.creative`(HTML 페이로드)를 WebView로 렌더링하는 것이 그 방식입니다.

`properties.asset` 필드는 원본 이미지 또는 영상 파일의 URL을 반환합니다. 이 값은 썸네일 표시나 매체사 자체 분석 등 **부가 메타데이터** 용도로 노출된 것이며, 재생을 직접 처리하기 위한 것이 아닙니다.

<Warning>
  영상 네이티브 광고의 `properties.asset` URL을 자체 영상 플레이어(`video_player`, `chewie` 또는 서드파티 플레이어)에 직접 전달하지 마세요. SDK 미디어 컨테이너를 거치지 않으면 다음 현상이 발생합니다.

  * 해당 지면에서 **VTR(영상 완수율)이 수집되지 않습니다.**
  * `onAdVideoStart` / `onAdVideoEnd` 콜백이 **호출되지 않습니다.**
  * 해당 유닛의 영상 성과 집계가 누락되거나 0으로 보고됩니다.

  클릭(`onAdClicked`)과 노출(`onAdImpression`) 트래킹은 `AdropNativeAdView`에 연결되어 있으므로 계속 동작하지만, 영상 관련 신호는 손실됩니다.
</Warning>

### 권장 패턴

영상 크리에이티브는 WebView + HTML 크리에이티브 패턴을 사용하세요.

```dart theme={null}
if (ad.properties.creative != null) {
  webViewController.loadHtmlString(ad.properties.creative!);
}
```

WebView를 `AdropNativeAdView` 내부에 배치해 클릭과 노출 트래킹이 광고 컨테이너에 계속 연결되도록 유지하세요.

<Note>
  특정 지면이 반드시 자체 플레이어를 사용해야 하고 VTR이 측정되지 않는 것을 수용한다면, 광고를 `AdropNativeAdView`에 바인딩한 채로 유지해 클릭과 노출 어트리뷰션은 계속 확보할 수 있습니다. 다만 해당 지면을 내부 리포트에서 **비-VTR 인벤토리**로 분류하고 SDK가 측정한 영상 성과와 섞지 마세요.
</Note>

***

## 추가 필드 사용

매체사가 정의한 추가 필드는 `extra` 맵에서 접근할 수 있습니다.

```dart theme={null}
onAdReceived: (ad) {
  // 추가 필드 접근
  final customField = ad.properties.extra['customFieldKey'];
  if (customField != null) {
    debugPrint('커스텀 필드: $customField');
  }

  setState(() {
    isLoaded = true;
  });
}
```

***

## 에러 처리

### 백필 관련 에러

백필 광고가 설정된 경우, 직광고와 백필 광고 모두 없을 때 `backfillNoFill` 에러 코드가 반환됩니다.

```dart theme={null}
onAdFailedToReceive: (ad, errorCode) {
  if (errorCode == AdropErrorCode.backfillNoFill) {
    debugPrint('백필 광고도 없습니다');
  }
}
```

### 일반 에러 처리

```dart theme={null}
class _NativeAdState extends State<NativeAdWidget> {
  bool isLoaded = false;
  AdropErrorCode? errorCode;
  AdropNativeAd? nativeAd;

  @override
  void initState() {
    super.initState();

    nativeAd = AdropNativeAd(
      unitId: 'YOUR_UNIT_ID',
      listener: AdropNativeListener(
        onAdReceived: (ad) {
          setState(() {
            isLoaded = true;
            errorCode = null;
          });
        },
        onAdFailedToReceive: (ad, error) {
          // 백필 관련 에러 처리
          if (error == AdropErrorCode.backfillNoFill) {
            debugPrint('백필 광고도 없습니다');
          }

          setState(() {
            isLoaded = false;
            errorCode = error;
          });
        },
      ),
    );

    nativeAd?.load();
  }

  @override
  Widget build(BuildContext context) {
    if (isLoaded) {
      return _buildNativeAdView();
    } else if (errorCode != null) {
      return Text('광고 로드 실패: ${errorCode?.code}');
    } else {
      return const SizedBox.shrink();
    }
  }

  Widget _buildNativeAdView() {
    // 네이티브 광고 UI 구성
    return AdropNativeAdView(
      ad: nativeAd,
      child: Container(
        // ...
      ),
    );
  }
}
```

***

## 모범 사례

### 1. 광고 재생성

새로운 광고를 로드하려면 새 `AdropNativeAd` 인스턴스를 생성합니다.

```dart theme={null}
void resetAd() {
  setState(() {
    isLoaded = false;
  });

  nativeAd = AdropNativeAd(
    unitId: 'YOUR_UNIT_ID',
    listener: AdropNativeListener(
      onAdReceived: (ad) {
        setState(() {
          isLoaded = true;
        });
      },
    ),
  );

  nativeAd?.load();
}
```

### 2. 조건부 렌더링

광고가 로드될 때까지 적절한 플레이스홀더를 표시합니다.

```dart theme={null}
Widget buildNativeAd() {
  if (isLoaded && nativeAd != null) {
    return _buildNativeAdView();
  } else if (isLoading) {
    return const SizedBox(
      height: 200,
      child: Center(child: CircularProgressIndicator()),
    );
  } else {
    return const SizedBox.shrink();
  }
}
```

### 3. 반응형 레이아웃

다양한 화면 크기에 대응하도록 레이아웃을 구성합니다.

```dart theme={null}
Widget _buildNativeAdView() {
  return AdropNativeAdView(
    ad: nativeAd,
    child: LayoutBuilder(
      builder: (context, constraints) {
        return Container(
          width: constraints.maxWidth,
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // 광고 콘텐츠
            ],
          ),
        );
      },
    ),
  );
}
```

### 4. 광고 속성 null 체크

네이티브 광고 속성은 null일 수 있으므로 항상 확인합니다.

```dart theme={null}
Widget _buildHeadline() {
  final headline = nativeAd?.properties.headline;
  if (headline == null || headline.isEmpty) {
    return const SizedBox.shrink();
  }
  return Text(
    headline,
    style: const TextStyle(
      fontSize: 18,
      fontWeight: FontWeight.bold,
    ),
  );
}
```

***

## 다음 단계

<CardGroup cols={2}>
  <Card title="전면 광고" href="/ko/sdk/flutter/interstitial">
    화면 전체를 덮는 전면 광고 구현하기
  </Card>

  <Card title="보상형 광고" href="/ko/sdk/flutter/rewarded">
    보상을 제공하는 보상형 광고 구현하기
  </Card>

  <Card title="팝업 광고" href="/ko/sdk/flutter/popup">
    팝업 형태로 표시되는 광고 구현하기
  </Card>

  <Card title="레퍼런스" href="/ko/sdk/flutter/reference">
    타입, 메서드, 에러 코드 참고하기
  </Card>
</CardGroup>
