Android 앱 ANR 해결 방법 — 메인 스레드 블로킹, Coroutine, SQLite와 WebView 성능 분석

Android 앱을 개발하다 보면 앱이 갑자기 멈추거나 화면을 터치해도 반응하지 않는 문제가 발생할 수 있습니다.

잠시 기다리면 다시 동작하는 경우도 있지만, 일정 시간 이상 응답하지 않으면 Android 시스템에서 다음과 같은 메시지가 표시되기도 합니다.

"앱이 응답하지 않습니다."

이러한 현상을 ANR(Application Not Responding)이라고 합니다.

특히 SQLite 데이터베이스를 직접 사용하거나, WebView에서 JavaScript를 실행하거나, 네트워크 요청과 이미지 처리가 많은 앱에서는 메인 스레드가 차단되지 않도록 주의해야 합니다.

이번 글에서는 Kotlin + XML + MVVM 기반 Android 프로젝트에서 ANR이 발생하는 원인을 분석하고, Coroutine과 Android Studio의 성능 분석 도구를 이용해 문제를 해결하는 방법을 정리하겠습니다.

작성 기준: 2026년 10월


1. ANR이란?

ANR은 Android 시스템이 앱의 응답 지연을 감지했을 때 발생하는 현상입니다.

Android 앱은 일반적으로 메인 스레드에서 다음 작업을 처리합니다.

  • 화면 그리기

  • 사용자 터치 이벤트 처리

  • Activity와 Fragment의 생명주기 콜백

  • View 업데이트

  • UI 관련 이벤트 실행

메인 스레드가 오래 차단되면 이러한 작업을 제때 처리할 수 없습니다.

대표적으로 입력 이벤트에 응답하지 못하는 상황에서는 약 5초 수준의 타임아웃이 ANR 판단에 사용됩니다.

다만 모든 ANR이 동일한 시간 기준으로 발생하는 것은 아닙니다.

ANR 유형과 실행 환경에 따라 타임아웃 조건이 달라질 수 있습니다.

ANR과 앱 크래시의 차이

구분ANRCrash
주요 현상앱이 응답하지 않음앱이 비정상 종료됨
대표 원인메인 스레드 차단, 락 대기처리되지 않은 예외
확인 자료ANR trace, 시스템 로그Exception stack trace
주요 분석 도구Perfetto, Play Console, ProfilerLogcat, Crashlytics

ANR은 단순히 앱이 느린 현상과는 다릅니다.

화면이 느리게 갱신되는 현상은 프레임 지연이나 Jank일 수도 있으며, 반드시 ANR로 이어지는 것은 아닙니다.

2. Android 메인 스레드 이해하기

Android 앱은 기본적으로 하나의 메인 스레드에서 UI 작업을 처리합니다.

Android Application
        │
        ▼
    Main Thread
        │
        ├── Touch Event
        ├── Activity Lifecycle
        ├── Fragment Lifecycle
        ├── View Drawing
        └── UI Update

메인 스레드에서 오래 걸리는 작업을 실행하면 뒤에 대기 중인 이벤트가 처리되지 못합니다.

예를 들어 다음과 같은 코드가 있다고 가정하겠습니다.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContentView(R.layout.activity_main)

    Thread.sleep(6000)
}

이 코드는 메인 스레드를 6초 동안 멈춥니다.

이 시간 동안 앱은 정상적으로 UI 이벤트를 처리하기 어렵습니다.

다만 이 코드가 실행되었다고 해서 모든 기기에서 반드시 ANR 대화상자가 표시되는 것은 아닙니다.

실제 ANR 발생 여부는 앱 상태와 시스템의 타임아웃 조건에 따라 달라집니다.

3. ANR의 대표적인 원인

실무에서 자주 확인하는 원인은 다음과 같습니다.

원인대표적인 상황
SQLite 조회메인 스레드에서 대량 데이터 검색
파일 I/O앱 실행 시 대용량 DB 복사
네트워크 요청동기 HTTP 요청 실행
이미지 처리고해상도 이미지 직접 디코딩
WebViewJavaScript 결과 처리 중 무거운 작업
락 경합메인 스레드가 다른 스레드의 락을 기다림
동기 대기Thread.sleep(), runBlocking() 사용
복잡한 UI 작업메인 스레드에서 과도한 계산이나 바인딩

이 중에서도 SQLite, 네트워크, 파일 I/O는 메인 스레드에서 실행하지 않도록 설계하는 것이 중요합니다.

4. SQLite 조회로 발생하는 ANR

Room을 사용하지 않고 SQLiteOpenHelper를 직접 사용하는 프로젝트에서는 다음과 같은 코드를 작성할 수 있습니다.

val db = dbHelper.readableDatabase

val cursor = db.rawQuery(
    "SELECT * FROM korean_birds",
    null
)

while (cursor.moveToNext()) {
    // 데이터 변환
}

cursor.close()

이 코드 자체가 잘못된 것은 아닙니다.

문제는 실행 위치입니다.

Activity나 Fragment의 메인 스레드에서 대량 데이터를 조회하면 UI 처리가 지연될 수 있습니다.

특히 JOIN, LIKE 검색, 정렬이 포함된 쿼리는 데이터 규모와 인덱스 상태에 따라 실행 시간이 길어질 수 있습니다.

해결 방법: Dispatchers.IO

class BirdRepository(
    private val dbHelper: BirdDatabaseHelper
) {

    suspend fun getBirdCount(): Int =
        withContext(Dispatchers.IO) {

            val db = dbHelper.readableDatabase

            db.rawQuery(
                "SELECT COUNT(*) FROM korean_birds",
                null
            ).use { cursor ->

                if (cursor.moveToFirst()) {
                    cursor.getInt(0)
                } else {
                    0
                }
            }
        }
}

Dispatchers.IO는 디스크 I/O와 같은 블로킹 작업을 처리할 때 사용하는 Coroutine Dispatcher입니다.

ViewModel에서는 다음과 같이 호출합니다.

class BirdViewModel(
    private val repository: BirdRepository
) : ViewModel() {

    private val _birdCount = MutableLiveData<Int>()
    val birdCount: LiveData<Int> = _birdCount

    fun loadBirdCount() {
        viewModelScope.launch {
            try {
                _birdCount.value = repository.getBirdCount()
            } catch (e: CancellationException) {
                throw e
            } catch (e: Exception) {
                Log.e("BirdViewModel", "DB query failed", e)
            }
        }
    }
}

이렇게 하면 SQLite 조회 작업이 메인 스레드를 직접 차단하는 문제를 줄일 수 있습니다.

단, Dispatchers.IO를 사용한다고 느린 SQL이 자동으로 빨라지는 것은 아닙니다.

쿼리 자체가 느리다면 인덱스와 실행 계획을 별도로 분석해야 합니다.

5. SQLite 트랜잭션과 락 경합

SQLite에서 ANR이 발생하는 또 다른 원인은 데이터베이스 락 경합입니다.

예를 들어 한 스레드가 오랜 시간 트랜잭션을 유지하고 있고, 메인 스레드에서 같은 데이터베이스에 접근하면 대기 상황이 발생할 수 있습니다.

Worker Thread
    │
    └── SQLite Transaction
            │
            └── 장시간 작업

Main Thread
    │
    └── SQLite 접근
            │
            └── 락 대기

이 경우 메인 스레드에서 실행한 SQL이 단순하더라도 다른 작업 때문에 응답이 지연될 수 있습니다.

트랜잭션을 짧게 유지하기

db.beginTransaction()

try {
    // 필요한 DB 변경 작업만 실행

    db.setTransactionSuccessful()
} finally {
    db.endTransaction()
}

트랜잭션 내부에서는 불필요한 네트워크 요청이나 장시간 계산을 수행하지 않는 것이 좋습니다.

또한 동일한 데이터베이스에 대한 읽기·쓰기 작업이 동시에 발생하는 구조라면 연결 관리와 동시성 정책을 점검해야 합니다.

6. runBlocking 사용 시 주의사항

Kotlin Coroutine을 사용한다고 해서 모든 코드가 비동기로 실행되는 것은 아닙니다.

특히 runBlocking()은 호출한 스레드를 차단합니다.

다음 코드는 메인 스레드에서 실행하면 위험합니다.

runBlocking {
    val birds = repository.getBirds()
    adapter.submitList(birds)
}

repository.getBirds() 내부에서 Dispatchers.IO를 사용하더라도 runBlocking()을 호출한 메인 스레드는 Coroutine이 완료될 때까지 차단됩니다.

권장 방법

lifecycleScope.launch {
    val birds = repository.getBirds()
    adapter.submitList(birds)
}

이 코드는 Activity나 Fragment의 생명주기에 연결된 Coroutine을 시작합니다.

다만 Fragment의 View를 갱신하는 작업이라면 viewLifecycleOwner.lifecycleScope와 생명주기 상태를 고려하는 것이 좋습니다.

데이터 조회와 UI 상태 관리가 복잡하다면 ViewModel에서 조회하고 LiveData 또는 StateFlow로 결과를 전달하는 구조가 더 적절합니다.

7. Retrofit 네트워크 요청과 ANR

Retrofit을 사용하는 앱에서는 일반적으로 suspend 함수를 이용해 비동기 네트워크 요청을 처리할 수 있습니다.

interface BirdApi {

    @GET("birds")
    suspend fun getBirds(): List<BirdDto>
}

ViewModel에서는 다음과 같이 호출합니다.

fun loadBirds() {
    viewModelScope.launch {
        try {
            val result = api.getBirds()
            _birds.value = result
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            Log.e("BirdViewModel", "Network failed", e)
        }
    }
}

Retrofit의 Coroutine 지원을 사용하면 네트워크 응답을 기다리는 동안 메인 스레드를 직접 차단하지 않는 방식으로 구현할 수 있습니다.

반면 다음과 같은 동기 호출은 주의해야 합니다.

val response = api.getBirdsCall().execute()

execute()는 동기 방식이므로 호출한 스레드에서 응답을 기다립니다.

이를 메인 스레드에서 실행하면 ANR 또는 네트워크 관련 예외가 발생할 수 있습니다.

8. WebView에서 발생하는 ANR

WebView를 사용하는 앱에서는 JavaScript 실행과 결과 처리 과정에서도 성능 문제가 발생할 수 있습니다.

예를 들어 다음과 같은 코드를 사용한다고 가정하겠습니다.

webView.evaluateJavascript(
    "document.body.innerText"
) { result ->
    Log.d("WebView", result)
}

evaluateJavascript()는 JavaScript 실행 결과를 비동기 콜백으로 전달합니다.

따라서 메서드 호출 자체가 JavaScript 결과가 나올 때까지 일반적인 동기 방식으로 대기하는 것은 아닙니다.

하지만 결과 콜백에서 대량의 문자열을 파싱하거나 SQLite에 저장하는 작업을 메인 스레드에서 수행하면 문제가 발생할 수 있습니다.

문제가 될 수 있는 구조

webView.evaluateJavascript(
    "document.body.innerText"
) { result ->

    val parsed = parseLargeHtml(result)

    saveToDatabase(parsed)
}

WebView의 JavaScript 결과 콜백은 UI 스레드에서 호출됩니다.

따라서 콜백 내부에서 무거운 파싱과 DB 작업을 직접 실행하면 UI가 지연될 수 있습니다.

개선 방법

webView.evaluateJavascript(
    "document.body.innerText"
) { result ->

    lifecycleScope.launch {

        val parsed = withContext(Dispatchers.Default) {
            parseLargeHtml(result)
        }

        withContext(Dispatchers.IO) {
            saveToDatabase(parsed)
        }

        updateUi(parsed)
    }
}

이 예시에서는 작업의 성격에 따라 Dispatcher를 구분했습니다.

  • Dispatchers.Default: CPU 사용량이 큰 파싱 및 계산

  • Dispatchers.IO: SQLite 저장과 파일 I/O

  • Main Dispatcher: UI 업데이트

단, WebView 객체를 조작하는 코드는 WebView의 스레드 요구사항을 지켜야 합니다.

백그라운드 스레드에서 WebView 메서드를 직접 호출하는 방식은 피해야 합니다.

9. addJavascriptInterface 사용 시 주의사항

WebView와 JavaScript 사이에서 데이터를 주고받기 위해 addJavascriptInterface()를 사용하는 경우도 있습니다.

webView.addJavascriptInterface(
    JsBridge(),
    "AndroidBridge"
)

JavaScript 인터페이스 메서드는 WebView가 제공하는 별도의 스레드에서 호출될 수 있습니다.

따라서 해당 메서드에서 UI를 직접 변경하면 안 됩니다.

예를 들어 다음과 같이 처리할 수 있습니다.

class JsBridge(
    private val activity: Activity
) {

    @JavascriptInterface
    fun onMessage(message: String) {

        activity.runOnUiThread {
            // UI 업데이트
        }
    }
}

하지만 위 코드는 스레드 전환을 설명하기 위한 단순 예시입니다.

실제 프로젝트에서는 Activity 참조의 수명주기와 메모리 누수 가능성을 고려해야 합니다.

또한 JavaScript 인터페이스는 보안상 주의가 필요한 기능입니다.

신뢰할 수 없는 웹 콘텐츠에 민감한 네이티브 기능을 노출하지 않도록 설계해야 합니다.

10. RecyclerView 바인딩과 ANR

RecyclerView의 onBindViewHolder()는 스크롤 중 반복적으로 호출됩니다.

다음과 같은 코드는 피하는 것이 좋습니다.

override fun onBindViewHolder(
    holder: BirdViewHolder,
    position: Int
) {
    val bird = items[position]

    val bitmap = BitmapFactory.decodeFile(
        bird.imagePath
    )

    holder.imageView.setImageBitmap(bitmap)
}

이미지 디코딩은 이미지 크기와 파일 상태에 따라 비용이 클 수 있습니다.

이를 메인 스레드에서 반복 실행하면 스크롤이 끊기거나 심한 경우 응답 지연이 발생할 수 있습니다.

Glide 사용

Glide.with(holder.imageView)
    .load(bird.imagePath)
    .placeholder(R.drawable.ic_placeholder)
    .error(R.drawable.ic_placeholder)
    .into(holder.imageView)

Glide를 사용하면 이미지 로딩, 디코딩, 캐싱, View 재활용 처리를 보다 효율적으로 관리할 수 있습니다.

다만 이미지 라이브러리를 사용한다고 모든 UI 성능 문제가 해결되는 것은 아닙니다.

목록 전체를 반복 갱신하거나 onBindViewHolder()에서 복잡한 계산을 수행하면 여전히 성능 문제가 발생할 수 있습니다.

11. StrictMode로 메인 스레드 작업 탐지하기

개발 중 메인 스레드에서 발생하는 디스크 I/O나 네트워크 작업을 확인하려면 StrictMode를 사용할 수 있습니다.

if (BuildConfig.DEBUG) {

    StrictMode.setThreadPolicy(
        StrictMode.ThreadPolicy.Builder()
            .detectDiskReads()
            .detectDiskWrites()
            .detectNetwork()
            .penaltyLog()
            .build()
    )
}

이 코드는 개발용 빌드에서 실행하도록 구성하는 것이 좋습니다.

예를 들어 Application.onCreate()에서 디버그 빌드일 때만 설정할 수 있습니다.

StrictMode가 문제를 감지하면 Logcat에 관련 경고가 출력될 수 있습니다.

하지만 StrictMode는 모든 ANR 원인을 찾아주는 도구는 아닙니다.

락 경합이나 복잡한 계산처럼 디스크·네트워크 작업과 직접 관련이 없는 문제는 별도의 분석이 필요합니다.

12. Android Studio Profiler로 메인 스레드 분석하기

Android Studio의 Profiler를 사용하면 앱 실행 중 CPU 사용량과 스레드 동작을 분석할 수 있습니다.

특히 다음과 같은 상황을 확인하는 데 유용합니다.

  • 메인 스레드에서 긴 작업이 실행되는지

  • 특정 함수가 CPU 시간을 많이 사용하는지

  • 앱 시작 시 초기화 작업이 집중되는지

  • 반복적인 데이터 변환이 발생하는지

  • 메모리 사용량이 급격히 증가하는지

분석할 때는 문제가 발생하는 동작을 재현하는 것이 중요합니다.

예를 들어 검색 화면에서 ANR이 발생한다면 다음과 같이 테스트할 수 있습니다.

  1. 앱 실행

  2. 검색 화면 이동

  3. 긴 검색어 입력

  4. 검색 조건 변경

  5. RecyclerView 스크롤

  6. 메인 스레드의 긴 작업 확인

Profiler에서 오래 실행되는 메서드를 찾았다면 해당 작업이 메인 스레드에서 실행되어야 하는지 먼저 검토합니다.

13. Perfetto로 ANR 분석하기

Perfetto는 Android 시스템과 앱의 성능을 추적할 수 있는 도구입니다.

ANR 분석에서는 특히 메인 스레드가 어떤 상태에 있었는지 확인하는 것이 중요합니다.

예를 들어 다음과 같은 상황을 구분할 수 있습니다.

Main Thread
    │
    ├── CPU 작업 중
    │
    ├── 락 대기
    │
    ├── 디스크 I/O 대기
    │
    └── 다른 스레드 작업 완료 대기

메인 스레드가 CPU를 계속 사용하고 있다면 복잡한 계산이나 반복 작업이 원인일 수 있습니다.

반대로 메인 스레드가 대기 상태라면 락 경합이나 동기 작업 대기를 의심할 수 있습니다.

다만 Perfetto의 추적 결과를 해석할 때는 단순히 특정 스레드가 대기 중이라는 사실만으로 ANR 원인을 단정해서는 안 됩니다.

ANR 발생 시점과 시스템 로그, 스레드 상태를 함께 확인해야 합니다.

14. Google Play Console에서 ANR 확인하기

Google Play에 앱을 배포했다면 Android vitals에서 ANR 관련 정보를 확인할 수 있습니다.

일반적으로 다음과 같은 정보를 분석할 수 있습니다.

  • ANR 발생 빈도

  • 영향을 받은 사용자 및 기기

  • Android 버전별 분포

  • ANR 클러스터

  • 관련 스택 트레이스

실제 운영 중 발생한 ANR은 개발 환경에서 재현하기 어려울 수 있습니다.

따라서 Play Console에서 특정 기기나 Android 버전에 문제가 집중되는지 확인하는 것이 중요합니다.

특히 사용자에게 영향을 주는 ANR 비율은 앱 품질을 평가하는 중요한 지표 중 하나입니다.

15. ANR 스택 트레이스 읽는 방법

ANR 분석에서는 메인 스레드의 스택 트레이스를 확인해야 합니다.

예를 들어 다음과 같은 스택이 있다고 가정하겠습니다.

"main" tid=1
  at java.lang.Thread.sleep(Native Method)
  at com.example.app.MainActivity.loadData(MainActivity.kt:85)
  at com.example.app.MainActivity.onCreate(MainActivity.kt:42)

이 예시는 설명을 위해 작성한 가상의 스택 트레이스입니다.

여기서는 MainActivity의 loadData()에서 Thread.sleep()이 실행되고 있다는 사실을 확인할 수 있습니다.

다른 예시를 보겠습니다.

"main" tid=1
  at java.lang.Object.wait(Native Method)
  at com.example.app.DataManager.waitForResult(...)
  at com.example.app.MainActivity.onResume(...)

이 경우 메인 스레드가 다른 작업의 결과를 기다리고 있을 가능성을 검토할 수 있습니다.

하지만 스택 트레이스에 표시된 최상단 메서드가 항상 실제 원인은 아닙니다.

다른 스레드가 락을 보유하고 있거나 시스템 서비스의 응답을 기다리는 상황도 있기 때문입니다.

따라서 관련 스레드의 스택과 실행 흐름을 함께 분석해야 합니다.

16. ANR이 발생했을 때 점검 순서

ANR이 발생했다면 무작정 Coroutine을 추가하기보다 다음 순서로 접근하는 것이 좋습니다.

1단계: 발생 시점 확인

  • 앱 시작 시 발생하는가?

  • 특정 화면 진입 시 발생하는가?

  • 검색 또는 저장 버튼을 누를 때 발생하는가?

  • WebView 페이지 로딩 중 발생하는가?

  • 앱을 백그라운드에서 복귀할 때 발생하는가?

2단계: 메인 스레드 상태 확인

ANR trace와 Profiler를 통해 메인 스레드가 어떤 작업을 수행했는지 확인합니다.

3단계: 원인 작업 분류

  • 디스크 I/O

  • 네트워크 I/O

  • CPU 계산

  • 락 경합

  • 동기 대기

  • UI 처리

4단계: 실행 구조 개선

작업 성격에 따라 적절한 Dispatcher와 비동기 구조를 적용합니다.

5단계: 동일한 환경에서 재검증

수정 전과 동일한 기기, Android 버전, 데이터 규모에서 문제를 재현해 봅니다.

단순히 ANR 대화상자가 표시되지 않는 것만으로 해결되었다고 판단하기보다 메인 스레드 지연과 실행 시간을 함께 확인하는 것이 좋습니다.

17. Coroutine을 사용해도 ANR이 발생하는 이유

Coroutine을 도입했는데도 ANR이 발생하는 경우가 있습니다.

대표적인 원인은 다음과 같습니다.

viewModelScope.launch {
    heavyCalculation()
}

viewModelScope.launch는 기본적으로 메인 Dispatcher에서 실행됩니다.

따라서 heavyCalculation()이 CPU를 오래 사용하는 동기 함수라면 메인 스레드를 차단할 수 있습니다.

이 경우 다음과 같이 변경해야 합니다.

viewModelScope.launch {

    val result = withContext(Dispatchers.Default) {
        heavyCalculation()
    }

    _result.value = result
}

반면 파일이나 SQLite 조회처럼 블로킹 I/O 작업이라면 Dispatchers.IO가 적절합니다.

중요한 것은 Coroutine을 사용한다는 사실보다 실제 작업이 어떤 Dispatcher에서 실행되는지입니다.

18. ANR 예방을 위한 프로젝트 구조

Kotlin + MVVM 기반 앱에서는 다음과 같은 구조를 추천합니다.

Activity / Fragment
        │
        ├── 사용자 이벤트
        │
        ▼
     ViewModel
        │
        ├── viewModelScope
        │
        ▼
     Repository
        │
        ├── Dispatchers.IO
        │      ├── SQLite
        │      ├── File I/O
        │      └── Blocking I/O
        │
        └── Dispatchers.Default
               ├── 대량 데이터 변환
               └── CPU 계산

이 구조에서는 UI 계층에서 직접 데이터베이스를 조회하거나 파일을 복사하는 일을 줄일 수 있습니다.

또한 Repository가 메인 스레드 안전성을 보장하도록 설계하면 여러 ViewModel에서 같은 기능을 재사용하기 편리합니다.

19. ANR 예방 체크리스트

  • Activity와 Fragment에서 동기 SQLite 조회 제거

  • 파일 복사와 DB 초기화를 Dispatchers.IO에서 실행

  • Retrofit 동기 요청을 메인 스레드에서 실행하지 않음

  • 메인 스레드의 runBlocking 및 Thread.sleep 사용 확인

  • CPU 계산은 Dispatchers.Default 사용 검토

  • SQLite 트랜잭션을 불필요하게 오래 유지하지 않음

  • WebView JavaScript 콜백에서 무거운 작업 분리

  • addJavascriptInterface 스레드 및 보안 요구사항 확인

  • RecyclerView 바인딩에서 이미지 직접 디코딩 제거

  • StrictMode로 메인 스레드 디스크·네트워크 접근 확인

  • Profiler와 Perfetto로 실행 시간 분석

  • Play Console Android vitals의 ANR 확인

  • 수정 후 동일한 환경에서 재현 테스트

20. 마무리

Android ANR은 앱의 특정 기능이 느리다는 문제를 넘어, 사용자가 앱을 정상적으로 조작하지 못하는 상황입니다.

특히 SQLite, WebView, 네트워크 요청을 함께 사용하는 앱에서는 메인 스레드가 차단되지 않도록 설계하는 것이 중요합니다.

실무에서는 다음 순서로 대응하는 것이 좋습니다.

  1. ANR 발생 시점과 메인 스레드의 상태를 확인합니다.

  2. SQLite, 파일 I/O, 네트워크 요청이 메인 스레드에서 실행되는지 점검합니다.

  3. 작업 성격에 따라 Dispatchers.IO와 Dispatchers.Default를 구분합니다.

  4. WebView와 RecyclerView 콜백에서 무거운 작업을 분리합니다.

  5. StrictMode, Profiler, Perfetto, Play Console을 이용해 원인을 검증합니다.

ANR을 해결할 때 가장 중요한 것은 무조건 백그라운드 스레드를 사용하는 것이 아니라 어떤 작업이 메인 스레드를 차단하는지 정확히 파악하는 것입니다.

원인을 확인한 뒤 실행 구조를 개선하면 앱의 응답성과 안정성을 함께 높일 수 있습니다.


참고 자료

태그: Android, Kotlin, ANR, Application Not Responding, Coroutine, Dispatchers.IO, Dispatchers.Default, SQLite, WebView, Retrofit, RecyclerView, StrictMode, Perfetto, Android Studio Profiler, MVVM