SOPT 30기 Android 파트 세미나 과제입니다.
github: https://github.com/KINGSAMJO/iOS_Seunghyeon
해당 주차 브랜치(ex. seminar/1)에서 각 세미나별 과제 코드를 확인할 수 있습니다.
github를 통해 코드를 보시는 것을 추천드립니다.
SOPT에서는 매주 세미나가 진행되며, 각 세미나 내용과 관련한 요구사항의 과제가 마찬가지로 매주 있습니다. 파트별로 구체적인 과제 내용은 파트별로 다르지만, 현재 30기 Android 파트에서는 매주 배운 내용을 기반으로 하여 필수과제, 성장과제, 도전과제 3가지로 난이도가 분류되어 과제가 나옵니다.
SOPT Android 파트의 1주차 과제는 어떤지, 제 코드를 기반으로 함께 공부해보는 시간을 가져보겠습니다. 저는 3기수 연속으로 SOPT에서 Android를 공부하고 있기 때문에 과제에서 요구하는 사항 이상의, 평소 공부해보고 싶었던 내용을 세미나 과제에 접목시켜 구현해 보았습니다. 과제의 요구사항 이상의 내용들은 별도의 포스트를 통해 리뷰해보는 시간을 가져보겠습니다.
필수과제란 해당 주차 세미나의 내용을 기반으로 한, 모든 파트원들이 "최소한" 필수적으로 구현해야 하는 과제입니다. 그런만큼 난이도는 세미나 자료만으로도 구현할 수 있는 정도의 난이도입니다. 1주차 세미나 과제의 필수과제를 이제 함께 짚어보겠습니다.
필수과제 1은 로그인 화면(SignInActivity)를 구현하는 것입니다. 이 때, 과제의 요구사항은 다음과 같습니다.
제가 구현한 화면을 먼저 첨부하겠습니다.
먼저, EditText 관련한 요구사항인 필수과제 1 3번, 4번부터 보겠습니다. EditText 뿐만 아니라 Android의 다양한 View들은 사용할 수 있는 여러 속성들이 있습니다. 그 중에서 EditText의 inputType
이라는 속성과 hint
라는 속성에 대해 아는지 점검할 수 있는 과제입니다.
<EditText
android:id="@+id/et_user_id"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/text_user_id_hint"
android:importantForAutofill="no"
android:inputType="text"
android:text="@={viewmodel.userId}"
app:layout_constraintBottom_toTopOf="@id/tv_user_password"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_user_id" />
위 코드는 제가 작성한 activity_sign_in.xml
의 EditText 중 아이디를 입력하는 View입니다. 보시면 android:hint
속성에 string값을 넣은 것을 확인할 수 있습니다. Android Project의 res/values/strings.xml에 문자열을 등록하고 사용할 수 있기 때문에 저는 strings.xml에 name이 text_user_id_hint
인 LOGIN
이라는 문자열을 등록해두었고 이를 사용했습니다.
hint 속성을 사용할 경우, EditText에 text가 비어있는 상황, 즉 아무것도 입력되지 않은 상황에서 hint에 할당한 문자열이 EditText 필드에 보이게 됩니다. 그래서 hint 속성은 주로 이 EditText에 어떤 값을 입력해야 하는지에 대해 사용자에게 알려주는 역할을 합니다.
<EditText
android:id="@+id/et_user_password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/text_user_password_hint"
android:importantForAutofill="no"
android:inputType="textPassword"
android:text="@={viewmodel.userPassword}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_user_password" />
위 코드는 제가 작성한 activity_sign_in.xml
의 EditText 중 비밀번호를 입력하는 View입니다. 보시면 android:inputType
속성에 textPassword
를 넣은 것을 확인할 수 있습니다. inputType 속성의 경우, EditText의 입력 타입을 결정합니다. 대표적으로 textPassword
를 사용하면, EditText의 입력 타입을 비밀번호로 간주하여 문자열을 보여주지 않고 가려줍니다. 비밀번호로 abcd를 입력한다면, textPassword
로 인해 **** 처럼 보입니다.
다음으로 Intent
를 사용한 Activity 이동
관련한 요구사항인 필수과제 1 1번, 5번을 보겠습니다. Android에서는 Activity 전환 시 Intent라는 것을 이용합니다. Intent는 메시징 객체입니다. Intent를 이용해 다른 앱 구성요소(ex. Activity)로부터 작업을 요청합니다.
val intent = Intent(this, HomeActivity::class.java)
startActivity(intent)
위 코드는 SignInActivity.kt
의, HomeActivity를 시작하는 코드입니다. Intent 객체를 생성하고, 그 Intent 객체를 startActivity()
메서드에 넘겨주는 방식으로 구현할 수 있습니다.
Intent 객체를 생성할 때는 첫 번째 파라미터로 Context
를, 두 번째 파라미터로 Class 객체
를 제공해야 합니다. Activity에서 Intent를 이용해 다른 Activity 이동하고 싶은 경우 Activity는 자신의 context를 가지고 있기 때문에 context로 this를 넣어줄 수 있습니다. 두 번째 파라미터인 Class 객체에는 이동할 Activity의 이름 뒤에 ::class.java
를 붙여주면 됩니다. ::class.java
가 궁금하다면 Kotlin Reflection
을 검색해보시는 것을 추천드립니다.
회원가입 화면으로 이동하는 코드는 startActivity()가 아닌 다른 메서드를 사용했습니다. 이 내용은 성장과제 1. 화면 이동 + @에서 다루도록 하겠습니다.
마지막으로 Android의 Toast Message
관련한 요구사항인 필수과제 1 2번입니다. Android에서는 Toast Message라는 개념이 있습니다. Toast Message는 화면 아래에서 올라오는 메시지로, 마치 토스트 기계에서 식빵이 올라오는 모습과 비슷하다고 해서 Toast라고 불립니다.
Toast.makeText(this, "아이디/비밀번호를 확인해주세요", Toast.LENGTH_SHORT).show()
Toast Message의 사용 방법은 다음과 같습니다. Toast.makeText().show()
라는 메서드를 활용하는데, makeText()
메서드 안에 3개의 파라미터를 전달해야 합니다. 첫 번째는 Context입니다. context는 위에서 설명한 것처럼 모든 Activity는 자신의 context를 가지기 때문에 this를 넣으면 됩니다. 두 번째 파라미터는 Toast Message로 표시하고 싶은 메시지 문자열입니다. 마지막으로 세 번째 파라미터는 이 Toast Message를 얼마나 띄우고싶은지에 해당하는 표시 시간입니다. Toast Class에는 사전에 정의된 LENGTH_SHORT
와 LENGTH_LONG
이라는 값이 있습니다. 짧게 보여주고 싶다면 전자를, 오래 보여주고 싶다면 후자를 넣으면 됩니다.
아이디, 비밀번호 미입력 검사는 ViewModel에서 수행하도록 구현했습니다. 따라서 아이디, 비밀번호 미입력 검사는
도전과제 2. MVVM
에서 다루도록 하겠습니다.
필수과제 2는 회원가입 화면(SignUpActivity)를 구현하는 것입니다. 과제의 요구사항은 다음과 같습니다.
필수과제 2의 2번, 3번, 4번은 필수과제 1과 겹칩니다. 따라서 여기서는 필수과제 2 1번만 확인하겠습니다. Activity를 startActivity() 메서드를 통해 새로 시작해 이동하는 방법이 있다면, 반대로 Activity를 종료시키고 이전 화면으로 복귀하는 메서드 또한 존재합니다.
finish()
Android에서는 새로운 Activity를 시작할 때마다 기존의 Activity를 BackStack이라는 공간에 저장합니다. 그리고 현재 보여지는 Activity가 종료될 때마다 BackStack의 가장 상단에 있는 Activity를 꺼내와 다시 화면에 보여줍니다. finish() 메서드를 사용하면, 현재 Activity를 종료시키고 BackStack을 활용해 이전 화면으로 복귀합니다.
회원가입 시 이름, 아이디, 비밀번호 미입력 검사는 ViewModel에서 수행하게 구현했습니다. 따라서 이름, 아이디, 비밀번호 미입력 검사는
도전과제 2. MVVM
에서 다루도록 하겠습니다.
필수과제 3은 자기소개 페이지(HomeActivity)를 구현하는 것입니다. 과제의 요구사항은 다음과 같습니다.
제가 구현한 화면을 먼저 첨부하겠습니다.
필수과제 3의 1번을 fragment_home.xml
을 보며 확인하겠습니다. 원래는 Activity만 사용해서 구현해도 되지만, 저는 별도로 추가 구현해보고 싶은 내용이 있어 Fragment로 구현하게 되었습니다.
<ImageView
android:id="@+id/iv_profile_image"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/description_profile_image"
setProfileImage="@{viewmodel.userImage}"
app:layout_constraintBottom_toTopOf="@id/tv_profile_name"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.33" />
0dp
를 적극적으로 활용한 ImageView입니다. width
를 0dp로 설정하고 start
와 end
제약조건을 parent
에 건 후, app:layout_constraintWidth_percent
속성을 활용해 width를 33%로 설정했습니다.
위 ImageView에는
src
를 설정하는 부분이 없습니다. 이 이유는도전과제 1. DataBinding
과 관련이 있습니다. 이 부분은 추후 도전과제 설명에서 다루도록 하겠습니다.
<TextView
android:id="@+id/tv_profile_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:text="@{`이름: ` + viewmodel.user.userName}"
android:textColor="@color/black"
android:textSize="20sp"
app:layout_constraintBottom_toTopOf="@id/tv_profile_age"
app:layout_constraintEnd_toEndOf="@id/iv_profile_image"
app:layout_constraintStart_toStartOf="@id/iv_profile_image"
app:layout_constraintTop_toBottomOf="@id/iv_profile_image" />
별 다를 것 없는 TextView입니다. 이름을 표시하는 TextView입니다.
위 TextView에는
text
를 설정하는 부분이@{}
로 감싸져 있습니다. 이 또한도전과제 2. DataBinding
과 관련이 있습니다. 이 부분 역시 추후 도전과제 설명에서 다루도록 하겠습니다.
성장과제란 해당 주차의 세미나 내용을 기반으로 하지만 한 걸음 더 성장하기 위해 고민하며 공부한 후 구현하는 과제입니다. 필수과제보다는 조금 더 어렵지만 조금만 검색하고 공부해보면 구현할 수 있는 과제이기 때문에 개발자로서 성장하기 위해 구현해 보시는 것을 추천드립니다.
성장과제 1은 단순히 startActivity()
로 화면을 이동하는 것을 넘어, 특정 결과를 위해 새 화면으로 이동하고, 이동한 화면에서 작업을 한 뒤, 복귀할 때 이동한 화면에서의 작업 결과를 기존 화면에서 활용하는 것입니다. 과제의 요구사항은 다음과 같습니다.
제가 구현한 화면을 먼저 첨부하겠습니다.
먼저, 성장과제 1을 이해하기 위해서는 콜백(Callback)
이라는 개념에 대한 이해가 필요합니다.
콜백의 사전적 정의는 이렇습니다.
콜백이란,
1. 다른 함수의 인자로써 이용되는 함수
2. 어떤 이벤트에 의해 호출되는 함수
이렇게 보면 무슨 말인가 싶습니다. 최대한 이해하기 쉽게, 일상 속의 예제로 한 번 들어보겠습니다.
승민이는 보현이와 팀 프로젝트를 합니다. 승민이는 자료조사와 발표를, 보현이는 발표자료 제작을 맡았습니다.
승민이는 자료를 조사하고 보현이한테 자료를 보내줬습니다. 자료를 보내며 승민이는 말합니다.
네가 PPT를 다 만들면 그 때 내가 그 PPT를 가지고 발표를 준비할테니 PPT 다 만들면 알려줘(보내줘)
그러면 시간이 흐른 뒤 보현이가 말합니다.
내가 PPT 다 만들어서 메일로 보냈어, 이제 발표 준비해줘
보현이의 말을 들은 승민이는 이제 발표를 준비합니다.
이해가 가시나요? 일상 속의 모습을 코드로 비유해보겠습니다.
A 함수와 B 함수는 엮여 있습니다. A가 a, c 작업을 수행하고, B는 b 작업을 수행합니다.
A 함수는 a 작업을 마친 뒤 B 함수에게 a 작업의 결과를 전달하고콜백함수(c)
를 등록합니다.A:
B 너가 a를 가지고 b 작업을 다 수행하면 내가 b 작업의 결과를 가지고 c 작업을 할게. 그러니 B 너의 작업이 다 끝나면 알려줘(콜백해줘)
그러면 B 함수가 b 작업을 마친 뒤 A 함수에게 알립니다(콜백).
B:
나 b 작업 끝나서 너한테 알려주는거야(콜백), 너 c 작업 해
B 함수가 콜백하면 A 함수는 콜백함수를 수행합니다.
register: 등록하다
ActivityResult:Activity의 Result, 즉 결과
registerForActivityResult() 메서드를 사용하면, 해당 Activity의 Result를 활용하는 콜백을 등록할 수 있습니다. 위에서는 함수로 예시를 들었지만 Activity간에도 콜백을 등록할 수 있습니다.
세미나 과제를 위의 예시에 적용해볼까요? 그렇다면 이런 상황이라고 할 수 있습니다.
SignInActivity:
얘, SignUpActivity야. 너 회원가입 성공하면 그 때의 ID랑 비밀번호를 나한테 알려줄래? 너가 알려주면 내가 그 ID랑 비밀번호를 내 EditText 필드에 채워넣을게!
SignUpActivity:
내가 방금 한승현이라는 사람을 회원가입 성공시켰어(콜백), 얘의 ID는 qwer이고 비밀번호는 1234야!
SignInActivity:
오 그래? 그러면 내 EditText 필드에 너가 알려준 qwer, 1234를 채울게!
개념을 확실하게 잡았으니 이제 코드를 이해해봅시다.
private val activityResultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
val userId = it.data?.getStringExtra("userId") ?: ""
val userPassword = it.data?.getStringExtra("userPassword") ?: ""
binding.etUserId.setText(userId)
binding.etUserPassword.setText(userPassword)
}
}
하나하나 분석해보겠습니다.
registerForActivityResult
: ActivityResult에 대한 콜백을 등록하고 Launcher를 생성.ActivityResultContracts
: ActivityResult에 대한 Contract(계약) 여러 개(s)가 포함된 클래스(ActivityResultContracts)입니다. ActivityResultContract 클래스는 Result를 생성하는 데 필요한 입력 유형과 Result의 출력 유형을 정의함.StartActivityForResult
: 새 Activity를 열어주되, 기존의 startActivity와 달리 데이터를 쌍방향으로 교류함을 의미.resultCode
: 새 Activity는 작업을 수행한 후 Result, 즉 결과가 성공 후 종료로 나온 결과인지 혹은 실패 후 종료로 나온 결과인지에 대한 값을 RESULT_OK
또는 RESULT_CANCELED
로 반환함. 그러면 결과를 받은 Activity, 즉 콜백을 받은 Activity는 resultCode
가 RESULT_OK
인 경우에는 성공한 것으로 간주해 성공 시에는 무엇을 수행할 것인지, RESULT_CANCELED
인 경우에는 실패한 것으로 간주해 실패 시에는 무엇을 수행할 것인지 결정할 수 있음.위의 코드에서는 콜백이 등록된 런처를 생성
만 했고, 이를 activityResultLancher
라는 변수에 할당했습니다. 그렇다면 이 런처를 가지고 어떻게 SignUpActivity
를 실행시키고 결과를 받을 수 있을까요?
binding.btnSignUp.setOnClickListener {
val intent = Intent(this, SignUpActivity::class.java)
activityResultLauncher.launch(intent)
}
SIGNUP
버튼을 누르면 SignUpActivity로 이동할 수 있는 Intent를 생성합니다. 그리고 생성한 Intent 객체를 아까 생성한 런처 activityResultLauncher
의 launch()
메서드에 인자로 실어서 실행시킵니다.
그 결과로, SignUpActivity
가 실행됩니다. 단순 startActivity()
와 다른 점은, startActivity()
는 그저 새 Activity를 시작하는 것뿐이었다면, 이제는 새 Activity로부터 콜백을 받을 수 있게 되었습니다!
위 과정을 통해 SignUpActivity
가 실행되었습니다. 그렇다면 회원가입 후 아이디와 비밀번호를 다시 SignInActivity
에게 돌려주는 코드도 작성해야 합니다. 이 과정은 Intent
와 setResult()
를 사용합니다.
회원가입 시 이름, 아이디, 비밀번호 미입력 검사는 ViewModel에서 수행합니다. 검사 로직에 대해서는 별도로 언급하지 않습니다. 해당 내용이 궁금하신 분들은 추후
도전과제 2. MVVM
을 확인해주시면 감사하겠습니다.
val intent = Intent(this, SignInActivity::class.java)
intent.putExtra("userId", binding.etUserId.text.toString())
intent.putExtra("userPassword", binding.etUserPassword.text.toString())
setResult(RESULT_OK, intent)
if (!isFinishing) {
finish()
}
SignInActivity
에 대한 Intent 객체를 생성하고, putExtra()
메서드를 사용해 전달할 데이터를 담습니다(put). putExtra()
는 인자 2개를 받는 메서드입니다. 첫 번째 인자로 전달할 값의 name
을, 두 번째 인자로 전달할 값을 넣어줍니다. 우리는 아이디와 비밀번호를 전달해야 하므로, 아이디와 비밀번호를 putExtra
를 통해 Intent 객체에 담아줍니다.
그 후, 이 결과(Result)가 성공한 케이스(RESULT_OK
)인지, 취소된(실패된) 케이스인지(RESULT_CANCELED
) 설정해야 합니다(set). 그래서 setResult()
메서드를 통해 첫 번째 인자로 resultCode
를, 두 번째 인자로 Intent 객체를 넣습니다. setResult()
메서드가 수행되는 순간, SignInActivity
에서는 콜백함수가 수행됩니다.
하지만 SignInActivity
가 콜백함수를 수행하면 뭐합니까, 회원가입을 했으면 회원가입 화면을 종료시켜야 합니다. 그래서 finish()
메서드를 통해 SignUpActivity
를 종료시키고 로그인 화면으로 돌아갑니다.
콜백함수는 registerForActivityResult()
의 중괄호 부분입니다. 해당 콜백함수만 살펴보도록 하겠습니다. (왜 중괄호 안에 쓰는지 궁금하신 분은 Android 람다
를 검색해보시는 것을 추천드립니다)
if (it.resultCode == RESULT_OK) {
val userId = it.data?.getStringExtra("userId") ?: ""
val userPassword = it.data?.getStringExtra("userPassword") ?: ""
binding.etUserId.setText(userId)
binding.etUserPassword.setText(userPassword)
}
resultCode
가 RESULT_OK
라면, 즉 정상적으로 결과를 반환했다는 콜백을 받으면 우리는 Result에 해당하는 it
에서 보낸 결과를 꺼내서 사용해야 합니다. it.data
에는 SignUpActivity
가 Intent가 들어있습니다. 우리는 Intent에서 이번엔 Extra를 가져와 보겠습니다.
getExtra()
메서드는 putExtra()
메서드와 달리, 타입을 지정해줘야 합니다. String Extra를 가져오려면 getStringExtra()
를, Int Extra를 가져오려면 getIntExtra()
를 사용하면 됩니다. getExtra()
메서드의 인자로는 putExtra()
시 입력한 name
을 전달해주면 됩니다.
이렇게 SignUpActivity
로부터 결과(Result)를 전달받았으니, 이제 화면의 EditText의 필드에 그 값을 넣습니다. EditText의 경우 setText() 메서드를 이용해 String을 text로 set 할 수 있습니다.
성장과제 2의 요구사항은 다음과 같습니다.
제가 구현한 화면을 먼저 첨부하겠습니다.
성장과제 2의 1번은 ScrollView
를 사용해 화면을 스크롤 가능하게 만들어보는 과제입니다. ScrollView를 사용하면 ScrollView 안의 View들이 차지하는 영역이 ScrollView가 차지하는 영역보다 더 클 때, 스크롤을 통해 더 표시할 수 있습니다. Android 공식문서의 ScrollView를 한 번 보겠습니다.
이 공식문서에는 가장 중요한 사실 하나가 들어있습니다.
ScrollView may have only one direct child placed within it ScrollView는 그 안에 하나의 직계 자식(View)만 가질 수 있다.
이것도 말로만 하니까 잘 와닿지 않습니다. 우리는 ScrollView 안에 ImageView와 여러 TextView를 넣어야 하는데, 하나의 직계 자식만 가질 수 있다니. 이 문제를 어떻게 해결해야 할까요?
<ScrollView
android:id="@+id/sv_profile"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="20dp"
app:layout_constraintBottom_toTopOf="@id/btn_edit"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout_profile"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- ImageView와 TextView들-->
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
fragment_home.xml
의 일부입니다. ScrollView
안에 하나의 직계 자식인 ConstraintLayout
만 들어 있습니다. 우리가 ScrollView 안에 넣어야 할 ImageView와 TextView들은 ConstraintLayout 안에 들어있습니다.
공식문서에 적힌 단 하나의 직계 자식만 가진다
는 조건을 만족시켰습니다. ScrollView의 직계 자식은 ConstraintLayout
뿐입니다. 즉, 하위 View들을 모두 포함하는 ViewGroup
을 만들고, ScrollView가 그 ViewGroup을 하나의 직계 자식으로 삼도록 구현하면 됩니다.
성장 과제 2의 2번은 ImageView의 width(가로), height(세로) 비율을 1:1로 설정하라는 요구사항입니다. 이 요구사항은 constraintDimensionRatio
속성을 사용해 충족시킬 수 있습니다.
<ImageView
android:id="@+id/iv_profile_image"
setProfileImage="@{viewmodel.userImage}"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/description_profile_image"
android:scaleType="centerCrop"
app:layout_constraintBottom_toTopOf="@id/tv_profile_name"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.33" />
constraintDimensionRatio
속성을 사용하면 width
나 height
값을 비율에 맞게 설정할 수 있습니다. 이 속성을 사용하려면, 조건이 있습니다.
적어도 한 면(가로 또는 세로)은 크기가 정해져야 하며,
그 정해진 크기에 맞춰 비율로 크기를 정할 나머지 면은0dp
로 설정되어야 한다.
제가 짠 ImageView에서는 위 조건을 충족할까요? 충족합니다.
layout_constraintWidth_percent="0.33"
에 의해 가로 폭의 33%만큼 width가 고정됩니다.그런데, 만약 width도 0dp고 height도 0dp이며 둘 다 화면에 꽉 제약이 걸려있다면, constraintDimensionRatio로 1:1을 지정하면 어떻게 될까요?
이렇게, 짧은 쪽을 기준으로 1:1 비율을 맞춥니다!
도전과제는 세미나에서 따로 학습하지는 않지만 혼자, 또 파트원들과 함께 따로 공부하며 도전해볼 수 있는 난이도의 과제입니다. 필수과제와 성장과제 내용을 이해한 후 도전과제에도 도전해보시면 더욱 좋을 것 같습니다.
과제의 요구사항은 ViewBinding이 아닌 DataBinding으로 구현하는 것입니다.
findViewById란 뭘까요? ViewBinding이란 뭘까요? DataBinding이란 뭘까요? 1차 세미나에 대해서는 ViewBinding에 대해 다룹니다. 그렇지만 한 번 다시 짚고 넘어가보겠습니다.
findViewById
ViewBinding 이전에는 findViewById라는 메서드를 이용해 View에 접근했습니다. 예를 들면 이렇습니다. id가 "button_login"이라는 Button
이 존재하고, 이 Button에 대한 ClickEvent를 구현한다면 아래와 같이 구현할 수 있습니다.
val button = findViewById<Button>(R.id.buttonLogin)
button.setOnClickListener {
// 클릭 시 수행할 코드
}
하지만 이 방법에는 단점이 있습니다.
ViewBinding
위의 이유로 1차 세미나에서는 findViewById가 아닌 ViewBinding을 배웠습니다. Android 공식문서는 ViewBinding이 뭐라고 설명했을까요?
ViewBinding은 View와 상호작용하는 코드를 더 쉽게 작성해주는 기능입니다. ViewBinding을 사용하도록 설정하면, 각각의 layout 파일(xml)에 대한 Binding 클래스가 자동으로 생성되며, 이 Binding 클래스의 인스턴스를 사용하여 해당 layout(xml) 안에 있는 id를 가진 모든 View에 대한 직접 참조가 가능해집니다.
원래 말로만 설명을 들으면 어려운 법입니다. 위의 글 중 꼭 알아야 하는 내용은 다음 2가지입니다.
- ViewBinding을 사용하면 각각의 layout 파일에 대한 Binding 클래스가 자동으로 생성된다.
- 이렇게 자동으로 생성된 Binding 클래스의 인스턴스를 사용해 해당 layout 파일 안에 있는 id를 가진 모든 View에 접근할 수 있다.
ViewBinding을 사용하려면 아래 순서를 따라야 합니다. MainActivity에서 ViewBinding을 사용하는 상황으로 예시를 들겠습니다.
먼저, build.gradle
(Module: ~.app)에서 ViewBinding을 사용하겠다고 선언해야 합니다. android { }
중괄호 안에 buildFeature { }
내부에 viewBinding true
라고 선언합니다.
android {
....
buildFeatures {
viewBinding true
}
....
}
다음으로, MainActivity.kt
에서 Binding 클래스의 인스턴스를 만들어야 합니다. MainActivity에 대한 Binding 클래스의 이름은 ActivityMainBinding입니다.
Binding 클래스의 이름은 아래 규칙을 따릅니다.
xml 파일
의 이름은snake_case
로 작성됩니다. 이를CamelCase
로 바꾼 후, 뒤에Binding
을 붙인 게 Binding 클래스명입니다.
ex -activity_main.xml
의 Binding 클래스 이름 =ActivityMainBinding
class MainActivity: AppCompatActivity() {
....
private lateinit var binding: ActivityMainBinding
....
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
....
}
...
}
마지막으로, id를 가진 View에 접근할 때 binding
이라는 Binding 클래스의 인스턴스를 사용해 접근합니다.
private fun login() {
binding.btnSignIn.setOnClickListener {
....
}
}
위의 과정을 통해 ViewBinding
을 사용하여 id를 가진 모든 View에 접근할 수 있습니다. findViewById
대비 ViewBinding
의 장점은 뭘까요? 이에 대해 Android 공식문서는 다음과 같이 말합니다.
- Null-Safe
- Type-Safe
첫째로, Null-Safe
하다는 특징이 있습니다. ViewBinding은 View에 대한 직접 참조를 생성하기 때문에, 유효하지 않은 ID를 참조하려다 null pointer exception
이 발생할 위험이 없습니다. 유효하지 않은 ID를 참조하려고 시도하면 컴파일 에러가 발생하기 때문입니다.
두 번째로, Type-Safe
하다는 특징이 있습니다. findViewById
를 사용할 때는, 해당 id의 View Type도 같이 알려줘야 했습니다. 만약 findViewById<TextView>(R.id.buttonLogin)
라는 코드를 작성했는데, buttonLogin
이라는 id를 가진 View가 TextView
가 아닌 Button
일 경우 class cast exception
이 발생하게 됩니다. 하지만 ViewBinding은 각 Binding 클래스의 필드가 xml 파일에서 참조하는 View와 일치하기 때문에 Type-Safe
합니다.
DataBinding
그렇다면 DataBinding은 뭘까요? DataBinding에 대한 Android 공식문서를 살펴보겠습니다.
DataBinding은 programmatically한 방식보다 declarative한 형식을 사용해 레이아웃의 UI 구성요소와 앱의 데이터 스소를 결합할 수 있게 해주는 support 라이브러리입니다.
이렇게만 보면 또 어렵습니다. Android 공식문서에 적힌 예시를 함께 보겠습니다.
sample_text
라는 id를 가진 TextView의 text를 viewModel
이라는 인스턴스의 userName
이라는 필드의 값으로 할당하고 싶은 상황이라고 가정하겠습니다. findViewById
를 사용하는 경우에는 Kotlin 코드로 이렇게 작성해야 했습니다.
findViewById<TextView>(R.id.sample_text).apply {
text = viewModel.userName
}
DataBinding
을 사용하는 경우 Kotlin 코드가 아닌, xml 코드로 아래와 같이 작성할 수 있습니다.
<TextView
...
android:text="@{viewmodel.userName}"
... />
그리고 참고사항으로는 이렇게 적혀있습니다.
많은 경우, DataBinding이 제공하는 이점들을 ViewBinding은 더 간단하고 우수한 성능으로 제공할 수 있다.
findViewById
를 대체하기 위해 주로 DataBinding을 사용하는 경우 그 대신 ViewBinding을 사용하는 것을 고려해 봐라.
이 내용에 대해서는 DataBinding 마지막 부분에서 다뤄보겠습니다. 우리는 이제 직접 DataBinding을 사용해보며 이해해보겠습니다. SignInActivity에서 DataBinding을 사용하는 상황으로 예시를 들겠습니다.
먼저, build.gradle
(Module: ~.app)에서 DataBinding을 사용하겠다고 선언해야 합니다. android { }
중괄호 안에 buildFeatures { }
내부에 dataBinding true
라고 선언합니다.
android {
....
buildFeatures {
dataBinding true
}
....
}
DataBinding 레이아웃 파일은 layout
이라는 루트 태그로 시작하고, data
태그 및 나머지 view
태그들로 구성됩니다. 말로만 보면 어려우니 아래 예시를 보겠습니다.
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewmodel"
type="co.kr.sopt_seminar_30th.presentation.viewmodel.SignInViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dp"
tools:context=".presentation.ui.auth.SignInActivity">
....
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
뭔가 달라진 게 보이시나요? ViewBinding을 사용할 때의 루트 태그는 ConstraintLayout
이었습니다. 하지만 DataBinding을 사용할 때는 루트 태그가 layout
으로 바뀐 것을 확인할 수 있습니다. 또, 추가로 data
라는 태그 안에 variable
이라는 태그가 있는 것을 알 수 있습니다.
- 루트 태그는
layout
로 한다.data
내variable
의name
이라는 이름의 변수를 활용해서 표현식을 작성한다.type
에는 해당 변수의 클래스 경로를 넣는다.
DataBinding은 단방향 데이터 결합과 양방향 데이터 결합이 있습니다. 단방향 데이터 결합은 DataBinding 변수를 read
만 할 수 있습니다. 양방향 데이터 결합은 DataBinding 변수를 read
하고 write
할 수 있습니다.
단방향 데이터 바인딩:
@{}
구문을 사용합니다.
양방향 데이터 바인딩:@={}
구문을 사용합니다.
저의 경우에는 activity_sign_in.xml
의 아이디, 비밀번호를 입력하는 EditText의 text
속성을 SignInViewModel
의 userId
와 userPassword
라는 변수에 양방향 데이터 바인딩을 시켜줬습니다. 아래 xml 코드는 activity_sign_in.xml
의 EditText
부분입니다.
<EditText
android:id="@+id/et_user_id"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/text_user_id_hint"
android:importantForAutofill="no"
android:inputType="text"
android:text="@={viewmodel.userId}"
app:layout_constraintBottom_toTopOf="@id/tv_user_password"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_user_id" />
<EditText
android:id="@+id/et_user_password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/text_user_password_hint"
android:importantForAutofill="no"
android:inputType="textPassword"
android:text="@={viewmodel.userPassword}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_user_password" />
위 두 EditText
를 보시면, android:text
속성에 각각 viewmodel.userId
와 viewmodel.userPassword
를 양방향 데이터 바인딩으로 결합시켜 준 것을 확인할 수 있습니다.
그러면 화면에는 어떻게 표시될까요? 처음 화면에 표시될 때는, viewmodel
에서 userId
와 userPassword
값을 읽어(read
) EditText 필드에 값을 보여줍니다. 하지만 사용자가 EditText에 직접 값을 입력할 경우, viewmodel
의 userId
와 userPassword
의 값이 사용자가 입력한 값으로 변경(write
)됩니다.
하지만 여전히 뭔가 의문스러운 점이 있습니다. 우리는 xml 코드에 viewmodel
이라는 친구가 어떤 친구인지 알려준 적이 없습니다. 어떤 클래스 타입인지만 알려줬을 뿐, 어떤 인스턴스가 들어가는지 알려준 적은 없는데, 어떻게 userId
와 userPassword
에 접근하는지, 이상하지 않으신가요? 해답은 SignInActivity.kt
안에 있습니다.
class SignInActivity: AppCompatActivity() {
private lateinit var binding: ActivitySignInBinding
private val viewModel by viewModels<SignInViewModel>()
....
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 1. DataBindingUtil 클래스를 사용하는 방법
binding = DataBindingUtil.setContentView(this, R.layout.activity_sign_in)
// 2. ActivitySignInBinding 클래스를 사용하는 방법
binding = ActivitySignInBinding.inflate(layoutInflater)
// 레이아웃의 variable 할당
binding.viewmodel = signInViewModel
}
....
}
DataBindingUtil
클래스나 ActivitySignInBinding
클래스를 사용하여 binding 객체를 생성한 후, binding 객체의 변수 viewmodel
에 SignInActivity
의 signInViewModel
을 할당해 주었습니다. 이제 의문이 해결되셨나요? 어떤 variable
인지는 Activity에서 binding 객체를 초기화해준 후 할당해주기 때문에, signInViewModel
인스턴스의 userId
와 userPassword
에 접근하게 됩니다.
DataBindingUtil
vsActivity~Binding
무슨 차이일까요?저도 잘 모릅니다. 공부한 뒤 또 새로운 포스트로 찾아뵙겠습니다.
그럼 ViewBinding
대비 DataBinding
의 장점은 뭘까요? 이에 대해 Android 공식문서는 이렇게 말하고 있습니다.
Observable
, 그러니까 관찰할 수 있는 데이터 객체와 함께 사용하면, 데이터 변경 시 UI를 업데이트하는 것을 신경쓰지 않아도 된다고 합니다. Observable
객체와 DataBinding
을 함께 사용할 경우, 데이터가 변경되면 UI가 자동으로 업데이트된다는 장점이 있습니다. 이 내용은 LiveData
라는 내용과 함께 공부하면 좋을 것 같다고 생각합니다.
도전과제 2는 MVVM
아키텍처 패턴을 알아보고, 실제로 적용해보는 과제입니다. MVVM
이란 뭘까요?
Android 아키텍처에 대해 A부터 Z까지 제가 모두 다루기엔 너무 양이 많을 뿐 아니라, 다룰 능력도 아직은 부족합니다. 하지만 MVVM으로 과제를 구현해보라는 요구사항이 있기 때문에, 이를 위해서는 일단 MVVM
이라는 게 어떤 개념인지는 최소한 알아야 구현할 수 있습니다.
MVVM
이란, 기존의 MVC
패턴의 God Class
의 단점들을 보완하기 위해 나타난 대안 중 하나입니다. MVVM
이라는 단어는 Model
, View
, ViewModel
에서 각 어절의 첫 문자를 따서 만들어졌습니다.
View
: Activity, Fragment가 View의 역할을 합니다. 사용자의 Action을 받습니다(ex - EditText에 텍스트 입력, Button 클릭 등). View
는, ViewModel
에게 View를 그리는 데 필요한 데이터를 요청하고 ViewModel
을 통해 필요한 데이터를 제공받습니다.ViewModel
: View
가 요청한 데이터를 Model
에게 요청합니다. Model
에게서 제공받은 데이터를 View
에게 제공합니다.Model
: ViewModel
이 요청한 데이터를 ViewModel
에게 제공합니다. Room
같은 DB 사용 혹은 Retrofit
등을 사용한 API 호출을 통해 적합한 데이터를 제공합니다.MVVM
패턴은 MVC
패턴에 비해 이러한 장점이 있습니다.
View
는 ViewModel
의 데이터를 관찰(Observe
)하고 있습니다. 따라서 UI 업데이트가 간편해집니다.View
가 직접 Model
에 접근하는 대신, ViewModel
을 통해 데이터를 얻기 때문에 Memory Leak의 위험이 줄어듭니다. 직접 Model
에 접근하지 않아 View
의 수명 주기에 의존하지 않기 때문입니다.Android, 그러니까 Google 측에서는 Android Architecture Component
, 즉 AAC
라고 하는 것을 제공합니다. 조금 더 편하라고 아키텍처 구성요소에 관한 라이브러리를 제공하는데, 여기에는 ViewModel
(ViewModel), LiveData
(Observable Data Object), Room
(Local Database) 등이 포함됩니다. 이 포스트에서 각각에 대한 자세한 설명이나 사용법을 하나하나 상세히 적기에는 너무 글이 길어져, 추후 다른 포스트에서 다뤄보도록 하겠습니다.
저의 경우에는 MVVM 패턴으로 이렇게 구현했습니다.
View
는 ViewModel
에게 데이터를 요청하고, ViewModel
의 데이터를 관찰해 변경사항이 있을 시 UI를 자동으로 업데이트합니다.ViewModel
에서 필요로 하는 데이터는 Repository
를 통해 Model
, 즉 로컬 데이터베이스인 Room
에 요청해서 받아옵니다.Repository
는 ViewModel
과 Model
사이에 위치하며, 사용자 동작에 따라 필요한 데이터를 로컬 데이터베이스나 외부 서버에서 가져오는 역할을 수행합니다. Repository
의 존재로 인해 ViewModel
은 직접 데이터를 관리할 필요가 없습니다.View
는 Model
을 직접적으로 참조하지 않습니다. 대신 중간에 있는 ViewModel
을 통해 필요한 데이터를 제공받습니다. ViewModel
또한 Model
에 직접 접근하지 않고 Repository
를 통해 필요한 데이터를 제공받습니다.이만큼 읽으셨다면, 아마 이런 생각이 드실 수도 있습니다.
이거 너무 복잡해졌는데... 좋은 거 맞아?
MVVM의 단점이기도 합니다. 구조가 복잡하고, 배우기에 진입장벽이 분명하게 존재합니다. 하지만 저는 왜
사용하는지를 아는게 가장 중요하다고 생각합니다. 제가 생각하고 느낀 MVVM
패턴을 사용하는 이유는 이렇습니다.
관심사를 분리시킨다. View는 UI를 업데이트하는데 신경을 기울이고, ViewModel은 View가 필요로 하는 데이터를 적재적소에 제공할 수 있도록 준비하고, Model은 앱 전체에서 사용할 데이터를 전반적으로 관리하는 것에 온 신경을 기울이자.
한 마디로,의존성을 줄이자
.
3기수째 세미나 과제를 하면서 돌아보니 매 기수마다 제 코드는 조금씩 다릅니다. 28기 과제에서는 필수과제 하나에도 허덕이며 끙끙댔고, 29기 과제에서는 잘 이해하지도 못한 채로 일단 마구 적용하려고 했습니다. 이번 30기에서는 과제를 수행하며 최대한 이해하고, 남에게 설명할 수 있을 정도로 공부했습니다. 사실 이 글도 제 공부를 정리함과 동시에, 이 글만 보고도 YB 파트원들이 조금이나마 더 쉽게 이해했으면 좋겠다는 생각으로 최대한 쉬운 말들로 풀어서 쓰려고 노력했습니다. 그 과정에서 저 또한 더 잘 이해하고 한 번 더 공부하는 시간을 가졌습니다.
28기, 첫 기수의 제 코드는 정말 엉망이었고 29기, 두 번째 기수 때의 코드 또한 마음에 들지 않습니다. 과거의 코드가 마음에 들지 않는다는 것은 제가 그만큼 조금이나마 성장했다는 뜻인가 생각이 들곤 합니다. 그 성장은 저 혼자서 이룬 것이 아닙니다. 다양한 주변 동료 개발자들을 통해 배우고 얼굴 모르는 수많은 블로거들의 도움을 받았을 것입니다.
좋은 개발자란 무엇일까요? 코드를 잘 짜는 사람? 남들과 활발하게 의견을 공유하고 교류하는 사람? 저는 아직은 잘 모르겠습니다. 하지만 전자와 후자 양쪽 모두에 해당되면 좋은 게 아닐까요? 저 또한 아직은 많이 부족한 개발자지만 다양한 분들과 함께 성장하고 싶다
는 생각을 항상 합니다.
이 글을 읽으신 여러분들이 어떤 경로를 통해 읽게 되셨는지는 모르겠지만, 함께 성장할 수 있었으면 좋겠습니다. 긴 글 읽어주셔서 감사합니다.
정말 열심히 정리하시네요