[Android] Compose 중복 클릭 방지하기 (feat. Monkey Test)

김준영·2024년 7월 27일
0

Android

목록 보기
14/17
post-thumbnail

서론

이걸 이렇게 쓴다고...?!

개발하다보면 정말 예기치 않는 오류들을 마주 할 때가 많습니다
그 중 가장 찾기 힘든것이 개발자가 의도한 시나리오와 전혀 다른, 좋게말하면 창의적인 플로우 방식으로 서비스를 이용한다는 것입니다

기억나는일 중에 프로젝트 최종발표 시연시간에
컴포넌트 클릭을 연타하시는 분이 있어서 많이 당황한 기억이 있습니다ㅎㅎㅎ (오늘 주제랑 직결되는 상황..)

이런 오류를 최소화하고자 등장한 것이
몽키 테스트인데요! 말그대로 원숭이가 테스트 하듯이
서비스에 대한 사전지식이 전혀없는 상태로 막말로 무식하게 사용하면서 오류를 잡아내는 행위를 말합니다

사용자는 개발자의도대로 서비스를 이용한다는 이상적이고 아름다운 생각은 최대한 버려야되는 것 같습니다 😂

중복클릭 현상


그 중 가장 흔한 오류 중 하나인 중복 클릭을 방지해보겠습니다!
위 화면과 같이 연속으로 2번클릭하면 화면이 2개 나오는 의도치 않는 상황이 나오게 됩니다

만약 저 화면이 단순히 UI를 보여주는 것이 아닌, 결제완료창으로 넘어가는 것 같은 중요한 화면이라면 어떻게될까요?
최악의 상황은 결제가 2번되는 현상이 벌어지거나 추가적인 오류가 발생할 수 있습니다 (극단적인 예시이긴 하네요)

아무튼! 쓸데없는 자원을 잡아먹기도하고
이런오류는 최소화하는 것이 매우 중요합니다
이를 Compose에서 방지하는 코드를 알아보겠습니다!

코드

internal interface MultipleEventsCutter {
    fun processEvent(event: () -> Unit)

    companion object
}

internal fun MultipleEventsCutter.Companion.get(): MultipleEventsCutter =
    MultipleEventsCutterImpl()

private class MultipleEventsCutterImpl : MultipleEventsCutter {
    private val now: Long
        get() = System.currentTimeMillis()

    private var lastEventTimeMs: Long = 0

    override fun processEvent(event: () -> Unit) {
        if (now - lastEventTimeMs >= 300L) {
            event.invoke()
        }
        lastEventTimeMs = now
    }
}

가장 중요하게 봐야할 부분을 보겠습니다

override fun processEvent(event: () -> Unit) {
        if (now - lastEventTimeMs >= 300L) {
            event.invoke()
        }
        lastEventTimeMs = now
    }

processEvent()라는 함수는 추후 클릭이벤트에 적용될 함수입니다
now(현재 시각)-lastEventTimeMs(클릭이벤트의 마지막 시간)의 의미는 이벤트가 마지막으로 처리된 시간 300ms가 지나야만 클릭이벤트가 발생하도록 구현되어있습니다.

코드를 자세히 보시면 lastEventTimeMs의 초기값은 0이고 최초 클릭이벤트가 발생하면 당연히 300ms보다 크기때문에 이벤트가 발생합니다.
이후 lastEventTimeMs값이 마지막처리된 값(now)이 되며 300ms이내에 클릭을 한다면 조건문을 통과하지 못하게 되는로직이라는 것을 이해할 수 있습니다

300ms라는 값이 절대적인 기준은 아니지만 일반적으로 300ms정도면 적당한 값인 것 같습니다! (이건 개발 프로세스에 따라 달라질 수 있을 것 같아요)

다음은 해당 함수를 적용하는 Modifier 확장함수를 보겠습니다

@Composable
fun Modifier.clickableSingle(
    enabled: Boolean = true,
    onClickLabel: String? = null,
    role: Role? = null,
    onClick: () -> Unit
) = composed(
    inspectorInfo = debugInspectorInfo {
        name = "clickable"
        properties["enabled"] = enabled
        properties["onClickLabel"] = onClickLabel
        properties["role"] = role
        properties["onClick"] = onClick
    }
) {
    val multipleEventsCutter = remember { MultipleEventsCutter.get() }
    Modifier.clickable(
        enabled = enabled,
        onClickLabel = onClickLabel,
        onClick = { multipleEventsCutter.processEvent { onClick() } },
        role = role,
        indication = LocalIndication.current,
        interactionSource = remember { MutableInteractionSource() }
    )
}

clickable를 구현하기 위한 기본적인 메타데이터를 설정한 후
onClick부분에 방금 구현한 코드를 적용하면 됩니다!

구현

Box(
        (...)
        .clickableSingle { onEventSend(HomeContract.Event.NavigationToPlaceDetail(place)) }

기존 clickable에서 구현한 Modifier확장함수인 clickableSingle을 적용해주면 끝입니다!

결론

어떻게보면 놓치기 쉽지만 상당히 중요한 오류인 것 같아
주의해야할 부분인 것 같습니다
개발하면서 놓치기 쉬운 부분을 최대한 잡기위해 생각하고 노력해야 할 것 같습니다!

참고

https://al-e-shevelev.medium.com/how-to-prevent-multiple-clicks-in-android-jetpack-compose-8e62224c9c5e

profile
Android, Flutter를 공부하고 있습니다🧐

0개의 댓글

관련 채용 정보