1. Strings.xml에서 다양한 특수문자(@)를 쓰고 싶을 때
- 다양한 이스케이프 시퀀스 활용 또는 CDATA 활용(문자를 그대로 입력해서 가독성은 좋음)
<!-- 원본 문자 그대로 사용 : <![CDATA[특수문자]]> -> 특수문자 -->
<string name="sign_up_email_error_include"><![CDATA[@]]>가 포함되어야 합니다.</string>
<!-- 유니코드 이스케이프 시퀀스 사용 : \u0040 -> @ -->
<string name="sign_up_email_error_include1">\u0040가 포함되어야 합니다.</string>
<!-- HTML 이스케이프 시퀀스 사용 : @ -> @ -->
<string name="sign_up_email_error_include2">@가 포함되어야 합니다.</string>
2. 회원가입 시 각 입력항목 별로 Error State를 관리하는 방법
- 기존의 나였다면 enum class를 활용했을 것 같은데, enum의 확장 버전인 sealed interface나 sealed class를 활용한다고 한다.
- 데이터 클래스의 companion object-init() 대신에 name, email의 프로퍼티에 직접적으로 default value를 지정해도 되지 않을까 하는 의문이 있었다.
-> Q. 재사용성이나 가독성을 위해 init 함수를 만든 것 인지 질문함
-> A. " 재사용성이나 가독성을 위한 것은 아니였다. 개인 취향인 것 같고, 그렇게 해도 무관하다." 라고 답변을 주셨다.
data class SignUpErrorUiState(
val name: SignUpValidUiState,
val email: SignUpValidUiState,
val passwordInput: SignUpValidUiState,
val passwordConfirm: SignUpValidUiState,
val enabled: Boolean,
) {
companion object {
fun init() = SignUpErrorUiState(
name = SignUpValidUiState.Init,
email = SignUpValidUiState.Init,
passwordInput = SignUpValidUiState.Init,
passwordConfirm = SignUpValidUiState.Init,
enabled = false
)
}
}
sealed interface SignUpValidUiState {
// 초기 상태
data object Init: SignUpValidUiState
// 적합
data object Valid: SignUpValidUiState
// 이름
data object NameBlank: SignUpValidUiState
// 이메일
data object EmailBlank: SignUpValidUiState
data object EmailIncludeAt: SignUpValidUiState
data object EmailTypeNotAllowed: SignUpValidUiState
// 비밀번호 입력
data object PasswordInputBlank: SignUpValidUiState
data object PasswordInputLength: SignUpValidUiState
data object PasswordInputSpecialChar: SignUpValidUiState
data object PasswordInputUpperCase: SignUpValidUiState
// 비밀번호 더블 체크
data object PasswordConfirmBlank: SignUpValidUiState
data object PasswordConfirmEquals: SignUpValidUiState
}
-> 위 코드로 구현했지만, 팀원분이 "SignUpValidUiState에 주석이 많은 것을 보니 각 항목 별로 분리하는 게 더 좋을 것 같다"는 코드 리뷰를 남겨주셔서 아래와 같이 수정할 수 있었습니다.
https://github.com/rlaxodud214/NBC-Introduce/commit/98ea7e5c6052153a4cb258aed9ed8179f5626388
data class SignUpInputErrorState(
val name: CommonValidState,
val email: CommonValidState,
val passwordInput: CommonValidState,
val passwordConfirm: CommonValidState,
val enabled: Boolean,
) {
companion object {
fun init() = SignUpInputErrorState(
name = CommonValidState.Init,
email = CommonValidState.Init,
passwordInput = CommonValidState.Init,
passwordConfirm = CommonValidState.Init,
enabled = false
)
}
}
sealed interface CommonValidState {
data object Init : CommonValidState // 초기 상태
data object Blank : CommonValidState // 비어 있음
data object Valid : CommonValidState // 적합
}
sealed interface NameValidState : CommonValidState {
data object Length : NameValidState
}
sealed interface EmailValidState : CommonValidState {
data object IncludeAt : EmailValidState
data object TypeNotAllowed : EmailValidState
}
sealed interface PasswordInputValidState : CommonValidState {
data object Length : PasswordInputValidState
data object SpecialChar : PasswordInputValidState
data object UpperCase : PasswordInputValidState
}
sealed interface PssswordConfirmValidState : CommonValidState {
data object Equals : PssswordConfirmValidState
}
3. (챌린지반 3주차 과제) EditText인 name, email, password, passwordconfirm 4가지 항목에 대해 하나의 addTextChangedListener로 처리해보기
// 기존
private fun liveValidation() = with(binding) {
etName.addTextChangedListener {
viewModel.checkValidName(etName.getTextElement())
}
etEmail.addTextChangedListener {
viewModel.checkValidEmail(etEmail.getTextElement())
}
etPasswordInput.addTextChangedListener {
viewModel.checkValidPassword(etPasswordInput.getTextElement())
}
etPasswordConfirm.addTextChangedListener {
viewModel.checkValidPasswordConfirm(
passwordInput = etPasswordInput.getTextElement(),
passwordConfirm = etPasswordConfirm.getTextElement(),
)
}
}
private fun EditText.getTextElement() = this.text.toString()
- 처음 시도한 방법
EditText 4개를 하나의 리스트에 넣은 뒤, forEachIndexed를 활용하여 index로 EditText를 구분했다.
- 0 -> Name, 1 -> email, 2 -> password, 3 -> passwordConfirm
private fun liveValidation() = with(binding) {
editTexts.forEachIndexed { index, editText ->
editText.addTextChangedListener {
when(index) {
0 -> viewModel.checkValidName(etName.getTextElement())
1 -> viewModel.checkValidEmail(etEmail.getTextElement())
2 -> viewModel.checkValidPassword(etPasswordInput.getTextElement())
3 -> {
viewModel.checkValidPasswordConfirm(
passwordInput = etPasswordInput.getTextElement(),
passwordConfirm = etPasswordConfirm.getTextElement(),
)
}
}
}
}
}
위 코드도 분명히 돌아가는 로직이지만, editTexts 내부 요소의 순서가 뒤바뀐다면 언제든 오류가 발생할 수 있었고 명시적이지 않다는 단점이 분명한 로직이였다.
그렇다면, 명시적으로 etName 일때 viewModel.checkValidName()를 호출할 수는 없을까?
-> 정답은 튜터님의 라이브 코딩에서 발견할 수 있었다.
private fun liveValidation() = with(binding) { // 처음과 비교했을 때, 코드가 굉장히 간결해졌다.
editTexts.forEach { editText ->
editText.addTextChangedListener {
editText.checkValidElement() // 아래 함수 호출
}
}
}
private fun EditText.checkValidElement() = with(binding) {
// when { }으로 작업한다면 -> this@checkValidElement == et~~~~가 각 분기의 조건이 된다.
when (this@checkValidElement) {
etName -> viewModel.checkValidName(etName.getTextElement())
etEmail -> viewModel.checkValidEmail(etEmail.getTextElement())
etPasswordInput -> viewModel.checkValidPassword(etPasswordInput.getTextElement())
etPasswordConfirm -> viewModel.checkValidPasswordConfirm(
passwordInput = etPasswordInput.getTextElement(),
passwordConfirm = etPasswordConfirm.getTextElement(),
)
}
}
private fun EditText.getTextElement() = this.text.toString()
이런 방법이 있었다니,,, 명시적을 넘어서 코드가 직관적이여서 가독성이 확보된 코드를 볼 수 있었다.
몰랐기 때문에, 굉장히 신기했고 앞으로 잘 활용할 수 있도록 하나하나 분석해보았다.
- this 뒤에 "@checkValidElement"가 있던데 왜 포함이 됐는지 알아보자
when안에서 this@를 입력하면 자동완성으로 3가지가 뜬다. -> 1. SignUpActivity, 2. with, 3. checkValidElement
그렇다. when 조건식에서 입력한 this는 3가지를 나타낼 수 있기 때문에 컴파일러에게 명확하게 어떤 this를 쓴 것인지 알려줘야 했고, 그래서 확장함수의 타입인 EditText를 this로 지칭한 것이라고 명시해줘야 했다.
- 그렇다면 이제 조건 분기를 살펴보자
말 그대로 this인 EditText가 etName인지 etEmail인지 체크해서 그에 상응하는 메서드를 호출하는 로직이였다.
덕분에 가독성과 코드의 중복을 제거할 수 있었다.
4. 과제에서 내가 저지른 실수
- 튜터님께서 항상 강조하시던 말이 있었다 "viewModel에는 Action만 담아야 한다."
나는 위 원칙을 어기고, UI 로직을 viewModel에 error를 출력하는 TextView를 넘겨서 viewModel에서 errorMessage를 수정하고, visibility를 활성화 / 비활성화 하는 등의 실수를 했었다.
-> 코드를 본 튜터님의 말씀 : "만약 채용과제에서 지원자가 이렇게 코딩했으면 다른 부분은 볼 필요도 없이 탈락이다."
앞으로는 ViewModel과 View의 명확한 경계를 인지할 수 있기를 바라며, 경계와 관련해서 이해하는데 도움이 됐던 링크를 첨부하며 과제 리뷰를 마친다.
https://velog.io/@renovatio_hyuns/Hilt-%EB%93%A4%EA%B3%A0-MVVM-%EC%A0%95%EB%B3%B5-2.-ViewModel
'Android' 카테고리의 다른 글
Adapter, AdapterView 및 ListView, GridView 정리 (0) | 2024.04.11 |
---|---|
스탠다드 2주차 강의내용 정리 및 과제(LifeCycle) (0) | 2024.04.10 |
앱 개발 입문 과제 해설 후기 (1) | 2024.03.29 |
안드로이드 Button 디자인(shape, corners, stroke, gradient) (3) | 2024.03.29 |
[학습 1장] findViewById / Kotlin Extensions / ViewBinding / DataBinding 정리 (0) | 2024.03.28 |