Album_Screen_구현_관련_기록 - boostcampwm-2024/and04-Nature-Album GitHub Wiki

관련 PR: Album Screen 구현 및 Navigation 연결 #87

https://github.com/boostcampwm-2024/and04-Nature-Album/pull/87

✅ 구현 기능

  • Hilt를 통해 AlbumViewModel을 주입받아 생성하고, AlbumScreen UI에 필요한 데이터를 AlbumDto로 관리.
  • Room Database에서 쿼리를 통해 앨범 데이터를 가져와서 UI에 표시하며, 더미 데이터를 생성하여 초기 테스트에 사용.
  • Navigation을 구현하여 Album 관련 화면을 Compose와 Hilt를 통합하여 탐색 가능하게 구성.

💡 진행한 것들

  • Hilt를 사용한 AlbumViewModel 생성
  • Room Database 쿼리 함수 수정
  • AlbumDto 생성
  • 더미 데이터 생성하여 Room DB에 저장
  • ViewModel에서 Repository를 통한 Room DB 접근
  • Navigation 구현 및 Hilt 통합
  • 더미 데이터로 도감 UI Test

image

💡문제 해결 과정 기록

Hit를 사용하여 AlbumViewModel 생성

@HiltViewModel 어노테이션을 통해 AlbumViewModel을 생성하고, ViewModel의 매개변수로 Repository를 주입받아 데이터 관리 및 가공을 담당하게 구성하였다.

@HiltViewModel
class AlbumViewModel @Inject constructor(
    private val repository: DataRepository
)

AppDataBase 내의 쿼리 함수 수정

getPhotoDetailUriById(): List<String> 와 기타 몇 몇 함수의 반환값이 잘못됐다.

  • 단일 Uri 를 가져오는 부분인데 List<String> 으로 되어있었기에 String 반환 값으로 수정
  • AlbumDao 에 존재하던 위 쿼리 함수를 PhotoDetailDao 로 이동

AlbumDto 생성 및 더미 데이터 표시

실제 UI에 필요한 데이터들을 가지고 있는 AlbumDto 생성했다.

AlbumDto 의 역할은 Room DB에서 쿼리를 통해 가져온 값들 중 AlbumScreen 에 사용되는 값들을 저장한다.

data class AlbumDto(
    val labelId: Int,
    val labelName: String,
    val labelBackgroundColor: String,
    val photoDetailUri: String
)

또한, 초기 데이터가 없는 상황에서 UI 테스트를 위해 createDummyData() 함수를 사용해 더미 데이터를 Room DB에 삽입하였다.

ViewModel에서 Repository를 통한 Room DB 접근

ViewModel의 loadAlbums() 함수를 통해 Room DB에서 데이터를 가져와 UI에 필요한 형태로 가공한 후 AlbumDto에 담아 LiveData를 통해 UI에 반영되도록 하였다.

또한, 도감 로딩에 관련된 세부 동작은 아래와 같다.

image

도감 로딩 과정

  1. Room DB에서 앨범 전체 가져오기
    • repository.getALLAlbum()을 호출하여 List<Album> 형태로 전체 앨범 데이터를 가져온다.
  2. Album의 label_idphoto_detail_id를 이용한 추가 쿼리
    • Album 항목의 label_idphoto_detail_id를 사용하여 관련 데이터를 개별적으로 조회한다.
  3. 각 항목별 데이터 조회
    • Label 이름 조회: Album.labelId를 이용해 repository.getLabelNameById(labelId)를 호출하여 라벨 이름을 가져온다.
    • 대표 이미지 URI 조회: Album.photoDetailId를 통해 repository.getPhotoDetailUriById(photoDetailId)를 호출하여 대표 이미지 URI를 가져온다.
    • 라벨 배경 색상 조회: Album.labelId를 사용해 repository.getLabelBackgroundColorById(labelId)를 호출하여 라벨의 배경 색상을 가져온다.
  4. 가공한 데이터 AlbumDto에 저장
    • 조회된 데이터를 AlbumDto에 저장하여 UI에서 필요한 정보를 담도록 구성한다. AlbumDto에는 다음 정보가 포함된다:
      • labelId: 라벨 ID (추후 쿼리용으로 사용)
      • labelName: 라벨 이름
      • labelBackgroundColor: 라벨 배경 색상
      • photoDetailUri: 대표 이미지 URI
  5. UI에 반영
    • 최종적으로 LiveData를 통해 가공된 AlbumDto 리스트가 UI에 반영되어 앨범 화면에 표시된다.

Navigation 구현 및 Hilt 통합

Compose 내에서 hiltViewModel()을 사용하여 Hilt와 Navigation을 통합하였다. AlbumScreen을 탐색하는 화면에서 viewModel() 대신 hiltViewModel()을 사용하여 Hilt가 주입된 ViewModel 인스턴스를 관리하도록 설정하였다.

fun AlbumScreen(viewModel: AlbumViewModel = hiltViewModel())

이렇게 설정함으로써, AlbumScreen에서 ViewModel 주입이 정상적으로 이루어지며, Hilt를 통한 의존성 관리가 유지되었다.

❓ 주요 개념 및 노트

  • DTO, Mapper

    UI에 보여줄 용도로 데이터를 변환하고 가공하는 작업을 한다면 DTO와 함께 Mapper 패턴을 사용하는 것이 유용하다. DTO는 데이터를 전달하기 위한 객체로서, DAO에서 가져온 데이터와는 다른 형태로 UI에 최적화된 데이터를 가질 수 있다. 반면, Mapper는 데이터 변환 로직을 담당하는 객체로서, 다음과 같은 차이점이 있다.

    1. DTO (Data Transfer Object):
      • DTO는 순수하게 데이터를 전달하는 용도로 생성된 객체다.
      • 변환된 데이터를 담아 UI와 함께 사용되거나, 다양한 계층 간의 데이터를 전달할 때 쓰인다.
      • UI가 필요로 하는 최소한의 정보만 담고 있어 더 가볍고, UI가 필요한 형식에 맞게 데이터를 제공한다.
      • DTO 자체는 데이터를 단순히 전달하는 역할이기 때문에 데이터 변환 로직은 포함하지 않는다.
    2. Mapper:
      • Mapper는 데이터 변환 로직을 추상화하는 객체다.
      • 일반적으로 DAO로부터 가져온 데이터 엔티티를 UI에 필요한 DTO로 변환하거나 반대로 변환하는 역할을 한다.
      • Mapper는 엔티티와 DTO 사이의 데이터 이동을 돕고, 이를 통해 데이터의 형식을 UI에 맞게 가공한다.
      • 예를 들어, Room DB에서 가져온 UserEntityUserDTO로 변환할 때 Mapper를 사용해 변환 로직을 관리하면 DTO의 목적을 더 명확하게 유지할 수 있다.

    정리하자면: UI에 보여줄 용도로 데이터를 가공해야 한다면, 데이터 전달을 위한 DTO를 정의하고, 이 데이터로 변환하는 책임은 Mapper에서 관리하는 것이 좋다. 이 방법을 통해 코드의 책임이 분리되어 유지보수성과 가독성이 높아지며, 데이터 변환이 필요한 다른 영역에서도 재사용할 수 있다.

  • LiveData에서 postValue() vs setValue() 의 차이

    • LiveData에서 postValue() vs setValue() 의 차이
      • postValue()는 백그라운드 스레드에서 메인 스레드에 있는 LiveData의 값에 접근하기 위해 사용
      • setValue() 는 메인스레드에서 LiveData의 값을 변경하는 것
  • Room DB에서 데이터를 가져올 때 DisPatcher.IO 를 명시해줘야할까?

    • Room DB에서 데이터를 가져올 때 DisPatcher.IO 를 명시해줘야할까?
      • Retrofit2 에서는 [Dispatcher.IO](http://dispatcher.io/) 를 명시하지 않는 이유는 Retrofit2에서 내부적으로 비동기 네트워크 요청을 처리하도록 설계되어 있어, 네트워크 통신 시 별도의 쓰레드에서 작업을 수행하기 때문이다.
      • 반면에 Room DB는 Retrofit2의 처리방식과 다르기에 [DisPatcher.IO](http://dispatcher.io/) 를 지정하여 앨범 가져오는 등의 작업을 별도의 IO 쓰레드에서 하는 것이 적절하다.
  • LiveData에서 get() 을 쓰지 않아도 되는 이유는?

    • LiveData에서 get() 을 쓰지 않아도 되는 이유는?
     private val _issues = MutableLiveData<List<IssueGet>>()
        val issues: LiveData<List<IssueGet>> get() = _issues

DTO vs. Mapper 패턴

  • DTO (Data Transfer Object): 데이터를 전달하기 위한 객체로, UI에 필요한 최소한의 정보만 포함하여 가볍게 만든다.
  • Mapper: 데이터 변환 로직을 담당하는 객체이다.

정리하자면: UI에 보여줄 용도로 데이터를 가공해야 한다면, 데이터 전달을 위한 DTO를 정의하고, 이 데이터로 변환하는 책임은 Mapper에서 관리하는 것이 좋다. 이 방법을 통해 코드의 책임이 분리되어 유지보수성과 가독성이 높아지며, 데이터 변환이 필요한 다른 영역에서도 재사용할 수 있다.

LiveData에서 postValue() vs. setValue()

  • postValue(): 백그라운드 스레드에서 LiveData 값을 변경할 때 사용.
  • setValue(): 메인 스레드에서 LiveData 값을 변경할 때 사용.

Dispatcher.IO와 Room DB 접근

Room DB는 비동기 처리가 자동화되어 있지 않으므로 Dispatcher.IO를 명시하여 IO 쓰레드에서 데이터 작업을 수행하는 것이 적절하다.

LiveData에서 get()을 사용하지 않는 이유

LiveData는 get() 없이도 데이터를 안전하게 노출할 수 있으며, 외부 수정 없이 데이터 접근을 가능하게 하는 방식으로 설계되었다.

💥 트러블 슈팅

1. compileOptions 문제

image

원인 : 1.8 → 17 (options)

  • 내 자바 문제인가?? 싶어서 자바 1.8 버전으로 변경했음에도 계속 에러 발생했다.
  • 해당 에러를 검색해보니 자바 컴파일 옵션을 17로 내리라는 문제였다.
compileOptions {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
    jvmTarget = "17"
}
  • 또한 위 에러가 뜨지만 앱 빌드는 정상적으로 실행됐다.

해결

  • 기존의 1.8 버전을 썼음에도 갑자기 에러가 사라졌다..

2. viewmodel Inject관련 문제

image

해결

  • 도윤님께서 이 부분에 관한 문제를 해결하여 dev 브랜치에 merge했다.

3. Hilt with Compose Navigation

image

  • 원인 : Viewmodel 인스턴스를 생성할 수 없음
  • 추정 : 에러 코드로 들어가보니 Compose Navigation 하면서 AlbumScreen 매개변수의 viewModel에서 에러 발생
//Error
fun AlbumScreen(viewModel: AlbumViewModel = viewModel())

//정상적으로 실행
fun AlbumScreen(viewModel: AlbumViewModel = hiltViewModel())

참고 자료 : https://velog.io/@wlsrhkd4023/Compose-hiltViewModel%EA%B3%BC-viewModel-%EC%B0%A8%EC%9D%B4

  • 결론 : Hilt를 통해 의존성을 주입받은 Viewmdoel에서는 hiltViewModel() 로 사용해야한다.

4. 더미 데이터 URI 변환 문제 해결

image

문제점:

처음 더미 데이터를 생성할 때 photoUri를 제대로 된 URI 형식으로 저장하지 못하여, drawable 리소스 이미지를 가져오는 데 실패했다. URI가 "uri_up_to_~" 형태로 저장되어 앱에서 사용하기 어려운 상태였다.

해결 과정:

  1. getDrawableUri 함수 생성:

    Android에서 drawable 리소스를 URI 형식으로 변환하기 위해 getDrawableUri 함수를 생성했다. 이 함수는 drawable ID를 Uri.parse를 통해 android.resource:// 형식의 URI로 변환한다.

    fun getDrawableUri(context: Context, drawableId: Int): String {
        return Uri.parse("android.resource://${context.packageName}/$drawableId").toString()
    }
  2. 더미 데이터 생성 시 URI 변환 적용:

    createDummyData 함수에서 getDrawableUri를 사용하여 drawable 리소스를 URI로 변환하고, 이를 photoUri에 할당하여 Room DB에 저장하였다.

    val uri01 = getDrawableUri(context, R.drawable.sample_image_01)
    val uri02 = getDrawableUri(context, R.drawable.sample_image_02)
    val uri03 = getDrawableUri(context, R.drawable.sample_image_01)

결과:

변환된 URI가 올바르게 저장되면서 더미 데이터 테스트가 성공적으로 이루어졌다. drawable 리소스 이미지를 올바르게 불러와 UI에 표시할 수 있게 되었다. 이를 통해 더미 데이터를 활용한 UI 테스트가 가능해졌으며, 이후 실제 데이터 로딩 과정에서도 유효한 URI를 사용할 수 있게 되었다.

image

⚠️ **GitHub.com Fallback** ⚠️