네이티브 광고는 앱의 콘텐츠와 자연스럽게 어우러지도록 커스터마이징할 수 있는 광고입니다. AdropNativeAd와 AdropNativeAdView를 사용하여 앱의 UI에 맞게 광고를 구성할 수 있습니다.
주요 특징
- 앱 디자인에 맞춤 레이아웃 구성 가능
- 헤드라인, 본문, CTA 버튼, 프로필 등 다양한 요소 제공
- 이미지 및 HTML 크리에이티브 지원
- 커스텀 클릭 핸들링 지원
- 백필 광고 지원
개발 환경에서는 테스트 유닛 ID를 사용하세요: PUBLIC_TEST_UNIT_ID_NATIVE
AdropNativeAd
생성자
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> | 광고를 로드합니다 |
다른 광고 타입(AdropInterstitialAd, AdropRewardedAd, AdropPopupAd)과 달리 AdropNativeAd는 dispose() 메서드를 제공하지 않습니다.
새로운 광고를 로드하려면 새 AdropNativeAd 인스턴스를 생성하세요. 이전 인스턴스의 리소스는 가비지 컬렉터에 의해 자동으로 해제됩니다.
AdropNativeAdView
네이티브 광고를 화면에 표시하는 위젯입니다.
생성자
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 |
기본 사용법
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
네이티브 광고 이벤트를 처리하는 리스너입니다.
콜백 함수
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을 사용합니다.
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를 사용하여 표시할 수 있습니다.
네이티브 광고의 영상 크리에이티브는 반드시 properties.creative(HTML 페이로드)를 WebView로 렌더링해야 합니다. properties.asset을 video_player 등 자체 플레이어에 직접 전달하면 SDK의 영상 트래킹 파이프라인을 거치지 않게 되어 VTR이 측정되지 않고 onAdVideoStart / onAdVideoEnd 콜백도 호출되지 않습니다. 영상 트래킹과 VTR 섹션을 참고하세요.
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!),
),
),
],
),
),
);
}
}
HTML 크리에이티브를 사용하려면 webview_flutter 패키지를 추가해야 합니다.flutter pub add webview_flutter
백필 광고 처리
네이티브 광고가 백필 광고인 경우 isBackfilled 속성으로 확인하고 처리할 수 있습니다.
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을 설정하세요.
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 | 우측 하단 |
이 설정은 백필 네트워크에 전달되는 선호 위치 힌트로, 백필 네이티브 광고에만 적용됩니다 — 직광고에는 영향이 없습니다. 백필 네트워크가 정책상 다른 위치로 표시할 수 있습니다.
영상 트래킹과 VTR
Adrop은 네이티브 광고의 영상 지표 — VTR(영상 완수율), onAdVideoStart, onAdVideoEnd — 를 SDK 미디어 컨테이너 안에서 영상이 렌더링될 때에만 수집합니다. Flutter에서는 HTML 크리에이티브 표시 섹션처럼 properties.creative(HTML 페이로드)를 WebView로 렌더링하는 것이 그 방식입니다.
properties.asset 필드는 원본 이미지 또는 영상 파일의 URL을 반환합니다. 이 값은 썸네일 표시나 매체사 자체 분석 등 부가 메타데이터 용도로 노출된 것이며, 재생을 직접 처리하기 위한 것이 아닙니다.
영상 네이티브 광고의 properties.asset URL을 자체 영상 플레이어(video_player, chewie 또는 서드파티 플레이어)에 직접 전달하지 마세요. SDK 미디어 컨테이너를 거치지 않으면 다음 현상이 발생합니다.
- 해당 지면에서 VTR(영상 완수율)이 수집되지 않습니다.
onAdVideoStart / onAdVideoEnd 콜백이 호출되지 않습니다.
- 해당 유닛의 영상 성과 집계가 누락되거나 0으로 보고됩니다.
클릭(onAdClicked)과 노출(onAdImpression) 트래킹은 AdropNativeAdView에 연결되어 있으므로 계속 동작하지만, 영상 관련 신호는 손실됩니다.
권장 패턴
영상 크리에이티브는 WebView + HTML 크리에이티브 패턴을 사용하세요.
if (ad.properties.creative != null) {
webViewController.loadHtmlString(ad.properties.creative!);
}
WebView를 AdropNativeAdView 내부에 배치해 클릭과 노출 트래킹이 광고 컨테이너에 계속 연결되도록 유지하세요.
특정 지면이 반드시 자체 플레이어를 사용해야 하고 VTR이 측정되지 않는 것을 수용한다면, 광고를 AdropNativeAdView에 바인딩한 채로 유지해 클릭과 노출 어트리뷰션은 계속 확보할 수 있습니다. 다만 해당 지면을 내부 리포트에서 비-VTR 인벤토리로 분류하고 SDK가 측정한 영상 성과와 섞지 마세요.
추가 필드 사용
매체사가 정의한 추가 필드는 extra 맵에서 접근할 수 있습니다.
onAdReceived: (ad) {
// 추가 필드 접근
final customField = ad.properties.extra['customFieldKey'];
if (customField != null) {
debugPrint('커스텀 필드: $customField');
}
setState(() {
isLoaded = true;
});
}
에러 처리
백필 관련 에러
백필 광고가 설정된 경우, 직광고와 백필 광고 모두 없을 때 backfillNoFill 에러 코드가 반환됩니다.
onAdFailedToReceive: (ad, errorCode) {
if (errorCode == AdropErrorCode.backfillNoFill) {
debugPrint('백필 광고도 없습니다');
}
}
일반 에러 처리
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 인스턴스를 생성합니다.
void resetAd() {
setState(() {
isLoaded = false;
});
nativeAd = AdropNativeAd(
unitId: 'YOUR_UNIT_ID',
listener: AdropNativeListener(
onAdReceived: (ad) {
setState(() {
isLoaded = true;
});
},
),
);
nativeAd?.load();
}
2. 조건부 렌더링
광고가 로드될 때까지 적절한 플레이스홀더를 표시합니다.
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. 반응형 레이아웃
다양한 화면 크기에 대응하도록 레이아웃을 구성합니다.
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일 수 있으므로 항상 확인합니다.
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,
),
);
}
다음 단계
전면 광고
화면 전체를 덮는 전면 광고 구현하기
보상형 광고
보상을 제공하는 보상형 광고 구현하기