질문 정리

연속된 버튼 클릭, api 요청 처리법

five2week 2024. 4. 24. 13:02

해당 기능이 필요하게 된 이유

현재 개발중인 앱에서 회원가입 버튼을 연속으로 누르는 경우, 요청을 두번하게 된다. 이 문제를 해결하기 위해서는 이벤트 처리 시점에 추가적인 상태 관리 또는 조건 검사를 통해 중복된 이벤트의 실행을 방지하는 방법이 필요합니다.

하지만 무조건 연속된 클릭을 막아야할까요? 제가 생각한 클릭의 종류는 3가지였습니다. 연속된 클릭을 허용하면 안되는 버튼, 연속된 클릭을 해도 상관없는 버튼, 연속된 클릭이 필요한 버튼입니다.

이 부분 중 연속된 클릭을 허용하면 안되는 버튼의 예시로는 회원가입 버튼 등이 있습니다.

생각한 버튼 연타 방지 기법

  1. Debounce 기법 사용:
    • 사용자의 입력에 대해 일정 시간 동안의 입력을 무시하고, 마지막 입력만 처리하는 방법입니다. 이를 통해 사용자가 버튼을 빠르게 여러 번 탭했을 경우, 일정 시간 내의 모든 탭을 하나의 이벤트로 취급하여 중복 실행을 방지할 수 있습니다.
  2. 상태 플래그 관리:
    • 이벤트가 발생했을 때, 특정 플래그(예: isNavigating)를 설정하여 추가적인 이벤트 처리를 막는 방법입니다. 이벤트 처리가 완료되면 플래그를 초기화하여 다음 이벤트를 받을 준비를 합니다.
  3. Unique 처리 기법:
    • 각 이벤트에 고유 식별자(예: 타임스탬프)를 부여하고, 최근 처리된 이벤트와 비교하여 중복된 이벤트의 처리를 거부하는 방법입니다.

Debounce 기법 적용

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce

class LoginFormViewModel : ViewModel() {
    private val _navigationEvent = MutableStateFlow<Event<NavigationTarget>?>(null)
    val navigationEvent = _navigationEvent.asStateFlow().debounce(1000)

    fun updateNavigationTarget(target: NavigationTarget) {
        _navigationEvent.value = Event(target)
    }
}

Debounce 기법 적용의 문제점

debounce에 대한 시간을 길게 잡으면, 사용자가 기다리는 시간이 길어집니다. 반면에 시간을 적게 잡으면 회원가입 중복 요청을 방지할 수 없습니다. 적절한 시간을 알 수 없어 다음 방법으로 넘어갔습니다.

상태 플래그 관리 방법 적용

class LoginFormViewModel : ViewModel() {
    private var isNavigating = false
    private val _navigationEvent = MutableStateFlow<Event<NavigationTarget>?>(null)
    val navigationEvent: StateFlow<Event<NavigationTarget>?> = _navigationEvent.asStateFlow()

    fun updateNavigationTarget(target: NavigationTarget) {
        if (!isNavigating) {
            isNavigating = true
            _navigationEvent.value = Event(target)
        }
    }

    fun resetNavigation() {
        isNavigating = false
    }
    
    // ...
}
@Composable
fun LoginActivityContent(viewModel: LoginFormViewModel) {
    val navigationEvent by viewModel.navigationEvent.collectAsState()
    val context = LocalContext.current

    LaunchedEffect(navigationEvent) {
        navigationEvent?.getContentIfNotHandled()?.let { target ->
            if (!viewModel.isNavigating) {
                viewModel.isNavigating = true
								// ...
                viewModel.resetNavigation()
            }
        }
    }
    // ...
}

제가 원하던 방식으로 잘 작동해서 해당 방법을 프로젝트에 적용했습니다.