본문으로 건너뛰기

iOS 에이전트 적용

WhaTap 모바일 에이전트를 iOS 앱에 적용하면 앱 시작 성능, 화면 로딩, 네트워크 호출, 크래시, 리소스 사용량, WebView 페이지 성능을 수집할 수 있습니다.

이 문서는 앱 개발자를 대상으로 라이브러리 추가부터 데이터 수집 확인까지 전체 절차를 안내합니다. WebView 안의 웹 페이지까지 수집하려면 웹 담당자의 작업도 필요합니다. 자세한 내용은 WebView 페이지 수집 설정에서 다룹니다.

지원 환경​

빌드 환경​

ItemValue
iOS15.0 이상
Xcode16.0 이상
언어Swift 5.9 이상 또는 Objective-C

배포 타깃​

앱의 배포 타깃(IPHONEOS_DEPLOYMENT_TARGET)을 iOS 15.0 이상으로 설정하세요. 배포 패키지의 platforms 선언도 .iOS(.v15)이므로, Swift Package Manager로 추가하면 15.0 미만인 앱은 패키지 해석 단계에서 막힙니다.

XCFramework을 직접 추가한 경우에는 해석 단계의 검사가 없어 링크할 때 경고로만 나타납니다.

building for iOS x.y, but linking with dylib … which was built for newer version 15.0

빌드는 통과하지만 15.0 미만 기기에서의 동작은 보증하지 않습니다.

주의

Xcode 15 이하는 지원하지 않습니다.

모듈 인터페이스가 SwiftUICore를 참조하는데, 이 모듈은 iOS 18 SDK(Xcode 16)에서 분리 신설된 것이라 그 이전 SDK에는 존재하지 않습니다.

WebView 연동 요구 버전​

앱 안의 WebView 페이지까지 함께 수집하려면 iOS 에이전트와 브라우저 에이전트가 모두 요구 버전을 충족해야 합니다. 한쪽이라도 버전이 낮으면 일부 항목이 수집되지 않습니다. 이때 앱과 웹 어느 로그에도 오류가 남지 않습니다.

ComponentMinimum versionOwner
iOS 에이전트2.7.15앱 개발자
브라우저 에이전트3.2.0웹 개발자

2.7.15 미만에서는 웹뷰 데이터가 대시보드에 표시되지 않습니다. 적용을 마친 뒤 대시보드의 agent_version 값에서 실제로 동작 중인 버전을 확인하세요. 파일을 교체하는 것만으로는 새 버전이 반영되었는지 알 수 없습니다.

.zip으로 받은 프레임워크는 체크섬이 안내받은 값과 일치하는지 확인합니다.

swift package compute-checksum WhatapAgent.xcframework.zip

브라우저 에이전트 파일의 버전은 첫 4줄의 배너로 확인합니다.

head -4 whatap-browser-agent.js
노트

콘솔의 iOS WebView 설치 화면은 SPM 패키지 버전을 2.7.3으로, 브라우저 에이전트 스크립트 주소를 v1 경로로 안내합니다. WebView 연동에는 위 요구 버전이 필요하므로 화면의 값을 그대로 따르지 마세요.

에이전트 설치​

다음 순서로 진행합니다.

  1. 라이브러리 추가
  2. Agent 초기화
  3. 설치 확인

1. 라이브러리 추가​

세 가지 방식 중 하나를 선택합니다. Swift Package Manager 방식을 권장합니다.

Swift Package Manager(권장)​

  1. Xcode에서 프로젝트를 열고 File > Add Package Dependencies 메뉴를 선택하세요.

  2. 패키지 URL 필드에 다음 주소를 입력하세요.

    https://github.com/whatap/WhatapIOSAgent-Release

  3. Releases에서 최신 버전을 확인해 그 버전을 선택하고 Add Package 버튼을 클릭하세요. branch를 지정하거나 상한 없는 from:을 쓰지 마세요.

  4. WhatapAgent 라이브러리를 앱 타겟에 추가하세요.

Package.swift로 관리하는 프로젝트라면 다음과 같이 지정합니다.

dependencies: [
.package(url: "https://github.com/whatap/WhatapIOSAgent-Release.git", exact: "<최신 버전>")
],
targets: [
.target(
name: "YourApp",
dependencies: [
.product(name: "WhatapAgent", package: "WhatapIOSAgent-Release")
]
)
]
주의

Swift Package Manager 방식도 CDN 접근이 필요합니다.

GitHub 저장소에는 바이너리가 없고 binaryTarget(url:checksum:)이 CDN을 가리킵니다. 빌드 머신에서 github.com과 repo.whatap-mobile-agent.io가 모두 열려 있어야 합니다. 방화벽으로 CDN이 막혀 있으면 swift package resolve 단계에서 실패합니다. 이 경우 로컬 파일 설치를 사용하세요.

수동 XCFramework 설치​

  1. XCFramework을 내려받습니다. <최신 버전> 자리에는 Releases에서 확인한 버전을 넣으세요.

    curl -L -o WhatapAgent.xcframework.zip \
    https://repo.whatap-mobile-agent.io/uploads/<최신 버전>/WhatapAgent.xcframework.zip
    unzip WhatapAgent.xcframework.zip
  2. Xcode 프로젝트에 추가합니다.

    • 프로젝트 타겟을 선택하고 General 탭을 선택하세요.
    • Frameworks, Libraries, and Embedded Content 섹션에서 + 버튼을 클릭하세요.
    • Add Other > Add Files를 선택하고 WhatapAgent.xcframework를 선택하세요.
    • Embed & Sign 설정을 확인하세요.

로컬 파일 설치(폐쇄망, 사내망)​

외부 저장소에 접근할 수 없는 환경에서는 와탭이 제공한 zip 파일을 받아 압축을 푼 뒤 프로젝트에 추가합니다. 이후 수동 XCFramework 설치와 같은 방법으로 Xcode에서 Embed & Sign으로 추가하세요.

unzip WhatapAgent.xcframework.zip
mv WhatapAgent.xcframework /path/to/YourApp/

2. Agent 초기화​

SDK는 앱이 시작될 때 초기화해야 합니다. 초기화가 늦어지면 pre-main time, 초기 메모리 사용량 등 앱 시작 초기의 성능 데이터를 수집하지 못합니다.

Framework초기화 위치
SwiftUI@main struct의 init(). 앱 구조체가 만들어지는 시점
UIKitapplication:willFinishLaunchingWithOptions:. didFinishLaunching보다 먼저 호출됨
위험

build() 뒤에 initialize()를 반드시 호출하세요.

build()는 설정만 구성합니다. initialize()를 호출하지 않아도 setupWebViewBridge()는 그대로 동작해 window.whatapBridge가 주입되므로, 브라우저 에이전트는 브릿지가 정상이라고 판단해 HTTP 폴백도 하지 않습니다. 그 결과 웹과 네이티브 데이터가 모두 유실되고 앱 로그에만 다음 오류가 남습니다.

[ERROR] [WhatapWebviewBridge] WhatapAgent가 초기화되지 않았습니다.

initialize()는 두 번째 호출부터 무시됩니다. 이미 운영 중인 앱에 WebView 연동을 추가한다면 초기화 코드를 새로 만들지 말고 기존 코드를 수정하세요. 초기화를 두 곳에 나누어 넣어도 뒤쪽 호출은 적용되지 않습니다.

Swift​

SwiftUI 앱은 @main struct의 init()에서 초기화합니다.

import SwiftUI
import WhatapAgent

@main
struct MyApp: App {
init() {
let agent = WhatapAgentBuilder()
.setProjectKey("<PROJECT_ACCESS_KEY>")
.setPCode(12345)
.setServerUrl("https://<수집 서버 도메인>/m") // 끝의 /m 필수
.build()

agent.initialize()
}

var body: some Scene {
WindowGroup {
ContentView()
}
}
}

UIKit 앱은 AppDelegate의 application:willFinishLaunchingWithOptions:에서 초기화하는 것을 권장합니다.

import UIKit
import WhatapAgent

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

func application(_ application: UIApplication,
willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

let agent = WhatapAgentBuilder()
.setProjectKey("<PROJECT_ACCESS_KEY>")
.setPCode(12345)
.setServerUrl("https://<수집 서버 도메인>/m")
.build()

agent.initialize()

return true
}
}

Objective-C​

willFinishLaunchingWithOptions와 didFinishLaunchingWithOptions 중 어느 위치에서 초기화해도 동작합니다. 다음은 didFinishLaunchingWithOptions에서 초기화하는 예제입니다.

AppDelegate.m
#import <WebKit/WebKit.h>                   // WhatapAgent 헤더보다 먼저
#import <WhatapAgent/WhatapAgent-Swift.h>

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

WhatapAgentBuilder *b = [[WhatapAgentBuilder alloc] init];
(void)[b setProjectKey:@"발급받은 액세스 키"];
(void)[b setPCode:12345];
(void)[b setServerUrl:@"https://수집서버-주소/m"]; // /m 포함, 끝에 / 없이
(void)[b setSamplingRate:1.0]; // 0.0 ~ 1.0 (웹의 0 ~ 100 과 다릅니다)

WhatapIOSAgent *agent = [b build];
[agent initialize]; // 이 호출이 없으면 데이터가 전송되지 않습니다

return YES;
}

헤더를 들여오는 순서. #import <WhatapAgent/WhatapAgent-Swift.h>보다 먼저 #import <WebKit/WebKit.h>를 넣으세요. 생성 헤더는 WKWebView를 전방 선언만 하고 정의를 가져오지 않습니다. 모듈을 끈 프로젝트(CLANG_ENABLE_MODULES = NO)에서는 이 헤더를 import하는 모든 파일이 먼저 WebKit을 import해야 합니다. 그렇지 않으면 cannot find protocol declaration for 'WKNavigationDelegate' 오류가 발생하고 빌드가 실패합니다. WebView를 다루지 않는 AppDelegate도 마찬가지입니다.

@import WhatapAgent; 대신 #import 형태를 사용하세요. @import도 동작하지만, 모듈을 끈 프로젝트에서는 use of '@import' when modules are disabled 오류가 발생해 빌드가 실패합니다. 또한 umbrella 헤더가 공개 대상이 아닌 선언까지 자동완성에 노출합니다.

빌더 setter는 모두 warn_unused_result입니다. 위 예제처럼 체인을 끊어 여러 줄로 작성하면서 (void) 캐스팅을 생략하면 각 줄에서 -Wunused-result 경고가 발생합니다. -Werror를 사용하는 프로젝트에서는 이 경고 때문에 빌드가 실패합니다. 한 줄 체인으로 작성하면 캐스팅이 필요 없습니다.

3. 설치 확인​

앱을 실행한 뒤 Xcode Console에서 초기화 로그를 확인하세요. 디버그 빌드에서 로그를 자세히 보려면 다음 설정을 추가합니다.

#if DEBUG
WhatapLogger.isDebug = true
#endif

데이터가 수집되지 않으면 프로젝트 키, PCode, 서버 URL, initialize() 호출, 샘플링 비율과 디스크 버퍼를 확인하세요.

수집 항목​

build()와 initialize()를 호출하면 다음 항목을 자동으로 수집합니다.

  • 앱 시작 성능(Cold start, Warm start, pre-main)
  • 크래시, 예외, 시그널
  • CPU, 메모리, 배터리, 열 상태

다음 세 항목은 build()와 initialize()를 호출하는 것만으로는 자동으로 켜지지 않습니다. 별도의 작업이 필요합니다.

Item필요한 작업
화면별 로딩 시간화면 추적의 네 가지 방법 중 하나 적용
네트워크 요청, 응답 정보수집과 HTTP 옵션의 setCollectNetwork(true) 지정
WebView 페이지 로드와 성능 정보WebView 페이지 수집 설정의 브릿지 등록
주의

화면별 로딩 시간은 자동으로 켜지지 않습니다.

UIViewController 생명주기를 기반으로 하는 화면 추적은 기본적으로 꺼져 있습니다. setCollectScreenLoading의 기본값은 true지만, 에이전트의 disableAutoScreenTracking 기본값도 true입니다. Builder가 이 값을 되돌리지 않기 때문에 화면 추적은 자동으로 켜지지 않습니다. 켜는 방법은 화면 추적을 참조하세요.

자동 화면 추적을 켜지 않아도 WebView 데이터는 정상적으로 수집됩니다. initialize()를 호출하고 0.3초가 지나면 앱 이름(CFBundleName)으로 기본 ScreenGroup이 하나 시작됩니다. 웹뷰 페이지 로드는 이 그룹에 연결됩니다. 자동 추적을 사용하지 않으면 네이티브 화면 단위의 구분만 생기지 않습니다.

Builder 옵션​

모든 Builder 옵션은 선택 사항이며, 지정하지 않으면 기본값이 적용됩니다.

전송과 버퍼 옵션​

MethodDefaultDescription
setFlushInterval(_:)10.0배치 전송 주기(초)
setQueueSize(_:)1000메모리 큐 크기
setMaxDiskBytes(_:)512000오프라인 디스크 버퍼 상한(bytes)
setMaxDiskFiles(_:)5디스크 버퍼 파일 개수

수집과 HTTP 옵션​

MethodDefaultDescription
setCollectScreenLoading(_:)true화면 로딩 수집. 화면 추적 설정이 함께 필요
setCollectNetwork(_:)false자동 네트워크 수집
setKeepAlive(_:)trueHTTP keep-alive
setMaxConnectionsPerHost(_:)4호스트당 최대 연결 수
주의

네트워크 수집은 opt-in입니다.

iOS SDK 2.7.3부터 자동 네트워크 수집의 기본값은 false입니다. 이 기능을 활성화하면 URLProtocol을 통해 요청을 계측합니다. 따라서 인증서 pinning, 사용자 정의 URLSessionDelegate, 자체 쿠키나 인증 헤더, HTTP/2 협상에 의존하는 앱은 영향을 먼저 확인하세요. 보안에 민감한 요청은 별도의 custom URLSession으로 분리하는 것을 권장합니다.

커스텀 endpoint​

MethodDefaultDescription
setLogServerUrl(_:)serverUrl + "/log"log 엔드포인트를 직접 지정할 때만 사용
setSpanServerUrl(_:)serverUrl + "/trace"trace 엔드포인트를 직접 지정할 때만 사용

setLogServerUrl(_:)과 setSpanServerUrl(_:)을 직접 지정했더라도 setServerUrl은 반드시 설정해야 합니다. 자세한 내용은 수집 서버 주소 확인을 참조하세요.

ScreenGroup과 추적 옵션​

MethodDefaultDescription
setGroupWaitingInterval(_:)3.0화면 그룹 닫힘 지연(초)
setExcludeLifecycleEventsFromScreenGroup(_:)false화면 그룹에서 생명주기 이벤트 제외
enableMethodTracing(_:)falseMethod Tracing 사용 여부
setSamplingRate(_:)1.0수집 비율. 0.0 ~ 1.0 스케일. 세션 단위로 추첨
setSingleSessionPerLaunch(_:)falsetrue이면 앱 실행 시 발급한 세션을 프로세스가 종료될 때까지 유지
주의

setSamplingRate(_:)는 0.0 ~ 1.0 범위의 값을 사용합니다. 반면 브라우저 에이전트의 sampleRate는 0 ~ 100 범위의 값을 사용합니다. 따라서 모바일 에이전트의 값을 브라우저 에이전트에 그대로 입력하면 안 됩니다. 모바일의 1.0을 웹에 그대로 넣으면 수집 비율이 1%가 됩니다.

주의

범위를 벗어난 값은 무시되어 전수 수집으로 남습니다.

0.0 ~ 1.0을 벗어난 값을 넣으면 상한이나 하한으로 조정하지 않고 직전에 설정한 유효한 값을 그대로 유지합니다. 한 번도 설정하지 않았다면 기본값 1.0입니다. 즉 잘못된 값을 넣으면 수집이 꺼지는 것이 아니라 전수 수집으로 남고, 증상은 「샘플링이 걸리지 않는다」로 나타납니다.

이때 다음 경고가 남지만 WhatapLogger.isDebug를 켜야 보입니다. 수집량이 설정과 어긋나면 디버그 로그를 켜고 이 문구를 확인하세요.

[WARN] [SESSION] configure: invalid samplingRate=50.0 - keeping 1.0
노트

세션이 나뉘는 기준

기본값에서 세션은 15분 동안 사용자 활동이 없거나 시작 후 4시간이 지나면 새로 발급됩니다. 앱을 다시 실행해도 새 세션입니다. 샘플링 추첨은 세션 단위여서 세션이 새로 발급될 때마다 다시 추첨하며, 한 세션 안의 데이터는 전부 수집되거나 전부 수집되지 않습니다.

setSingleSessionPerLaunch(true)는 위 두 만료 조건을 적용하지 않아 세션이 앱 실행당 하나만 생기고 샘플링 추첨도 앱 실행 시 한 번만 일어납니다. 세션 수로 과금되는 SaaS 프로젝트에서는 기본값 false를 유지하세요. 세션 수가 과금과 무관한 설치형 환경에서만 켜는 옵션입니다.

WhatapAgentBuilder()
.setProjectKey("<PROJECT_ACCESS_KEY>")
.setPCode(12345)
.setServerUrl("https://<수집 서버 도메인>/m")
.setFlushInterval(120)
.setMaxDiskBytes(2 * 1024 * 1024)
.setMaxDiskFiles(5)
.setQueueSize(1000)
.setKeepAlive(true)
.setMaxConnectionsPerHost(4)
.setGroupWaitingInterval(3.0)
.build()

화면 추적​

네이티브 화면별 로딩 시간을 수집하려면 다음 네 가지 중 하나를 사용하세요.

방법 1. 자동 추적 플래그 내리기. 이 방법을 사용하면 모든 UIViewController를 자동으로 추적합니다. initialize()를 호출하기 전에 설정하세요.

Swift
// 방법 1) initialize() 전에 플래그를 내린다. 모든 UIViewController 를 자동 추적
WhatapIOSAgent.disableAutoScreenTracking = false
agent.initialize()

// 방법 2) 특정 화면만 등록한다 (플래그를 내리지 않아도 동작)
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
WhatapIOSAgent.trackViewController(self)
}
Objective-C
WhatapIOSAgent.disableAutoScreenTracking = NO;   // 방법 1, initialize 호출 전
[WhatapIOSAgent trackViewController:self]; // 방법 2

방법 3. SwiftUI modifier. SwiftUI 화면에서는 전용 modifier를 사용하세요. trackViewController(_:)는 전달받은 인스턴스의 클래스에 swizzling을 적용하는 API입니다. 따라서 UIHostingController를 즉석에서 만들어 전달하면 의도한 화면이 잡히지 않을 수 있습니다.

struct CheckoutView: View {
var body: some View {
ContentView()
.whatapScreenTracking("결제 화면") // onAppear / onDisappear 자동 연결
}
}

직접 제어하려면 WhatapIOSAgent.trackScreen("결제 화면")과 WhatapIOSAgent.endScreenTracking("결제 화면")을 짝으로 호출합니다.

방법 4. 화면 Task 직접 제어. swizzling을 전혀 사용하지 않는 경우입니다. 모두 인스턴스 메서드입니다.

guard let taskId = WhatapIOSAgent.shared?.startScreenTaskFor(self) else { return }
// 또는 startScreenTaskWithName("결제 화면")
WhatapIOSAgent.shared?.endScreenTaskWithTaskId(taskId)
노트

자동 화면 추적에서 클래스명 대신 다른 이름을 사용하려면 ViewController에서 WhatapScreenNaming 프로토콜을 채택하고 whatapScreenName을 반환하세요. 이 프로토콜은 웹뷰 화면 이름에는 적용되지 않습니다. 웹뷰 화면 이름을 지정하는 방법은 화면 이름 지정을 참조하세요.

수동 연동​

수동 Task API​

결제나 이미지 로딩처럼 한 화면 안의 비동기 작업을 별도 span으로 기록할 수 있습니다.

WhatapIOSAgent.startTask("checkout", taskId: "task-001")
WhatapIOSAgent.endTask("task-001")

Method Tracing​

Method Tracing은 opt-in 기능입니다. Builder에 enableMethodTracing(true)를 추가한 뒤 필요한 메서드의 성능을 기록하세요.

methodStart와 methodEnd

func validateBiometric(
agent: WhatapIOSAgent,
validate: () throws -> Void
) rethrows {
agent.methodStart(className: "AuthService", methodName: "validateBiometric")
defer {
agent.methodEnd(className: "AuthService", methodName: "validateBiometric")
}

try validate()
}

StackSpan

func charge(
agent: WhatapIOSAgent,
operation: () async throws -> Void
) async rethrows {
let stackSpan = agent.start(className: "PaymentService", methodName: "charge")

do {
try await operation()
stackSpan.end()
} catch {
stackSpan.endWithError(error)
throw error
}
}

전역 컨텍스트​

ExtrasStore의 값은 모든 로그와 span에 자동으로 붙습니다.

Swift
import WhatapAgent

ExtrasStore.shared.setExtra(key: "user_id", value: userId)
ExtrasStore.shared.removeExtra(key: "user_id")
ExtrasStore.shared.clearExtras()
Objective-C
[[ExtrasStore shared] setExtraValue:userId forKey:@"user_id"];
팁

ExtrasStore에 저장한 키는 Android 호환을 위해 전송할 때 자동으로 .c suffix가 붙습니다. user_id는 user_id.c로 전송됩니다.

화면 그룹​

여러 화면에 걸친 흐름을 하나의 그룹으로 묶습니다.

ChainView
ChainView.shared.startChain(chainName: "LoginFlow")

ChainView.shared.endChain()

네트워크와 크래시​

네트워크 보안 설정​

HTTPS 수집 endpoint에는 App Transport Security 예외가 필요하지 않습니다. 레거시 HTTP endpoint를 반드시 사용해야 한다면 필요한 단일 도메인에만 별도로 예외를 적용하세요. 하위 도메인이나 임의 로드는 허용하지 마세요.

크래시 리포팅​

크래시는 자동으로 수집되며 다음에 앱을 실행할 때 전송됩니다. 기본값은 내장 리포터입니다. PLCrashReporter를 사용하려면 별도의 라이브러리가 필요합니다.

WhatapAgentBuilder()
.useNativeCrashReporter()
.build()

WhatapAgentBuilder()
.usePLCrashReporter()
.build()

WebView 페이지 수집 설정​

앱의 WKWebView에 표시되는 웹 페이지는 네이티브 SDK가 아니라 브라우저 에이전트가 수집합니다. 네이티브 화면과 WebView 페이지를 같은 세션으로 연결하려면 앱과 웹을 모두 설정해야 합니다.

  1. 수집 서버 주소 확인
  2. WebView 등록
  3. 브라우저 에이전트 스크립트 추가

세 단계를 모두 마쳐야 연동이 완료됩니다. 하나라도 빠지면 브릿지에 도달하지 못한 데이터는 표준 HTTP 전송으로 폴백합니다(브라우저 에이전트 옵션 webViewHttpFallback, 기본값 true). 이 경우 데이터 자체는 수집됩니다. 하지만 대시보드에서는 일반 브라우저(BROWSER)로 분류되고 네이티브 세션과 연결되지 않습니다. 폴백은 데이터 유실을 막기 위한 안전망일 뿐, 연동이 완료되었다는 뜻은 아닙니다.

WebView 연동에는 iOS 에이전트 2.7.15 이상과 브라우저 에이전트 3.2.0 이상이 필요합니다. 먼저 WebView 연동 요구 버전을 확인하세요. Agent 초기화의 initialize() 호출도 끝나 있어야 합니다. initialize()를 호출하지 않으면 브릿지는 주입되지만 데이터는 전량 유실됩니다.

조합별 수집 항목​

iOS 26.5 실기기와 실제 수집 서버로 검증한 결과입니다.

Item표준 적용연동 적용, 브릿지 연결연동 적용, 브릿지 미도달
페이지 로드○○○ (폴백)
리소스, AJAX○○○ (폴백)
화면 전환○○○ (폴백)
JS 에러 5종○○○ (폴백)
사용자 이벤트○○○ (폴백)
세션 리플레이○○○ (폴백)
Core Web Vitals○○○ (폴백)
커스텀 로그○✗○ (폴백)
대시보드 분류BROWSERWEBVIEWBROWSER
적재 프로젝트웹 페이지 pcode앱의 모바일 프로젝트웹 페이지 pcode
네이티브와 세션 통합-○✗

표준 적용은 브라우저 에이전트를 isWebView 없이 초기화한 상태이고, 연동 적용은 isWebView: true로 초기화한 상태입니다. JS 에러 5종은 uncaught 에러, unhandled promise rejection, console.error(), noticeError(), CSP violation입니다.

주의

커스텀 로그는 연동 적용에서 수집되지 않습니다.

브라우저 에이전트는 커스텀 로그(enableCustomLog와 logger)를 브릿지 메서드 log로 보냅니다. 하지만 모바일 브릿지에는 해당 인터페이스가 없기 때문에 커스텀 로그가 조용히 버려집니다. iOS에서는 디버그 모드에서도 별도의 표시가 남지 않으므로, 로그가 버려지고 있다는 사실을 콘솔에서 확인할 수 없습니다. 커스텀 로그가 필요한 페이지는 isWebView를 지정하지 않고 표준 적용으로 두세요.

데이터가 적재되는 프로젝트​

연동이 동작하는 동안 웹 데이터는 앱의 모바일 프로젝트에 적재됩니다. 페이지에 설정한 pcode나 projectAccessKey로는 들어가지 않습니다. 브릿지로 넘어간 데이터는 업로드 직전에 전송 본문 meta의 프로젝트 식별값을 앱의 setPCode, setProjectKey 값에 맞춥니다. sessionID와 userID도 네이티브 값으로 통일됩니다. 네이티브 화면과 웹뷰 화면을 한 세션으로 보려면 두 데이터가 같은 프로젝트에 있어야 합니다. 이를 위해 두 구성요소가 같은 규격을 사용합니다.

Case조회할 프로젝트
표준 적용(isWebView 미설정)웹 페이지의 pcode
연동 적용, 브릿지 연결됨앱의 모바일 프로젝트 pcode
연동 적용, 브릿지 미도달(폴백)웹 페이지의 pcode

따라서 페이지의 pcode와 projectAccessKey는 폴백 전송에만 사용합니다. 연동이 정상적으로 동작하는 동안에는 사용하지 않습니다. 하지만 브릿지가 끊기는 경우를 대비해 브라우저(RUM) 프로젝트의 유효한 값을 넣어야 합니다. 임의의 값이나 앱의 모바일 pcode를 넣어도 초기화 단계의 형식 검사는 통과합니다. 하지만 폴백으로 전송한 데이터는 어디에도 적재되지 않습니다.

iOS 앱과 Android 앱이 서로 다른 모바일 프로젝트를 사용한다면 같은 웹 페이지의 웹뷰 데이터도 플랫폼별로 나뉘어 적재됩니다. HTML을 앱별로 나눌 필요는 없습니다. 웹 화면 하나의 전체 지표를 보려면 두 프로젝트를 각각 조회하세요.

1. 수집 서버 주소 확인​

여기서 확인할 값은 모바일 에이전트의 setServerUrl입니다. 웹뷰 데이터도 이 주소로 전송되므로 WebView 연동에서 특히 중요합니다.

RuleExample
스킴, 호스트(포트), /m 순서로 끝냄https://수집서버-주소:9443/m
리버스 프록시 경로 포함 가능https://내부호스트:9443/22/m
끝에 /를 붙이지 않음https://…/m/은 …/m//pageLoad가 되어 실패

iOS는 /m을 자동으로 붙이지 않으므로 serverUrl에 항상 /m을 포함하세요. setLogServerUrl과 setSpanServerUrl을 직접 지정했더라도 setServerUrl은 반드시 설정해야 합니다. 웹뷰 데이터는 이 두 값을 사용하지 않습니다. 대신 serverUrl 뒤에 엔드포인트를 붙여 전송합니다. 따라서 serverUrl을 비워 두면 웹뷰 데이터가 조용히 사라지고 첫 span 전송에서 앱이 종료될 수 있습니다.

사내 리버스 프록시를 앞단에 두고 프록시 경로를 포함한 값을 사용해도 됩니다. 에이전트는 이 값 뒤에 경로를 그대로 이어 붙입니다. 따라서 프록시가 다음 경로를 모두 전달하도록 설정하면 됩니다.

Data<serverUrl> 뒤에 붙는 경로
네이티브 트레이스, 로그/trace /log
웹뷰 페이지 로드/pageLoad
리소스, 화면 전환, 에러, 이벤트, 메모리/resource /routeChange /onError /event /memory
Web Vitals/webVitals /v2/webVitals
세션 리플레이/sessionreplay /v2/sessionreplay

2. WebView 등록​

WebView를 표시할 화면에서 등록하세요. 반드시 메인 스레드에서 load()를 호출하기 전에 등록해야 합니다. WebView 인스턴스 하나당 한 번만 호출하세요.

UIKit 앱​

Swift
import WebKit
import WhatapAgent

final class WebViewController: UIViewController {

private var webView: WKWebView!

override func viewDidLoad() {
super.viewDidLoad()

webView = WKWebView(frame: view.bounds)
view.addSubview(webView)

// 브릿지 등록. context 에는 WebView 를 소유한 ViewController 를 전달합니다
WhatapIOSAgent.setupWebViewBridge(webView, context: self)

// 등록이 반영된 뒤 로드되도록 다음 런루프에서 호출합니다
DispatchQueue.main.async { [weak self] in
guard let self, let url = URL(string: "https://your.web.app") else { return }
self.webView.load(URLRequest(url: url))
}
}
}
Objective-C
#import <WebKit/WebKit.h>                   // WhatapAgent 헤더보다 먼저
#import <WhatapAgent/WhatapAgent-Swift.h>

- (void)viewDidLoad {
[super viewDidLoad];

self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:self.webView];

// 클래스 메서드(+)입니다. 메인 스레드에서, load 이전에, 인스턴스당 1회만
[WhatapIOSAgent setupWebViewBridge:self.webView context:self];

// 등록이 반영된 뒤 로드되도록 다음 런루프에서 호출합니다
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
NSURL *url = [NSURL URLWithString:@"https://your.web.app"];
[weakSelf.webView loadRequest:[NSURLRequest requestWithURL:url]];
});
}

Storyboard 또는 XIB에서 @IBOutlet WKWebView *webView를 받는 구조라면 WebView를 생성하는 코드만 제외하세요. setupWebViewBridge:context:는 그대로 viewDidLoad에서 호출합니다.

등록 규칙

Rule지키지 않으면
load()를 다음 런루프로 미루기(권장)setupWebViewBridge는 핸들러 등록과 스크립트 주입을 DispatchQueue.main.async 안에서 처리함. 같은 흐름에서 곧바로 load()해도 실제로는 주입이 먼저 반영되는 것이 일반적이지만, 캐시된 페이지, file://, 느린 기기에서는 타이밍이 달라질 수 있음. 세 줄 비용의 안전판이므로 신규 코드에는 두는 편을 권장
WKWebView 인스턴스당 1회만 등록(viewWillAppear 아님)두 번째 호출은 건너뛰고 화면 그룹 연결만 한 번 더 실행되어 불필요한 WebView_* 그룹이 생김. WebView 생성 시점 1회로 고정할 것. WebView를 다시 만들었다면 새 인스턴스에 다시 등록
[WhatapIOSAgent setupWebViewBridge:context:]로 호출WhatapWebViewBridge(대문자 V)와 WhatapWebViewManager는 공개 API가 아님. 사용하면 링크 단계에서 Undefined symbols for architecture …: "_OBJC_CLASS_$_WhatapWebViewBridge"로 실패. 브릿지 인스턴스를 직접 다뤄야 하는 경우에만 소문자 v의 WhatapWebviewBridge 사용
enableWebViewAutoBridge(_:)를 쓰지 않기지원이 끝난 옵션이며 호출해도 아무 동작을 하지 않음. YES로 설정해도 브릿지가 켜지지 않음
startDataUploadTimer()를 직접 호출하지 않기브릿지를 만들 때 자동으로 시작됨. 또 호출하면 같은 일을 하는 타이머만 하나 더 생김
JavaScript를 끄지 않기브릿지가 JavaScript로 동작함. WKWebView는 기본으로 켜져 있으나 앱에서 껐다면 브릿지가 동작하지 않음
자사 콘텐츠를 로드하는 WebView에만 등록등록한 WebView 안에서 로드되는 콘텐츠는 iframe을 포함해 브릿지에 접근 가능. 출처 이탈을 막는 지점은 WKNavigationDelegate의 webView(_:decidePolicyFor:decisionHandler:)

브릿지가 받은 데이터는 큐에 쌓였다가 5초마다 수집 서버로 전송됩니다. 이 타이머는 브릿지를 만들 때 자동으로 시작되므로 앱에서 할 일이 없습니다. WebView 화면을 벗어난 뒤에도 브릿지 인스턴스가 살아 있으면 타이머는 계속 동작합니다.

노트

iOS에서는 웹에서 페이지 로드를 추적합니다. setupWebViewBridge()는 navigationDelegate를 변경하지 않으므로 기존 delegate는 그대로 유지됩니다. 앱이 만드는 페이지 로드 구간은 없습니다. 화면에 붙기 전에 로드가 끝나는 WebView가 있다면 화면 표시 시점에 WhatapRUM.endPageLoad()를 호출하세요.

화면 이름 지정​

setupWebViewBridge()는 WebView를 소유한 ViewController의 title을 대시보드의 화면 이름으로 사용합니다. title이 없으면 클래스 이름을 사용합니다. 화면 이름을 직접 지정하려면 브릿지 인스턴스를 만들어 사용하세요. 수집 결과는 setupWebViewBridge()를 사용했을 때와 같으며 화면 이름만 달라집니다.

Swift
private var bridge: WhatapWebviewBridge!   // 인스턴스로 보관 (지역 변수 금지)

override func viewDidLoad() {
super.viewDidLoad()

bridge = WhatapWebviewBridge(context: self, screenName: "결제 화면")
bridge.configureWebView(webView)
}
Objective-C
// @interface 또는 클래스 확장에. 지역 변수로 두지 말고 인스턴스로 보관합니다
@interface MyWebViewController ()
@property (nonatomic, strong) WhatapWebviewBridge *bridge;
@end

// @implementation 안에
- (void)viewDidLoad {
[super viewDidLoad];

self.bridge = [[WhatapWebviewBridge alloc] initWithContext:self screenName:@"결제 화면"];
[self.bridge configureWebView:self.webView];
}

화면 이름은 수집 데이터의 screen_name과 activity_name 속성에 화면명 (URL) 형태로 저장됩니다. WebView 화면이 여러 개인 앱에서는 이 이름을 사용해 대시보드에서 각 화면을 구분합니다. 이름을 정할 필요가 없다면 더 짧은 setupWebViewBridge()를 사용하세요.

노트

자동 화면 추적의 WhatapScreenNaming 프로토콜은 웹뷰 화면 이름에 적용되지 않습니다. 웹뷰는 WhatapWebviewBridge(context:screenName:)의 screenName을 쓰고, 지정하지 않으면 ViewController의 title, 그것도 없으면 클래스명을 사용합니다.

SwiftUI 앱​

setupWebViewBridge(_:context:)를 사용하려면 UIViewController가 필요합니다. 따라서 SwiftUI에서 WKWebView를 감싸는 방식에 따라 등록 방법이 달라집니다.

감싸는 방식등록 방법
UIViewControllerRepresentable(권장)ViewController가 있으므로 setupWebViewBridge(webView, context: self)를 그대로 사용
UIViewRepresentableViewController가 없으므로 WhatapWebviewBridge(screenName:)와 configureWebView(webView)를 사용

UIViewControllerRepresentable에서는 UIKit과 같은 코드를 사용할 수 있으므로 이 방식을 권장합니다.

import SwiftUI
import WebKit
import WhatapAgent

struct WhatapWebView: UIViewControllerRepresentable {
let urlString: String

final class Host: UIViewController {
var webView: WKWebView!
var urlString = ""

override func viewDidLoad() {
super.viewDidLoad()
title = "결제 화면" // 대시보드 화면 이름이 됩니다

webView = WKWebView(frame: view.bounds)
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(webView)

WhatapIOSAgent.setupWebViewBridge(webView, context: self)

if let url = URL(string: urlString) { webView.load(URLRequest(url: url)) }
}
}

func makeUIViewController(context: Context) -> Host {
let vc = Host()
vc.urlString = urlString
return vc
}
func updateUIViewController(_ vc: Host, context: Context) {}
}

UIViewRepresentable만 사용할 수 있다면 screenName으로 초기화하세요. 이 경우 브릿지가 내부에서 대체 ViewController를 만들어 context 자리를 채웁니다.

struct WhatapWebView: UIViewRepresentable {
let urlString: String

final class Coordinator {
var bridge: WhatapWebviewBridge?
}
func makeCoordinator() -> Coordinator { Coordinator() }

func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView(frame: .zero)

let bridge = WhatapWebviewBridge(screenName: "결제 화면")
bridge.configureWebView(webView)
context.coordinator.bridge = bridge // 아래 설명 참고

if let url = URL(string: urlString) { webView.load(URLRequest(url: url)) }
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {}
}

브릿지는 Coordinator에 보관하는 것을 권장합니다. 지역 변수로 두어도 WebKit이 메시지 핸들러로 보유하기 때문에 동작합니다. 하지만 브릿지의 수명을 코드에서 명시적으로 관리하는 편이 안전합니다.

SwiftUI에서도 WebView를 생성할 때 한 번만 등록합니다. makeUIView와 makeUIViewController는 뷰가 다시 만들어질 때만 호출되지만 updateUIView는 매번 호출됩니다. 따라서 등록 코드를 update…에 넣으면 중복으로 등록됩니다.

SwiftUI 앱은 AppDelegate가 없으므로 SDK 초기화도 App의 init()에서 수행합니다. Agent 초기화를 참조하세요.

3. 브라우저 에이전트 스크립트 추가​

WebView가 여는 모든 HTML 페이지의 head 태그 최상단에 다음 스크립트를 넣으세요. 웹 담당자가 진행하는 작업입니다.

Async
<script>
(function(w, h, _a, t, a) {
w = w[a] = w[a] || {};
w.config = {
projectAccessKey: "{PROJECT_ACCESS_KEY}",
pcode: {PCODE},
sampleRate: 100,
isWebView: true,
proxyBaseUrl: "https://rum-ap-northeast-2.whatap-browser-agent.io/"
};
a = h.createElement(_a);
a.async = 1;
a.src = t;
t = h.getElementsByTagName(_a)[0];
t.parentNode.insertBefore(a, t);
})(window, document, 'script', 'https://repo.whatap-browser-agent.io/rum/prod/v2/whatap-browser-agent.js', 'WhatapBrowserAgent');
</script>
Sync
<script>
window.WhatapBrowserAgent = {
config: {
projectAccessKey: "{PROJECT_ACCESS_KEY}",
pcode: {PCODE},
sampleRate: 100,
isWebView: true,
proxyBaseUrl: "https://rum-ap-northeast-2.whatap-browser-agent.io/"
}
};
</script>
<script src="https://repo.whatap-browser-agent.io/rum/prod/v2/whatap-browser-agent.js"></script>
주의

설정 키와 스크립트 주소를 그대로 옮겨 적으세요.

  • isWebView는 대소문자를 구분합니다. isWebview처럼 적으면 알 수 없는 키로 경고 없이 무시되어 네이티브 세션과 이어지지 않습니다.
  • WebView용 스크립트 주소는 /rum/prod/v2/whatap-browser-agent.js입니다. v1 주소에는 Bridge 연동이 없습니다.
  • Async 스니펫의 5번째 인자 'WhatapBrowserAgent'는 바꾸지 마세요. 이 값은 설정을 담는 전역 객체의 이름입니다. 5번째 인자를 번들 경로로 바꾸면 에이전트가 설정을 찾지 못합니다. 이때 콘솔에 메시지도 남지 않고 초기화도 되지 않습니다. 에이전트 파일을 자체 호스팅한다면 번들 경로를 지정하는 4번째 인자를 바꾸세요.
ConfigDescription
projectAccessKey브라우저(RUM) 프로젝트의 액세스 키. 폴백 전송에 사용
pcode브라우저(RUM) 프로젝트의 프로젝트 코드. 폴백 전송에 사용
sampleRate수집 비율(%). 0 ~ 100 스케일이며 모바일 에이전트의 setSamplingRate(0.0 ~ 1.0)와 다름. 적용 확인 중에는 100 사용
isWebView데이터 전송을 모바일 에이전트에 위임하는 스위치. 연동 적용에서 true 지정
proxyBaseUrl폴백 전송 시 데이터를 보낼 주소. 연동 적용에서도 필수. 설정하지 않으면 데이터가 전혀 전송되지 않음

브라우저(RUM) 프로젝트의 관리 > 에이전트 설치 화면에서 projectAccessKey, pcode, proxyBaseUrl 값을 확인할 수 있습니다. 각 항목에는 현재 프로젝트의 값이 채워져 있습니다.

보안 정책 확인​

하이브리드 앱은 index.html에 CSP(Content Security Policy)를 선언하는 경우가 많습니다.

<meta http-equiv="Content-Security-Policy"
content="connect-src 'self' https://수집서버-주소;
script-src 'self'">
Directive허용할 출처
connect-src수집 서버의 출처. 표준 적용은 상시 필요하고, 연동 적용도 webViewHttpFallback(기본값 true)이 켜져 있으면 브릿지 미도달 시 이 경로로 전송하므로 필요. webViewHttpFallback: false로 폴백을 끈 경우에만 생략 가능
script-src에이전트 파일의 출처. 에이전트 파일을 페이지와 같은 출처에 두면 'self'로 충분하고, 다른 출처에 두었다면 그 출처를 추가

CSP에서는 포트가 다르면 호스트 소스를 서로 다른 출처로 취급합니다. 따라서 :9443처럼 포트까지 적으세요. 초기화 스크립트를 인라인으로 작성하는 경우에는 nonce 또는 해시가 필요합니다.

폴백 전송은 POST와 Content-Type: text/plain을 사용하므로 OPTIONS preflight가 없습니다. WAF가 JSON이 아니라는 이유로 요청을 차단하는 규칙이 없는지 확인하세요. 또한 pageLoad, routeChange, resource, onError, event, v2/webVitals, sessionreplay, v2/sessionreplay, rumlog 경로가 허용되어 있는지도 확인하세요.

노트

모바일 에이전트가 네이티브에서 전송하는 데이터는 WebView의 CSP 대상이 아닙니다. 따라서 connect-src에는 모바일 에이전트의 수집 호스트가 아니라 웹 페이지의 proxyBaseUrl 호스트를 허용해야 합니다.

WebView 데이터가 수집되지 않을 때​

증상원인조치
웹뷰 데이터가 어디에도 없어 보임웹 페이지의 pcode를 조회 중앱의 모바일 프로젝트 조회
웹과 네이티브 데이터가 모두 유실initialize() 미호출앱 로그에 WhatapAgent가 초기화되지 않았습니다가 남음. Agent 초기화 확인
데이터는 오지만 일반 브라우저로 분류됨WebView 등록이 페이지 로드보다 늦음등록 후 로드 순서 확인. 브라우저 에이전트는 초기화 시점에 한 번만 WebView 환경을 판별하므로 로드 후에 브릿지가 주입되어도 전송 경로가 바뀌지 않음
데이터가 전혀 수집되지 않음proxyBaseUrl 미설정연동 적용에서도 설정
웹뷰 데이터가 조용히 사라짐setServerUrl이 비어 있음log와 span URL을 직접 지정했더라도 setServerUrl을 채울 것
모든 요청이 404setServerUrl 끝의 /로 경로가 //로 생성됨끝의 / 제거
데이터가 거의 안 옴(약 1%)모바일의 1.0을 웹 sampleRate에 그대로 입력웹은 0 ~ 100 스케일. sampleRate: 100 지정
링크 단계에서 Undefined symbols대문자 V의 WhatapWebViewBridge, WhatapWebViewManager 사용[WhatapIOSAgent setupWebViewBridge:context:] 또는 소문자 v의 WhatapWebviewBridge 사용
cannot find protocol declaration for 'WKNavigationDelegate'WhatapAgent-Swift.h보다 WebKit/WebKit.h가 뒤에 있음헤더 순서 변경. 해당 헤더를 import하는 모든 파일에 적용
커스텀 로그만 수집 안 됨연동 적용에서는 브릿지가 log를 처리하지 않음해당 페이지는 표준 적용으로 유지
불필요한 WebView_* 그룹이 생김viewWillAppear에서 중복 등록등록을 WebView 생성 시점 1회로 고정
앱 로그에 WebView already configured - skipping duplicate해당 WebView에 브릿지가 주입되지 않은 상태등록 대상 WebView 인스턴스 확인

먼저 Xcode Console에서 초기화 로그와 브릿지 연결 로그를 확인하세요. 그런 다음 Safari 웹 인스펙터 콘솔에서 WhatapRUM.VERSION과 WhatapRUM.getAgentContext()?.platform 값을 확인하세요.

정상적으로 연동되면 Mobile 대시보드에서 네이티브 화면과 WebView 페이지가 같은 세션에 시간순으로 나타납니다. WebView 대시보드에서는 페이지 로드, Web Vitals, 리소스 타이밍을 확인할 수 있습니다.

문제 해결​

SDK 초기화 실패​

  • 프로젝트 키와 PCode에 올바른 값이 설정되었는지 확인하세요.
  • 디바이스가 인터넷에 연결되어 있는지 확인하세요.
  • 제공받은 수집 서버 주소가 정확한지 확인하세요. /m으로 끝나야 합니다.
  • build() 뒤에 initialize()를 호출했는지 확인하세요.

데이터가 수집되지 않음​

  • setSamplingRate(1.0)으로 전수 수집되도록 설정되어 있는지 확인하세요.
  • Info.plist의 App Transport Security 설정이 올바른지 확인하세요.
  • 화면별 로딩 시간이 보이지 않는다면 화면 추적을 확인하세요. 자동 추적은 기본값이 꺼짐입니다.
  • 네트워크 데이터가 보이지 않는다면 setCollectNetwork(true)를 지정했는지 확인하세요. 기본값이 false입니다.

크래시 리포트가 전송되지 않음​

  • 크래시는 다음 앱 실행 시 전송됩니다. 앱을 다시 실행해 보세요.
  • 시뮬레이터에서는 일부 크래시가 캡처되지 않을 수 있습니다. 실제 디바이스에서 테스트하는 것을 권장합니다.

빌드 오류​

  • building for iOS x.y, but linking with dylib … 경고는 배포 타깃을 확인하세요.
  • Objective-C 프로젝트의 헤더 관련 오류는 Objective-C 절의 헤더 순서 안내를 확인하세요.

지원 요청​

문제가 발생하면 다음 채널로 문의하세요.

기술 지원을 요청할 때 다음 정보를 함께 전달하면 더 빠르게 해결할 수 있습니다.

  • 프로젝트 키
  • iOS 버전과 Xcode 버전
  • 에이전트 버전
  • 에러 메시지 또는 Xcode Console 로그