/App

이 프로젝트는 운동복 플랫폼을 위한 애플리케이션으로, Hilt, Jetpack Compose, Room, Coroutine, Flow, TossPayment를 활용하여 MVVM 패턴의 클린 아키텍처를 구현했습니다.

Primary LanguageKotlin

운동복 쇼핑 및 커뮤니티 Android 프로젝트


기술 스택

image

아키텍처

arch1
해당 프로젝트는 Clean Architecture를 기반으로 설계되었습니다.
Clean Architecture는 도메인 중심 설계 아키텍처로 Presentation Layer와 Data Layer가 Domain을 의존하는 형태로 개발하였습니다.

Data Layer

arch2
데이터 계층은 Repository 패턴으로 설계되며, DB 쿼리 및 네트워크 작업을 처리합니다.
또한, 오프라인 및 캐싱 작업도 이 계층에서 수행합니다.

Domain Layer

arch3

도메인 계층은 비지니스 로직이 존재하는 계층입니다. 핵심 업무 규칙인 Entity가 존재하며, UseCase는 Entity의 데이터의 흐름을 조정하도록 설계하였습니다.

또한 Repository 의존성 역전을 통해서 Data Layer의 의존 하지 않는 형태로 진행하였습니다.

Presentation Layer

arch4

프레젠테이션 계층은 안드로이드에서 권장하는 단방향 데이터 흐름(UDF)을 따르고 있습니다.

Event

Button, CheckBox, Switch, Tabs 등의 UI 요소에서 이벤트가 발생하면 ViewModel을 호출합니다.

UI State

UI 상태에 따라 Loading, Error, Success 등으로 분기 처리하여, UI State(ViewModel)에 따라 사용자에게 표시합니다.

트러블슈팅

상품 태그를 배치하면서 생겼던 어려움점들

크리에이터가 게시한 사진에 상품 태그를 입력해야 하는 상황이 있었습니다. 서버에서 제공해주는 좌표에 맞게 상품 태그를 표시해야 했지만, 일반적인 Jetpack Compose Layout으로는 불가능하여서 Custom Layout을 활용하여 좌표에 맞게 상품 태그를 표시하는 방법을 구현했습니다.

  • 이미지

  • 코드

     1  @Composable
     2  fun CreatorDetailProductItemTagLayout(
     3      imageContent: @Composable () -> Unit,
     4      productTageContent: @Composable () -> Unit,
     5      offsetList: List<Offset>
     6  ) {
     7      Layout(
     8          contents = listOf(imageContent, productTageContent),
     9          measurePolicy = { (imageContentMeasurableList, productTageContentMeasurableList), constraint ->
    10              val imagePlaceable = imageContentMeasurableList.first().measure(constraint)
    11              val productItemTagPlaceable = productTageContentMeasurableList.map { it.measure(constraint) }
    12  
    13              layout(imagePlaceable.width, imagePlaceable.height) {
    14                  imagePlaceable.place(0, 0)
    15  
    16                  productItemTagPlaceable.mapIndexed { index, placeable ->
    17                      placeable.place(
    18                          x = (offsetList[index].x * imagePlaceable.width.toDp() - (placeable.width.toDp() / 2)).toPx().roundToInt(),
    19                          y = (offsetList[index].y * imagePlaceable.width.toDp() - (placeable.height.toDp() / 2) ).toPx().roundToInt()
    20                      )
    21                  }
    22              }
    23          }
    24      )
    25  }
    
    // https://github.com/AblebodyAndroid/App/blob/bb85673b70b0f4848d338bdb17a516a882be5363/app/src/main/java/com/smilehunter/ablebody/presentation/creator_detail/ui/CreatorDetailScreen.kt#L864-L888
  1. Custom Layout 사용: @Composable 함수를 imageContentproductTageContent를 각각 나누어  인자로 받았습니다.
  2. 부모 Layout 배치: imageContent composable layout의 넓이와 높이를 각각 배치했습니다.
  3. 상품 태그 표시: 레이아웃 위에 상품 태그들의 Offset 리스트를 받아서 좌표에 맞게 태그를 표시했습니다.

이렇게 Custom Layout을 활용하여 서버에서 제공하는 좌표에 맞게 상품 태그를 정확히 표시할 수 있었습니다.

(BoxWithConstraints 출시 전 입니다)

Repository를 SingletonComponents로 (무조건)선언했던 문제들

프로젝트 초기에는 Dagger와 Hilt를 제대로 이해하지 못해 모든 모듈에 SingletonComponent를 선언하는 문제가 있었습니다

모듈에 사용하는 생명주기에 맞게 다음과 같이 수정하였습니다 :

hilt

문제를 수정하여 메모리 낭비와 디바이스의 Cold boot 시간을 단축할 수 있었습니다

자주 변경되는 서버의 데이터의 해결하는 과정들

초반에 아키텍처의 제대로 된 설계 없이 persenation Layer가 Network Model (or Network DTO)를 의존하는 문제가 있었습니다.

이러한 문제는 서버가 변경될 때마다 앱 단에서 대규모 리팩토링을 야기했고 이러한 문제를 해결하기 위해 도메인 중심 설계(DDD) 아키텍처인 Clean Architecture를 도입시켰습니다.

에러 처리 하면서 생각 했던 과정들

초반에는 **Sealed interface**를 감싸서 **Result<Data>**를 계층마다 판단하는 방식을 사용했습니다.

하지만, 계층마다 **Sealed interface**를 판단하는 것은 옳지 않다고 판단했습니다.

그 이유는 데이터 판단 코드들이 보일러플레이트 로직과 중복된 코드 등 여러 문제가 발생했기 때문입니다.

또한, 이러한 방식은 코드에서 **throw**를 제대로 이해하지 못한 것이라고 생각했습니다.

따라서, **ViewModel**에서 에러를 **catch**한 후, UI 상태에 따라 판단하는 것이 더 좋은 방향이라고 결론지었습니다.

이미지

운동복 브랜드 목록

카테고리 별 아이템 필터링

유저 코디 게시물

아이템 상세 화면

북마크

마이 페이지

시연영상

https://youtu.be/wuBvWVeh-0c