코틀린 & 컴포즈 & Java/컴포즈 Compose

[Compose] Compose에서 클릭시 이전 데이터가 찍히는 문제

코딩하는후운 2025. 7. 31. 09:42
반응형

.pointerInput(Unit) 사용시 주의할 점

클릭 작업 중에 onLongPress를 하기위해 pointerInput을 사용했었는데..! 문제가 발생 했다.

 

1. 문제 상황

Compose에서 아래처럼 pointerInput 안에서 originalItem을 직접 참조하면

리컴포즈 후에도 클릭 시 이전 데이터가 찍히는 현상이 발생할 수 있습니다.

Box(
    modifier = Modifier.pointerInput(Unit) {
        detectTapGestures(
            onTap = {
                println("클릭 시 originalItem: $originalItem") // ⛔ 옛날 값 찍힘
            }
        )
    }
)

2. 왜 이런 문제가 생길까? (원인)

1) pointerInput의 key 문제

  • pointerInput(Unit)은 key가 Unit이므로 최초 한 번만 실행
  • 이 안의 람다는 생성 시점의 originalItem을 캡처(클로저)함
  • 이후 리컴포즈가 되어도 pointerInput이 재생성되지 않으면 여전히 옛날 값 참조

2) Compose의 상태 갱신 방식

  • Compose는 값이 바뀌면 UI를 리컴포즈하지만,
  • remember / pointerInput처럼 key가 변하지 않으면 내부 람다는 그대로 유지
  • 그래서 람다 안에서 외부 변수를 직접 참조하면 stale(오래된 값) 문제가 생김

 

3. 해결 방법

방법 1: pointerInput key에 상태값 넣기

.pointerInput(originalItem) {
    detectTapGestures {
        println(originalItem) // 항상 최신 값
    }
}
  • key가 바뀌면 pointerInput 재생성
  • 단점: originalItem이 자주 바뀌면 매번 재생성되어 성능 부담

방법 2: rememberUpdatedState 사용

val currentItem by rememberUpdatedState(originalItem)

.pointerInput(Unit) {
    detectTapGestures {
        println(currentItem) // 최신 값
    }
}
  • key는 Unit 그대로 두고
  • 내부에서 항상 최신 값을 참조할 수 있도록 래핑

방법 3: 클릭 로직을 외부로 분리 (추천)

val onClick = { println(originalItem) } // 리컴포즈마다 최신 originalItem 캡처

.pointerInput(Unit) {
    detectTapGestures(
        onTap = { onClick() }
    )
}
  • pointerInput은 터치 감지만 담당
  • 실제 데이터 참조는 외부 람다에서 처리 → 항상 최신 값

4. 더 안전하게: 확장함수로 추출

저는 아예 pointerInput을 확장함수로 만들어서
외부 람다를 항상 최신 상태로 호출하도록 처리했습니다.

@Composable
fun Modifier.throttleTapWithPress(
    skipMs: Long = 300,
    onPress: (Boolean) -> Unit = {},
    onLongPress: () -> Unit = {},
    onTap: (tapOffset: Offset) -> Unit,
): Modifier {
    var isClickable by remember { mutableStateOf(true) }
    val coroutineScope = rememberCoroutineScope()
    val currentOnTap by rememberUpdatedState(onTap)
    val currentOnPress by rememberUpdatedState(onPress)
    val currentOnLongPress by rememberUpdatedState(onLongPress)

    return pointerInput(Unit) {
        detectTapGestures(
            onPress = {
                try {
                    currentOnPress(true)
                    tryAwaitRelease()
                } finally {
                    currentOnPress(false)
                }
            },
            onTap = { tapOffset ->
                if (isClickable) {
                    isClickable = false
                    currentOnTap(tapOffset)
                    coroutineScope.launch {
                        delay(skipMs)
                        isClickable = true
                    }
                }
            },
            onLongPress = {
                currentOnLongPress()
            }
        )
    }
}

사용시

Box(
    modifier = Modifier
        .throttleTapWithPress(
            onTap = { println(originalItem) }, // 항상 최신 값
            onLongPress = { println("롱프레스") }
        )
)

 

요약

  • 문제: pointerInput(Unit) 안에서 상태값을 직접 참조하면 옛날 값이 찍힘
  • 원인: key가 변하지 않아 람다가 재생성되지 않고, 초기 캡처값만 유지됨
  • 해결:
    1. key에 상태값 넣기 → pointerInput 재생성
    2. rememberUpdatedState로 항상 최신 값 참조
    3. 클릭 로직을 외부 람다로 분리(추천)
    4. 확장함수로 만들고 내부에서 rememberUpdatedState로 방어
반응형