XML

[Android] 양방향 데이터 바인딩으로 EditText 중복 여부 확인하기

Marchbreeze 2025. 5. 14. 03:40
2023년 8월 7일에 작성되었던 글입니다.

 

구현 사진

 

온보딩 화면에서 추천인 아이디 입력 및 진행 버튼 동작을 다음과 같이 개선했습니다.

  • 입력 필드가 빈칸일 때 완료 버튼을 실시간으로 비활성화했습니다.
  • 입력 필드가 포커스 상태일 때 배경색이 변경되도록 처리했습니다.
  • 존재하지 않는 아이디 입력 후 버튼 클릭 시 빨간색 배경과 에러 메시지를 표시하고 화면 이동을 막았습니다.
  • 존재하는 아이디 입력 후 버튼 클릭 시 다음 화면으로 정상 전환했습니다.

 

 


1. Binding이란?

뷰 바인딩(View Binding)

  • 뷰 바인딩은 레이아웃 XML에 선언된 뷰를 타입 안전하게 참조할 수 있도록, 컴파일 시점에 각 레이아웃 파일에 대응하는 Binding 클래스를 자동 생성해 주는 기능입니다.
  • 기존의 findViewById() 호출 없이도, null 가능성을 제거하고 바로 뷰 프로퍼티에 접근할 수 있습니다.
  • 모듈의 build.gradleviewBinding { enabled = true } 한 줄만 추가하면, ActivityMainBinding, FragmentHomeBinding 같은 클래스를 바로 사용할 수 있습니다.
  • 코드 가독성과 안정성을 크게 높이며, 런타임 크래시를 사전에 방지할 수 있습니다.

 

데이터 바인딩(Data Binding)

  • 데이터 바인딩은 XML 레이아웃 안에 뷰와 데이터(주로 ViewModel)를 직접 연결하는 기능입니다.
  • 레이아웃 파일 최상단에 <layout> 태그를 추가하고, <data> 블록에서 변수를 선언하여 사용합니다.
  • 뷰 속성에 android:text="@{vm.userName}"처럼 바인딩 표현식(@{})을 사용하면, ViewModel의 프로퍼티가 변경될 때 UI가 자동으로 갱신됩니다.
  • 추가로 @BindingAdapter 어노테이션을 활용해, ImageView에 URL을 로딩하거나 커스텀 속성을 처리하는 등 확장성 있는 바인딩 로직을 구현할 수 있습니다.

 

양방향 데이터 바인딩(Two-Way Data Binding)

  • 양방향 데이터 바인딩은 사용자가 입력한 값과 ViewModel 프로퍼티를 양쪽으로 동기화해 주는 기능입니다.
  • EditText 같은 입력 뷰에 android:text="@={vm.inputText}"처럼 @={} 구문을 사용합니다.
  • 이를 통해 버튼 활성화/비활성화, 실시간 유효성 검사 같은 UI 반응을 매우 간단한 바인딩 표현식만으로 구현할 수 있습니다.

 

 


2. 배경 리소스 생성

(1) 포커스 대응용 Selector 설정

XML의 <selector>는 상태별로 다른 Drawable을 적용하기 위한 리소스이며, 사용자가 뷰를 터치하거나 포커스될 때 배경을 쉽게 전환할 수 있도록 해 줍니다.

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 포커스 상태일 때 -->
    <item android:state_focused="true">
        <shape android:shape="rectangle">
            <stroke android:width="1dp" android:color="@color/grayscales_600" />
            <corners android:radius="8dp" />
            <solid android:color="@color/grayscales_900" />
        </shape>
    </item>
    <!-- 포커스가 아닐 때 -->
    <item android:state_focused="false">
        <shape android:shape="rectangle">
            <stroke android:width="1dp" android:color="@color/grayscales_600" />
            <corners android:radius="8dp" />
            <solid android:color="@color/black" />
        </shape>
    </item>
</selector>

 

(2) 에러 시 대체할 배경 리소스

에러 상태일 때 입력 필드에 빨간 테두리와 반투명 배경을 적용했습니다.

<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <stroke
        android:width="1dp"
        android:color="@color/semantic_red_500" />
    <corners android:radius="8dp" />
    <solid
        android:color="#33F04646" />
</shape>

 

 


3. 양방향 데이터바인딩 적용

(1) data variable 설정

양방향 데이터바인딩은 View와 ViewModel 간에 데이터 흐름을 양쪽으로 자동 동기화해 주는 기능입니다.

XML 속성에 @={} 구문을 사용하면, 값 변경 시 ViewModel과 View가 서로의 상태를 즉시 반영할 수 있습니다.

<data>
  <import type="android.view.View" />
  <variable
      name="vm"
      type="com.el.yello.presentation.onboarding.activity.OnBoardingViewModel" />
</data>

 

액티비티 또는 프래그먼트에서 해당 binding과 viewModel을 연결을 해주어야 정상적으로 작동됩니다.

binding.vm = viewModel
  • XML에서 <data> 블럭에서 선언했던 이름인 "vm"을, 연동된 viewModel과 연동합니다.
  • 이름을 "viewModel"로 선언하는 경우, 데이터바인딩이 아닌 뷰모델 객체를 가져오는 경우가 있어 저는 vm으로 이름을 구별하는 방식을 선호합니다.

 

(2) EditText 연결

<EditText
    android:id="@+id/et_code"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:background="@{@drawable/sel_onboarding_edittext_focus}"
    android:hint="@string/onboarding_tv_code_hint"
    android:inputType="text"
    android:maxLength="20"
    android:text="@={vm.codeText}"
    android:textColor="@color/white"
    android:textSize="16sp"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/tv_code_warning" />
  • 앞서 작성한 Selector와 Error Shape를 EditText background에 적용했습니다.
  • @={vm.codeText}를 사용하여 ViewModel의 codeText와 EditText 텍스트를 양방향으로 바인딩했습니다.
  • 사용자가 EditText에 값을 입력하면 ViewModel의 codeText가 바로 업데이트되고, 코드에서 codeText 값을 변경하면 EditText에도 즉시 반영됩니다.

 

(3) 가이드 텍스트 및 완료 버튼 연결

<ImageView
    android:id="@+id/iv_code_delete"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@{vm.isValidCode() ? @drawable/ic_onboarding_delete : @drawable/ic_onboarding_delete_red}"
    android:visibility="@{vm.codeText.isBlank() ? View.GONE : View.VISIBLE}"
    ... />

<TextView
    android:id="@+id/tv_code_warning"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="@{vm.isValidCode() ? @string/onboarding_tv_code_skip : @string/onboarding_name_id_id_error}"
    android:textColor="@{vm.isValidCode() ? @color/grayscales_500 : @color/semantic_red_500}"
    ... />
    
<TextView
    android:id="@+id/btn_code_next"
    android:layout_height="wrap_content"
    android:background="@{!vm.codeText.isEmpty()? @drawable/shape_yello500_fill_100_rect : @drawable/shape_grayscales800_fill_100_rect}"
    android:clickable="@{!vm.codeText.isEmpty()}"
    android:textColor="@{!vm.codeText.isEmpty()? @color/black : @color/grayscales_700}"
    tools:background="@drawable/shape_yello500_fill_100_rect" 
    ... />
  • XML 안의 @{ ... } 구문은 데이터바인딩 표현식입니다.이를 통해 View의 속성 값을 ViewModel의 함수 호출이나 조건식 결과에 따라 실시간으로 변경할 수 있습니다.
  • ? : 연산자는 삼항 연산자와 유사하게, 조건에 따라 두 값 중 하나를 반환합니다.

 

 


4. ViewModel 통신 및 상태 처리

MutableLiveData를 활용하여 양방향 데이터 바인딩에 연결할 값을 저장하는 방식을 활용했습니다.

// ViewModel
val codeText = MutableLiveData("")

private val _getValidYelloId = MutableLiveData<UiState<Boolean>>()
val getValidYelloId: LiveData<UiState<Boolean>> get() = _getValidYelloId

fun getValidYelloId(unknownId: String) {
    viewModelScope.launch {
        onboardingRepository.getValidYelloId(unknownId)
            .onSuccess { isValid ->
                _getValidYelloId.value = UiState.Success(isValid)
            }
            .onFailure { t ->
                _getValidYelloId.value = UiState.Failure(t.code().toString())
            }
    }
}
  • codeText: 사용자가 입력한 추천인 아이디를 저장하는 MutableLiveData입니다.
  • _getValidYelloId: 서버로부터 아이디 유효 여부를 가져올 때 로딩·성공·실패 상태를 담는 MutableLiveData입니다.

 

 


5. 버튼 클릭 및 에러 UI 대응

// Fragment
binding.btnCodeNext.setOnSingleClickListener {
    viewModel.getValidYelloId(viewModel.codeText.value.orEmpty())
}

private fun setupGetValidYelloIdState() {
    viewModel.getValidYelloId.observe(viewLifecycleOwner) { state ->
        when (state) {
            is UiState.Success -> {
                if (!state.data) {
                    // 중복 확인 실패(false) 시 에러 표시
                    initIdEditTextViewError()
                } else {
                    // 중복 확인 통과 시 회원가입 진행
                    viewModel.postSignup()
                }
            }
            is UiState.Failure -> {
                yelloSnackbar(binding.root, getString(R.string.msg_error))
            }
        }
    }
}

private fun initIdEditTextViewError() {
	with(binding) {
        etCode.setBackgroundResource(R.drawable.shape_onboarding_error)
        ivCodeDelete.setBackgroundResource(R.drawable.ic_onboarding_delete_red)
        tvIdError.text = getString(R.string.onboarding_code_duplicate_msg)
        tvIdError.setTextColor(ContextCompat.getColor(requireContext(), R.color.semantic_red_500))
    }
}
  • btnCodeNext 클릭 시 getValidYelloId를 호출하여 서버 통신을 시작했습니다.
  • 서버 응답이 true일 경우 다음 회원가입 API (postSignup())를 호출했습니다.
  • 서버 응답이 false일 경우 initIdEditTextViewError()를 호출해 에러 배경·아이콘·텍스트 색상을 모두 변경했습니다.
  • 이후 EditText에 새로운 값이 입력되게 되면, 양방향 데이터바인딩을 통해 에러 메세지와 배경 등이 모두 사라지게 됩니다.

 


이로써 사용자의 입력 상태와 서버 응답에 따라 UI가 직관적으로 변화하는 온보딩 화면을 완성했습니다.