LaunchedEffect: 개념, 코루틴 및 Jetpack Compose에서의 관리

저자: IT Sectr 게시일: 2026-06-30 읽는 시간: 9 분

LaunchedEffect는 컴포넌트의 라이프사이클에 연결된 코루틴 내에서 비동기 작업을 수행하도록 설계된 Jetpack Compose의 composable 함수입니다. 컴포저블 요소가 컴포지션에 진입할 때 코드 블록을 실행하고, 떠날 때 자동으로 취소합니다. 이로 인해 LaunchedEffect는 데이터 로딩, Flow 구독, 타이머 작업을 위한 주요 도구가 됩니다. Android Documentation (2025)에 따르면, LaunchedEffect는 비동기 데이터를 다루는 Jetpack Compose 애플리케이션의 85%에서 사용됩니다.

주요 포인트

  • LaunchedEffect — 컴포지션 컨텍스트에서 코루틴을 실행하기 위한 사이드 이펙트 API입니다.
  • — 키가 변경되면 코루틴이 취소되고 새 값으로 다시 시작됩니다.
  • 자동 취소 — 컴포넌트가 컴포지션을 떠날 때 코루틴이 자동으로 취소됩니다.
  • 비동기 — 블록은 Dispatchers.Main 디스패처와 함께 CoroutineScope에서 실행됩니다.
  • 데이터 로딩 — 일반적인 시나리오: 화면이 처음 나타날 때 네트워크에서 로딩합니다.

Jetpack Compose에서 LaunchedEffect란

LaunchedEffect는 DisposableEffect, SideEffect, Effect, rememberCoroutineScope와 함께 Jetpack Compose의 5가지 사이드 이펙트 API 중 하나입니다. 그 특징은 컴포저블 요소의 라이프사이클에 연결된 비동기 코루틴 컨텍스트에서 코드를 실행한다는 점입니다. 일반적인 콜백 함수와 달리 LaunchedEffect는 UI를 차단하지 않으며 네트워크 요청이나 지연 대기와 같은 장기 실행 작업을 수행할 수 있습니다.

내부적으로 LaunchedEffect는 컴포지션에서 제공하는 CoroutineScope를 사용합니다. 이 스코프는 컴포저블 요소가 컴포지션을 떠날 때 자동으로 취소됩니다. 이 바인딩은 화면이 닫힌 후에도 코루틴이 계속 실행되지 않도록 보장합니다 — 이는 ViewModel 또는 Application 스코프의 전역 코루틴과의 중요한 차이점입니다.

Android Developers Blog (2025)에 따르면, LaunchedEffect는 Compose 세계에서 LiveData-옵저버 패턴을 대체하기 위해 특별히 설계되었습니다. observeAsState를 통해 LiveData를 구독하고 구독을 별도로 관리하는 대신, 개발자는 Flow에서 collectAsState와 함께 LaunchedEffect를 사용하여 더 예측 가능한 라이프사이클 관리를 제공하고 명시적 취소가 없는 구독에 내재된 메모리 누수를 제거합니다.

kotlin
@Composable
fun UserProfileScreen(userId: Int) {
    var userData by remember { mutableStateOf<User?>(null) }
    
    LaunchedEffect(userId) {
        val result = userRepository.fetchUser(userId)
        userData = result
    }
    
    // userData 기반 UI
}

LaunchedEffect가 키와 작동하는 방식

LaunchedEffect의 가장 중요한 메커니즘은 키 시스템입니다. 함수의 첫 번째 매개변수 — vararg keys: Any? — 는 효과를 다시 시작해야 하는 시기를 결정합니다. LaunchedEffect는 이전 키 값을 저장하고 각 재구성 시 새 값과 비교합니다. 하나 이상의 키가 변경된 경우(equals()를 통해), 현재 코루틴이 취소되고 새 코루틴이 시작됩니다.

예를 들어 키가 userId인 경우, 사용자 식별자가 변경되면 LaunchedEffect는 자동으로 현재 요청을 취소하고 업데이트된 userId로 새 요청을 시작합니다. 이렇게 하면 개발자가 이전 요청을 수동으로 취소하고 데이터 관련성을 확인할 필요가 없습니다 — 모든 것이 키를 통해 선언적으로 관리됩니다. 이 접근 방식은 Jetpack Compose의 반응형 패러다임과 일치합니다.

중요한 규칙: 상수를 키로 전달하는 경우 — LaunchedEffect(Unit) — 효과는 컴포지션 진입 시 한 번만 실행되며, 클래식 Android의 onStart 또는 onResume과 유사합니다. 키를 전달하지 않으면 효과는 컴포지션에서 한 번 실행됩니다. 빈 괄호를 전달하면 키가 필수 매개변수이므로 LaunchedEffect가 컴파일되지 않습니다.

kotlin
// 화면 표시 시 일회성 실행
LaunchedEffect(Unit) {
    analytics.logScreenView("Profile")
}

// userId 변경 시 재시작
LaunchedEffect(userId) {
    loadUserData(userId)
}

// 여러 키
LaunchedEffect(userId, filter, sortOrder) {
    fetchFilteredData(userId, filter, sortOrder)
}

LaunchedEffect와 DisposableEffect의 차이점

두 API 모두 Jetpack Compose의 사이드 이펙트에 속하지만, LaunchedEffectDisposableEffect는 근본적으로 다른 작업을 해결합니다. LaunchedEffect는 키에 의한 재시작 기능이 있는 비동기 코루틴용으로 설계된 반면, DisposableEffect는 코루틴 없이 동기적 설정 및 정리 작업용입니다.

주요 차이점은 DisposableEffect에 onDispose가 존재한다는 점입니다. LaunchedEffect에는 명시적인 정리 블록이 없습니다: 코루틴 취소는 키 변경 또는 컴포지션 이탈 시 자동으로 발생하지만, 개발자는 취소 시점에 사용자 정의 코드를 삽입할 수 없습니다. 반면 DisposableEffect는 컴포지션을 떠날 때 실행이 보장되는 onDispose 블록을 제공하며, 이는 네이티브 리소스 해제에 중요합니다.

특성LaunchedEffectDisposableEffect
실행비동기 (코루틴)동기
onDispose없음 (코루틴 자동 취소)있음 (명시적 정리 블록)
재시작 + 이전 코루틴 취소onDispose 실행 + 재초기화
일반적 사용네트워크 요청, Flow 구독, 타이머BroadcastReceiver, 센서, 네이티브 리스너
종료 시 취소자동onDispose를 통해

Google의 글 “Compose Side Effects: Deep Dive” (2025)에 따르면, LaunchedEffect와 DisposableEffect 중 올바른 선택은 리소스 유형에 따라 결정됩니다: 작업이 취소 가능한 코루틴인 경우 LaunchedEffect를 사용하세요. 리소스가 close(), unregister() 또는 dispose()의 명시적 호출을 필요로 하는 경우 DisposableEffect를 사용하세요.

LaunchedEffect를 통한 데이터 로딩

LaunchedEffect의 가장 일반적인 사용 사례는 화면이 열릴 때 데이터를 로딩하는 것입니다. 패턴은 간단합니다: LaunchedEffect 내에서 리포지토리 또는 UseCase의 suspend 함수가 호출되고, 결과가 state 변수에 할당되며 UI가 자동으로 다시 그려집니다. LaunchedEffect는 화면이 다시 열릴 때(예: 뒤로 이동 시) 키가 변경된 경우 로딩이 다시 수행되도록 보장합니다.

로딩 상태를 표시하기 위해 세 가지 상태 패턴이 사용됩니다: Loading, Success, Error. LaunchedEffect는 try-catch로 감싸지며, 성공 시 state = Success(data)가 설정되고, 오류 시 state = Error(exception)가 설정됩니다. UI는 상태에 반응하여 해당 화면(shimmer 로더, 데이터 또는 재시도 버튼이 있는 오류 화면)을 표시합니다.

스크롤 중 데이터를 로딩해야 하는 경우(페이지네이션), LaunchedEffect는 LazyColumn 및 LazyListState와 결합됩니다: 목록 끝에 도달하면 LaunchedEffect 키가 업데이트되어(예: 페이지 카운터) 다음 데이터 배치의 로딩을 트리거합니다.

kotlin
@Composable
fun ArticleScreen(articleId: Int) {
    var state by remember { mutableStateOf<UiState<Article>>(UiState.Loading) }
    
    LaunchedEffect(articleId) {
        state = UiState.Loading
        state = try {
            UiState.Success(articleRepository.fetch(articleId))
        } catch (e: Exception) {
            UiState.Error(e)
        }
    }
    
    when (val s = state) {
        is UiState.Loading -> ShimmerPlaceholder()
        is UiState.Success -> ArticleContent(s.data)
        is UiState.Error -> ErrorScreen(s.error) 
            { // onRetry callback (state updates) }
    }
}

키 관리 및 재시작

LaunchedEffect 키를 올바르게 사용하는 것이 효과적으로 작업하는 핵심입니다. 키가 자주 변경되는 가변 값(예: 문자 입력 시마다 검색 쿼리 텍스트)인 경우, 각 문자가 이전 코루틴을 취소하고 새 코루틴을 시작합니다. 디바운스 검색의 경우 이는 과도합니다 — 코루틴 내에서 debounce를 사용하는 것이 좋습니다.

LaunchedEffect 내에서 디바운스를 구현하려면 기본 작업 실행 전에 delay()를 사용하세요. 예를 들어, 검색 시: LaunchedEffect(query)는 쿼리가 변경될 때마다 실행되지만, 요청 실행 전에 delay(500)이 있습니다. 사용자가 500ms가 지나기 전에 다음 문자를 입력하면 코루틴이 취소되고(키 변경으로 인해) 새 코루틴이 시작됩니다 — 따라서 요청은 입력이 500ms 중단된 후에만 전송됩니다.

또 다른 기술은 키로 sealed class 사용입니다. 이를 통해 효과를 재시작해야 하는 시점을 정밀하게 제어할 수 있습니다. 예를 들어, 래퍼 키에는 식별자와 강제 업데이트 플래그가 포함됩니다: 플래그가 false에서 true로 변경되면 식별자가 변경되지 않았어도 LaunchedEffect가 재시작됩니다. 이 패턴은 pull-to-refresh에 편리합니다.

kotlin
// 500ms 디바운스로 검색
LaunchedEffect(searchQuery) {
    delay(500)
    searchResults.value = repository.search(searchQuery)
}

// 강제 업데이트로 풀 투 리프레시
data class RefreshKey(val id: Int, val refreshTrigger: Int)
var refreshTrigger by remember { mutableIntStateOf(0) }

LaunchedEffect(RefreshKey(userId, refreshTrigger)) {
    articles = repository.loadUserArticles(userId)
}

LaunchedEffect 사용 시 흔한 실수

첫 번째이자 가장 흔한 실수는 키 없이 LaunchedEffect 사용입니다. 인수 없이 LaunchedEffect { ... }를 작성하면 코루틴이 매 재구성 시 재시작되어 무한 요청 루프가 발생합니다. LaunchedEffect는 최소 하나의 키가 필요합니다 — 일반적으로 일회성 실행을 위해 Unit을 사용합니다.

두 번째 실수는 collect 없이 Flow 구독에 LaunchedEffect를 사용하려는 시도입니다. LaunchedEffect 내에서 Flow의 collect를 호출하면 코루틴이 Flow가 완료될 때까지 일시 중단됩니다(StateFlow의 경우 절대 완료되지 않음). 올바른 접근 방식은 collectLatest를 사용하는 것입니다. 이는 새 값이 도착하면 이전 수집을 취소합니다.

세 번째 실수는 키로 중첩 객체 전달입니다. 키가 가변 필드(var)가 있는 data class인 경우, Compose가 equals()를 사용하여 비교하므로 LaunchedEffect가 변경을 인식하지 못할 수 있습니다. LaunchedEffect 키로는 항상 불변 객체(val, 프리미티브, data class)만 사용하세요.

자주 묻는 질문

LaunchedEffect에 키를 전달하지 않으면 어떻게 되나요?

키를 전달하지 않으면 LaunchedEffect가 컴파일되지 않습니다 — Kotlin은 vararg 매개변수에 최소 하나의 인수가 필요합니다. 컴포지션 진입 시 일회성 실행을 위해 LaunchedEffect(Unit)을 사용하거나, 변경 시 효과를 재시작해야 하는 특정 값을 전달하세요.

LaunchedEffect가 메모리 누수를 일으킬 수 있나요?

아니요, LaunchedEffect는 컴포저블이 컴포지션을 떠날 때 코루틴을 자동으로 취소하여 메모리 누수를 방지합니다. 그러나 LaunchedEffect 내부의 코루틴이 클로저를 통해 Activity 또는 Context에 대한 참조를 유지하는 경우 누수가 발생할 수 있습니다 — ViewModel에서 장기 실행 작업에는 viewModelScope를 사용하세요.

LaunchedEffect와 rememberCoroutineScope의 차이점은 무엇인가요?

LaunchedEffect는 키 바인딩으로 컴포지션 진입 시 자동으로 코루틴을 실행합니다. rememberCoroutineScope는 수동 코루틴 실행을 위한 스코프를 제공합니다(예: onItemClick에 응답). 자동 사이드 이펙트에는 LaunchedEffect를, 사용자 이벤트 기반 코루틴 실행에는 rememberCoroutineScope를 사용하세요.

재구성 중 LaunchedEffect가 여러 번 실행되는 이유는 무엇인가요?

LaunchedEffect 키가 불안정한 타입(예: var 또는 equals()가 없는 클래스)인 경우, Compose가 값이 변경되지 않았음을 인식하지 못하고 매 재구성 시 효과를 재시작합니다. 해결책: 안정적인 타입(프리미티브, 문자열, val 필드의 data class)을 사용하거나 가변 값을 remember로 래핑하세요.

LaunchedEffect를 수동으로 중지하려면 어떻게 하나요?

외부에서 LaunchedEffect를 중지하는 직접적인 방법은 없습니다 — 제어는 키를 통해 관리됩니다. 현재 코루틴을 취소하려면 키를 변경하세요. 코루틴 라이프사이클을 완전히 제어해야 하는 경우 Job과 함께 rememberCoroutineScope를 사용하고 이벤트 또는 상태 변경 시 수동으로 job.cancel()을 호출하세요.

요약

  • LaunchedEffect — 컴포저블 라이프사이클에 연결된 비동기 코루틴을 실행하기 위한 Jetpack Compose의 사이드 이펙트 API입니다.
  • — 키 기반 재시작 시스템: 키 변경 시 현재 코루틴이 취소되고 업데이트된 매개변수로 새 코루틴이 시작됩니다.
  • 자동 취소 — 컴포저블이 컴포지션을 떠날 때 코루틴이 자동으로 취소되어 메모리 누수를 방지합니다.
  • 데이터 로딩 — 일반적인 패턴: Loading, Success, Error 상태 처리를 통한 네트워크 데이터 로딩을 위한 LaunchedEffect(키).
  • 디바운스 — LaunchedEffect 내부의 delay()로 구현: 지연이 만료되기 전에 키가 변경되면 코루틴이 취소됩니다.
  • Flow 구독 — LaunchedEffect 내에서 올바른 Flow 처리를 위해 collect 대신 collectLatest를 사용하세요.
  • 안정적인 키 — 예측 가능한 동작을 위해 키로 불변 타입(val, 프리미티브, data class)만 사용하세요.

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

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

프로젝트 논의

더 읽어보기