머니몽 프로젝트에서 디자인 시스템을 맡았고, 유지보수 도중 디자이너분에게 dm 이 왔다.
👩🏼💻 디자이너분: ㅎㅈ님 TextField 컴포넌트에 필수 표시(별표) 넣으면 보수가 많이 드나요?
🧑🏻💻 필자: 이미 그렇게 구현되어 있지 않아요??
👩🏼💻 디자이너분: 이거 단순히 글자 하나 추가한 게 아니라서요,,
🧑🏻💻 필자: 확인하고 반영할게요!~
여기서 필수 표시(*)는 TextField 에 존재할 수도, 존재하지 않을 수도 있다.(Toggle 의 느낌)
그렇기에 두 가지 상황을 모두 고려해야한다.
본글은 Jetpack Compose 기반으로 작성되어 있습니다
아래 이미지가 기존 프로젝트에 반영되어 있던 별표가 존재하는 TextField 였다.
코드적으로 아래처럼 AnnotatedString 을 사용해 색상만 변경된 * 를 추가했었다.
단순히 글자(별표) 하나가 더 추가되고 있었다.
fun withRequiredMark(title: String) = buildAnnotatedString {
val markColor = Red03
append(text = title)
withStyle(style = SpanStyle(color = markColor)) {
append(text = "*")
}
}
아래의 새로 디자인된 TextField 에는 title 과 별표 사이에 margin 값이 추가되었다.
이제부터 위 디자인을 만족하기 위해 필자의 생각이 어떻게 흘러갔는지 알아보자.
첫 번째 사고
사용할 때 Row 로 감싸서 title 옆에 * 컴포넌트를 하나 추가하면 되지 않을까?
결론은 그렇게 할 수 없다.
실제 사용되는 MDSTextField 코드를 확인하자
@Composable
fun MDSTextField(
modifier: Modifier = Modifier,
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
title: String,
placeholder: String,
isFilled: Boolean,
isError: Boolean = false,
helperText: String? = null,
maxCount: Int? = null,
singleLine: Boolean,
minLines: Int = 1,
icon: MDSTextFieldIcons? = null,
onIconClick: (() -> Unit) = {},
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default
) {
보다시피 title 파라미터의 타입이 String 이다. (AnnotatedString 타입을 받는 MDSTextField 도 존재한다.)
title 타입을 Composable() -> Unit 으로 했다면 디자인 요구사항을 만족할 수 있겠지만, 처음 코드를 설계할 때 title 의 UI 부분은 내부에서 처리하도록 강제하는 게 팀원들이 사용할 때 편하다고 생각했다.
물론 title 의 UI 에 별표가 존재할 수도, 안 할수도 같이 여러 형태가 추가될 지는 설계할 때 생각하지 못했다..
두 번째 사고
MDSTextFIeld 에 isRequired 파라미터를 추가하면 되지 않을까?
결론은 가능하다.
isRequired 파라미터를 Boolean 타입으로 받아오면 해당 값에 맞게 컴포넌트 내부적으로 분기문을 처리해 UI 를 구현하면 가능하다.
코드로 예시를 들어보자.
아래는 title 영역 UI 를 내부적으로 그리는 코드이다.
@Composable
private fun MDSTextFieldContainerTop(
title: AnnotatedString,
state: MDSTextFieldState,
isRequired: Boolean
) {
Row {
Text(
text = title,
color = state.titleColor,
style = Body2
)
Spacer(modifier = Modifier.width(2.dp))
if (isRequired) {
Text(
text = "*",
color = Red03,
style = Body2
)
}
}
}
하지만 이렇게 되면 MDSTextField 에 파라미터가 추가되어야 한다.
물론 하면 된다. 하지만 들었던 생각은 이 별표(*) 는 단순히 UI 요소이다.
TextField 내부적으로 어떠한 로직에 영향을 단 하나도 주지 않는다.
그럼 이렇게 하나씩 UI 의 형태가 추가될 때마다 파라미터를 넣고 내부적으로 분기를 처리해줘야 할까?
이건 좋지 않은 방식이라 생각했다.
무엇보다 UI 적인 요소 하나가 파라미터 하나를 차지하는게 솔직히 맘에 들지 않았다.
TextField 의 동작에 관여한다면 파라미터를 추가해서 내부적으로 처리하는 게 맞다고 생각했다.
결과적으로 이런 건 사용하는 쪽(팀원들이)에서 추가(처리)를 해주는 게 맞다고 판단했다.
그래서 해당 방식도 내가 원하는 방식은 아니였다.
세 번째 사고
유니 코드를 활용하자
두 번째 방식을 제외한다면 결국 String 또는 AnnotatedString 만으로 Title 과 별표간의 margin 값을 추가해야한다.
이 중, AnnotatedString 으로 공백의 크기를 조절하고자 했다.
그래서 공백을 나타내는 유니코드를 찾아보았다.
그 중 아래 두 개가 적절하다 판단했다.
- u+2002(En Space - 엔 스페이스)
- u+2003(Em Space - 엠 스페이스)
적절하다고 판단한 이유는 두 유니코드의 공백 크기는 이름에서 볼 수 있듯 각각 폰트의 알파벳 n, m 크기를 가지도록 되어있다.
그래서 withStyle 에서 fontSize 를 적절하게 조정하면 원하는 공백(margin)을 가지도록 구성할 수 있다고 판단했다.
테스트를 거친 결과 u+2003 이 지정한 fontSize 만큼 공백을 차지했다.
테스트 결과를 layout Inspector 로 확인해보자
테스트 코드는 다음과 같다.
val density = LocalDensity.current
val spacingInSp = with(density) { 2.dp.toSp() }
Row(
modifier = Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Text("title")
Text(text = buildAnnotatedString {
withStyle(style = SpanStyle(fontSize = spacingInSp)) {
append(text = "\u2003")
}
})
Text("*")
}
알아볼 건 중간에 있는 \u2003 이 2dp 만큼 width 를 가지는 지이다.
다음과 같이 정확하게 2dp 를 가진다.
결과
이제 위 세번째 방식을 채택하고 적용한 코드를 보자.
아래 함수가 title 에 2dp 만큼의 margin 과 별표(*) 를 제공할 것이다.
@Composable
fun withRequiredMark(title: String) = buildAnnotatedString {
val density = LocalDensity.current
val spacingInSp = with(density) { 2.dp.toSp() }
val markColor = Red03
append(text = title)
withStyle(style = SpanStyle(fontSize = spacingInSp)) {
append(text = "\u2003")
}
withStyle(style = SpanStyle(color = markColor)) {
append(text = "*")
}
}
위 함수를 단순히 아래처럼 title 파라미터에 추가시켜서 사용하면 된다.
MDSTextField(
modifier = Modifier
.fillMaxWidth()
.onFocusChanged { isFilled = !it.isFocused },
value = userInput,
onValueChange = { userInput = it },
title = withRequiredMark("title"),
placeholder = "placeholder",
isFilled = isFilled,
isError = isError,
helperText = "글자수는 ${maxCount}자 이하로 입력해주세요.",
maxCount = maxCount,
singleLine = true,
icon = MDSTextFieldIcons.Clear,
onIconClick = { userInput = userInput.copy("") },
keyboardActions = KeyboardActions(onDone = {
focusManager.clearFocus()
})
)
실제 반영되어 수정된 UI 는 다음과 같다.
머니몽의 디자인 시스템을 개발하고 유지보수하면서 강제성과 자유도에 많은 고민을 했다.
"내부적으로 어느정도까지 가져가야 하지"
"이렇게 구현하면 컴포넌트를 사용할 때 제한되는 부분은 없을까"
"해당 컴포넌트를 사용하는 방법은 여러가지일텐데 내가 억제하는 건 아닐까"
"추가되는 기능이 발생한다면 대처할 수 있을까"
사용하는 쪽에 자유도를 많이 제공하면 컴포넌트가 제각각이 될 가능성도 있으며, '이런 건 어차피 동일하면 내부에서 하는 게 더 편할 것 같은데...' 란 의견이 나올수도 있다.
그렇다고 강제성을 많이 부여하면 컴포넌트의 유연성이 낮아지며, 위와 같이 유지보수에 영향을 끼칠 수 있다.
하지만 유지보수 측면에선 강제성을 부여하는 게 더 좋았던 경우가 크기에 (내부적으로만 바꾸면 사용하는 쪽에선 변경할 필요가 없으니)
둘 사이에 중간점을 찾는 게 중요한 것 같다.
아래는 본문의 내용이 담긴 PR 이다.
https://github.com/MONEYMONG/Android-Moneymong/pull/1
Feature/moneymong 308 mds text field 필수 항목 표시 추가 by jhg3410 · Pull Request #1 · MONEYMONG/Android-Moneymong
요약 버튼 컴포넌트 large 사이즈 상하여백 길이 변경 MDSTextField 필수 항목 표시 추가 작업내용 MDSButton large 의 verticalPadding 을 2 만큼 증가시켰습니다! MDSTextField 의 title 에 필수 표시(*) 을 추가할
github.com
'MoneyMong' 카테고리의 다른 글
Compose Popup 이 특정 기기에서 버그가 발생했다 (0) | 2024.08.01 |
---|---|
기존 화면을 기반한 Onboarding 화면을 구현해보자 (0) | 2024.06.20 |
Markdown 을 파싱해서 compose 로 그린 이야기 (0) | 2024.02.29 |
TextField 의 VisualTransformation 를 사용해 입맛대로 보여주자 (0) | 2023.12.03 |