🤖 Compose

[Compose] XML처럼 TextField 쓰면 Compose에선 망해요

콩드로이드 2025. 5. 16. 22:38

Compose에서 TextField로 입력할 때, 글자 하나 칠 때마다 뭔가 버벅임을 느꼈습니다..

 

처음 Compose로 전환했을 때  XML에서 하던 습관대로

OutlinedTextField(
    value = text,
    onValueChange = {
        text = it
        viewModel.search(it) // ❌ 글자 칠 때마다 API 호출
    }
)

이렇게 작성했는데… 텍스트 입력할 때마다  밀리는 느낌?,, 스크롤과 겹치면 거의 끊기는 수준까지 가더라고요.

 


 

원인은 recomposition....

Compose는 상태가 바뀌면 해당 Composable이 다시 그려지니, TextField도 마찬가지였습니다,

근데 onValueChange에서 상태 변경 + API 호출까지 동시에 하니 ☠️

  • 입력 → 상태 변경 → recomposition
  • recomposition 중 launch, joinToString, format 등 무거운 연산 있음 → 스크롤 버벅임 

XML 기반 View에선 이런 식으로 해도 이벤트만 전달되기 때문에 이런 문제가 안 드러났어서 모르고 있었지만,

Compose에선 진짜 망하는 길이더라구요.. 

 

XML vs Compose 

상태 변화 내부에서 직접 보유 (EditText 자체에 있음) 외부에서 상태를 전달 (state)
이벤트 처리 방식 텍스트 변경 이벤트만 따로 발생 value가 바뀌면 recomposition 전체 발생
API 호출 위치 TextWatcher에서 직접 호출 가능 (re-draw 영향 없음) onValueChange에서 호출하면 UI 전체 다시 그림
렌더링 방식 OS 뷰 시스템이 직접 그림 (뷰 재사용 가능) 선언형 구조라 매번 다시 그릴 수 있도록 설계됨
성능 최적화 필요성 뷰 재사용 구조라 성능 최적화 덜 민감함 recomposition 민감 → 조금만 무거워도 랙 발생

 


해결 방법: 입력은 입력만,  로직은 분리

val input = remember { mutableStateOf("") }

OutlinedTextField(
    value = input.value,
    onValueChange = { input.value = it }
)

LaunchedEffect(Unit) {
    snapshotFlow { input.value }
        .debounce(300) // 입력 끝나고 300ms 기다림
        .filter { it.length >= 2 }
        .collectLatest {
            viewModel.search(it)
        }
}

 

이런 식으로 입력과 로직을 분리시켰어요 

  • snapshotFlow로 Compose 상태를 Flow처럼 다룸
  • debounce로 중복 호출 방지 
  • 실제 검색 로직은 입력과 완전히 분리됨 → 스크롤 개선

 

Compose TextInput 사용 시 고려 사항

  • ✅ onValueChange엔 상태 변경만 — 무거운 로직은 절대 ❌
  • ✅ 포맷팅, 스타일 계산 등은 remember {} 안에서
  • ✅ recomposition이 자주 일어나는 부분은 가능한 단순하게

 

 

 


XML 습관 그대로 Compose에서 쓰면 성능이 무너지는 대표적인 예가 바로 TextField입니다..

Compose에선 "입력은 입력만 처리하고, 로직은 나중에 처리"가 진짜 진짜 중요하더라구요 

recomposition에 대해 글을 여러 번 적지만, 매번 겪어보지 않고 고려하기가 어렵네요.. 🥲

 

필요하다면 기존 LazyColumn 성능 튜닝과도 연결해서 글 이어보세요 
👉 LazyColumn 성능 정리한 글