Jank는 개별 프레임이 누락되어 인터페이스 애니메이션에서 눈에 띄는 끊김이나 “멈칫”을 나타내는 용어입니다. 모바일 앱에서 Jank는 프레임 렌더링 시간이 디스플레이 재생률에 의해 할당된 예산을 초과할 때 발생합니다. Android Developers, 2025에 따르면, Jank는 “느림”에 대한 주관적 느낌의 주요 원인입니다 — 앱이 기능적으로 완벽할지라도 사용자는 불안정한 FPS로 인해 앱을 느리게 인식합니다.
핵심 요약
Jank는 컴퓨터 그래픽스 용어로, 애니메이션이 부드럽게 움직이지 않고 덜컹거리는 시각적 결함을 의미합니다. 모바일 개발에서 Jank는 단위 시간당 누락된 프레임 수(skipped frames)로 측정됩니다. 시스템이 VSync 시점까지 프레임을 준비하지 못하면 디스플레이가 이전 프레임을 반복합니다 — 60Hz에서 16.6ms의 일시 중지가 발생합니다. 한 번의 프레임 누락은 눈에 띄지 않을 수 있지만, 3~5회 연속 프레임 누락은 50~80ms 동안 지속되는 “끊김” 느낌을 만들어 사용자가 명확히 인지합니다.
Jank는 일정한 속도로 작동해야 하는 애니메이션(피드 스크롤, 메뉴 열기 애니메이션, 패럴랙스 효과, 화면 전환)에 특히 중요합니다. Google UX 연구(2024)에 따르면, 스크롤 세션의 3% 이상에서 Jank 비율이 발생하는 앱은 0.5% 미만인 앱보다 22% 더 많은 1성 리뷰를 받습니다. Android Vitals 도구는 Jank를 자동으로 추적하고 심각도(보통, 심각, 치명적)별로 분류합니다.
Jank의 원인은 여러 범주로 나뉩니다. 첫째는 Layout Jank: View 크기 변경, LayoutTransition 애니메이션 또는 동적 콘텐츠 로딩으로 인한 빈번한 requestLayout() 호출로 발생합니다. 각 requestLayout 호출은 전체 View 하위 트리에 대해 Measure + Layout을 트리거하여 5~30ms가 소요될 수 있습니다. 둘째는 Draw Jank: overdraw와 무거운 drawable 사용과 관련됩니다. 셋째는 Thread Jank: 동기 작업(파일 로딩, 메인 스레드의 데이터베이스 작업, Bitmap 디코딩)으로 인한 메인 스레드 차단입니다.
네 번째 범주는 GC Jank: ART/Dalvik 또는 Swift ARC의 가비지 컬렉션(Garbage Collection)입니다. 힙에 많은 객체가 축적되면 GC가 5~15ms의 Stop-The-World 일시 중지를 시작합니다. Android에서 GC 일시 중지는 루프 내의 빈번한 할당(onDraw()에서 객체 생성, 어댑터에서 할당, 사용되지 않는 람다 표현식) 중에 가장 자주 발생합니다. 다섯째는 IPC Jank: 메인 스레드에서의 프로세스 간 통신(ContentProvider, Binder)입니다. 여섯째는 Rendering Jank: 최적화되지 않은 셰이더나 큰 텍스처로 인한 느린 GPU 렌더링입니다.
| Jank 유형 | 원인 | 일반적인 지속 시간 | 감지 도구 |
|---|---|---|---|
| Layout | requestLayout, relayout | 5~30ms | Perfetto, Systrace |
| Draw | Overdraw, 무거운 drawable | 3~20ms | GPU Profiling |
| Thread | 메인 스레드 차단 | 10~200ms | Android Studio Profiler |
| GC | Garbage Collection | 5~15ms | Memory Profiler |
| Rendering | GPU 부하 | 10~50ms | GPU Tracer, Xcode GPU |
Android에서 Jank 진단은 Perfetto 시스템 트레이스로 시작됩니다. Perfetto는 모든 스레드, CPU, GPU 및 스케줄러의 활동을 기록합니다. Jank의 명확한 지표는 Choreographer.doFrame 및 Choreographer.doCallbacks 라인입니다: 두 연속 doFrame 호출 사이의 간격이 16.6ms를 초과하면 프레임이 누락된 것입니다. Perfetto는 정확한 원인(어떤 시스템 호출, 잠금 또는 GC가 지연을 일으켰는지)을 보여줍니다. Android Studio Profiler에서는 CPU Profiler를 통해 유사한 기능을 사용할 수 있습니다.
프로덕션에서 자동 Jank 감지를 위해 FrameMetricsAggregator가 사용됩니다 — 각 프레임에 대한 통계를 수집하고 세션별로 집계하는 API입니다. Android 12+에는 PerformanceHintManager가 도입되었습니다 — 시스템에 목표 프레임 속도를 힌트하는 API입니다. 앱이 120 FPS 시나리오에서 실행 중임을 표시하면 시스템이 Jank를 방지하기 위해 CPU/GPU 주파수를 높일 수 있습니다. 모든 누락된 프레임을 간단히 로깅하려면 Choreographer.FrameCallback을 구독하면 충분합니다.
Kotlin 코드는 Choreographer.FrameCallback을 구독하고 지연 시간과 함께 각 누락된 프레임을 로깅합니다. 콜백은 각 VSync에서 호출됩니다.
class JankDetector {
private val frameBudget = 16_666_666L
private var previousFrameTime = 0L
private val callback =
Choreographer.FrameCallback { currentTime ->
if (previousFrameTime != 0L) {
val frameDuration =
currentTime - previousFrameTime
val skippedFrames =
(frameDuration / frameBudget) - 1
if (skippedFrames > 0) {
Log.w("Jank",
"Skipped $skippedFrames frames")
}
}
previousFrameTime = currentTime
Choreographer.getInstance()
.postFrameCallback(this)
}
fun start() {
Choreographer.getInstance()
.postFrameCallback(callback)
}
}
iOS에서 Jank 진단은 Core Animation 템플릿과 함께 Instruments를 통해 수행됩니다. Instruments는 실시간 FPS, 오프스크린 렌더링 수 및 히트 테스트를 보여줍니다. iOS에서 Jank의 주요 지표: Core Animation 타임라인의 빨간색 막대(프레임 예산 초과), 높은 Renderer 값(오프스크린 렌더링 표시), 낮은 FPS입니다. 프로덕션 모니터링을 위해 MetricKit은 평균 FPS, P50 및 P95 프레임 시간을 포함하는 MXAnimatoryMetric 메트릭으로 보고서를 수집합니다.
iOS의 기본 Jank 진단에는 타임스탬프 및 targetTimestamp 확인이 포함된 CADisplayLink가 포함됩니다. 현재 타임스탬프가 targetTimestamp보다 크게 뒤처지면 하나 이상의 프레임이 누락된 것입니다. Apple은 또한 사용자 정의 프로파일링을 위해 os_signpost 사용을 권장합니다: 프레임 렌더링 시작과 끝에 signpost-interval을 배치하고 Instruments에서 16.6ms를 초과하는 간격을 확인합니다. SwiftUI에서 Jank 진단에는 UIView.invalidateIntrinsicContentSize가 사용됩니다 — 이 메서드의 빈번한 호출은 불안정한 Layout을 나타냅니다.
Swift 코드는 CADisplayLink를 통해 누락된 프레임을 감지합니다. 타임스탬프와 targetTimestamp의 차이가 16.6ms를 초과하면 Jank가 기록됩니다.
class JankMonitor {
private var displayLink: CADisplayLink?
private var totalJank = 0
func start() {
displayLink = CADisplayLink(
target: self,
selector: #selector(detectJank)
)
displayLink?.add(to: .current,
forMode: .common)
}
@objc
private func detectJank() {
guard let link = displayLink else { return }
let delay = link.targetTimestamp
- link.timestamp
if delay > 0.0167 {
totalJank += 1
}
}
}
Jank 프로파일링에는 내장 OS 도구와 타사 SDK가 모두 사용됩니다. Android에서 핵심 도구는 Perfetto입니다(Systrace를 대체). Perfetto는 최대 30초 동안 트레이스를 기록하고 웹 인터페이스 ui.perfetto.dev를 통해 분석할 수 있습니다. Choreographer 활동, 렌더링 스레드(RenderThread) 및 GPU를 포함한 정확한 타임라인을 표시합니다. GPU 문제의 상세 분석을 위해 AGI(Android GPU Inspector)가 사용되며, 프레임 시간뿐만 아니라 특정 GPU 블록(셰이더, 래스터라이저, 텍스처 유닛)의 부하도 표시합니다.
iOS에서는 Core Animation, Metal System Trace 및 GPU Driver 템플릿과 함께 Instruments가 그에 해당합니다. Core Animation은 FPS와 프레임 시간을 표시하고, Metal System Trace는 각 드로우 콜까지 GPU 작업을 표시합니다. 부하가 걸린 실제 기기에서의 프로파일링을 위해 Firebase Performance(Screen Rendering 메트릭 수집) 및 Sentry(Jank 시 스택 트레이스 캡처)가 사용됩니다. 새로운 Android 15 Performance Hint API를 통해 개발자는 시스템에 어떤 프레임이 중요한지 알리고 Jank가 임박했을 때 시스템으로부터 경고를 받을 수 있습니다.
Kotlin 코드는 세션 동안 프레임 통계를 수집하기 위해 FrameMetricsAggregator를 사용합니다. 집계기를 중지한 후 누락된 프레임 수가 출력됩니다.
class JankAggregator(private val activity: Activity) {
private val aggregator = FrameMetricsAggregator()
fun startCollection() {
aggregator.add(activity.window)
}
fun stopAndReport() {
aggregator.remove()
val result = aggregator.getMetrics()
val totalFrames = result
?.get(FrameMetrics.TOTAL_DURATION)
?.size ?: 0
val jankFrames = result
?.get(FrameMetrics.TOTAL_DURATION)
?.count { it > 16_666_666L} ?: 0
Log.d("JankReport",
"Jank ratio: \${jankFrames * 100 / totalFrames}%")
}
}
Jank를 제거하려면 유형에 따른 기술 조합이 필요합니다. Layout Jank의 경우: 깊은 계층 구조를 ConstraintLayout/Compose/SwiftUI로 대체하고, merge 태그를 사용하며, 애니메이션에서 requestLayout을 피합니다. Draw Jank의 경우: Debug GPU Overdraw를 사용하여 4x+ overdraw를 찾고, 무거운 drawable을 벡터 그래픽(VectorDrawable/PDF)으로 대체하며, 하드웨어 레이어는 주의해서 사용합니다 — 렌더링은 가속화하지만 GPU 메모리를 더 많이 소비합니다. Thread Jank의 경우: 모든 I/O 작업, 데이터베이스 작업 및 Bitmap 디코딩을 백그라운드 스레드로 이동하고, 올바른 Dispatcher와 함께 Kotlin Coroutines 또는 Schedulers.io()와 함께 RxJava를 사용합니다.
GC Jank의 경우: onDraw() 및 getView()에서 할당을 최소화하고, 객체 풀(ObjectPool)을 사용하며, for-each를 인덱스 for로 대체하고, Kotlin에서 copy()와 함께 불변 data class를 주의해서 사용합니다 — copy는 새 객체를 생성합니다. IPC Jank의 경우: App Startup을 통해 ContentProvider를 지연 초기화하고, Binder 호출을 백그라운드 스레드로 이동합니다. Rendering Jank의 경우: 텍스처 크기를 최대 화면 해상도로 줄이고, ASTC 또는 ETC2 압축을 사용하며, 과도한 셰이더 컴파일을 피합니다(셰이더를 미리 컴파일). 포괄적인 솔루션은 CI에서 정기적인 Perfetto/Instruments 프로파일링과 Jank 회귀 추적입니다.
Kotlin 코드는 reportFullyDrawn 후 화면에 데이터를 비동기적으로 로드하여 무거운 작업이 첫 번째 프레임을 차단하지 않도록 합니다. 콜백은 사용자가 인터페이스를 본 후에 호출됩니다.
class JankSafeLoader {
suspend fun loadAfterFirstFrame(
activity: Activity
) {
// 첫 번째 프레임이 이미 렌더링되었음을 보장
if (Build.VERSION.SDK_INT >= 29) {
activity.reportFullyDrawn()
}
// 무거운 로딩 — 첫 번째 프레임 이후
withContext(Dispatchers.IO) {
val data = fetchHeavyData()
withContext(Dispatchers.Main) {
updateUI(data)
}
}
}
}
자주 묻는 질문
Jank는 누락된 렌더링 프레임으로, 애니메이션에서 눈에 띄는 끊김이나 덜컹거림으로 나타납니다. 프레임 준비 시간이 시간 예산(60 FPS에서 16.6ms)을 초과할 때 발생합니다.
Layout Jank(빈번한 requestLayout), Draw Jank(overdraw), Thread Jank(메인 스레드 차단), GC Jank(가비지 컬렉션), IPC Jank(Binder 호출) 및 Rendering Jank(무거운 셰이더)입니다.
시스템 트레이싱에는 Perfetto, 프레임 단계 분석에는 GPU Profiling, 프로덕션 모니터링에는 FrameMetricsAggregator를 사용합니다. Android Studio에서는 — Deep Java Trace와 함께 CPU Profiler를 사용합니다.
Instruments를 통해 Core Animation 또는 Metal System Trace 템플릿을 사용합니다. 프로덕션의 경우 — MXAnimatoryMetric과 함께 MetricKit을 사용합니다. 프로그래밍 방식으로는 — timestamp와 targetTimestamp의 차이를 확인하는 CADisplayLink를 사용합니다.
Google에 따르면, 스크롤 세션의 3%를 초과하는 Jank 비율(스크롤 100회 중 3회에서 끊김 발생)은 부정적 리뷰가 22% 증가합니다. 목표 비율은 스크롤 세션의 0.5% 미만입니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.