개발에서의 프리즈 — 본질, 원인 및 예방

저자: IT Sectr 게시일: 2026-07-28 읽는 시간: 9 분

프리즈(행)는 모바일 애플리케이션이 장시간 동안 사용자의 어떤 동작에도 응답하지 않는 상태입니다. 랙(느려짐) 및 글리치(잘못된 동작)와 달리 프리즈는 UI를 완전히 차단합니다: 터치가 처리되지 않고, 애니메이션이 멈추며, 화면이 “얼어붙습니다”. 원인은 동기 작업에 의한 메인 스레드 차단, 멀티스레드 코드의 데드락, 또는 비정상적으로 긴 가비지 컬렉션입니다. Apple Main Thread Checker 문서에 따르면 iOS 크래시 리포트의 40% 이상이 메인 스레드 차단과 관련됩니다. Android에서는 유사한 상황이 ANR — 시스템 대화상자 “앱이 응답하지 않습니다” — 로 이어집니다.

핵심 사항

  • 프리즈 — 장시간(초에서 수십 초) 동안 UI가 완전히 차단되는 상태, 랙 및 글리치와 다름
  • 주요 원인 — I/O에 의한 메인 스레드 차단, 스레드 간 데드락, 무한 루프, 긴 GC가 있는 메모리 누수
  • 진단에는 iOS의 Main Thread Checker, Android의 ANR 로그 /data/anr/traces.txt 및 스레드 덤프 분석이 포함됨
  • 해결 — 잠재적으로 긴 모든 작업을 백그라운드 스레드로 오프로드, Structured Concurrency 사용 및 UI 스레드에서 synchronized 회피
  • 예방 — StrictMode, Debug 스키마의 Main Thread Checker, 데드락 정적 분석 및 응답 시간 측정 정기 테스트

모바일 개발에서 프리즈란 무엇인가

프리즈(행)는 모바일 애플리케이션에서 앱이 몇 초 이상 입력 이벤트 처리 및 인터페이스 업데이트를 중단하는 상태입니다. 기술적으로 이는 메인 스레드가 차단되어 다음 런 루프 반복을 실행할 수 없음을 의미합니다.

프리즈, 랙 및 ANR의 차이

랙은 최대 500ms의 지연으로 사용자가 느려짐을 인지하지만 앱은 계속 작동합니다. 프리즈는 1초에서 수십 초까지 지속됩니다. Android의 ANR은 5초 이상 지속되어 시스템이 감지한 프리즈의 특수한 경우입니다. 모든 프리즈가 ANR로 이어지지는 않지만, 모든 ANR은 시스템이 기록한 프리즈입니다.

프리즈의 결과

Android에서는 5초 이상의 프리즈가 ANR 대화상자를 트리거하여 앱 종료를 제안합니다. iOS에서는 시스템에 워치독이 있습니다 — 앱이 10–20초 동안 이벤트에 응답하지 않으면 워치독이 코드 0x8badf00d(ate bad food)로 프로세스를 종료합니다. 사용자는 앱이 갑자기 홈 화면으로 닫히는 것만 볼 수 있습니다.

Android 및 iOS에서 프리즈의 원인

100ms 이상 걸리고 메인 스레드에서 실행되는 모든 작업은 잠재적으로 프리즈를 유발할 수 있습니다. 차단의 주요 원인을 살펴보겠습니다.

UI 스레드의 동기 I/O

대용량 파일 읽기, 비동기 없는 네트워크 요청, 동기 메서드 apply와 commit을 통한 SharedPreferences 데이터 저장 — 이러한 모든 작업은 메인 스레드를 차단합니다. Android에서 10MB 파일을 동기적으로 읽는 데는 플래시 메모리 속도에 따라 200–500ms가 걸릴 수 있습니다. iOS에서 completionHandler 없는 동기 URLSession 로드는 서버 응답 시간 동안 UI를 차단합니다.

멀티스레드 코드의 데드락

두 스레드가 서로가 보유한 리소스를 기다릴 때 데드락이 발생합니다. 모바일 애플리케이션의 일반적인 시나리오는 스레드 A가 Lock1을 잠그고 Lock2를 기다리는 반면, 스레드 B가 Lock2를 잠그고 Lock1을 기다리는 것입니다. 두 스레드 모두 영원히 프리즈됩니다. 그중 하나가 메인 스레드이면 애플리케이션이 완전히 프리즈됩니다.

무한 루프 또는 재귀

로직 오류 — 예를 들어 종료 조건 없는 while(true) 또는 기본 케이스 없는 재귀 — 는 메인 스레드에서 무한 실행으로 이어집니다. Android는 5초 후 ANR을 통해 이를 감지하고, iOS는 무한히 반복되는 호출 스택을 캡처하는 Stackshot을 통해 감지합니다.

  • Android — 닫히지 않은 Cursor, enqueue() 대신 execute()를 통한 동기 요청, UI 스레드의 FileInputStream.read()
  • iOS — performSelectorOnMainThread:withObject:waitUntilDone:YES, NSURLConnection sendSynchronousRequest 실행, dataWithContentsOfURL로 이미지 로드
  • 크로스 플랫폼 — 전용 isolate 없는 Flutter compute, 동기 React Native NativeModule

프리즈 진단 방법

프리즈 진단에는 차단 순간에 모든 스레드의 상태를 캡처할 수 있는 도구가 필요합니다.

Android의 ANR 로그

각 ANR 시 Android 시스템은 각 앱 스레드의 스택 덤프가 포함된 파일 /data/anr/traces.txt를 저장합니다. 이 파일 분석이 주요 진단 방법입니다: main 스레드를 찾아 어떤 메서드에서 중지되었는지 확인합니다. 스택이 Thread.sleep, InputStream.read 또는 Lock.lock으로 끝나면 — 원인을 찾은 것입니다.

iOS의 Stackshot

Xcode는 앱이 프리즈될 때(SIGSTOP 신호) Stackshot — 모든 스레드 스택의 스냅샷 — 을 찍을 수 있습니다. 스키마에서 “Logging” → “Include Stackshot Logs”를 활성화합니다. 코드 0x8badf00d로 크래시가 발생하면 Devices & Simulators에서 크래시 로그를 추출하고 스택이 멈춘 com.apple.main-thread를 찾습니다.

Xcode의 Main Thread Checker

Main Thread Checker는 앱 실행 중 백그라운드 스레드에서 UIKit 호출을 자동으로 감지합니다. 스키마에서 활성화합니다(Diagnostics → Main Thread Checker). 각 경고는 프리즈의 잠재적 원인이며, 특히 네트워크 요청 completionHandler 클로저에서 발생하는 경우입니다.

Android에서 StrictMode를 통한 차단 감지 예:

kotlin
class App : Application() {
    override fun onCreate() {
        super.onCreate()
        StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
            .detectLeakedSqlLiteObjects()
            .detectLeakedClosableObjects()
            .penaltyLog()
            .build())
        StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
            .detectAll()
            .penaltyDeath()
            .build())
    }
}

UI 차단 제거 방법

프리즈 제거는 잠재적으로 긴 모든 작업을 백그라운드 스레드로 이동하는 것부터 시작됩니다. 각 플랫폼의 구체적인 기술을 살펴보겠습니다.

코루틴을 사용한 구조적 동시성

Kotlin Coroutines를 viewModelScope.launch(Dispatchers.IO)와 함께 사용하면 네트워크 작업이나 데이터베이스 읽기가 백그라운드 스레드에서 실행됩니다. Dispatchers.Main은 UI 업데이트에만 사용됩니다. 중요: 모든 suspend 함수는 구조화되어야 합니다 — 하위 코루틴은 상위가 취소될 때 취소되어 스레드 누수를 방지합니다.

iOS의 비동기 큐

Grand Central Dispatch에서 백그라운드 작업에는 DispatchQueue.global(qos: .userInitiated), UI 업데이트에는 DispatchQueue.main.async를 사용하는 것이 표준 패턴입니다. 메인 큐에서 sync()를 피하세요 — 이것은 보장된 데드락입니다. MainActor를 통해 메인 스레드로 자동 복귀하는 async/await(Swift 5.5+)를 사용하여 더 읽기 쉬운 비동기 코드를 작성하세요.

UI 스레드에서 synchronized 회피

메인 스레드에서 Kotlin의 synchronized 블록과 Swift의 @synchronized는 위험합니다: 다른 스레드가 이미 이 잠금을 획득한 경우 메인 스레드가 대기 중 프리즈됩니다. 잠금 대신 원자적 유형(AtomicInteger, Swift의 원자적 속성) 또는 직렬 큐를 사용하세요.

Android에서 코루틴을 사용한 비동기 데이터 로딩 예:

kotlin
class DataViewModel : ViewModel() {
    private val _data = MutableStateFlow<List<Item>>(emptyList())
    val data: StateFlow<List<Item>> = _data.asStateFlow()

    fun loadData() {
        viewModelScope.launch(Dispatchers.IO) {
            val result = fetchFromNetwork()
            _data.emit(result)
        }
    }
}

개발 단계에서 프리즈 예방

도구, 아키텍처 원칙 및 코드 리뷰 프로세스의 조합이 프리즈를 체계적으로 예방하는 데 도움이 됩니다.

penaltyDeath를 사용한 StrictMode

스레드 정책에 penaltyDeath를 사용하여 StrictMode를 구성하면 메인 스레드에서 네트워크 호출 또는 디스크 I/O가 감지될 때 즉시 앱이 크래시됩니다. 개발자는 문제를 무시할 수 없습니다. 프로덕션 빌드에서는 크래시 없이 통계를 수집하기 위해 penaltyLog를 사용하세요.

Debug 스키마의 Main Thread Checker

iOS에서 Debug 스키마에서 Main Thread Checker를 활성화하고 CI가 이 옵션으로 테스트를 실행하도록 구성합니다. 테스트에 백그라운드 스레드의 UIKit 호출이 포함된 경우 실패해야 합니다. TestFlight에 보내기 전에 문제를 식별하는 유일한 신뢰할 수 있는 방법입니다.

멀티스레딩 확인을 포함한 피어 리뷰

코드 리뷰 프로세스에 필수 항목을 추가합니다: 모든 네트워크 호출, 파일 작업, 데이터베이스 액세스 또는 무거운 계산이 백그라운드 스레드에서 실행되는지 확인합니다. 데드락은 정적 분석기로 감지할 수 있습니다: Facebook의 Infer와 Xcode의 Thread Safety Checker는 런타임 전에 잠재적 잠금을 찾습니다.

  • Android — StrictMode, Infer, Android Lint Multithread, viewModelScope를 사용한 Kotlin Coroutines
  • iOS — Main Thread Checker, TSAN(Thread Sanitizer), Xcode Analyze, MainActor를 사용한 Swift async/await
  • 크로스 플랫폼 — Flutter compute isolate, requestAnimationFrame을 사용한 React Native interaction manager

자주 묻는 질문

프리즈와 ANR의 차이점은 무엇인가요?

ANR(Application Not Responding)은 메인 스레드가 5초 이상 프리즈될 때 나타나는 Android 시스템 알림입니다. 프리즈는 더 넓은 개념으로, 모든 기간의 UI 차단을 의미합니다. iOS에는 ANR이 없지만 10–20초 타임아웃의 워치독이 있습니다.

Android에서 traces.txt를 읽는 방법은?

파일은 /data/anr/traces.txt에 있습니다. 액세스하려면 루트 액세스 또는 adb shell이 필요합니다: 루트 권한으로 adb shell cat /data/anr/traces.txt \> traces.txt를 실행합니다. 스택에서 “main” 스레드를 찾습니다 — 마지막으로 호출된 메서드가 차단 원인을 나타냅니다.

iOS에서 앱이 프리즈되지만 크래시되지 않는 이유는?

프리즈가 10초 미만이면 워치독이 트리거되지 않고 앱이 차단 작업이 완료될 때까지 단순히 “멈춥니다”. 사용자는 크래시를 보지 못하지만 불편을 겪습니다. 이러한 경우를 감지하려면 사용자 정의 실행 시간 추적과 함께 MetricKit을 사용하세요.

프리즈에 대해 앱을 테스트하는 방법은?

화면이 1초 이내에 열리는지 확인하는 UI 테스트를 사용합니다. 탭과 다음 화면 표시 사이의 시간 측정을 CI에 추가합니다. Android에서는 IdlingResource를 사용하여 비동기 작업을 기다리는 Espresso를 사용합니다. iOS에서는 XCTWaiter를 사용한 XCTest를 사용하여 로딩 시간을 확인합니다.

SwiftUI가 프리즈를 유발할 수 있나요?

SwiftUI 자체는 프리즈를 유발하지 않지만 body 속성의 복잡한 계산은 유발합니다. 무거운 작업으로 인해 body 계산에 500ms가 걸리면 UI가 프리즈됩니다. 해결책은 계산을 Task.detached로 오프로드하고 메인 액터에서 @State를 비동기적으로 업데이트하는 것입니다.

요약

  • 프리즈 — 메인 스레드 차단, 데드락 또는 무한 루프로 인한 수초에서 수십 초의 UI 완전 차단
  • 진단 — Android의 /data/anr/traces.txt, iOS의 Stackshot 및 Main Thread Checker
  • 주요 원인 — 동기 I/O, 스레드 간 데드락, 무한 재귀, 긴 GC
  • 해결 — 올바른 디스패처를 사용한 코루틴, MainActor를 사용한 async/await, 모든 I/O 작업을 백그라운드 스레드로 오프로드
  • 예방 — penaltyDeath를 사용한 StrictMode, Main Thread Checker, 데드락 정적 분석(Infer, TSAN)
  • Android에서 프리즈 > 5초 = ANR; iOS에서 > 10–20초 = 워치독 크래시(0x8badf00d)
  • 권장 사항: Debug 스키마에서 Thread Sanitizer를 활성화하고 데이터 경합 및 데드락 감지를 위해 TSAN으로 테스트를 실행하도록 CI를 구성합니다

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기