데이터 바인딩(Data Binding)은 데이터를 UI 요소와 연결하는 기술입니다. 이 방식을 사용하면 입력 요소가 업데이트될 때마다 그 뒤에 있는 데이터도 함께 갱신되므로, 코드를 훨씬 깔끔하게 관리할 수 있습니다.
사실 데이터 바인딩은 전혀 새로운 개념이 아닙니다. AngularJS, React, Vue 같은 프론트엔드 프레임워크들은 이미 오래전부터 이 개념을 핵심 설계에 도입해 왔습니다.
하지만 이 글의 초점은 프론트엔드 프레임워크가 아니라 모바일 개발에 있습니다. 구글은 안드로이드 Jetpack의 일부로 Data Binding Library를 제공하며, 이를 활용하면 반복적인 코드 작성을 크게 줄일 수 있습니다.
Jetpack 라이브러리에 익숙하지 않다면, 구글이 기존 서포트 라이브러리(Support Library) 개발을 중단하고 AndroidX 라이브러리(서포트 라이브러리의 최신 버전)로 전환하고 있다는 점을 기억해 두시기 바랍니다.
어댑터(Adapter)를 활용한 데이터 바인딩 설명은 이미 많은 글에서 다루고 있기 때문에, 이 글에서는 어댑터 없이 최소한의 코드로 데이터 바인딩을 구현하는 가장 기본적인 방법을 소개하겠습니다.
왜 데이터 바인딩을 사용해야 할까요?
아직 확신이 서지 않는다면, 간단한 예제를 통해 데이터 바인딩의 장점을 살펴보겠습니다. 세 개의 커스텀 버튼으로 구성된 메뉴 화면이 있고, 각 버튼은 자체적인 레이아웃을 가지고 있다고 가정해 봅시다.

이 화면을 만드는 한 가지 방법은 네 개의 서로 다른 XML 레이아웃 파일을 작성하는 것입니다. 하나는 메인 레이아웃용, 나머지 셋은 각각의 버튼용입니다.
버튼마다 사용자를 앱의 다른 화면으로 안내해야 하므로, 각각 다른 텍스트와 다른 이미지가 필요하기 때문입니다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_gravity="center_horizontal"
android:layout_height="wrap_content"
android:layout_width="wrap_content">
<ImageView
android:id="@+id/imageView"
android:layout_height="100dp"
android:layout_width="100dp"
android:src="@drawable/image_name"
android:adjustViewBounds="true"
android:scaleType="centerInside"
/>
<TextView
android:id="@+id/textView"
android:gravity="center_horizontal"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:text="Image Text"
android:textSize="16sp" />
</LinearLayout>물론 레이아웃이 세 개뿐이라 코드 중복이 크게 느껴지지 않을 수 있습니다. 하지만 잘 생각해 보면, 이런 반복 코드를 일일이 관리하는 것은 상당히 비효율적입니다. 제품 목록과 이미지를 보여주는 것처럼 더 복잡한 레이아웃을 가진 애플리케이션이라면, 지루한 코드 복제가 눈덩이처럼 불어날 것입니다.
반면 데이터 바인딩을 활용하면, 단 하나의 XML 레이아웃만 만들어 모든 버튼에 재사용할 수 있습니다.
어디서부터 시작해야 할까?
먼저 프로젝트에서 데이터 바인딩을 활성화해야 합니다. 애플리케이션의 build.gradle 파일에 dataBinding 요소를 추가하면 됩니다.
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
defaultConfig {
applicationId "com.tomerpacific.example"
minSdkVersion 15
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
dataBinding { //<-------
enabled = true
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}프로젝트를 동기화한 후에는, 레이아웃에 바인딩할 데이터 클래스를 생성합니다.
package com.tomerpacific.example
import android.graphics.drawable.Drawable
data class ButtonData(val buttonText: String, val buttonImageSrc : Drawable)ButtonData 클래스에는 두 개의 필드가 있습니다.
buttonText— 이미지 아래에 표시될 텍스트입니다.buttonImageSrc— 버튼의 이미지를 담당합니다.
더 많은 데이터가 필요하다면, 데이터 클래스에 필드를 추가하기만 하면 됩니다.
실제 바인딩 구현하기
다음은 레이아웃에서 변수를 선언하여 사용할 수 있도록 만드는 단계입니다. 이 변수는 앞서 만든 데이터 클래스와 연결됩니다. 이를 위해 두 가지 작업이 필요합니다.
- 루트 레이아웃 요소를
<layout>태그로 감쌉니다. - 변수(
buttonData) 선언을 담을<data>태그를 추가합니다.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="https://schemas.android.com/apk/res/android"> // <---- 1
<data>
<variable name="buttonData" type="com.tomerpacific.example.ButtonData"/> // <---- 2
</data>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:app="https://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Example"
android:textSize="30dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/linearLayout3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView2">
<ImageButton
android:layout_width="100dp"
android:layout_height="100dp"
android:adjustViewBounds="true"
android:scaleType="centerInside"
android:src="@drawable/android">
</ImageButton>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="Image Text"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/linearLayout3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView2"
app:layout_constraintVertical_bias="0.504">
<ImageButton
android:layout_width="100dp"
android:layout_height="100dp"
android:adjustViewBounds="true"
android:scaleType="centerInside"
android:src="@drawable/android_p_logo">
</ImageButton>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="Image Text"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/linearLayout2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.200"
app:layout_constraintStart_toEndOf="@+id/linearLayout3"
app:layout_constraintTop_toBottomOf="@+id/textView2"
app:layout_constraintVertical_bias="0.504">
<ImageButton
android:layout_width="100dp"
android:layout_height="100dp"
android:adjustViewBounds="true"
android:scaleType="centerInside"
android:src="@drawable/android_studio_icon">
</ImageButton>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="Image Text"
android:textSize="16sp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>루트 <layout> 태그로 스키마가 옮겨졌으므로, 기존 메인 레이아웃의 스키마 선언은 제거해도 됩니다. 또한 추가한 변수가 데이터 클래스와 직접 연결된다는 점에 주목하세요.
이제 MainActivity 파일에 바인딩을 처리하는 코드를 추가해야 합니다.
package com.tomerpacific.example
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.databinding.DataBindingUtil
import com.tomerpacific.example.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(
this, R.layout.activity_main)
binding.buttonData = ButtonData("First", resources.getDrawable(R.drawable.android))
}
}레이아웃 내에 변수를 선언하면, 해당 레이아웃을 위한 바인딩 클래스가 자동으로 생성됩니다. 이 예제에서 레이아웃 이름은 activity_main이므로, 바인딩 클래스 이름은 ActivityMainBinding이 됩니다.
네이밍 규칙은 항상 '레이아웃 이름 + Binding'입니다.
레이아웃에서 buttonData라는 변수를 선언했기 때문에, 이 변수는 바인딩 객체에 포함되며 ButtonData 클래스의 새 인스턴스를 할당할 수 있습니다.
여기까지 완료했다면, 드디어 레이아웃에서 방금 바인딩한 데이터를 사용할 수 있습니다.
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/linearLayout3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView2">
<ImageButton
android:layout_width="100dp"
android:layout_height="100dp"
android:adjustViewBounds="true"
android:scaleType="centerInside"
android:src="@{buttonData.buttonImageSrc}"> // <----
</ImageButton>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="@{buttonData.buttonText}" // <----
android:textSize="16sp" />실행 결과는 다음과 같습니다.

잠깐, 문제가 있습니다
버튼은 세 개인데 데이터 클래스는 하나의 버튼에만 사용할 수 있다면 어떻게 해야 할까요?
이럴 때는 여러 버튼 데이터를 담는 리스트 형태의 클래스를 만들어 해결할 수 있습니다.
package com.tomerpacific.example
data class ButtonsData(val buttonsData : List<ButtonData>) {
fun get(index: Int) : ButtonData {
return buttonsData.get(index)
}
}레이아웃 내에서 사용할 때 이 메서드가 인식되어야 하기 때문에, get 메서드를 오버라이드해야 합니다.
그다음 activity_main.xml의 참조를 변경합니다.
<data>
<variable name="buttonsData" type="com.tomerpacific.example.ButtonsData"/>
</data>새로운 데이터 클래스에 대한 바인딩도 새로 생성해야 합니다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(
this, R.layout.activity_main)
val firstButton : ButtonData = ButtonData("First", resources.getDrawable(R.drawable.android))
val secondButton : ButtonData = ButtonData("Second", resources.getDrawable(R.drawable.android_p_logo))
val thirdButton : ButtonData = ButtonData("Third", resources.getDrawable(R.drawable.android_studio_icon))
val buttonsData : ButtonsData = ButtonsData(listOf(firstButton, secondButton, thirdButton))
binding.buttonsData = buttonsData
}ButtonData 클래스의 인스턴스 세 개를 생성한 뒤, 이를 담는 ButtonsData 객체를 만들어 바인딩 객체에 연결합니다.
마지막으로, 이제 레이아웃에서 데이터 클래스를 제대로 활용할 수 있습니다.
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/linearLayout3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView2">
<ImageButton
android:layout_width="100dp"
android:layout_height="100dp"
android:adjustViewBounds="true"
android:scaleType="centerInside"
android:src="@{buttonsData.get(0).buttonImageSrc}"> // <-------
</ImageButton>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="@{buttonsData.get(0).buttonText}" // <--------
android:textSize="16sp" />
</LinearLayout>
마치며
이 글은 필자가 다음 애플리케이션을 개발하면서 얻은 경험을 바탕으로 작성되었습니다.
(어쩌면 여러분에게도 유용할지 모르니 소개해 봅니다)
Laundry Symbols — Google Play 앱: 세탁 기호의 의미를 알기 어려웠던 적이 있나요? 이 앱은 사용하기 쉬운 인터페이스와 그림과 함께 낯선 세탁 기호들의 의미를 모두 설명해 줍니다.
tomerpacificApps on Google Play
전체 소스 코드는 GitHub에서 확인할 수 있습니다:
TomerPacific/LaundrySymbols — 다양한 세탁 기호와 그 설명을 보여주는 Kotlin으로 작성된 애플리케이션입니다.
TomerPacificGitHub