원티드 앱에 Compose 적용해보기 — 4

Nampyo Jeong
원티드랩 기술 블로그
10 min readNov 5, 2021

이번 글에서는 지난 글에서 만든 커뮤니티 게시글 목록에 상단 고정 게시물을 가로 스크롤로 추가하는 내용에 대해 적어보겠습니다.

기존 방식의 UI였다면

  1. CommunityPinnedPostAdapter 생성
  2. CommunityPinnedPostViewHolder 및 xml 생성
  3. CommunityPinnedPostSectionViewHolder 및 xml 생성
  4. CommunityBoardAdapter에서 ItemViewType 및 ItemCount 조정
  5. 위의 모든 항목에 데이터 연결

등의 과정을 거쳐야 했을텐데 compose에서는 어떻게 구현되는지 알아보겠습니다.

Text minLines

앞선 글에서처럼 상단 고정 게시물 Item UI부터 구성합니다.

@Composable
private fun PinnedPostItem(
index: Int,
useCase: CommunityPinnedPostUseCase,
) {
val backgroundColorRes = when (index % 3) {
0 -> R.color.secondary_pastel_blue_50
1 -> R.color.secondary_pastel_green_100
else -> R.color.secondary_pastel_purple_50
}

Column(
modifier = Modifier
.width(260.dp)
.clip(RoundedCornerShape(size = 10.dp))
.background(color = colorResource(
id = backgroundColorRes
))
.padding(all = 20.dp),
) {
Text(
text = stringResource(id = useCase.type.titleRes),
style = TextStyle(
color = colorResource(
id = R.color.neutral_gray_500
),
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
),
)
Spacer(modifier = Modifier.height(5.dp))

Text(
text = useCase.title,
style = TextStyle(
color = colorResource(
id = R.color.neutral_black_100
),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
),
maxLines = 2,
overflow = TextOverflow.Ellipsis,
lineHeight = 22.sp,
)

Spacer(modifier = Modifier.height(21.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
UserAvatarImage(
url = useCase.writerAvatar,
isInfluencer = useCase.isInfluencer
)
Spacer(modifier = Modifier.width(4.dp))
Text(
modifier = Modifier.weight(1f),
text = useCase.writerName,
style = TextStyle(
color = colorResource(
id = R.color.neutral_black_100
),
fontSize = 13.sp,
fontWeight = FontWeight.Bold,
),
maxLines = 1,
textAlign = TextAlign.Start,
)
}
}
}

특별할 것 없어 보이지만 한가지 문제점이 있습니다.
제목이 1줄인 경우에도 Text height가 wrap_content로 되지 않고 2줄일 때와 같은 높이를 유지해야하고, Text 정렬이 Top으로 되어야 합니다.

xml 형식이었다면 minLines를 2로 설정해주면 간단히 해결되는 문제인데 아쉽게도 compose의 Text에는 minLines 파라미터가 없습니다. Modifier에서 height를 고정하기에도 sp 단위의 특성상 맞지 않아 lineHeight를 이용해 minHeight를 직접 계산하기로 했습니다.

@Composable
private fun PinnedPostItem(
index: Int,
useCase: CommunityPinnedPostUseCase,
) {
...

Column(
modifier = Modifier
.width(260.dp)
.clip(RoundedCornerShape(size = 10.dp))
.background(color = colorResource(
id = backgroundColorRes
))
.padding(all = 20.dp),
) {
...

val lineHeight = 22.sp
val maxLines = 2

Text(
text = useCase.title,
modifier = Modifier.sizeIn(
minHeight = LocalDensity.current.run {
(lineHeight * maxLines).toDp()
}
),

style = TextStyle(
color = colorResource(
id = R.color.neutral_black_100
),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
),
maxLines = maxLines,
overflow = TextOverflow.Ellipsis,
lineHeight = lineHeight,
)
...

}
}

불필요한 코드가 추가된 것 같아 살짝 찝찝하지만 원하던 결과는 얻을 수 있었습니다. (하루 빨리 Text에 minLines 파라미터가 추가되길 바랍니다.)

상단 고정 게시물 섹션

Item UI를 구성했으니 이제 상단 고정 게시물 섹션을 구현합니다.
이 부분은 특별한 것 없이 LazyRow를 사용해 구현하면 되고 index에 따라 item의 배경색이 변경되기 때문에 itemsIndexed를 사용했습니다.

@Composable
private fun PinnedPostListSection(
posts: List<CommunityPinnedPostUseCase>,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(color = colorResource(
id = R.color.semantic_contents_bg
))
.padding(top = 25.dp),
) {
Text(
modifier = Modifier.padding(start = 15.dp),
text = stringResource(
id = R.string.user_community_wantedpick_title
),
style = TextStyle(
color = colorResource(
id = R.color.neutral_black_100
),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
),
)
Spacer(modifier = Modifier.height(13.dp))
LazyRow(
modifier = Modifier.fillMaxWidth(),
state = rememberLazyListState(),
contentPadding = PaddingValues(start = 15.dp),
) {
itemsIndexed(posts) { index, useCase ->
PinnedPostItem(index, useCase)
Spacer(modifier = Modifier.width(15.dp))
}
}
Spacer(modifier = Modifier.height(25.dp))
Divider(
color = colorResource(id = R.color.semantic_base_bg),
thickness = 10.dp,
)
}
}

LazyColumn에 item으로 section 추가

마지막으로 이미 구현되어 있는 LazyColumn에 PinnedPostListSection을 추가합니다.

LazyColumn에서 item { }으로 PinnedPostListSection을 추가하기만 하면 되는데 고정 게시물이 없는 경우도 있으니 if문으로 감싸줍니다.
그리고 상단 고정 게시물이 표시되는 경우에는 LazyColumn의 topPadding은 없어야 해서 이 부분도 조치해줍니다.

UI가 코드레벨에서 구성되기 때문에 조건문 사용이 자유로워 특정 조건에 따라 간격이 조정되는 작업을 하는 것이 한결 편해졌습니다.

@Composable
private fun PostList(
pinnedPosts: List<CommunityPinnedPostUseCase>,
posts: List<CommunityPostUseCase>,
onLoadMore: () -> Unit,
) {
val topPadding = if (pinnedPosts.isEmpty()) 8.dp else 0.dp

val listState = rememberLazyListState()
LazyColumn(
modifier = Modifier.fillMaxWidth(),
state = listState,
contentPadding = PaddingValues(
top = topPadding,
bottom = 80.dp,
),
) {
if (pinnedPosts.isNotEmpty()) {
item {
PinnedPostListSection(pinnedPosts)
}
}


items(posts) {
...
}
}

...
}

마무리

이번 작업에서는 compose 같은 선언형 UI의 장점을 확실하게 체감할 수 있었습니다.
기존 UI였다면 Adapter, ViewHolder, xml 등의 파일을 생성하고 많은 보일러플레이트 코드가 필요했겠지만 compose에서는 LazyColumn에서 item/items가 여러 개 구성이 가능하기 때문에 아주 손쉽고 빠르게 작업할 수 있었습니다.
특히 조건문 사용이 자유로워 padding이나 visible 처리를 하기 수월했다는 점이 가장 좋았습니다.
다만, Text minLines 같은 세세한 옵션이 빠진 것은 아쉬웠습니다. 아직 초기버전이라는 점을 감안하더라도 자주 사용되는 옵션인 만큼 당연히 있을 거라고 생각했기 때문에 당혹스러웠고 빠른 시일 내에 개선되길 바랍니다.

원티드에서는 다양한 직군에서 적극적으로 채용중입니다! 서버, 웹, 앱, 디자인 등 제품을 만들어가는 각자의 분야에서 전문적인 분들과 함께 일하기를 기대하고 있습니다. 회사 채용 정보 페이지를 확인해 주세요!

--

--