"Activity onCreate는 100ms밖에 안 걸리는데, 왜 사용자들은 앱이 느리다고 하는 걸까요?" 이 가이드는 Android 앱의 실제 사용자 경험을 정확하게 측정하고 최적화하는 WhaTap Screen Group의 완전한 활용법을 제공합니다.
금요일 오후, 스프린트 리뷰 미팅에서:
PM: "사용자 리뷰를 보니 '앱이 너무 느리다'는 불만이 많네요."
개발자: "이상한데요? 성능 프로파일러 돌려봤는데
MainActivity onCreate: 95ms
HomeFragment onCreateView: 42ms
모두 정상 범위인데요?"
PM: "그런데 실제 사용자들은 앱 켜고 3초는 기다린다고 하는데..."
개발자: "...🤔"
이 상황, 어디선가 경험해보신 적 있으신가요?
우리는 보통 이렇게 성능을 측정합니다:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val startTime = System.currentTimeMillis()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val duration = System.currentTimeMillis() - startTime
Log.d("Performance", "MainActivity onCreate: ${duration}ms")
// 출력: 100ms ✓
}
}
결과: "100ms! 완벽해!"
하지만 사용자가 실제로 체감하는 것은...
사용자 관점에서 본 실제 타임라인:
[0ms] 앱 아이콘 클릭 👆
[500ms] Splash 화면 표시
[1000ms] MainActivity 시작
[1100ms] HomeFragment 로딩 시작
[1150ms] 레이아웃 인플레이션 완료
[1200ms] API 호출 시작 🌐
[2100ms] 데이터 로딩 완료
[2300ms] 이미지 렌더링 완료
[2500ms] 화면 완전히 사용 가능 ✅
실제 대기 시간: 2.5초!
- 개발자가 측정한 것: 100ms
- 사용자가 느낀 것: 2500ms
25배 차이! 😱
우리가 측정하는 것:
우리가 측정하지 못하는 것:
"개별 컴포넌트의 성능은 좋은데, 전체 사용자 경험은 나쁘다"
이것이 바로 우리가 해결해야 할 과제입니다.
부품 하나하나는 완벽 ✓
하지만 조립된 완성품은? ❌
"부분의 합 ≠ 전체" - 성능 측정에서도 마찬가지입니다.
대부분의 Android APM(Application Performance Monitoring) 도구들은 개별 컴포넌트 단위로 성능을 측정합니다:
// ✅ Activity 측정
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// 측정 시작
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 측정 종료 → 100ms
}
}
// ✅ Fragment 측정
class HomeFragment : Fragment() {
override fun onCreateView(...): View? {
// 측정 시작
val view = inflater.inflate(R.layout.fragment_home, container, false)
// 측정 종료 → 50ms
return view
}
}
// ✅ API 호출 측정
suspend fun loadUserData() {
// 측정 시작
val response = api.getUserInfo()
// 측정 종료 → 200ms
}
✅ MainActivity onCreate: 100ms (정상)
✅ HomeFragment onCreateView: 50ms (정상)
✅ API getUserInfo: 200ms (정상)
→ "모든 지표가 정상입니다!"
사용자 액션: 앱 아이콘 클릭 👆
실제 발생하는 일들:
[0ms] ━━━━ SplashActivity 시작
└─ onCreate: 80ms
└─ 로고 애니메이션: 420ms
└─ 총 500ms
[500ms] ━━━━ MainActivity 시작
└─ onCreate: 100ms
└─ 레이아웃 인플레이션: 50ms
└─ 총 150ms
[650ms] ━━━━ HomeFragment 로딩
└─ onCreateView: 50ms
└─ 뷰 바인딩: 30ms
└─ 총 80ms
[730ms] ━━━━ 데이터 로딩
└─ API 호출: 200ms
└─ 파싱: 50ms
└─ UI 업데이트: 120ms
└─ 총 370ms
[1100ms] ━━━━ 이미지 로딩
└─ 메인 배너: 800ms
└─ 썸네일: 300ms
└─ 총 1100ms
[2200ms] ✅ 화면 완전히 사용 가능
개발자 측정 시점:
├─ Activity.onCreate() 시작 → 끝
└─ 100ms
사용자 체감 시점:
├─ 앱 아이콘 클릭 → 화면 완전 표시
└─ 2200ms
차이: 22배!
// Activity A → Activity B 전환
Activity A:
onPause() // 측정 안 됨
onStop() // 측정 안 됨
↓ [50ms 전환 시간 - 블랙홀!]
Activity B:
onCreate() // 여기부터 측정
전환 과정의 시간은 어디에도 기록되지 않습니다.
class ProductDetailFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Fragment 생성: 50ms 측정 완료 ✓
// 하지만 진짜 로딩은 지금부터 시작!
lifecycleScope.launch {
loadProductInfo() // 200ms
loadProductImages() // 800ms
loadReviews() // 300ms
// 총 1300ms 추가 - 측정 안 됨!
}
}
}
측정 도구 입장:
사용자 입장:
class HybridFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// Fragment 생성: 50ms
webView.loadUrl("<https://shop.example.com/products>")
// WebView 로딩: 1500ms
// 하지만 측정은 Fragment 50ms만 잡힘!
}
}
기존 모니터링 방식으로는 이런 것들을 측정할 수 없습니다:
Splash + Permission + Main + Fragment + Data
= 하나의 "앱 시작"으로 봐야 하는데...
→ 개별 측정만 가능
[회원가입 플로우]
약관동의 → 정보입력 → 인증 → 완료
각 단계는 빠른데, 전체는 왜 느리지?
→ 측정 불가
언제 사용자가 실제로 화면을 쓸 수 있는가?
Fragment 생성: 50ms ✓
하지만 데이터 로딩까지: 1500ms
→ TTI는 1500ms인데 50ms로 기록됨
플래그십: "빠르네요!" (실제 1.2초)
보급형: "느려요" (실제 3.8초)
하지만 개별 측정은 비슷:
플래그십: 80ms
보급형: 120ms
→ 차이가 안 보임
결국 문제는:
개별 컴포넌트는 최적화되어 있다
↓
하지만 전체 사용자 경험은 나쁘다
↓
왜? 측정하지 못하고 있으니까
우리에게 필요한 것:
"사용자가 실제로 기다린 시간"을 측정하는 방법
"개별 컴포넌트가 아닌, 사용자가 실제로 기다린 시간을 측정합니다."
기존 APM 도구들이 개발자 관점에서 측정했다면, WhaTap Screen Group은 사용자 관점에서 측정합니다.
관점의 전환:
[기존 방식]
개발자: "Activity는 100ms, Fragment는 50ms니까 빠르네요!"
[Screen Group]
사용자: "앱 켜고 2.5초 기다렸어요."
Whatap: "네, 정확히 2500ms 측정됐습니다."
사용자가 하나의 화면 로딩으로 인식하는 모든 컴포넌트를 묶어서 측정하는 것입니다.
예시: 앱 시작 화면
사용자 입장:
"앱을 켰다" → "홈 화면이 나왔다"
(이게 하나의 화면 로딩)
실제 내부 구조:
SplashActivity
↓
MainActivity
↓
HomeFragment
↓
API 데이터 로딩
↓
이미지 렌더링
→ 이 모든 과정을 하나의 Screen Group으로 측정
// Whatap Screen Group이 자동으로 측정하는 것:
[앱 시작 Screen Group]
시작: 사용자가 앱 아이콘 클릭
│
├─ [500ms] SplashActivity 표시
├─ [100ms] MainActivity 초기화
├─ [80ms] HomeFragment 생성
├─ [370ms] 초기 데이터 로딩
└─ [1100ms] 이미지 렌더링
종료: 화면이 완전히 사용 가능해진 시점
총 측정 시간: 2150ms
📊 WhaTap 대시보드
Screen Group: "앱 시작"
━━━━━━━━━━━━━━━━━━━━━━━━━
총 로딩 시간: 2150ms
컴포넌트 구성:
- SplashActivity: 500ms (23%)
- MainActivity: 100ms (5%)
- HomeFragment: 80ms (4%)
- 데이터 로딩: 370ms (17%)
- 이미지 렌더링: 1100ms (51%) ← 병목 발견!
핵심: 시간 중첩(Time Overlap) 감지
Screen Group의 핵심 원리는 간단합니다: "화면 로드 시간이 중첩(Overlap)되는 화면들을 하나의 그룹으로 묶는다"
시간축 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━→
0ms 500ms 600ms 680ms 1800ms
Splash [━━━━━━━━━━━━━━━━━]
Main [━━━━━━━━]
Frag [━━━━━]
Data [━━━━━━━━━━━━━━━━]
← 중첩! →← 중첩! →← 중첩! →
├──────────────────────────────────────────────────┤
하나의 Screen Group (총 1800ms)
→ 사용자는 1800ms 동안 "앱이 시작되는 중"으로 인식
시간축 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━→
0ms 1000ms 4000ms 4500ms
Main [━━━━━━━━━━━━]
(3초 공백 - 사용자가 화면 보는 중)
Detail [━━━━━━]
├──────────────────┤ ├──────────┤
Screen Group 1 Screen Group 2
→ Main 끝나고 3초 후에 Detail 시작 (안 겹침!)
→ 사용자: "첫 화면 나왔고, 이제 다른 버튼 눌렀어"
실제 Whatap 서비스에서 Screen Group 데이터를 어떻게 확인하는지 살펴보겠습니다.

리소스 목록에서 SCREENGROUP EVENT 확인:

해당 세션의 전체 활동 타임라인:
사용자 관점에서 전체 여정(Journey) 확인:

Screen Group 내부 구성 확인:
예시:
SplashActivityGroup (총 14.8초)
├─ SplashActivity: 46ms (0.3%)
├─ App Launch (ChainView): 5,729ms (38.7%)
├─ MainActivity: 5,831ms (39.4%)
└─ TestFragment: 941ms (6.4%)

특정 화면을 클릭하면:
실전 활용:
"MainActivity가 느리다"
→ Screen Group 목록에서 MainActivity 포함된 그룹 찾기
→ MainActivity 클릭하여 상세 분석
→ 어떤 작업이 시간을 소비하는지 파악

스레드별 메서드 호출 트리:
실제 케이스:
MainActivity onCreate: 3,740ms
├─ testDepth1: 1,008ms
├─ testDepth2: 908ms
├─ testDepth3: 756ms
├─ testDepth4: 554ms
└─ testDepth5: 302ms
→ 중첩된 메서드 호출이 시간 소비의 원인!
→ 리팩토링 필요
기존: "이 함수는 몇 ms 걸렸나?"
Screen Group: "사용자는 얼마나 기다렸나?"
→ 비즈니스 임팩트를 바로 확인
// 설정 한 줄이면 끝
WhatapAgent.Builder.newBuilder()
.setScreenGroupDelay(2000)
.build(this)
// 이후 모든 화면 전환이 자동으로 추적됨
// 코드 수정 불필요!
// 중요한 플로우는 수동으로 명시
ChainView.getInstance().startChain("checkout-flow", chainId)
// 결제 플로우의 모든 화면을 하나로 측정
장바구니 → 주소입력 → 결제수단 → 완료
ChainView.getInstance().endChain(chainId)
// 전체 결제 소요 시간 측정 완료
📊 Screen Group 상세 분석
홈 화면 로딩: 2150ms
├─ Activity 초기화: 100ms (5%)
├─ Fragment 생성: 80ms (4%)
├─ 레이아웃 인플레이션: 150ms (7%)
├─ API 호출: 370ms (17%)
└─ 이미지 렌더링: 1100ms (51%) ⚠️ ← 여기!
💡 개선 제안:
- 이미지 크기 최적화
- 지연 로딩 적용
- 캐싱 전략 도입
예상 개선 효과: 2150ms → 1200ms (44% 향상)
"설정 한 줄로 자동화할까? 직접 코드로 명시할까?" Whatap Screen Group은 화면을 묶는 두 가지 전략을 제공합니다:
전략 1: Delayed Close (타이머 기반)
"설정한 시간 내 화면 전환을 자동 감지"
전략 2: ChainView (체인 기반)
"중요한 플로우는 직접 연결해서 확실하게"
핵심 아이디어: "마지막 화면이 종료되어도 바로 Screen Group을 닫지 말고, 혹시 다음 화면이 올까봐 조금 기다려보자"
시간축 ━━━━━━━━━━━━━━━━━━━━━━━━━━→
MainActivity 종료 [━━━━━━]
↓
[2-3초 대기 ⏳]
↓
NextActivity 시작 [━━━━━━]
만약 대기 시간 안에 다음 화면이 시작되면?
→ "아, 시간이 중첩되는구나!"
→ 같은 Screen Group으로 묶음 ✅
설정 방법:
// Application 클래스
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
WhatapAgent.Builder.newBuilder()
.setServerUrl("<https://api.whatap.io/mobile>")
.setPCode(YOUR_PROJECT_CODE)
.setProjectKey("YOUR_PROJECT_KEY")
.setScreenGroupDelay(2000) // 2초 대기
.build(this)
}
}
이게 전부입니다! 이후 모든 화면 전환이 자동으로 처리됩니다.
지연 시간 권장값:
📱 일반 앱: 2000ms (2초)
🏪 E-commerce 앱: 3000ms (3초)
🎮 게임 앱: 1000ms (1초)
🏦 금융 앱: 3000-5000ms (3-5초)
장단점:
✅ 장점:
❌ 단점:
"시간 간격이 얼마나 있든, 이 화면들은 무조건 하나의 플로우야!"
시간축 ━━━━━━━━━━━━━━━━━━━━━━━━━━→
ChainView.startChain("app-launch") ← 체인 시작
↓
SplashActivity [━━━━━━]
↓
(2초 경과) ← 권한 체크 중...
↓
PermissionActivity [━━━━━━]
↓
(5초 경과) ← 사용자가 권한 승인 버튼 누름
↓
MainActivity [━━━━━━]
↓
HomeFragment [━━━━━━]
↓
ChainView.endChain() ← 체인 종료
→ 모두 하나의 Screen Group으로 측정 ✅
→ 타이머 방식으로는 불가능 (5초 간격이 있음)
Delayed Close로 해결 안 되는 경우:
[예시 1: 첫 실행 시 앱 시작]
SplashActivity 표시
↓
권한 체크 중... (3초)
↓
초기 데이터 동기화... (5초)
↓
MainActivity 표시
→ 총 8초 간격!
→ Delayed Close (3초)로는 분리됨 ❌
→ 하지만 사용자는 계속 "앱이 시작되는 중"으로 인식 ✅
→ ChainView로 강제로 묶어야 함 ✅
[예시 2: WebView 페이지 로드]
Fragment 생성 (50ms)
↓
WebView 초기화 (100ms)
↓
페이지 요청 중... (5초) ← 네트워크 느림
↓
페이지 렌더링 완료
→ 총 5초 이상 소요
→ Fragment는 150ms만에 "완료"로 기록됨 ❌
→ 하지만 사용자는 페이지 로드까지 기다림 ✅
→ ChainView로 전체를 묶어야 정확 ✅
예시 1: 첫 실행 시 앱 시작 (권한 + 동기화)
// SplashActivity - 앱 시작 체인 시작
class SplashActivity : AppCompatActivity() {
private val chainId = "app-launch-${System.currentTimeMillis()}"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 🔗 앱 시작 체인 시작
ChainView.getInstance().startChain("app-launch", chainId)
// 권한 체크
checkPermissions { granted ->
if (granted) {
// 초기 데이터 동기화
syncInitialData {
// MainActivity로 이동
startActivity(Intent(this, MainActivity::class.java).apply {
putExtra("chain_id", chainId)
})
finish()
}
}
}
}
}
// MainActivity - 앱 시작 체인 종료
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val chainId = intent.getStringExtra("chain_id")
// HomeFragment 로드 완료 후 체인 종료
supportFragmentManager.commit {
replace(R.id.container, HomeFragment())
}
// Fragment 로드 완료 후
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
// 🔚 앱 시작 체인 종료
chainId?.let { ChainView.getInstance().endChain(it) }
}
})
// 결과: Splash → 권한 → 동기화 → Main → Fragment
// 전체가 하나의 "앱 시작" Screen Group으로 측정됨!
}
}
예시 2: WebView 페이지 로드
class WebViewFragment : Fragment() {
private val chainId = "webview-${System.currentTimeMillis()}"
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 🔗 WebView 로드 체인 시작
ChainView.getInstance().startChain("webview-load", chainId)
webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
// 🔚 페이지 로드 완료 시 체인 종료
ChainView.getInstance().endChain(chainId)
// 결과: Fragment 생성 → WebView 초기화 → 페이지 로드
// 전체가 하나의 Screen Group으로 측정됨!
}
}
webView.loadUrl("<https://example.com>")
}
}
장단점:
✅ 장점:
❌ 단점:

📊 의사결정 흐름도
시작
↓
"이 플로우가 중요한 비즈니스 프로세스인가?"
├─ Yes → ChainView 사용
│ (예: 회원가입, 결제, 인증)
│
└─ No → "화면 전환이 빠른가? (2-3초 이내)"
├─ Yes → Delayed Close로 충분
│ (예: 일반 화면 전환)
│
└─ No → ChainView 고려
(예: 사용자가 읽는 시간이 긴 화면)
최고의 전략은 두 방법을 함께 사용하는 것입니다!
// Application 클래스
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 1. 기본 안전망: Delayed Close
WhatapAgent.Builder.newBuilder()
.setScreenGroupDelay(2000) // 2초 자동 대기
.build(this)
// 이제 일반적인 화면 전환은 모두 자동으로 처리됨
}
}
// 2. 중요 플로우만 수동 지정: ChainView
class CheckoutActivity : AppCompatActivity() {
fun startCheckout() {
// 결제는 중요하니까 수동으로 보장
ChainView.getInstance().startChain("checkout", chainId)
}
}
class SignupActivity : AppCompatActivity() {
fun startSignup() {
// 회원가입도 중요하니까 수동으로 보장
ChainView.getInstance().startChain("signup", chainId)
}
}
효과:
📊 측정 커버리지
Delayed Close (자동):
├─ 홈 화면 전환: ✅ 자동 측정
├─ 설정 화면 이동: ✅ 자동 측정
├─ 상품 목록 탐색: ✅ 자동 측정
└─ 기타 모든 일반 화면: ✅ 자동 측정
ChainView (수동):
├─ 회원가입 플로우: ✅ 100% 정확 측정
├─ 결제 플로우: ✅ 100% 정확 측정
└─ 인증 플로우: ✅ 100% 정확 측정
→ 일반 화면 + 중요 플로우 모두 커버! 🎉
"이론은 충분합니다. 이제 바로 시작해봅시다!"
Screen Group을 시작하는 데 필요한 시간: 5분
1단계: Gradle 설정 (2분)
2단계: Application 초기화 (2분)
3단계: 확인 및 테스트 (1분)
// build.gradle (Project level)
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.1.0'
classpath 'io.whatap:whatap-android-plugin:2.1.0' // Whatap Plugin
}
}
// build.gradle (Module: app)
plugins {
id 'com.android.application'
id 'io.whatap.android.plugin' // Whatap Plugin 적용
}
dependencies {
// Whatap Android Agent (BOM 버전 - Screen Group 포함)
implementation 'io.whatap.android:whatap-android-agent:2.1.0'
}
Android Studio 상단의 "Sync Now" 클릭하여 동기화
// MyApplication.kt
package com.example.myapp
import android.app.Application
import io.whatap.android.agent.WhatapAgent
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// Whatap 초기화
WhatapAgent.Builder.newBuilder()
.setServerUrl("YOUR_SERVER_URL") // Whatap에서 발급받은 서버 URL
.setPCode(YOUR_PROJECT_CODE) // Whatap에서 발급받은 프로젝트 코드
.setProjectKey("YOUR_PROJECT_KEY") // Whatap에서 발급받은 프로젝트 키
.setScreenGroupDelay(2000) // 2초 Delayed Close
.build(this)
}
}
Java 버전:
// MyApplication.java
package com.example.myapp;
import android.app.Application;
import io.whatap.android.agent.WhatapAgent;
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// Whatap 초기화
new WhatapAgent.Builder()
.setServerUrl("YOUR_SERVER_URL")
.setPCode(YOUR_PROJECT_CODE)
.setProjectKey("YOUR_PROJECT_KEY")
.setScreenGroupDelay(2000)
.build(this);
}
}
<!-- AndroidManifest.xml -->
<manifest xmlns:android="<http://schemas.android.com/apk/res/android>"
package="com.example.myapp">
<!-- 인터넷 권한 추가 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".MyApplication" <!-- Application 클래스 지정 -->
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<!-- Activities... -->
</application>
</manifest>
WhaTap 프로젝트 정보가 없다면:
.setServerUrl("<https://api.whatap.io/mobile>") // 발급받은 서버 URL
.setPCode(12345) // 숫자로 입력
.setProjectKey("ABCD1234EFGH5678") // 문자열로 입력
# 앱 빌드 및 실행
./gradlew clean assembleDebug
adb install app/build/outputs/apk/debug/app-debug.apk
또는 Android Studio에서 Run ▶️ 클릭
Android Studio Logcat에서 Whatap 초기화 확인:
I/Whatap: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
I/Whatap: Whatap Android Agent initialized
I/Whatap: Project Code: 12345
I/Whatap: Screen Group: ENABLED
I/Whatap: Screen Group Delay: 2000ms
I/Whatap: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
I/ScreenGroupManager: ✅ ScreenGroup initialized
I/ScreenGroupManager: 🔗 Auto tracking enabled
앱에서 여러 화면을 전환하면서 Logcat 확인:
I/ScreenGroupManager: 🎯 ScreenGroup created: MainActivity
I/ScreenGroupManager: ✅ Task started: MainActivity (Active tasks: 1)
I/ScreenGroupManager: 📊 Task started: HomeFragment (Active tasks: 2)
I/ScreenGroupManager: ✅ Task ended: MainActivity (Active tasks: 1)
I/ScreenGroupManager: ⏳ ScreenGroup close scheduled in 2000ms
I/ScreenGroupManager: ⏹️ Scheduled close cancelled - new task added
I/ScreenGroupManager: 📊 Task started: DetailActivity (Active tasks: 2)
📊 Screen Group 목록
Screen Group: MainActivity → HomeFragment → DetailActivity
총 로딩 시간: 1850ms
발생 시간: 2025-10-01 14:23:45
디바이스: Samsung SM-A356N
축하합니다! 이제 모든 화면 전환이 자동으로 측정됩니다.
✅ Delayed Close로 자동 측정 중
✅ 화면 전환 시간 자동 기록
✅ Screen Group 자동 생성
✅ Whatap 대시보드에서 실시간 확인 가능
WebView를 사용하나요? WebView를 사용하는 앱이라면 다음 섹션에서 WebView 성능 측정을 확인하세요!
일반 Fragment: 50ms (빠름 ✅)
WebView Fragment: 3.5초 (느림! ⚠️)
→ WebView 페이지 로딩까지 포함한 정확한 측정이 필요합니다
WebView는 일반 화면과 다르게 측정되며, Browser Agent와 Bridge를 통해 Native와 Web 성능을 하나의 Screen Group으로 통합 측정할 수 있습니다.
"Native와 Web을 하나의 Screen Group으로 측정합니다"
많은 앱들이 WebView를 사용합니다:
문제는:
개발자: "WebViewFragment onCreate는 50ms예요. 빠르죠?"
사용자: "페이지 보는데 3초 넘게 걸렸어요..."
→ 웹페이지 내부의 성능은 어떻게 측정하나요?
WhatTap의 답:
✅ Native: Android Agent가 측정
✅ Web: Browser Agent가 측정
✅ Bridge: 두 데이터를 하나의 Screen Group으로 통합!
[Native 영역] [Web 영역]
Android Agent가 측정 ✅ ??? 측정 불가 ❌
WebViewFragment
├─ onCreate: 30ms
├─ onViewCreated: 20ms
└─ loadUrl() 호출
│
└─→ [WebView 내부]
├─ HTML 파싱: ???
├─ CSS 적용: ???
├─ JavaScript 실행: ???
├─ 이미지 로딩: ???
└─ 렌더링 완료: ???
→ Fragment는 50ms (빠름)
→ 웹페이지는 ??? (모름)
→ 사용자는 3초 기다림 (느림!)
[Native 영역] [Web 영역]
Android Agent ✅ Browser Agent ✅
↓
WebViewFragment <script>
├─ onCreate: 30ms whatap.browser.js
├─ onViewCreated: 20ms </script>
├─ Bridge 설정 ↓
└─ loadUrl() 웹페이지 성능 측정:
│ ├─ HTML 파싱: 300ms
│ ├─ CSS 적용: 200ms
│ Bridge 통신 ←→ ├─ JavaScript: 500ms
│ ├─ 이미지: 1200ms
│ └─ 렌더링: 400ms
│
└─→ Screen Group 통합!
총 2650ms = Native(50ms) + Web(2600ms)
1. 웹페이지에 Browser Agent 스크립트 추가
2. Native에서 WebView Bridge 설정
3. Browser Agent 데이터가 Android Agent로 전달
4. 하나의 Screen Group으로 통합 측정!
웹페이지(HTML)에 Whatap Browser Agent 스크립트를 추가합니다:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Whatap WebView Test</title>
<!-- Whatap Browser Agent -->
<script>
(function (w, h, _a, t, a, b) {
w = w[a] = w[a] || {
config: {
projectAccessKey: "YOUR_BROWSER_PROJECT_KEY",
pcode: YOUR_BROWSER_PROJECT_CODE,
sampleRate: 100,
proxyBaseUrl: "<https://rumote.whatap-mobile-agent.io/>",
agentError: true,
isWebView: true, // WebView 환경 필수 설정
xhrTracing: true
},
};
a = h.createElement(_a);
a.async = 1;
a.src = 'whatap-browser-agent.js'; // 또는 CDN URL
t = h.getElementsByTagName(_a)[0];
t.parentNode.insertBefore(a, t);
})(window, document, 'script', 'whatap-browser-agent.js', 'WhatapBrowserAgent', '');
</script>
<style>
body { font-family: sans-serif; padding: 2rem; }
img { max-width: 100%; margin-top: 2rem; }
</style>
</head>
<body>
<h1>🧪 WebView + Browser Agent Test</h1>
<p>This page loads resources and sends events to Whatap.</p>
<button onclick="trackClick()">Track Button Click</button>
<button onclick="sendXhr()">Send XHR</button>
<button onclick="sendFetch()">Send Fetch</button>
<script>
function trackClick() {
console.log("Button clicked!");
}
function sendXhr() {
const xhr = new XMLHttpRequest();
xhr.open("GET", "<https://jsonplaceholder.typicode.com/todos/1>");
xhr.onload = function () {
console.log("XHR 완료:", xhr.responseText);
};
xhr.send();
}
function sendFetch() {
fetch("<https://jsonplaceholder.typicode.com/todos/2>")
.then(res => res.json())
.then(json => console.log("Fetch 완료:", json));
}
// 페이지 로드 중 자동 XHR 발생
(function() {
const xhr = new XMLHttpRequest();
xhr.open("GET", "<https://jsonplaceholder.typicode.com/todos/1>");
xhr.onload = function () {
console.log("초기 XHR 완료:", xhr.responseText);
};
xhr.send();
})();
</script>
<!-- Heavy image resource -->
<img src="<https://picsum.photos/800/400>" alt="Sample Image" />
</body>
</html>
MainActivity에서 Fragment를 띄우고, Fragment 내부에 WebView를 설정합니다:
// MainActivity.kt
class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Fragment 추가
val fragment = WebViewFragment()
supportFragmentManager.beginTransaction()
.replace(android.R.id.content, fragment)
.commit()
}
}
// WebViewFragment.kt
class WebViewFragment : Fragment() {
private lateinit var webView: WebView
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.fragment_webview, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
webView = view.findViewById(R.id.webView)
// 🔗 Whatap Bridge 설정 (핵심!)
val bridge = WhatapWebviewBridge(requireContext())
bridge.configureWebView(webView)
bridge.startDataUploadTimer()
// WebView 기본 설정
webView.settings.apply {
javaScriptEnabled = true // Browser Agent에 필수
domStorageEnabled = true
cacheMode = WebSettings.LOAD_DEFAULT
}
// WebViewClient 설정
webView.webViewClient = WhatapWebViewClient(bridge, object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
Log.d("WebView", "페이지 로드 완료: $url")
}
})
// 페이지 로드
// 개발 환경: localhost:18000 (PC에서 웹서버 실행)
webView.loadUrl("<http://10.0.2.2:18000/>")
// 또는 실제 서비스 URL
// webView.loadUrl("<https://yourdomain.com>")
}
}
이제 Native와 Web 성능이 하나의 Screen Group으로 측정됩니다!
📊 Whatap Screen Group 분석 결과
WebViewFragment 로딩
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
총 로딩 시간: 2850ms
[Native 영역] - Android Agent 측정
├─ [0-30ms] Fragment onCreate (1%)
├─ [30-50ms] Fragment onViewCreated (1%)
└─ [50-150ms] WebView 초기화 (3%)
[Web 영역] - Browser Agent 측정 (Bridge 전달)
├─ [150-450ms] HTML 다운로드 및 파싱 (11%)
├─ [450-750ms] CSS 적용 (11%)
├─ [750-1250ms] JavaScript 실행 (18%)
├─ [1250-2450ms] 이미지 로딩 (42%) ← 병목!
└─ [2450-2850ms] 렌더링 완료 (14%)
[2850ms] 전체 로딩 완료
💡 통합 인사이트:
✅ Native Fragment: 150ms (5%)
✅ Web 페이지: 2700ms (95%)
✅ 실제 병목: 이미지 로딩 (42%)
→ 웹 이미지 최적화가 핵심!
WhaTap 콘솔에서 Screen Group을 확인하면 Native와 Web이 하나로 통합된 것을 볼 수 있습니다:

화면 설명:
TestFragment와 http://192.168.1.31:18000/ 두 컴포넌트가 하나의 Screen Group으로 묶임
Screen Group에서 WebView 컴포넌트를 클릭하면 Browser Agent가 측정한 웹 성능을 상세히 볼 수 있습니다:

Browser Agent (Web) Bridge Android Agent (Native)
↓ ↓ ↓
성능 데이터 측정 → JavaScript Interface → Screen Group 통합
"이제 사용자가 보는 것을 개발자도 볼 수 있습니다"
Activity A: 100ms ✅
Fragment B: 50ms ✅
Activity C: 80ms ✅
→ 그래서 사용자 경험은? ❓
→ 전체 플로우는 얼마나 걸렸나? ❓
→ 병목은 어디인가? ❓
Screen Group: 사용자 플로우 전체
├─ Activity A: 100ms (43%)
├─ Fragment B: 50ms (22%)
└─ Activity C: 80ms (35%)
총 230ms
→ 사용자가 느끼는 시간: 230ms ✅
→ 병목: Activity A (43%) ✅
→ 최적화 우선순위: 명확! ✅
// 개발자가 보는 것
Activity onCreate: 50ms
Fragment onViewCreated: 30ms
Network request: 200ms
// 사용자가 느끼는 것
"상품 상세 페이지 보는데 280ms 걸렸네"
→ Screen Group = 사용자 관점!
Native Fragment: 50ms
+ Web 페이지 로딩: 2500ms
─────────────────────────
= 사용자 대기 시간: 2550ms
→ WebView도 Screen Group으로 측정!
// 대부분: 자동으로 측정
MainActivity → ProductFragment → CartActivity
→ Screen Group 자동 생성 ✅
// 필요시: 수동으로 세밀하게 제어
WhatapScreenGroup.startTask("checkout_flow")
// ... 복잡한 비즈니스 로직
WhatapScreenGroup.endTask("checkout_flow")
→ 정확한 측정 ✅
// ✅ 자동 측정에 적합 (Delayed Close로 충분)
- 앱 시작 → 메인 화면
- 화면 전환 (리스트 → 상세)
- 탭 전환
// 🔗 수동 측정 필요 (ChainView 권장)
- 로그인 플로우 (입력 대기 시간 있음)
- 결제 플로우 (여러 단계, 사용자 입력)
- 회원가입 (약관 동의, 정보 입력 등)
// ⏸️ 나중에 측정해도 되는 것
- 설정 화면
- 도움말 페이지
- 기타 부가 기능
WhatapAgent.build(this)
.setScreenGroupCloseDelayMs(5000) // 기본: 5초
.start()
// 빠른 플로우: 3초
// 느린 플로우: 10초
// WebView 포함: 7-10초
왜 중요한가?
// ❌ 나쁜 예
WhatapScreenGroup.startTask("task1")
WhatapScreenGroup.startTask("process")
// ✅ 좋은 예
WhatapScreenGroup.startTask("user_registration_form")
WhatapScreenGroup.startTask("payment_3ds_verification")
WhatapScreenGroup.startTask("image_upload_to_s3")
// 사용자 정보 추가
WhatapExtra.getMap().put("user_id", userId)
WhatapExtra.getMap().put("user_level", userLevel)
// 비즈니스 컨텍스트 추가
WhatapExtra.getMap().put("order_amount", totalAmount)
WhatapExtra.getMap().put("product_category", category)
WhatapExtra.getMap().put("payment_method", paymentMethod)
// 결과: 성능 데이터와 비즈니스 데이터를 함께 분석!
✅ 사용자 관점: 개별 화면이 아닌, 전체 플로우 측정
✅ 자동화: 대부분 추가 코드 없이 자동 측정
✅ 통합: Native + Web 완전 통합 측정
✅ 실용성: 실제 병목 지점 즉시 파악
✅ 확장성: 간단한 설정부터 고급 커스터마이징까지
사용자가 보는 것 = 개발자가 측정하는 것Happy Monitoring! 여러분의 앱이 더 빨라지길 바랍니다! 🚀