이건 아니야...
프로젝트에서 마이페이지에 있는 개인정보처리 방침과 이용약관을 구현하려고 피그마를 킨
난 아래의 사진과 같이 수백 줄이 넘고 단순 Text 가 아닌 Bold, 번호 목록, 표 등 다양한 요소들이 들어있는 화면을 목격했다...
이걸 모두 Compose 로 일일이 구현하는 건 팀 리소스를 많이 잡아먹고 좋은 방법이 아니라 생각했다.
그래서 PM 님에게 "노션에 작성하고 웹뷰로 보여주면 어떨까요?"라고 제안을 드렸고
PM 님도 일일이 구현하는 건 좋지 않다고 판단하셔서 흔쾌히 승낙하셨다.
구현 후 appetize 로 완성된 UI 를 보여드렸고 완벽하다고 확인받았다.
그러고 추후 정기 회의 도중
"희직 님... 저희 이용약관 웹뷰로 구현한 게 너무 신경 쓰여요.. 리젝 당할 수도 있을 것 같은데..."
개인적으론 리젝 안 날 것 같았지만, PM 님이 조금이라도 걱정하고 바꿀 수 있다면 바꾸는 게 맞다 생각했다.
"우리 PM 님이 원하시면 바꿔야죠! 바꾸고 다시 DM 드릴게요."
라고 말씀드리고 고민을 시작했다.
고민의 시작
생각은 아래 순서로 진행됐다.
- String
- Markdown - 라이브러리
- Markdown - 직접 구현
1. 그냥 Text 하나에 모든 String 을 넣자.
생각하고 바로 Text에 넣어보고 실행시켜 보고 코드를 모두 지워버렸다...🙃
일단 내 성향 자체가 꽤나 완벽주의이고, 개발 과정에서 지고 타협하는 걸 되도록이면 지양한다.
String 으로 그냥 Text 에 넣어버리면 그게 줄글이지... 그건 디자인한 사람에게 예의가 아니다
2. Markdown 을 토대로 라이브러리를 써서 보여줘 볼까?
노션의 내용은 html 과 markdown 으로 export 할 수 있다.
html 보단 더 단순한 markdown 을 파싱 하면 잘 그릴 수 있겠다 생각했고 바로 검색했다.
"how to show markdown in jetpack compose"
여러 라이브러리가 나오고 살펴보던 중
https://github.com/jeziellago/compose-markdown
GitHub - jeziellago/compose-markdown: Markdown Text for Android Jetpack Compose 📋.
Markdown Text for Android Jetpack Compose 📋. Contribute to jeziellago/compose-markdown development by creating an account on GitHub.
github.com
해당 라이브러리가 괜찮다고 판단했지만 디자인을 만족하지 못하는 게 아쉬웠다.
3. 내가 직접 구현해 볼까?
그럼 직접 해보자. 그렇게 어려워 보이지 않잖아?
물론 하다가 시간이 많이 소비된다고 파악되면 바로 엎을 예정이었다.
내 구현 욕심보단 팀이 먼저기에
요구사항은 아래와 같다.
- 노션에 있는 개인정보 처리 방침, 서비스 이용약관과 최대한 동일하게 화면에 표시한다.
- 표는 디자인과 일치시킨다.
먼저 마크다운의 모든 문법을 파싱 할 필요는 없다.
우리 프로젝트에서 사용되는 문법들만 분석하고 파싱 하자.
다음과 같다.
- 글자 크기를 크게 하는 Heading
ex). 나는 Heading
2. 불릿으로 목록을 나타내는 BulletPoint
ex).
- 목록 1
- 목록 2
- 목록 3
3. 표를 나타내는 Table
ex).
title | title | title |
content | content | content |
content | content | content |
딱 이렇게 3개다. 할만하잖아?
구현은 이렇게
파일은 3개로 나뉜다.
- MyMongMarkdownView: Markdown 이 차지하는 화면을 책임진다.
- MyMongMarkdownText: 문법에 맞는 Text 를 그려준다.
- MyMongMarkdownTable: 표를 그린다.
MyMongMarkdownView
@Composable
fun MyMongMarkdownView(
modifier: Modifier = Modifier,
markdownText: String,
textColor: Color = Gray10
) {
var isTable = false
val tableLines = LinkedList<String>()
Column(modifier = modifier) {
val lines = markdownText.split("\n")
lines.forEach { line ->
if (line.startsWith(prefix = tableSymbol)) {
isTable = true
tableLines.add(line)
} else {
if (isTable) {
isTable = false
MyMongMarkdownTable(lines = tableLines)
tableLines.clear()
}
MyMongMarkdownText(
line = line,
textColor = textColor
)
}
}
if (isTable) {
MyMongMarkdownTable(lines = tableLines)
}
}
}
- 텍스트를 "\n" 으로 split() 해서 라인을 분류한다.
- 라인들의 첫번째 문자를 확인해서 Table(표) 을 그릴 지 Text 를 그릴 지 판단한다.
노션의 표를 Makrdown 으로 export 하면 아래와 같다.
| 수탁자 | 위탁 업무 내용 | 개인정보의 보유 및 이용기간 |
| --- | --- | --- |
| 네이버 클라우드 | OCR (이미지 텍스트 인식), 회원 정보 보관, 이미지 데이터 보관 | 회원 탈퇴 시 또는 위탁 계약 종료시까지 |
| 로 시작하면 table 내용이다.
| 로 시작하지 않는데 이미 table 상태라면 지금 까지 모은 tableLines 로 table 을 그린다.
혹시라도 마지막이 table 로 끝났을 경우를 대비해서 if 조건문을 추가했다.
MyMongMarkdownText
private enum class MarkdownLineType(val prefix: String, val style: TextStyle) {
Heading3(prefix = "# ", style = MDSHeading3),
Heading2(prefix = "## ", style = MDSHeading2),
Heading1(prefix = "### ", style = MDSHeading1),
BulletPoint(prefix = "- ", style = Body3),
Body(prefix = "", style = Body3),
}
@Composable
internal fun MyMongMarkdownText(
modifier: Modifier = Modifier,
line: String,
textColor: Color
) {
val lineType = MarkdownLineType.values()
.filter { type -> type != MarkdownLineType.Body }
.find { type -> line.startsWith(type.prefix) } ?: MarkdownLineType.Body
val markdownLine = when (lineType) {
MarkdownLineType.BulletPoint -> line.replaceFirst("- ", "▪ ")
else -> line.removePrefix(lineType.prefix)
}
Text(
modifier = modifier,
text = markdownLine,
color = textColor,
style = lineType.style
)
}
enum class 로 올 수 있는 Text 타입들을 나열하고 인자로는 타입을 분류할 prefix 와 (text)Style 이 들어가 있다.
MyMongMarkdownText Composable 은 line(text 한 줄) 을 기준으로
해당하는 type 을 찾아내고 알맞게 Text 를 표시한다.
MyMongMarkdownTable
internal const val tableSymbol = "|"
@Composable
internal fun MyMongMarkdownTable(
modifier: Modifier = Modifier,
lines: List<String>,
) {
Column(modifier = modifier.clip(shape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp))) {
val titles = lines.first().split(tableSymbol).filter { it.isNotEmpty() }
Row(
modifier = Modifier.background(Blue04),
verticalAlignment = Alignment.CenterVertically,
) {
titles.forEach { title ->
Text(
modifier = Modifier
.weight(1f)
.padding(10.dp),
text = title,
textAlign = TextAlign.Center,
color = White,
style = Body3
)
}
}
lines.drop(2).forEach { line ->
val contents = line.split(tableSymbol).filter { it.isNotEmpty() }
Row(
modifier = Modifier
.background(Gray01)
.height(intrinsicSize = IntrinsicSize.Min),
verticalAlignment = Alignment.CenterVertically
) {
contents.forEachIndexed { index, content ->
Text(
modifier = Modifier
.weight(1f)
.padding(10.dp),
text = content,
textAlign = TextAlign.Center,
color = Gray08,
style = Body3
)
if (index != contents.lastIndex) {
Divider(
modifier = Modifier
.fillMaxHeight()
.width(1.dp),
color = Gray03
)
}
}
}
}
}
}
목표하는 바는 아래와 같다.
위와 같은 table(표)일 때 실제 넘어오는 lines 먼저 보겠다.
| 수탁자 | 위탁 업무 내용 | 개인정보의 보유 및 이용기간 |
| --- | --- | --- |
| 네이버 클라우드 | OCR (이미지 텍스트 인식), 회원 정보 보관, 이미지 데이터 보관 | 회원 탈퇴 시 또는 위탁 계약 종료시까지 |
title(파란색) 과 content(그레이) 의 ui 가 구분되어 있다 보니
[수탁자, 위탁 업무 내용, 개인 정보의 보유 및 이용기간]은 title 로 분류해야한다.
val titles = lines.first().split(tableSymbol).filter { it.isNotEmpty() }
그리고 lines 에서 title 과 구분선을 드랍하면
content 이기에 drop 을 2번 하고 해당 라인들의 content 를 하나씩 화면에 나타낸다.
lines.drop(2).forEach { line ->
val contents = line.split(tableSymbol).filter { it.isNotEmpty() }
구현 화면
마지막으로 해당 글의 코드가 적용된 PR 이다
https://github.com/YAPP-Github/23rd-Android-Team-2-Android/pull/57
Feature/moneymong 224 개인정보처리방침,서비스 이용약관 수정 by jhg3410 · Pull Request #57 · YAPP-Github/23rd-
요약 개인정보 처리 방침, 서비스 이용약관을 수정했습니다. 🙂 작업내용 기존 WebView 를 제거하고 Compose 로 변경했습니다. 노션에 작성된 Text 를 Markdown 으로 추출해 strings 에 넣었습니다. Markdown
github.com
수정한 UI 를 PM 님에게 전달드리고 꽤나 강렬한 반응을 받았다.
이런 맛에 UI 개발하지😀
생각
개발자에겐 구현(개발) 능력도 당연히 매우 매우 중요하지만
- 서비스에 대한 깊은 생각과 이해, 특히 관심도
- 구현 요청에 대한 당연한 오케이보단 본인의 의견과 뒤받침하는 내용을 나열할 수 있는 능력
또한 중요하지 않을까?
YAPP 에 지원한 이유는 협업 경험이었다.
몇 달을 혼자 개발하면서, 디자이너의 디자인과 코드 리뷰가 목말랐고, 같은 기능에 대한 다른 사람들의 의견이 배고팠다.(그냥 사람이 고팠나...)
이번 프로젝트로 일정관리, 다양한 상황에서의 의사소통 능력 등 꽤나 많이 배우고 얻어간다.
프로젝트 자체에 대한 성과도 당연히 중요하지만 (여긴 회사가 아니기 때문에)
본인이 처음 YAPP (대외활동)에 지원한 목적을 달성했다면 그게 더 값진 게 아닐까 생각한다.
'MoneyMong' 카테고리의 다른 글
Compose Popup 이 특정 기기에서 버그가 발생했다 (0) | 2024.08.01 |
---|---|
기존 화면을 기반한 Onboarding 화면을 구현해보자 (0) | 2024.06.20 |
디자인 시스템 컴포넌트에 디자인이 추가됐다!? (0) | 2024.05.06 |
TextField 의 VisualTransformation 를 사용해 입맛대로 보여주자 (0) | 2023.12.03 |