임베디드 오퍼월 통합

앱의 특정 탭에 AdChain SDK를 임베디드 방식으로 통합하는 실무 가이드입니다.

  • 대상: 임베디드 오퍼월을 구현하는 개발팀

  • SDK 버전: v1.0.21

  • 마지막 업데이트: 2025-10-30


개요

이 가이드는 앱의 혜택/포인트 탭에 AdChain SDK를 임베디드 방식으로 통합하는 방법을 설명합니다. React Native의 <AdchainOfferwallView /> 컴포넌트를 사용하여 기존 화면에 오퍼월을 삽입합니다.

이 문서는 실제 프로덕션 환경의 구현 사례를 기반으로 작성되었습니다. 일반적인 SDK 사용법은 React Native API 레퍼런스를 참고하세요.


빠른 시작: 필수 Props

임베디드 오퍼월에서 사용하는 Props와 필수 여부는 다음과 같습니다:

Props
iOS
Android
용도

placementId

✅ 필수

✅ 필수

오퍼월 식별자

style

✅ 필수

✅ 필수

{ flex: 1 } 권장

onCustomEvent

✅ 필수

✅ 필수

WebView → Native 이벤트 처리

onBackPressOnFirstPage

❌ 불필요

✅ 필수

Android 백버튼: 첫 페이지일 때

onBackNavigated

❌ 불필요

✅ 필수

Android 백버튼: 뒤로가기 성공 시

onOfferwallOpened

선택

선택

WebView 로드 완료

onOfferwallClosed

선택

선택

오퍼월 닫힘

onOfferwallError

권장

권장

WebView 로딩 실패


placementId 설정

placementId는 애드체인 팀과 사전 협의하여 정의합니다. 이 예시에서는 "benefits_tab"을 사용합니다.

<AdchainOfferwallView
  placementId="benefits_tab"  // 실제 값은 애드체인 팀과 협의
  // ...
/>

주의: 실제 placementId는 애드체인 서버 설정과 일치해야 합니다. 값을 변경할 경우 AdChain 팀과 사전 협의가 필요합니다.


필수 이벤트 처리: onCustomEvent

임베디드 오퍼월에서 가장 중요한 Props입니다. WebView에서 발생하는 이벤트를 Native로 전달받아 처리합니다.

처리해야 하는 이벤트

1. buy_ticket (구현 예시)

사용자가 오퍼월에서 특정 액션을 요청할 때 발생합니다. 앱의 해당 기능 UI를 표시합니다.

payload 구조 (예시):

{
  ticketId: string,
  amount: number
}

처리 방법:

onCustomEvent={(eventType, payload) => {
  if (eventType === 'buy_ticket') {
    // 앱의 티켓 구매 UI를 표시
    ShowBuyTicketUI();
    // 또는 payload를 사용하여 처리
    // const { ticketId, amount } = payload;
  }
}}

참고: 이 이벤트는 특정 구현 사례입니다. 실제 이벤트 타입과 payload 구조는 애드체인 팀과 사전 협의하여 정의합니다.

2. show_ticket_list (구현 예시)

사용자가 보유 항목 목록을 보려고 할 때 발생합니다. 앱의 해당 리스트 화면으로 이동합니다.

payload 구조 (예시):

{
  userId: string
}

처리 방법:

onCustomEvent={(eventType, payload) => {
  if (eventType === 'show_ticket_list') {
    // 앱의 티켓 리스트를 표시
    ShowTicketListUI();
  }
}}

참고: payload를 사용하지 않는 경우도 있습니다. 실제 구현은 비즈니스 로직에 따라 다릅니다.

전체 onCustomEvent 예제

<AdchainOfferwallView
  placementId="benefits_tab"
  style={{ flex: 1 }}
  onCustomEvent={(eventType, payload) => {
    console.log('[WebView → App]', eventType, payload);

    // 구현 예시 이벤트 처리
    if (eventType === 'buy_ticket') {
      // 티켓 구매 UI 표시
      ShowBuyTicketUI();
    }
    else if (eventType === 'show_ticket_list') {
      // 티켓 리스트 표시
      ShowTicketListUI();
    }
    // 처리되지 않은 이벤트 로깅
    else {
      console.warn('알 수 없는 이벤트:', eventType, payload);
    }
  }}
/>

중요: 실제 이벤트 타입과 처리 방법은 애드체인 팀과 사전 협의하여 정의하세요. 위 예시는 참고용입니다.


SafeArea 처리

Android와 iOS에서 상태바/노치 영역을 올바르게 처리하기 위해 SafeArea를 적용해야 합니다.

문제 상황

  • Android: 오퍼월이 상태바 영역까지 올라가서 상단이 화면 끝에 붙어 표시됨

  • iOS: 노치나 Dynamic Island가 있는 기기에서 오퍼월 콘텐츠가 가려질 수 있음

해결 방법

SafeArea 라이브러리를 사용하여 오퍼월을 감쌉니다. react-native-safe-area-context 또는 다른 SafeArea 관련 라이브러리를 사용할 수 있습니다.

1. 라이브러리 설치 (예시):

npm install react-native-safe-area-context

2. SafeAreaView 적용:

import { SafeAreaView } from 'react-native-safe-area-context';
import { AdchainOfferwallView } from '@1selfworld/adchain-sdk-react-native';

const BenefitsTab = () => {
  return (
    <SafeAreaView style={{ flex: 1 }} edges={['top']}>
      <AdchainOfferwallView
        style={{ flex: 1, width: '100%' }}
        placementId="benefits_tab"
        // ... 다른 props
      />
    </SafeAreaView>
  );
};

edges 옵션 설명:

  • edges={['top']}: 상단만 SafeArea 적용

  • 하단 탭바가 있는 경우 bottom은 제외합니다

  • iOS와 Android 모두 자동으로 대응됩니다

주의사항:

  • SafeAreaView는 오퍼월을 포함하는 전체 탭 화면에 적용합니다

  • 다른 화면(홈, 설정 등)에는 영향을 주지 않습니다

  • iOS 노치/Dynamic Island 기기와 Android 상태바 영역 모두에서 올바르게 동작합니다

  • 프로젝트에서 이미 사용 중인 SafeArea 라이브러리가 있다면 그것을 사용해도 무방합니다


Android 백버튼 처리

Android에서는 하드웨어 백버튼을 직접 처리해야 합니다. 그렇지 않으면 WebView 내부 네비게이션을 무시하고 앱이 종료됩니다.

문제 상황

  • 사용자가 오퍼월 안에서 여러 페이지를 이동함 (예: 메인 → 이벤트 상세 → 참여 화면)

  • 백버튼을 누르면 앱이 종료됨 (WebView 뒤로가기 무시)

해결 방법

React Native의 BackHandler로 백버튼 이벤트를 캐치하고, UIManager.dispatchViewManagerCommand로 네이티브에 처리를 위임합니다.

import React, { useRef, useEffect, useState } from 'react';
import { BackHandler, findNodeHandle, UIManager } from 'react-native';
import { AdchainOfferwallView } from '@1selfworld/adchain-sdk-react-native';

const BenefitsTab = () => {
  const offerwallViewRef = useRef(null);
  const [shouldAllowExit, setShouldAllowExit] = useState(false);

  // Android 백버튼 처리
  useEffect(() => {
    const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
      if (offerwallViewRef.current) {
        const viewId = findNodeHandle(offerwallViewRef.current);
        if (viewId) {
          UIManager.dispatchViewManagerCommand(viewId, 'handleBackPress', []);
          return true; // 앱 종료 방지
        }
      }
      return false;
    });

    return () => backHandler.remove();
  }, []);

  // 앱 종료 처리
  useEffect(() => {
    if (shouldAllowExit) {
      const timer = setTimeout(() => BackHandler.exitApp(), 100);
      return () => clearTimeout(timer);
    }
  }, [shouldAllowExit]);

  return (
    <AdchainOfferwallView
      ref={offerwallViewRef}
      placementId="benefits_tab"
      style={{ flex: 1, width: '100%' }}

      // Android 백버튼 처리
      onBackPressOnFirstPage={() => {
        console.log('첫 페이지 - 앱 종료 허용');
        setShouldAllowExit(true);
      }}
      onBackNavigated={() => {
        console.log('WebView 뒤로가기 성공');
        setShouldAllowExit(false);
      }}

      // 이벤트 처리
      onCustomEvent={(eventType, payload) => {
        if (eventType === 'buy_ticket') {
          ShowBuyTicketUI();
        } else if (eventType === 'show_ticket_list') {
          ShowTicketListUI();
        }
      }}
    />
  );
};

동작 방식:

  1. 백버튼 누름 → BackHandler 이벤트 캐치

  2. handleBackPress 명령을 네이티브로 전송

  3. 네이티브 WebView에서 canGoBack() 확인:

    • true (2+ 페이지) → WebView 내부 뒤로가기 → onBackNavigated 호출

    • false (첫 페이지) → onBackPressOnFirstPage 호출

앱 종료 처리 커스터마이징:

위 예시에서는 BackHandler.exitApp()으로 앱을 즉시 종료하지만, 기존 앱의 종료 로직으로 변경할 수 있습니다:

onBackPressOnFirstPage={() => {
  // 예시 1: 종료 확인 토스트
  Toast.show('한 번 더 누르면 종료됩니다');

  // 예시 2: 종료 확인 팝업
  Alert.alert('앱 종료', '앱을 종료하시겠습니까?', [
    { text: '취소', style: 'cancel' },
    { text: '종료', onPress: () => BackHandler.exitApp() }
  ]);

  // 예시 3: 홈 화면으로 이동
  navigation.navigate('HomeTab');
}}

앱마다 종료 동작이 다를 수 있으므로 (즉시 종료, 토스트 표시, 확인 팝업 등) 기존 앱의 백버튼 동작에 맞춰 구현하세요.

주의: iOS에서는 백버튼이 없으므로 이 코드가 불필요합니다. Android에서만 작동합니다.


전체 샘플 코드

임베디드 오퍼월을 구현하는 전체 코드입니다:

import React, { useRef, useEffect, useState } from 'react';
import { BackHandler, findNodeHandle, UIManager, Alert, Platform } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { AdchainOfferwallView } from '@1selfworld/adchain-sdk-react-native';

const BenefitsTab = () => {
  const offerwallViewRef = useRef(null);
  const [shouldAllowExit, setShouldAllowExit] = useState(false);

  // Android 백버튼 처리 (Android만)
  useEffect(() => {
    if (Platform.OS !== 'android') return;

    const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
      if (offerwallViewRef.current) {
        const viewId = findNodeHandle(offerwallViewRef.current);
        if (viewId) {
          UIManager.dispatchViewManagerCommand(viewId, 'handleBackPress', []);
          return true; // 앱 종료 방지
        }
      }
      return false;
    });

    return () => backHandler.remove();
  }, []);

  // 앱 종료 처리
  useEffect(() => {
    if (shouldAllowExit) {
      const timer = setTimeout(() => BackHandler.exitApp(), 100);
      return () => clearTimeout(timer);
    }
  }, [shouldAllowExit]);

  return (
    <SafeAreaView style={{ flex: 1 }} edges={['top']}>
      <AdchainOfferwallView
        ref={offerwallViewRef}
        placementId="benefits_tab"
        style={{ flex: 1, width: '100%' }}

        // Android 백버튼 처리
        onBackPressOnFirstPage={() => {
          console.log('첫 페이지 - 앱 종료 허용');
          setShouldAllowExit(true);
        }}
        onBackNavigated={() => {
          console.log('WebView 뒤로가기 성공');
          setShouldAllowExit(false);
        }}

        // 기본 이벤트 (선택)
        onOfferwallOpened={() => console.log('오퍼월 열림')}
        onOfferwallClosed={() => console.log('오퍼월 닫힘')}
        onOfferwallError={(error) => {
          console.error('오퍼월 오류:', error);
          Alert.alert('오류', '오퍼월을 불러올 수 없습니다. 다시 시도해주세요');
        }}

        // WebView 이벤트 브릿지 (필수)
        onCustomEvent={(eventType, payload) => {
          console.log('[WebView → App]', eventType, payload);

          // 필수 이벤트 처리 (구현 예시)
          if (eventType === 'buy_ticket') {
            // 티켓 구매 UI 표시
            ShowBuyTicketUI();
          }
          else if (eventType === 'show_ticket_list') {
            // 티켓 리스트 표시
            ShowTicketListUI();
          }
          // 처리되지 않은 이벤트 로깅
          else {
            console.warn('처리되지 않은 이벤트:', eventType, payload);
          }
        }}
      />
    </SafeAreaView>
  );
};

export default BenefitsTab;

프로덕션 배포 체크리스트

앱을 배포하기 전에 다음 항목들을 확인하세요:

SDK 설정

초기화

인증

placementId

UI/UX 처리

이벤트 처리

에러 처리


문제 해결

오퍼월이 로딩되지 않아요

증상: 탭이 빈 화면으로 표시됨

확인사항:

  1. SDK가 초기화되었나요?

    const isReady = await AdchainSDK.isInitialized();
    console.log('SDK 준비:', isReady);
  2. 사용자가 로그인되었나요?

    const loggedIn = await AdchainSDK.isLoggedIn();
    console.log('로그인 상태:', loggedIn);
  3. onOfferwallError에서 에러가 발생했나요?

    onOfferwallError={(error) => {
      console.error('오류:', error);  // 로그 확인
    }}

Android 백버튼이 작동하지 않아요

증상: 백버튼을 누르면 앱이 종료됨 (WebView 뒤로가기 무시)

해결:

  1. ref={offerwallViewRef} 추가했는지 확인

  2. BackHandler.addEventListener 등록했는지 확인

  3. UIManager.dispatchViewManagerCommand 호출 코드 확인

위의 "Android 백버튼 처리" 섹션의 전체 코드를 참고하세요.


이벤트가 작동하지 않아요

증상: WebView에서 버튼을 눌러도 아무 반응이 없음

해결:

  1. onCustomEvent 핸들러가 구현되었는지 확인

  2. 애드체인 팀과 협의된 이벤트 타입을 처리하는지 확인

  3. 콘솔 로그 확인:

    onCustomEvent={(eventType, payload) => {
      console.log('[WebView → App]', eventType, payload);  // 이벤트가 들어오는지 확인
      // ...
    }}

환경 전환 (STAGING → PRODUCTION)

테스트 환경에서 프로덕션으로 전환할 때:

1. app.json 수정:

// 개발/테스트
"environment": "STAGING"

// 프로덕션
"environment": "PRODUCTION"

2. 재빌드 필수:

# iOS
npx expo prebuild --platform ios --clean
npx expo run:ios

# Android
npx expo prebuild --platform android --clean
npx expo run:android

중요: 환경 변경 시 반드시 npx expo prebuild --clean 실행!


추가 리소스


마지막 업데이트: 2025-10-30 문서 버전: 1.0 SDK 버전: v1.0.21

Last updated