XML
[Android] 양방향 데이터 바인딩으로 EditText 중복 여부 확인하기
Marchbreeze
2025. 5. 14. 03:40
2023년 8월 7일에 작성되었던 글입니다.
구현 사진
온보딩 화면에서 추천인 아이디 입력 및 진행 버튼 동작을 다음과 같이 개선했습니다.
- 입력 필드가 빈칸일 때 완료 버튼을 실시간으로 비활성화했습니다.
- 입력 필드가 포커스 상태일 때 배경색이 변경되도록 처리했습니다.
- 존재하지 않는 아이디 입력 후 버튼 클릭 시 빨간색 배경과 에러 메시지를 표시하고 화면 이동을 막았습니다.
- 존재하는 아이디 입력 후 버튼 클릭 시 다음 화면으로 정상 전환했습니다.
1. Binding이란?
뷰 바인딩(View Binding)
- 뷰 바인딩은 레이아웃 XML에 선언된 뷰를 타입 안전하게 참조할 수 있도록, 컴파일 시점에 각 레이아웃 파일에 대응하는 Binding 클래스를 자동 생성해 주는 기능입니다.
- 기존의 findViewById() 호출 없이도, null 가능성을 제거하고 바로 뷰 프로퍼티에 접근할 수 있습니다.
- 모듈의 build.gradle에 viewBinding { 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가 직관적으로 변화하는 온보딩 화면을 완성했습니다.