Skip to main content
네이티브
입문네이티브React Native 플랫폼 · 22

React Native 외부 SDK 생명주기: 요청·표시·닫힘을 어떻게 구분하는가

전면 화면 SDK의 로드 완료와 화면 종료를 같은 성공으로 합치는 실패를 재현하고, 요청 식별값과 SDK 객체를 함께 추적해 늦은 응답·표시·표시 실패·닫힘을 정확한 시도에 연결한다.

마지막 검증 재검증 정책: 제품 버전 의존: 새 주요 버전마다 재검증
목차
표준·구현·측정·해석 표시는 무엇인가요?
  • 표준웹 표준이나 언어 명세가 정한 동작
  • 구현특정 기술이나 브라우저가 실제로 구현한 동작
  • 측정명시한 환경에서 직접 실행해 관찰한 결과
  • 해석앞선 근거에서 도출한 설계 판단
  • 미확인아직 공식 근거나 재현 결과를 확인하지 못한 내용

30초 요약

콜백(callback)은 SDK가 결과나 사건을 알릴 때 앱이 실행하도록 넘긴 함수다. 이 글의 fixture는 같은 요청 순서와 실패를 반복해 관찰하도록 만든 가짜 SDK 실험이다. 상태 기계는 허용한 단계와 사건에 따라서만 상태를 옮기는 규칙이다.

외부 SDK의 작업은 한 번의 awaitboolean으로 끝나지 않을 수 있다. 전면 화면 SDK를 예로 들면 요청, 로드 성공·실패, 표시 요청, 플랫폼의 표시 시작·예정, 표시 실패와 닫힘이 서로 다른 사건이다.

idle
  └─ 요청 → loading
               ├─ load 실패 → loadFailed
               └─ load 성공 → ready
                                  └─ show 요청 → showRequested
                                                       ├─ 표시 시작·예정 → presenting → dismissed
                                                       └─ 표시 실패 → showFailed

각 시도에는 요청 식별값을 붙이고, load 성공으로 받은 SDK 객체도 같은 시도에 묶는다. 늦게 온 이전 callback은 새 시도의 상태를 바꾸지 않는다. show()가 반환됐다는 이유로 dismissed로 이동하지 않고, SDK가 보낸 표시·실패·닫힘 사건으로만 다음 상태를 정한다.

반복해서 기억할 문장은 이것이다.

SDK 함수 호출이 아니라, 식별된 SDK 객체가 보낸 생명주기 사건으로 상태를 옮긴다.

이 글을 관통하는 상황: B를 요청했는데 늦은 A가 표시됐다

배송 완료 화면은 다음 화면으로 넘어가기 전 전면 안내를 한 번 표시한다. 사용자가 A 요청을 시작한 직후 조건을 바꿔 B 요청을 다시 시작했다. B는 빨리 준비되고 A는 늦게 준비된다.

잘못된 구현은 “현재 요청 ID”와 “마지막에 load된 SDK 객체”를 각각 하나만 저장한다. 늦은 A 객체가 B의 준비 객체 자리를 덮고, show() 호출이 반환되자 실제 화면이 나타나기도 전에 앱 상태를 dismissed로 바꾼다.

이 글의 질문은 하나다.

여러 SDK 사건이 다른 시각에 도착해도 어느 요청이 준비·표시·종료됐는지 어떻게 일관되게 추적하는가?

load 성공과 화면 종료를 분리한다

두 플랫폼의 함수 이름은 다르지만 앱이 보존할 공통 단계는 같다.

load 성공       화면을 표시할 객체가 준비됨
show/present 호출 외부 화면 표시를 요청함
표시 시작 사건   표시가 시작됐거나 곧 시작될 것임을 SDK가 알림
표시 실패 사건   화면이 나타나지 못한 종료 경로
닫힘 사건        표시된 화면이 사라진 종료 경로

로드 성공을 completed로 기록하면 “사용자에게 실제로 보였는가?”를 답할 수 없다. 닫힘과 표시 실패를 같은 성공으로 합치면 다음 화면으로 진행할지, 다시 시도할지, 사용자가 이미 화면을 봤는지도 잃는다.

잘못된 최신 상태 덮어쓰기를 실행한다

15편에서 사용한 React Native 프로젝트에서 외부 패키지를 더 설치하지 않고 App.tsx만 교체한다. 실제 광고를 요청하지 않는 고정 fixture다. attemptId는 앱이 만든 시도 식별값이고, instanceId는 fixture SDK가 load 성공 때 돌려준 객체 식별값이다.

SdkSnapshot은 화면에 표시할 현재 시도와 단계를 담는다. currentAttemptIdRefloadedHandleRef는 렌더링하지 않지만 다음 SDK 사건과 비교해야 하는 현재 시도·객체를 보존한다. FullscreenHandle.show()는 표시 요청만 예약하고 곧바로 반환한다. 300ms 뒤 표시 사건, 다시 2초 뒤 닫힘 사건을 보낸다.

import React, { useEffect, useRef, useState } from 'react'
import { Button, SafeAreaView, StyleSheet, Text, View } from 'react-native'
 
const USE_ATTEMPT_STATE_MACHINE = false
const SHOULD_FAST_LOAD_FAIL = false
const SHOULD_PRESENT_FAIL = false
 
type Phase =
  | 'idle'
  | 'loading'
  | 'ready'
  | 'loadFailed'
  | 'showRequested'
  | 'presenting'
  | 'showFailed'
  | 'dismissed'
 
type SdkSnapshot = {
  attemptId: string | null
  instanceId: string | null
  phase: Phase
}
 
type PresentationCallbacks = {
  onPresented: () => void
  onDismissed: () => void
  onFailedToPresent: () => void
}
 
type FullscreenHandle = {
  instanceId: string
  show: (callbacks: PresentationCallbacks) => void
}
 
type LoadCallbacks = {
  onLoaded: (handle: FullscreenHandle) => void
  onLoadFailed: () => void
}
 
const INITIAL_SNAPSHOT: SdkSnapshot = {
  attemptId: null,
  instanceId: null,
  phase: 'idle',
}
 
let fixtureGeneration = 0
 
function invalidateFixtureCallbacks() {
  fixtureGeneration += 1
}
 
function loadFakeFullscreen(
  instanceId: string,
  delayMs: number,
  shouldFail: boolean,
  callbacks: LoadCallbacks,
) {
  const generation = fixtureGeneration
 
  setTimeout(() => {
    if (generation !== fixtureGeneration) {
      return
    }
 
    if (shouldFail) {
      callbacks.onLoadFailed()
      return
    }
 
    callbacks.onLoaded({
      instanceId,
      show(presentationCallbacks) {
        setTimeout(() => {
          if (generation !== fixtureGeneration) {
            return
          }
 
          if (SHOULD_PRESENT_FAIL) {
            presentationCallbacks.onFailedToPresent()
            return
          }
 
          presentationCallbacks.onPresented()
 
          setTimeout(() => {
            if (generation === fixtureGeneration) {
              presentationCallbacks.onDismissed()
            }
          }, 2000)
        }, 300)
      },
    })
  }, delayMs)
}
 
export default function App() {
  const currentAttemptIdRef = useRef<string | null>(null)
  const loadedHandleRef = useRef<FullscreenHandle | null>(null)
  const [snapshot, setSnapshot] = useState(INITIAL_SNAPSHOT)
  const [sdkSurface, setSdkSurface] = useState('표시되지 않음')
  const [trace, setTrace] = useState<string[]>([])
 
  function appendTrace(message: string) {
    setTrace(current => [...current, message])
  }
 
  function resetFixture() {
    invalidateFixtureCallbacks()
    currentAttemptIdRef.current = null
    loadedHandleRef.current = null
    setSnapshot(INITIAL_SNAPSHOT)
    setSdkSurface('표시되지 않음')
    setTrace(['fixture 초기화'])
  }
 
  useEffect(() => {
    return invalidateFixtureCallbacks
  }, [])
 
  function beginAttempt(attemptId: string, delayMs: number, shouldFail: boolean) {
    currentAttemptIdRef.current = attemptId
    setSnapshot({ attemptId, instanceId: null, phase: 'loading' })
    appendTrace(`${attemptId} 요청`)
 
    loadFakeFullscreen(attemptId, delayMs, shouldFail, {
      onLoaded(handle) {
        if (
          USE_ATTEMPT_STATE_MACHINE &&
          currentAttemptIdRef.current !== attemptId
        ) {
          appendTrace(`${attemptId} 늦은 load 성공 무시`)
          return
        }
 
        loadedHandleRef.current = handle
        setSnapshot({
          attemptId: currentAttemptIdRef.current,
          instanceId: handle.instanceId,
          phase: 'ready',
        })
        appendTrace(`${attemptId} 객체 준비`)
      },
      onLoadFailed() {
        if (currentAttemptIdRef.current !== attemptId) {
          appendTrace(`${attemptId} 늦은 load 실패 무시`)
          return
        }
 
        loadedHandleRef.current = null
        setSnapshot({ attemptId, instanceId: null, phase: 'loadFailed' })
        appendTrace(`${attemptId} load 실패`)
      },
    })
  }
 
  function requestAThenB() {
    resetFixture()
    beginAttempt('attempt-A', 1200, false)
    const generation = fixtureGeneration
 
    setTimeout(() => {
      if (generation === fixtureGeneration) {
        beginAttempt('attempt-B', 200, SHOULD_FAST_LOAD_FAIL)
      }
    }, 50)
  }
 
  function showLoadedObject() {
    const attemptId = currentAttemptIdRef.current
    const handle = loadedHandleRef.current
 
    if (attemptId === null || handle === null) {
      appendTrace('표시할 객체 없음')
      return
    }
 
    if (USE_ATTEMPT_STATE_MACHINE) {
      setSnapshot({ attemptId, instanceId: handle.instanceId, phase: 'showRequested' })
    }
 
    handle.show({
      onPresented() {
        setSdkSurface(`${handle.instanceId} 표시 중`)
 
        if (
          USE_ATTEMPT_STATE_MACHINE &&
          currentAttemptIdRef.current === attemptId &&
          loadedHandleRef.current === handle
        ) {
          setSnapshot({ attemptId, instanceId: handle.instanceId, phase: 'presenting' })
        }
        appendTrace(`${handle.instanceId} 실제 표시`)
      },
      onDismissed() {
        setSdkSurface('표시되지 않음')
 
        if (
          USE_ATTEMPT_STATE_MACHINE &&
          currentAttemptIdRef.current === attemptId &&
          loadedHandleRef.current === handle
        ) {
          loadedHandleRef.current = null
          setSnapshot({ attemptId, instanceId: handle.instanceId, phase: 'dismissed' })
        }
        appendTrace(`${handle.instanceId} 실제 닫힘`)
      },
      onFailedToPresent() {
        setSdkSurface('표시되지 않음')
 
        if (
          USE_ATTEMPT_STATE_MACHINE &&
          currentAttemptIdRef.current === attemptId &&
          loadedHandleRef.current === handle
        ) {
          loadedHandleRef.current = null
          setSnapshot({ attemptId, instanceId: handle.instanceId, phase: 'showFailed' })
        }
        appendTrace(`${handle.instanceId} 실제 표시 실패`)
      },
    })
 
    if (!USE_ATTEMPT_STATE_MACHINE) {
      loadedHandleRef.current = null
      setSnapshot({ attemptId, instanceId: handle.instanceId, phase: 'dismissed' })
      appendTrace('show 반환을 닫힘으로 오판')
    }
  }
 
  const presentationIsPending =
    snapshot.phase === 'showRequested' || snapshot.phase === 'presenting'
  const lifecycleButtonsDisabled =
    USE_ATTEMPT_STATE_MACHINE && presentationIsPending
 
  return (
    <SafeAreaView style={styles.screen}>
      <Text style={styles.title}>외부 전면 SDK 생명주기 fixture</Text>
      <Text>
        mode: {USE_ATTEMPT_STATE_MACHINE ? 'attempt-state-machine' : 'latest-value'}
      </Text>
      <Text>현재 시도: {snapshot.attemptId ?? '없음'}</Text>
      <Text>화면 기록 객체 ID: {snapshot.instanceId ?? '없음'}</Text>
      <Text>앱 단계: {snapshot.phase}</Text>
      <Text>SDK 화면: {sdkSurface}</Text>
      <Text>기록: {trace.join(' / ') || '없음'}</Text>
      <View style={styles.actions}>
        <Button
          title="fixture 초기화"
          disabled={lifecycleButtonsDisabled}
          onPress={resetFixture}
        />
        <Button
          title="A 느리게 → B 빠르게 요청"
          disabled={lifecycleButtonsDisabled}
          onPress={requestAThenB}
        />
        <Button
          title="준비된 객체 표시"
          disabled={snapshot.phase !== 'ready'}
          onPress={showLoadedObject}
        />
      </View>
    </SafeAreaView>
  )
}
 
const styles = StyleSheet.create({
  screen: { flex: 1, padding: 16, gap: 12 },
  title: { fontSize: 22, fontWeight: '700' },
  actions: { gap: 8 },
})

실행 전에 예측한다.

  1. B가 먼저 load 성공하고 A가 나중에 성공하면 현재 시도와 보관 객체는 각각 무엇일까?
  2. show()가 반환된 직후 앱 단계와 300ms 뒤 실제 SDK 화면은 일치할까?
  3. 실제 닫힘 전 새 요청 버튼을 열어도 될까?

개발자 메뉴에서 Reload해 기준선을 새로 만든 뒤 A 느리게 → B 빠르게 요청을 누르고 1.5초 기다린다.

mode: latest-value
현재 시도: attempt-B
화면 기록 객체 ID: attempt-A
앱 단계: ready
기록: fixture 초기화 / attempt-A 요청 / attempt-B 요청 / attempt-B 객체 준비 / attempt-A 객체 준비

늦은 A callback이 현재 B의 객체 자리를 덮었다. 준비된 객체 표시를 누르면 함수가 반환되자마자 다음처럼 보인다.

현재 시도: attempt-B
화면 기록 객체 ID: attempt-A
앱 단계: dismissed
SDK 화면: 표시되지 않음
기록 끝: show 반환을 닫힘으로 오판

하지만 300ms 뒤에는 SDK 화면: attempt-A 표시 중이 된다. 앱은 B가 이미 닫혔다고 말하지만 실제로는 A가 그제야 나타났다. 2초 뒤 실제 닫힘 사건이 와도 앱 단계는 이미 dismissed였으므로 어느 전환이 실제 종료를 만들었는지 구분할 수 없다.

요청 식별값과 SDK 객체를 함께 추적한다

상수 하나를 바꾼다.

const USE_ATTEMPT_STATE_MACHINE = true

Reload 뒤 같은 순서로 1.5초 기다린다.

mode: attempt-state-machine
현재 시도: attempt-B
화면 기록 객체 ID: attempt-B
앱 단계: ready
기록: fixture 초기화 / attempt-A 요청 / attempt-B 요청 / attempt-B 객체 준비 / attempt-A 늦은 load 성공 무시

A가 성공했다는 사실은 진단 기록에 남지만 B의 상태와 객체를 바꾸지 않는다. 취소 API가 있는 SDK라면 이전 요청을 취소할 수도 있다. 그러나 취소 호출만 믿지 않고, 이미 출발한 callback이 도착해도 현재 식별값과 다르면 무시하는 조건을 유지한다.

show 반환과 실제 종료를 분리한다

이제 준비된 객체 표시를 누른다.

직후
  앱 단계: showRequested
  SDK 화면: 표시되지 않음
 
약 300ms 뒤
  앱 단계: presenting
  SDK 화면: attempt-B 표시 중
 
다시 약 2초 뒤
  앱 단계: dismissed
  SDK 화면: 표시되지 않음

showRequested는 앱이 SDK에 표시를 요청했다는 사실, presenting은 SDK가 표시를 시작했거나 곧 시작할 것임을 알린 상태, dismissed는 실제 닫힘 사건을 받은 상태다. 교정 모드에서는 앞의 두 단계 동안 초기화와 새 요청 버튼이 비활성이고, dismissedshowFailed가 된 뒤 다시 활성화된다. 세 상태를 나누면 오디오 일시 정지, 다음 화면 이동, 재요청 가능 시점을 각각 올바른 사건에 연결할 수 있다. 이 공통 단계만으로 실제 가시성이나 광고 노출을 증명하지는 않는다.

fixture 초기화는 개발 실험의 JavaScript 상태와 앞으로 올 가짜 콜백만 무효화한다. 실제 네이티브 SDK 화면을 닫았다는 증거가 아니므로 표시 중인 실제 화면을 초기화 버튼으로 끝난 것처럼 처리하지 않는다.

표시 실패도 닫힘과 다른 종료다. 다음 상수를 true로 바꾸고 Reload한 뒤 같은 요청·표시 순서를 실행한다.

const SHOULD_PRESENT_FAIL = true
앱 단계: showFailed
SDK 화면: 표시되지 않음
기록 끝: attempt-B 실제 표시 실패

표시되지 않았으므로 닫힘을 기다리지 않는다. 실패 객체를 비우고 제품 정책에 따라 다음 화면으로 진행하거나 새 객체를 다시 요청한다. 자동 재시도 횟수와 지연 정책은 SDK 오류 종류·제품 흐름에 맞춰 별도로 정한다.

SHOULD_FAST_LOAD_FAIL = true로 바꾸면 B의 load 실패도 별도 loadFailed로 관찰할 수 있다. 이때 늦은 A 성공은 현재 B 실패를 준비 완료로 되돌리지 않는다.

종료 사건에서 객체를 소비하고 비운다

전면 SDK 객체의 소유권은 단계와 함께 움직인다.

loading       아직 SDK 객체 없음
ready         현재 attemptId에 속한 객체 하나 보관
showRequested 같은 객체의 표시 결과를 기다림
presenting    같은 객체의 닫힘 또는 표시 실패를 기다림
dismissed     객체 비움, 다음 요청 가능
showFailed    객체 비움, 다음 행동 선택

iOS 공식 전면 광고 객체는 한 번만 사용한다. Android Next-Gen 공식 예제도 닫힘과 표시 실패에서 객체 참조를 비운다. 따라서 dismissedshowFailed 뒤 같은 객체에 다시 show()를 호출하는 경로를 만들지 않는다. 다음 표시가 필요하면 새 요청으로 새 객체를 준비한다.

객체를 React state에 넣어 화면 계산과 섞을 필요는 없다. 화면에는 attemptId, 진단용 instanceId, phase, 안전한 오류 분류만 두고, 실제 SDK 객체는 ref나 플랫폼 SDK 사건을 앱 단계로 번역하는 연결 층이 소유한다. 종료 뒤 화면에 남은 instanceId는 마지막 사건을 설명하는 문자열일 뿐 재사용할 객체가 아니다. 객체 자체를 로그·영속 저장·분석 전송값에 넣지 않는다.

컴포넌트와 앱 상태를 표시 조건에 더한다

React 화면이 제거되면 Effect가 반환한 정리 함수에서 JavaScript 구독을 해제하고 현재 시도를 폐기한다. SDK가 실제 load 취소를 지원하면 그 API도 호출한다. 하지만 취소가 이미 전달 중인 사건까지 없앤다고 가정하지 않고 요청 식별값과 객체 식별 조건으로 늦은 사건을 막는다.

앱이 background가 됐다고 컴포넌트가 자동 제거되는 것도 아니다. 표시 버튼을 누른 시점에는 AppState의 active와 Android에서 현재 화면을 소유하는 Activity 객체 또는 iOS 표시 화면 소유자가 실제로 유효한지, 플랫폼 SDK 사건을 앱 단계로 번역하는 연결 코드에서 다시 확인한다. 조건이 맞지 않거나 SDK가 표시 실패를 보내면 showFailed로 닫는다. show() 호출 성공만으로 화면이 보였다고 기록하지 않는다.

Android와 iOS 사건 이름을 억지로 같게 만들지 않는다

앱의 공통 단계는 유지하되 플랫폼 SDK 사건을 앱 단계로 번역하는 연결 코드가 원본 사건도 함께 남긴다.

앱 단계Android GMA Next-Gen 1.3.1iOS Google Mobile Ads 13.7.0
loadingInterstitialAd.load 호출비동기 InterstitialAd.load 호출
readyonAdLoaded와 객체load 반환 객체와 delegate 연결
loadFailedonAdFailedToLoadload가 던진 오류
showRequested객체의 show(Activity) 호출객체의 present 호출
presentingonAdShowedFullScreenContentadWillPresentFullScreenContent
showFailedonAdFailedToShowFullScreenContentdidFailToPresentFullScreenContentWithError
dismissedonAdDismissedFullScreenContentadDidDismissFullScreenContent

Android는 “실제로 표시됨”, iOS는 “표시할 예정”이라는 원본 사건 이름까지 완전히 같지는 않다. 제품이 정확한 화면 노출 증거를 요구한다면 광고가 실제 노출됐음을 알리는 사건처럼 SDK가 별도로 정의한 신호를 검토한다. 공통 presenting은 앱 자원 일시 정지와 중복 표시 차단을 위한 단계이지, 광고 과금이나 노출 집계의 증거로 확대하지 않는다.

23편에서는 이 계약이 맞았던 앱에서 SDK 버전과 앱 코드가 함께 바뀌었을 때 어느 변경이 회귀를 만들었는지 분리한다. 이 글에서는 한 고정 버전의 생명주기만 닫는다.

가상 SDK와 실제 플랫폼 증거를 나눈다

여러 광고 네트워크를 한 SDK에서 중개하는 구성을 mediation이라고 한다.

fixture는 요청 식별값과 상태 전이만 증명한다. 실제 SDK 통합에서는 다음 순서로 증거를 확장한다.

검증 단계Android emulatoriOS Simulator지원 실기기
A 느림·B 빠름과 늦은 callback 무시고정 fixture고정 fixture회귀 확인
show 반환과 표시·닫힘 사건 분리고정 fixture고정 fixture회귀 확인
공식 테스트 광고 load·표시·닫힘GMA 테스트 IDGMA 테스트 ID테스트 기기 설정으로 확인
표시 실패와 화면 소유자 없음플랫폼 연결 코드 대역·통제 조건플랫폼 연결 코드 대역·통제 조건실제 화면 전환 조건 확인
background·복귀·회전·외부 화면통제된 상태 전환통제된 상태 전환제품 지원 기기 최종 확인
mediation 네트워크별 화면과 callback각 네트워크 테스트 설정각 네트워크 테스트 설정필요한 네트워크 조합 확인

실제 광고를 눌러 운영 트래픽을 만들지 않는다. 공식 테스트 광고 단위 ID나 등록한 테스트 기기를 사용하고, 요청 ID·SDK 버전·플랫폼 원본 사건·앱 단계만 기록한다. 광고 내용, 사용자 식별값과 타기팅 값은 진단 로그에 남기지 않는다.

pull request에서 달라져야 할 행동

외부 SDK 화면 흐름을 검토할 때 다음 표를 먼저 채운다.

시도 식별       요청 하나를 어떤 ID로 구분하는가
객체 소유       load된 SDK 객체를 누가 언제까지 보관하는가
load 종료       성공과 실패가 어느 사건으로 확정되는가
표시 요청       앱 active·화면 소유자·ready 조건을 어디서 확인하는가
표시 시작       show 반환과 플랫폼의 표시 시작·예정 사건을 구분하는가
종료 증거       닫힘과 표시 실패를 별도 상태로 남기는가
재요청 시점     표시 요청·표시 중에는 새 요청과 개발용 초기화를 막는가
늦은 사건       이전 요청·이전 화면의 callback을 어떻게 무시하는가
재사용          종료 객체를 비우고 새 요청으로 교체하는가
cleanup         화면 제거와 앱 background 조건을 각각 처리하는가
검증 환경       fixture·공식 테스트 광고·실기기 증거를 구분하는가

검토자는 다음을 확인한다.

  • isLoading, isShowing, completed 같은 boolean 하나로 모든 SDK 단계를 합치지 않는가?
  • callback이 현재 전역 요청 ID를 읽어 이전 요청 결과를 새 요청에 붙이지 않는가?
  • load 성공이나 show() 반환을 사용자 화면 종료로 기록하지 않는가?
  • 닫힘과 표시 실패 뒤 SDK 객체 참조를 비우는가?
  • 표시 전에 앱 active 상태와 실제 native 화면 소유자를 확인하는가?
  • 컴포넌트 cleanup만으로 앱 background와 늦은 callback까지 해결됐다고 가정하지 않는가?
  • 운영 광고 대신 공식 테스트 광고와 네트워크별 테스트 설정을 사용하는가?
  • 분석 사건에 attempt ID, SDK 버전, 원본 사건과 앱 단계가 함께 있어 순서를 복원할 수 있는가?

한 장으로 다시 보기

문제       빠른 B 뒤 늦은 A 객체가 B 자리를 덮고, show 반환을 닫힘으로 오판
관찰       앱은 attempt-B/dismissed인데 실제로 attempt-A 화면이 뒤늦게 표시
원인       요청 식별값, SDK 객체와 load·표시·종료 단계를 각각 연결하지 않음
행동       현재 attempt ID+객체가 같은 사건만 받아 상태 기계 전이
표시       showRequested → 플랫폼 표시 시작·예정 사건 → presenting
종료       dismissed와 showFailed에서 객체를 비우고 다음 요청 준비
경계       Android·iOS 원본 사건은 보존하고 공통 단계만 제품 상태로 번역
검증       JavaScript fixture 뒤 양 플랫폼 공식 테스트 광고와 기기 흐름 확인

외부 SDK 생명주기의 핵심은 callback 이름을 많이 외우는 것이 아니다. 어떤 시도의 어떤 객체가 보낸 사건인지 확인하고, 함수 호출이 아니라 SDK가 확정한 사건으로만 다음 제품 상태를 여는 것이다.

스스로 확인할 질문

  1. load 성공과 전면 화면 표시 단계를 같은 상태로 두면 어떤 판단을 잃는가?
  2. 늦은 A load 결과가 현재 B 상태를 바꾸지 못하게 하려면 callback 안에서 무엇을 비교해야 하는가?
  3. show() 반환 뒤 바로 dismissed로 바꾸면 실제 화면과 앱 상태가 어떻게 어긋나는가?
  4. 닫힘과 표시 실패 뒤 SDK 객체를 비워야 하는 이유는 무엇인가?
  5. fixture 통과 뒤 실제 Android·iOS 통합에서 어떤 증거를 추가해야 하는가?

Active recall

기억에서 꺼내 보기

답을 쓰지 않아도 됩니다. 먼저 머릿속으로 답한 뒤 펼쳐서 정답·이유·흔한 오해를 비교하세요.

  1. 01전면 화면 SDK의 load 성공 callback이 왔다면 사용자에게 화면이 보였고 닫히기까지 끝났다고 보아도 될까?

    정답

    아니다. load 성공은 표시할 SDK 객체가 준비됐다는 뜻이다. 화면 표시, 표시 실패와 닫힘은 별도 사건으로 확인한다.

    왜 그런가

    요청 준비와 사용자에게 보이는 실행은 서로 다른 생명주기 단계다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  2. 02느린 A 요청 뒤 빠른 B 요청을 시작했을 때 A의 load 성공이 늦게 오면 현재 B의 준비 객체로 저장해도 될까?

    정답

    아니다. callback이 닫아 둔 요청 식별값과 현재 식별값이 같을 때만 SDK 객체를 채택한다. 늦은 A 결과는 기록하고 무시한다.

    왜 그런가

    최신 화면 상태와 callback이 속한 요청의 정체성은 같지 않을 수 있다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  3. 03SDK의 show 함수를 호출한 코드가 반환됐다면 전면 화면도 이미 닫혔다고 상태를 바꿔도 될까?

    정답

    아니다. 함수 반환은 표시 요청을 넘겼다는 뜻일 뿐이다. 플랫폼의 표시 시작·예정, 표시 실패와 닫힘 callback이 실제 다음 상태를 결정한다.

    왜 그런가

    함수 호출 흐름과 외부 화면 생명주기는 같은 완료 신호가 아니다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  4. 04표시가 실패하거나 사용자가 화면을 닫은 뒤 같은 전면 SDK 객체를 다음 요청에 다시 사용해도 될까?

    정답

    이 글의 고정 SDK에서는 객체 참조를 비우고 새 요청으로 다음 객체를 준비한다. iOS 전면 광고 객체는 한 번만 사용하며, Android 공식 예제도 닫힘·표시 실패 때 참조를 비운다.

    왜 그런가

    종료 상태와 다음 준비 상태를 같은 객체에 겹치지 않는다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  5. 05JavaScript fixture에서 요청·표시·닫힘 상태 전이가 맞으면 실제 광고 SDK 통합도 성공했다고 볼 수 있을까?

    정답

    아니다. fixture는 앱의 상태 규칙만 증명한다. 실제 통합은 양 플랫폼의 테스트 광고로 load·표시·실패·닫힘과 화면 복귀를 확인한다.

    왜 그런가

    상태 계산과 네이티브 SDK 설치·화면 표시·네트워크 응답은 서로 다른 증거다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

출처와 검증 범위

아래 날짜는 링크를 마지막으로 열어 본 날입니다. 문서 상단의 검증일은 글의 설명과 적용 범위를 다시 확인한 날입니다.

  1. Set up GMA Next-Gen SDKGoogle for Developers · 공식 문서 · 확인 2026-08-09
  2. GMA Next-Gen SDK for Android release notesGoogle for Developers · 공식 문서 · 확인 2026-08-09
  3. Interstitial adsGoogle for Developers · 공식 문서 · 확인 2026-08-09
  4. InterstitialAdEventCallbackGoogle for Developers · 공식 문서 · 확인 2026-08-09
  5. Enable test adsGoogle for Developers · 공식 문서 · 확인 2026-08-09
  6. Google Mobile Ads SDK for iOS release notesGoogle for Developers · 공식 문서 · 확인 2026-08-09
  7. Interstitial adsGoogle for Developers · 공식 문서 · 확인 2026-08-09
  8. useRefReact · 공식 문서 · 확인 2026-08-09
  9. Updating Objects in StateReact · 공식 문서 · 확인 2026-08-09
  10. useEffectReact · 공식 문서 · 확인 2026-08-09
  11. AppStateReact Native · 공식 문서 · 확인 2026-08-09
이 문서의 마지막까지 읽었습니다.