본 글은 Gemini를 사용하여 초안을 작성한 후 검수하였습니다.
이번 세션에서 안정적인 텍스트 편집 환경을 구축하는 방법과 아키텍처 변화를 소개합니다.

Compose는 세 가지 주요 텍스트 입력 컴포넌트를 제공합니다
TextField / OutlinedTextField : Material 디자인 가이드라인을 구현한 표준 컴포넌트BasicTextField : 스타일이 없는 기본 컴포넌트. 커스텀 디자인 시스템 구축 시 사용하며 Compose 텍스트 필드 내부 구현 담당SecureTextField : 비밀번호 입력 특화 컴포넌트
BasicTextField, TextField, OutlinedTextField에서 (value, onValueChange) 오버로드를 Deprecated로 전환기존 (value, onValueChange) 콜백은 동기식 입력에서는 정상 작동했지만, StateFlow와 같은 비동기 데이터 스트림과 결합하면 IME 동기화 결함이 발생합니다.

TextFieldState.text는 읽기 전용 불변 스냅샷입니다. 코드로 텍스트를 직접 수정하려면 edit 함수를 사용해야 합니다.

state.edit { } 람다 내부에서 가변 객체인 TextFieldBuffer를 제공받아 글자 삽입, 치환, 삭제, 커서 이동 API를 호출합니다.
val state = rememberTextFieldState("Hello")
state.edit {
// 텍스트를 변경하고 커서를 끝으로 이동
replace(0, length, "Hello World")
placeCursorAtEnd()
}

성능 최적화: edit 호출을 하나로 묶기
여러 편집 작업을 처리할 때
edit을 반복 호출하지 말고 하나의edit { }블록 안에 묶어야 합니다. Compose는 단일TextFieldBuffer인스턴스에서 인플레이스(In-place)로 메모리를 변경한 뒤, IME 및 스냅샷 시스템과 1회만 동기화합니다.
clearText(), setTextAndPlaceCursorAtEnd() 등 자주 쓰는 동작은 이미TextFieldState확장 함수로 제공됩니다.

과거 onValueChange 방식의 입력 검증은 텍스트가 키보드에 이미 커밋된 후에 실행되었습니다. 잘못된 입력을 되돌릴 때 IME 내부 상태와 충돌하여 글자가 깜빡이거나 엉키는 현상이 있었습니다.
TextFieldState에 반영되기 전 가로채기 수행revertAllChanges()로 취소// 숫자만 허용하는 InputTransformation
inputTransformation = InputTransformation {
val text = asCharSequence()
if (!text.all { it.isDigit() }) {
// Reject non-digit characters
revertAllChanges()
}
}
신용카드 번호 4자리 구분 공백이나 비밀번호 마스킹처럼 원본 데이터는 유지하고 화면 표시 형식만 변경해야 할 때 사용합니다.
// Raw data untouched; formatting is purely visual
outputTransformation = OutputTransformation {
if (length > 6) {
insert(6, "-")
}
if (length > 3) {
insert(3, "-")
}
}
TextFieldState 데이터는 유지아키텍처 구조에 맞춰 텍스트 필드 상태의 위치를 결정합니다.

rememberTextFieldState() 사용TextFieldState를 ViewModel 프로퍼티로 직접 선언SavedStateHandle.saveable(saver = TextFieldState.Saver) 사용class FormViewModel : ViewModel() {
val username = TextFieldState()
val uiState: StateFlow<FormUiState> = ...
}
class FormViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
val username: TextFieldState = savedStateHandle.saveable(
key = "username",
saver = TextFieldState.Saver,
init = { TextFieldState() }
)
}
ViewModel 레이어에 Compose UI 런타임 종속성을 배제하고 순수 Kotlin 코루틴/Flow만 유지해야 한다면 Conforming 패턴을 적용합니다.

rememberTextFieldState()로 텍스트 필드의 단일 진실 공급원(Single Source of Truth) 상태 유지LaunchedEffect와 snapshotFlow를 사용하여 텍스트 변화를 감지하고 ViewModel 메서드로 전달@Composable
fun LoginScreen(viewModel: LoginViewModel) {
val usernameState = rememberTextFieldState(viewModel.initialUsername)
LaunchedEffect(usernameState, viewModel) {
snapshotFlow { usernameState.text.toString() }
.drop(1) // 최초 초기값 방출을 무시하여 ViewModel 상태 덮어쓰기 방지
.collect { viewModel.onUsernameChanged(it) }
}
BasicTextField(state = usernameState)
}
.drop(1) 호출 이유:
snapshotFlow는 수집을 시작할 때 현재 상태의 초기값을 즉시 방출합니다. ViewModel 초기값으로 텍스트 필드를 생성한 경우, 이 첫 번째 방출이 ViewModel로 역전달되어 중복 이벤트나 덮어쓰기 문제를 일으키므로.drop(1)로 초기 방출을 건너뜁니다.
Compose 1.12부터 서식 있는 텍스트 편집 API를 지원합니다.

AnnotatedString: 불변(Immutable) 객체. 글자를 입력할 때마다 문자열 전체와 스타일 리스트를 새로 복사·할당하므로 메모리 부하 발생TextFieldBuffer: 가변 인플레이스(In-place) 엔진 탑재. 텍스트 입력 중에도 메모리 재할당 없이 스타일 추가, 수정, 삭제 가능
특정 텍스트에 스타일을 지정한 후 경계 지점에서 타이핑을 이어갈 때 스타일을 유지할지 여부를 ExpandPolicy로 제어합니다.


Style rich text input : https://developer.android.com/develop/ui/compose/text/rich-text-input
사용자가 선택한 영역의 볼드 스타일을 켜고 끄는 구현입니다

fun TextFieldBuffer.toggleBoldSelection() {
if (selection.collapsed) return
val boldSpans = getSpandStyles(selection)
if (!isFullyBold(boldSpans, selection)) {
// 볼드가 아니면 선택 영역에 볼드 스타일 적용
addStyle(
SpanStyle(fontWeight = FontWeight.Bold),
selection,
ExpandPolicy.AtEnd
)
} else {
unboldSelection(boldSpans.first())
}
}
**단어** 입력 시 마크다운 기호를 제거하고 볼드 서식을 적용합니다

InputTransformation을 활용하여 텍스트가 상태에 반영되기 전 아스테리스크(**)를 제거하고 내부 텍스트에 볼드 스타일 적용검색창에 입력한 단어와 일치하는 본문 텍스트를 노란색 배경으로 강조 표시합니다

OutputTransformation 사용MutableState 기반 검색 쿼리 변경 시 스냅샷 시스템이 감지하여 하이라이트 자동 재계산Compose 팀에서 개발 중인 신규 라이브러리 프리뷰입니다.

Subscribe to this blog via RSS.
[요약] Building reliable text editing experiences in Compose (Android Dev Summit '26)
Posted on 12 Nov 2026[요약] Agentic engineering with Claude Code and Android CLI (Android Dev Summit '26)
Posted on 05 Nov 2026[요약] The latest in Android dev tools (Android Dev Summit '26)
Posted on 31 Oct 2026