본문으로 건너뛰기
:tech
← Articles

프리로드 API 없는 광고 SDK에 프리로딩을 적용하며 배운 것

Justin Jeong
Justin Jeong

Frontend Engineer

프리로드 API 없는 광고 SDK에 프리로딩을 적용하며 배운 것

매시간 황금알(플레이허브 v2.1)에서 광고마다 반복되는 대기를 줄이려고, 프리로드 API가 없는 SDK의 실제 광고 실행을 먼저 시작해 숨겼습니다. 그 대가로 화면에서 지운 광고가 SDK 안에서는 계속 실행 중인 문제까지 다뤄야 했습니다.

시작하며

매시간 황금알은 여러 모바일 앱 안에서 열리는 웹 기반 리워드 서비스로, 사용자는 광고에 참여하고 포인트 같은 보상을 받습니다. 한 참여 흐름에서 사용자는 코인을 쌓을 때마다 광고 참여 버튼(CTA)을 누르고, 황금알을 열기 위해 마지막으로 한 번 더 광고를 봅니다. 광고마다 몇 초씩 기다려야 한다면 대기 하나하나는 짧아도 흐름이 반복해서 끊깁니다.

당시 일부 광고 공급 경로는 입찰과 소재 준비에 기존 제한 시간보다 오래 걸려, 충분히 기다리면 보여줄 수 있는 광고가 후보에서 제외되는 경우가 많았습니다. 그렇다고 후보를 한꺼번에 평가하는 구조에서 제한 시간을 일괄로 늘리면 원래 빠르게 준비되던 광고까지 함께 기다려야 했습니다.

사용자가 버튼을 누르기 전에 광고를 요청해 두면 기다림을 줄일 수 있지 않을까?

하지만 구현해 보니 프리로딩은 요청 시점 하나를 앞당기는 일로 끝나지 않았습니다. 어떤 광고를 미리 요청할지, 준비한 광고가 노출 순서와 맞는지, 보이지 않는 광고의 노출을 언제 집계할지, 쓰지 않은 광고를 언제 폐기할지까지 정해야 했습니다. 미리 준비하려던 광고의 SDK는 프리로드를 정식으로 지원하지 않았습니다. 실제 광고 실행을 미리 시작하면 노출 시점과 SDK 상태가 어긋날 수 있다는 것을 알았지만, 반복되는 대기를 더 큰 제품 위험으로 판단했습니다.

1. 광고를 더 오래 기다리게 할 수는 없었습니다

광고를 보여주려면 여러 공급 경로에 후보를 요청하고, 응답한 후보의 우선순위를 정한 뒤, 선택된 광고의 소재나 SDK를 준비해야 합니다. 외부 광고 공급사는 그 안에서 다시 입찰하기도 합니다.

사용자가 CTA를 누른 뒤 더 오래 기다리게 하는 대신, 화면을 살펴보는 동안 준비를 먼저 시작하기로 했습니다.

제품 흐름상 광고를 시청하는 화면이 노출될 때 프리로딩이 시작되도록 정했습니다. 프리로딩을 필요 이상으로 앞당겨 시작하면 준비된 광고가 사용되지 않을 수 있습니다. 광고를 준비하는 시간을 확보할 수 있는 만큼 먼저 프리로딩을 시작하면서도 낭비되지 않도록 시작 조건을 다음처럼 정의했습니다.

  • 사용자가 제품을 이용하고 있고
  • 서버 상태와 사용자 식별 정보가 준비됐으며
  • 다음 행동이 코인 광고 또는 황금알 광고 시청일 때

2. 광고 후보를 미리 받는 것만으로는 부족했습니다

후보가 정해진 상태와 광고를 바로 보여줄 수 있는 상태는 달랐습니다. 이미지나 HTML 소재는 응답을 받으면 그릴 수 있지만, 일부 SDK형 광고는 스크립트를 준비하고 광고 실행을 요청한 뒤 SDK가 내부 로딩을 마칠 때까지 기다려야 했습니다. 후보를 미리 받아도 SDK 초기화를 클릭 뒤에 시작하면 대기는 남습니다. 그래서 저는 프리로딩을 두 단계로 나눴습니다. 후보를 미리 받는 선할당과, 대상 SDK 광고가 할당되면 보이지 않는 상태로 초기화를 시작하는 선실행입니다.

프리로드 API가 없으면 실제 실행을 숨겨야 했습니다

프리로딩을 정식으로 지원하는 Google Ad Placement API는 기본적으로 휴리스틱에 따라 광고를 미리 받아 캐시했다가 adBreak() 호출에 씁니다. 첫 adBreak()보다 먼저 adConfig({preloadAdBreaks: 'on'})를 호출하면 그 adBreak() 전에 광고가 준비되도록 보장하며, 한 번 정한 뒤의 변경 시도는 무시됩니다(Google 문서).

이번 SDK에는 이런 API가 없어, 광고를 준비하려면 실제 광고 실행 함수를 호출할 수밖에 없었습니다. 우리는 실제 렌더링 경로를 먼저 시작한 뒤 생성된 광고 프레임을 visibility: hidden으로 숨겨 사용자 입력을 받지 않게 하고, 노출 신호는 보류했습니다. 이하 이렇게 숨겨 진행 중인 세션을 '숨김 세션', 실제 차례에 새로 광고를 시작하는 대신 이 세션을 화면에 보이게 하는 일을 '드러내기(reveal)'라고 부릅니다.

구분프리로드 API 사용숨김 실행 프리로딩
준비 시작SDK가 자동으로 시작하거나 프리로드 설정·API로 요청실제 광고 실행 함수 호출
준비 중 상태SDK 내부에서 다운로드·캐시광고 DOM이 만들어지지만 숨김
실제 노출나중에 별도 노출 호출진행 중인 숨김 세션을 드러냄
수명주기 책임다운로드·캐시는 SDK, 노출 시점은 제품제품 코드가 DOM·SDK 세션·트래킹을 함께 관리
대표 위험설정 시점 제약, 첫 노출 전 미준비, 미리 받은 광고의 만료(예: AdMob 앱 오프닝 광고)이른 노출 집계, 실행 중 세션 충돌, 정리 누락, 숨은 리소스 사용
SDK 지원 프리로드에서는 제품이 노출 시점을 결정하고 SDK가 다운로드와 캐시를 관리하지만, hidden 실행형에서는 제품 코드가 실행·숨김·최종 선택·reveal 또는 폐기와 SDK 종료까지 책임지는 비교

숨김 실행은 스크립트 준비와 내부 로딩을 클릭 전에 진행해 두고 준비된 광고를 곧바로 드러낼 수 있습니다. 다만 visibility: hidden인 상자는 레이아웃에는 참여하지만 그려지지 않으므로(CSS Display 명세), 화면에 그리는 작업은 드러내는 시점에 남습니다. 숨겨진 <iframe>에서는 브라우저에 따라 requestAnimationFrame() 호출이 멈추기도 합니다(MDN).

기술적으로 가장 깔끔한 선택은 아니었습니다

정식 프리로드 API 지원 출시를 기다릴 수도 있었습니다. 그러나 기능을 준비하는 데 시간이 얼마나 걸릴지 예측이 어렵고, 출시 계획도 없는 상황이었습니다. 해당 광고를 송출 후보에서 빼면 그 SDK의 초기화 대기는 사라지지만, 그 광고를 보여줄 기회와 수익을 잃습니다. 반면 노출 시점 차이는 특정 SDK에 한정되어 있었고, 공급사와 집계 기준을 맞춰 조정할 수 있는 문제로 판단했습니다. 그래서 사용자에게 직접 반복되며 참여 흐름을 끊는 대기를 먼저 줄이기로 했습니다. 문제의 반복 빈도, 영향 범위의 통제 가능성, 되돌릴 수 있는지를 기준으로 어느 위험부터 줄일지 정한 비즈니스적 판단이었습니다.

대신 적용 범위를 특정 SDK로 제한하고, 피처 플래그로 즉시 되돌릴 수 있게 했으며, 준비·노출·폐기 상태를 별도 이벤트로 관측했습니다.

이 방식에는 광고를 준비하는 시점의 차이 말고도 비용이 따릅니다. 우선순위에서 밀려 폐기되는 광고는 실제 렌더링 경로를 시작했지만 사용자에게 보이지 않은 채 끝날 수 있습니다. 공급사가 이런 광고를 집계·과금이나 트래픽 품질 면에서 어떻게 판단하는지는 프론트엔드가 통제할 수 없습니다. 이 비용도 결정 당시 함께 검토했습니다.

프리로딩된 광고는 보이지 않는 동안에도 네트워크와 기기 자원을 쓰고, 쓰이지 않고 폐기되면 그 사용은 낭비로 끝납니다. 그래서 동시에 유지하는 숨김 세션은 하나로 제한했고, 외부 브라우저가 백그라운드로 가거나 사용자가 페이지를 떠나면 정리했습니다.

3. 서비스가 관리하는 노출 신호는 드러낼 때까지 보류했습니다

준비한 것과 보여준 것을 구분하지 않으면 노출 집계가 어긋나므로, 프리로딩 단계에서 생긴 다음 신호는 바로 보내지 않았습니다.

  • 서비스의 광고 노출 트래커
  • 프론트엔드의 노출 이벤트
  • SDK에 명시적으로 보내는 노출 신호

이 신호들은 해당 광고가 최종 목록에서 선택되어 드러날 때까지 보관했다가 함께 보냈습니다. 운영 지표도 서비스가 실제로 드러낸 시점을 기준으로 보도록 했습니다. 숨김 실행 방식과 집계 기준은 출시 전에 공급사와 협의했습니다. 다만 공급사 SDK 내부의 노출 집계(impression)나 과금 판단까지 프론트엔드가 통제했다고 보지는 않았습니다. 그 경계는 운영 데이터를 대조해 확인해야 하는 별도 위험으로 남았고, 대조한 범위에서는 차이가 관측되지 않았습니다.

4. 프리로딩이 끝나지 않아도 원래 흐름은 동작해야 했습니다

사용자는 프리로딩 도중에 CTA를 누를 수 있고, 광고 할당이나 SDK 초기화가 실패할 수도 있습니다. 프리로딩 성공을 제품 흐름의 전제로 삼지 않고, CTA를 누른 시점의 상태에 따라 처리했습니다.

상태처리
준비 완료(READY): 후보와 SDK가 준비됨준비한 목록과 세션을 소비
준비 중(PREPARING): 프리로딩 진행 중정해 둔 후보 확정 시점까지 기다린 뒤 결과에 따라 소비하거나 폴백
실패(FAILED): 준비된 결과가 없거나 실패함기존 CTA 시점 광고 요청으로 폴백
// 단순화한 흐름(의사코드)
async function onAdCtaClick(preload) {
  switch (preload.state) {
    case 'READY':
      return consume(preload.result);
    case 'PREPARING': {
      const result = await preload.waitForCandidateList(); // 정해 둔 후보 확정 시점까지(기존 제한 시간 안에서)
      return canConsume(result) ? consume(result) : requestAdsAtCta(); // 폴백은 남은 시간 안에서
    }
    default: // 준비된 결과가 없거나 실패함
      return requestAdsAtCta();
  }
}

프리로딩 중에도 CTA를 누를 수 있고 광고는 준비되는 대로 노출하도록 했습니다.

클릭 전에 광고 후보와 hidden SDK 세션을 준비하고 CTA 시점의 READY, PREPARING, FAILED 상태에 따라 reveal, 대기, 폴백을 선택하는 수명주기

기능 전체를 우회하는 복구 경로도 필요했습니다. 피처 플래그를 끄면 광고 요청·노출·보상 흐름 전체가 기존 CTA 시점 방식으로 돌아갑니다. 플래그 값은 off와 프리로딩 적용 값으로 나누고 유닛(광고 지면) 정보까지 함께 평가하게 했습니다. 배포 뒤에는 코드 수정 없이 어드민 설정만으로 유닛과 적용 범위를 골라 기능을 켜고 끄며 작은 범위에서 점진적으로 넓힐 수 있었습니다.

5. 화면에서 지웠다고 SDK까지 끝난 것은 아니었습니다

쓰지 않게 된 숨김 광고 프레임을 DOM에서 제거하면 세션도 끝난다고 생각했습니다. 그러나 외부 SDK 안에는 광고가 여전히 실행 중으로 남아 있었고, 다음 광고를 준비하며 실행을 다시 요청하면 SDK는 이미 광고가 진행 중이라며 거절했습니다. 사용자에게는 광고 없이 로딩 화면만 유지되다가 건너뛰기 안내로 넘어가는 현상으로 나타났습니다. 출시 뒤 관측 이벤트에서 드러난 이 문제는 당시 켜져 있던 적용 범위 전체에 영향을 줬습니다. 다만 건너뛰기 안내 뒤에도 참여와 보상 흐름은 이어졌기 때문에, 피처 플래그로 끄지 않고 코드 수정으로 대응했습니다.

팀의 후속 분석에서는 정리 책임을 두 단계로 다시 나눴습니다.

  1. 화면에서 숨김 광고 프레임을 제거한다.
  2. 아직 실행 중인 SDK 세션을 SDK의 종료 API로 명시적으로 종료한다.

같은 상황을 재현하는 회귀 테스트도 추가했습니다. React 컴포넌트와 DOM을 정리하는 것만으로 외부 시스템의 상태까지 정리됐다고 가정해서는 안 됐습니다.

반대로 같은 광고가 새 할당 식별자로 다시 내려와도 광고 자체와 광고 네트워크가 같으면 세션을 닫지 않고 재사용하며, 화면에서 세션을 찾는 식별 정보만 새 할당에 맞췄습니다. 세션을 새로 시작하면 이미 준비된 광고가 다시 로딩되며 화면이 깜빡일 수 있었기 때문입니다. 노출 트래킹은 실제 렌더링을 시작한 광고의 맥락을 유지해, 보류해 둔 노출 신호도 처음 렌더링한 할당으로 보냈습니다.

6. 무엇을 측정해야 하는지도 다시 정했습니다

목표는 요청을 앞당기는 것이 아니라 CTA를 누른 뒤 광고를 기다리는 시간을 줄이는 것이었으므로, 다음 결과를 하나의 기준으로 기록할 수 있게 했습니다.

  • 광고가 화면에 나타남
  • SDK 광고가 시작됨
  • 광고가 없어 안내 화면으로 전환됨
  • 일정 시간 뒤 대체 CTA가 나타남
  • 사용자가 광고 화면을 닫음

각 결과에는 CTA 이후 경과 시간, 피처 플래그의 원본 값과 실제 적용 모드, 할당받은 광고 목록과 프리로딩된 광고의 사용 여부를 함께 남겨, 같은 이벤트 스키마에서 적용 여부의 결과를 나눠 보며 적용 범위를 넓힐 근거로 삼을 수 있게 했습니다. 다만 광고 노출, NoFill, 대체 CTA, 사용자 종료는 의미가 달라 하나의 대기 시간 분포로 섞지 않고 유형별로 해석해야 했습니다.

요청과 소비가 분리되면 한 번의 행동이 흩어지므로, 원인을 재구성하려면 참여·광고 요청·후보·SDK 세션을 같은 맥락으로 이을 수 있어야 했습니다.

측정 범위와 한계

미리 받아 둔 후보로 비SDK 광고를 띄운 경우와 그렇지 않은 경우를 같은 기간(2026년 10월 22~28일)에 비교했습니다. CTA를 누른 뒤 광고가 표시되기까지의 시간입니다.

구분p50p75p90
프리로드 후보 미사용471ms1,161ms1,997ms
프리로드 후보 사용24ms43ms774ms

적용 전(2026년 7월 2~8일)과 비교해도 같은 방향이었습니다. CTA를 누른 뒤 광고 모달이 표시 상태로 바뀌기까지의 시간이 p50 708ms에서 74ms로 줄었습니다. p75는 893ms에서 445ms로 줄었고, p90은 1,352ms로 같았습니다. 이 지표는 모달 전환 시점이라서 SDK 광고가 실제로 그려진 시점보다 이릅니다.

숨김 실행(SDK형 광고). 효과가 가장 크게 드러난 곳은 SDK형 광고였습니다. 숨김 실행이 없을 때 이 광고는 10초를 기다려도 준비되지 않았고, 사용자는 광고 대신 대체 CTA를 보게 됐습니다. 숨김 실행을 적용한 뒤에는 시퀀스의 61%가 CTA를 누르고 중앙값 5.5초 만에 광고가 나타났습니다.

구분10초 안에 광고 준비준비까지 대기(중앙값)
숨김 실행 없음0%10초 넘게 기다린 뒤 대체 CTA
숨김 실행 적용61%5.5초 (p90 8.7초)

준비된 시퀀스에서는 대기가 중앙값 기준으로 적어도 4.5초, 약 절반 가까이 줄었습니다. 원래 대기는 10초를 넘었기 때문에 실제로 줄어든 폭은 이보다 큽니다. 이 SDK는 광고가 화면에 그려진 시점을 알려 주지 않아서, 준비 완료 신호를 기준으로 쟀습니다. 숨겨진 동안에는 준비가 끝까지 진행되지 않아 드러낸 뒤 약 4초가 남는데, 이 시간을 줄이는 것이 다음 개선 지점입니다.

프리로딩을 바라보는 기준이 달라졌습니다

반쪽짜리 프리로딩은 반복되는 대기가 크다고 판단되고, 적용 범위를 제한할 수 있으며, 피처 플래그로 즉시 되돌릴 수 있을 때 타당했습니다. 반대로 공급사 집계와의 차이가 나타나 문제가 될 만큼 커지거나, 예외 처리 비용이 기능 확장 속도를 막거나, 정식 프리로드 API를 지원하는 대안이 생기면 이 임시 구조를 유지할 이유가 없습니다.

숨김 세션, 노출 신호 보류, 재할당과 폐기 로직 등의 복잡성은 코드가 대신 떠안았습니다. 저는 이것이 의도적으로 선택한 기술부채에 가깝다고 생각합니다. 위 조건이 나타나면 정식 프리로드 API로 옮기거나 공급사와의 연동 조건을 개선한 뒤, 제품 코드의 보정 로직을 걷어내야 합니다. 중요한 것은 부채가 없는 척하기보다는 부채를 왜 감수했는지, 무엇을 관측할지, 언제 갚을지를 아는 것이라 생각합니다.

광고를 빨리 보여주려면 일을 일찍 시작하면 된다는 생각은 절반만 맞았습니다. 일찍 시작할수록 쓰지 않은 광고, 늦게 도착한 응답, 외부 SDK 세션처럼 관리할 중간 상태도 늘어납니다. 각 광고를 어느 참여를 위해 준비했고 언제 드러내며, 쓰지 않으면 어떻게 안전하게 끝낼지에 답할 수 있을 때 프리로딩은 단순한 선요청을 넘어 대기 시간을 줄이는 제품 기능이 될 수 있습니다.

같은 제품에서 광고 준비가 아닌 화면 에셋의 대기를 다룬 글은 영상 최적화의 대상은 영상만이 아니었다입니다.

전체 보기