본문 바로가기

MoneyMong

디자인 시스템 컴포넌트에 디자인이 추가됐다!?

 

머니몽 프로젝트에서 디자인 시스템을 맡았고, 유지보수 도중 디자이너분에게 dm 이 왔다.

 

👩🏼‍💻 디자이너분: ㅎㅈ님 TextField 컴포넌트에 필수 표시(별표) 넣으면 보수가 많이 드나요?

🧑🏻‍💻 필자:           이미 그렇게 구현되어 있지 않아요??

👩🏼‍💻 디자이너분: 이거 단순히 글자 하나 추가한 게 아니라서요,,

🧑🏻‍💻 필자:           확인하고 반영할게요!~

 

여기서 필수 표시(*)는 TextField 에 존재할 수도, 존재하지 않을 수도 있다.(Toggle 의 느낌)

그렇기에 두 가지 상황을 모두 고려해야한다.

본글은 Jetpack Compose 기반으로 작성되어 있습니다

 

 

아래 이미지가 기존 프로젝트에 반영되어 있던 별표가 존재하는 TextField 였다.

 

기존 TextFIeld

 

 

코드적으로 아래처럼 AnnotatedString 을 사용해 색상만 변경된 * 를 추가했었다.

단순히 글자(별표) 하나가 더 추가되고 있었다.

fun withRequiredMark(title: String) = buildAnnotatedString {
    val markColor = Red03

    append(text = title)
    withStyle(style = SpanStyle(color = markColor)) {
        append(text = "*")
    }
}

 

 

아래의 새로 디자인된 TextField 에는 title 과 별표 사이에 margin 값이 추가되었다.

변경된 TextField

 

이제부터 위 디자인을 만족하기 위해 필자의 생각이 어떻게 흘러갔는지 알아보자.

 

 

첫 번째 사고

사용할 때 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 으로 공백의 크기를 조절하고자 했다.

 

그래서 공백을 나타내는 유니코드를 찾아보았다.

그 중 아래 두 개가 적절하다 판단했다.

  1. u+2002(En Space - 엔 스페이스)
  2. 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