오늘의집 안드로이드 팀의 콘텐츠 업로더 개발 이야기
보다 나은 유저 경험을 위한 이미지 업로드 최적화 전략
2024년 10월 10일Pio

안녕하세요, 오늘의집 Android Engineer Pio 입니다. :) 2024 상반기에 Android 글로벌 TF 팀에서 진행했던 콘텐츠 업로더 개발기를 소개해 드리고자 합니다.


콘텐츠 업로더란?

유저들은 오늘의집에서 사진과 동영상을 통해 각각의 개성이 담긴 공간, 취향 그리고 라이프스타일을 공유할 수 있습니다. 콘텐츠 업로더는 바로 이러한 공유를 가능하게 하는 기능인데요.

보기에는 간단해 보이지만, 이 기능을 자세히 들여다보면 이미지 크롭, 원하는 상품 태그 추가, 해시태그 삽입 등 다양한 기능을 포함하고 있습니다. 그중에서도 구현이 까다로웠던 이미지 크롭 기능을 비롯해 몇 가지 성능 개선 경험에 대해 소개해드리겠습니다.


🖼️ 이미지 크롭

이미지 크롭 기능은 요구사항이 복잡하여 가장 도전적이었던 기능 중 하나였는데요. 여러 요구사항이 있었지만 주요하게 아래 사항들을 해결해야 했습니다.

✅ 유저는 이미지를 핀치(Zoom-In / Zoom-Out / Transition) 하여 특정 영역을 잘라낼 수 있다.

✅ 이미지 가로/세로 비율은 2.0 ~ 0.75

✅ 항상 유저가 선택한 첫 번째 이미지의 비율을 기준으로 나머지 이미지들의 비율을 통일한다.

먼저 다양한 화면에서 사용할 수 있는 핀치 기능을 개발했습니다.

<strong>(1)</strong>
(1)
<strong>(2)</strong> Horizontal Pager의 좌우 스와이프 액션과 핀치 액션의 충돌을 해결
(2) Horizontal Pager의 좌우 스와이프 액션과 핀치 액션의 충돌을 해결

(1) 개발을 시작할 당시, 오늘의집의 핀치 기능은 외부 라이브러리 코드에 의존하고 있었습니다. Java로 작성된 오래된 코드로 사용성이 떨어졌으며 Compose 시스템에서 사용할 수 없었습니다. 특히 글로벌 앱 개발 프로젝트에 예정되어 있던 다양한 화면에서 이 핀치 기능을 사용해야 했는데, 현재 사용하고 있는 코드로는 활용이 어렵다는 판단을 내렸습니다.

(2) 하지만 핀치 기능은 구현이 매우 복잡합니다. 단순한 Zoom-in / Zoom-out / Transition 간 인터랙션을 넘어, 핀치가 적용된 UI와 부모 UI 간의 이벤트 전달까지 고려해야 해요. 예를 들면, Horizontal Pager에서 좌우 스와이프 액션과 핀치 액션은 서로 충돌합니다.


솔루션을 찾기 위해 외부 오픈소스 라이브러리들의 코드를 탐색해 보았습니다. Compose로 구현된 핀치 라이브러리를 발견했지만, 요구사항을 충족하지 못해 사용할 수 없었습니다. 그렇지만 라이브러리 코드에서 얻은 아이디어를 바탕으로 저희는 오늘의집만의 핀치 기능을 구현할 수 있었습니다. 이 과정에서 오픈소스에 기여하는 경험과 Compose에 대한 숙련도를 쌓을 수 있었습니다. 마침내 만들어진 핀치 기능은 업로드 화면뿐만 아니라 다양한 글로벌 앱 환경에 활용되어 개발 리소스를 절약할 수 있었습니다. 😄


이후 이미지 UI 위에 이미지 비율을 고정하는 Frame UI를 올리고, 유저가 핀치하여 이미지를 크롭할 수 있도록 구현했습니다.

  • 크롭에 필요한 정보들(Scale, Offset 값 등)을 기억하여 Bitmap을 만들고 이를 렌더링했습니다.
  • 유저 이미지의 가로/세로 비율은 각각 다르기 때문에, 이를 동일한 비율의 이미지 프레임에 렌더링하기 위해 복잡한 계산 로직들이 추가되었습니다. 또한 안정성을 확보하기 위해 이러한 로직들에 대한 유닛 테스트 코드를 추가했습니다.
  • 업로드 시에도 Bitmap을 생성하고 Byte Array로 변환하여 서버에 전달했습니다. (뒤에서 설명하겠지만 이 방식은 성능 테스트 단계에서 큰 문제를 일으키게 됩니다. 😥)


📊 성능 개선

글로벌 앱이 런칭된 일부 국가에서는 제한된 네트워크 속도와 디바이스 사양의 제약을 극복하기 위해 더욱 신경 써야 했습니다.

업로드 대시보드

유저들이 얼마나 업로드를 시도하고 실패하는지, 또 실패한 원인은 무엇이었는지 파악하기 위해 에러 메시지, 유저의 디바이스 정보, 네트워크 정보, 파일 크기 등을 로깅 했습니다. 또한 저희는 이러한 문제들을 빠르게 발견하고 해결하기 위해 대시보드를 구축하여 모니터링하고 있습니다.


업로드 화면 진입 속도

업로드 화면 진입 시 External Storage에서 유저가 보유한 모든 미디어들을 쿼리해서 가져옵니다. 미디어 파일이 많지 않다면 문제가 없지만, 유저가 1~2만개 정도의 미디어를 가지고 있는 경우 화면 진입 시 버벅임 현상이나 심한 경우 ANR이 발생했습니다. 이를 개선하기 위해 Paging을 적용하여 한 번에 모든 미디어를 가져오지 않고 100개씩 단계적으로 불러오도록 변경했습니다. 그 결과, 약 5000개의 미디어를 기준으로 약 3배 정도 속도 향상 효과가 있었습니다! 🥳

또한 Firebase Performance Monitoring 도구를 이용하여 업로드 화면 진입 구간의 성능을 모니터링하고자 했습니다.


비트맵 메모리 캐시

크롭한 이미지들은 Bitmap을 통해 렌더링됩니다. 첫 번째 화면에서 이미지를 크롭한 후 그다음 화면, 그리고 그다음 화면에서도 동일한 이미지가 계속 사용되는데요. 처음에는 이미지들이 화면에 필요할 때마다 Bitmap을 새로 생성하여 렌더링했습니다. 그런데 이 과정에서 Bitmap이 재사용되지 않고 중복으로 생성되면서 저사양 디바이스나 고화질 이미지들을 업로드할 때 OOM 오류가 발생하는 문제가 발생했습니다. 😱 이를 해결하기 위해 Coil의 Transformation과 메모리 캐시를 활용하여 Bitmap이 중복으로 생성되지 않도록 최적화하였습니다.

val transformation = remember( memoryCacheKey, ... ) { object : Transformation { override val cacheKey: String get() = memoryCacheKey override suspend fun transform(input: Bitmap, size: Size): Bitmap { return getCroppedImageBitmap(..) } } } AsyncImage( model = ImageRequest.Builder(LocalContext.current) .transformations(transformation) .memoryCachePolicy(CachePolicy.ENABLED) .memoryCacheKey(memoryCacheKey) .crossfade(true) .data(Uri.parse(path)) .build(), ... )

사용했던 Bitmap을 Coil의 메모리 캐시에 저장했습니다. 캐시의 키 값으로는 이미지의 URI와 크롭을 위한 정보들(Scale, Offset 값)을 사용했습니다. 이를 적용한 결과 더 이상 OOM이 발생하지 않았고 Profiler를 통해 메모리 성능이 크게 향상했음을 확인할 수 있었습니다! 🎉

Profiler를 통해 Graphics 관련 메모리가 크게 감소했음을 확인
Profiler를 통해 Graphics 관련 메모리가 크게 감소했음을 확인


업로드 후 상세화면 이미지 다운로드 개선

유저는 이미지 업로드 후 콘텐츠 상세화면에 진입하게 됩니다.

콘텐츠 상세화면에서는 API 호출을 통해 응답에 포함된 이미지 URL로 이미지를 로드하고 렌더링을 시도합니다. 하지만 높은 확률로 이미지 로드가 지연되거나 실패하는 현상이 발생했습니다. 글로벌 앱 환경에서는 이미지를 불러올 때 리사이즈된 크기로 가져와 이미지 렌더링 시간을 단축시켰는데요. 그러나 이번 케이스에는 효과가 없었습니다. 백엔드에서도 즉각적인 해결책을 찾기 어려웠기에 다른 방법을 찾아야 했습니다.

이를 해결하기 위해 저희는 ‘로컬 이미지 사용’을 선택했습니다. 업로드 직후라면 유저의 디바이스에 이미지가 남아 있을 가능성이 매우 크고, Bitmap을 메모리 캐시에 저장했기 때문에 캐시 히트율도 굉장히 높습니다. 이 방식을 적용한 결과, 유저가 화면 진입과 거의 동시에 렌더링된 이미지를 볼 수 있게 되어 우수한 유저 경험을 제공할 수 있었습니다. 👏


업로드 / 다운로드 필드 테스트

개발 당시, 오늘의집은 레거시 이미지 서버를 새로운 이미지 서버(PRS Public Resource Service)로 교체하는 과도기에 있었습니다. 국내 환경에서는 새로운 이미지 서버의 안정성과 성능이 충분히 검증되었으나, 해외 환경에서는 여전히 의문점이 남아 있었습니다. Android 글로벌 TF 팀은 이에 대한 해답을 찾기 위해 인도네시아 출장길에 올랐습니다. 인도네시아 현지에서 저희는 이미지 사이즈별, 포맷별 업로드 및 다운로드를 직접 테스트했고, 결과를 로깅하여 레거시 서버와 새로운 이미지 서버 속도를 비교했습니다.

그 결과, 새로운 이미지 서버의 이미지 다운로드 속도가 느리다는 것을 발견했습니다. 이를 백엔드 팀에 전달했고, 당시 글로벌 앱 출시를 앞두고 있던 상황이기에 백엔드 팀에서는 즉각적으로 원인 파악에 나섰습니다.

이후 백엔드 팀은 이미지 해상도 조정, Tiered Distribution 적용, Server Region 변경 등에 대해 빠르게 대응했고, 그 결과 해외 환경에서도 안정성과 성능을 확보할 수 있게 되었습니다.


🚀 성능 개선은 앞으로도 계속됩니다.

  • 이미지 리사이즈 / 압축 후 업로드
  • 성능 대시보드 구축

앞으로 더 나은 성능을 위해 위와 같은 내용을 지속 개선해나갈 예정입니다.

언제나 개선의 여지가 있다고 믿으며 더 좋은 결과물을 만들기 위해 노력하는 오늘의집 Android 팀에서 일하다 보면 ‘아, 이게 탁월함을 추구하며 일하는 거구나’하는 생각이 자연스레 떠오르는데요. 앞으로도 오늘의집 Android 팀은 고객의 삶에 긍정적인 변화를 만들기 위해 끊임없이 고민하며 나아갈 예정입니다. 이러한 우리의 여정에 함께해주실 동료분들을 기다리고 있습니다. 많은 관심 부탁드리며, 이만 마무리하겠습니다. 감사합니다! 🙏


오늘의집에서 당신을 찾고 있습니다!
Technical Lead & Manager, GrowthTechnical Lead & Manager, AndroidTechnical Lead & Manager, Site Reliability EngineerSenior Technical Program ManagerSenior Software Engineer, BackendSenior Software Engineer, FrontendSenior Software Engineer, Machine LearningSenior Software Engineer, Machine Learning, XRTechnical Program ManagerSoftware Engineer, BackendSoftware Engineer, Backend, XRSoftware Engineer, Backend, AdsSoftware Engineer, DataSoftware Engineer, FrontendSoftware Engineer, Frontend, XRSoftware Engineer, AndroidSoftware Engineer, Machine LearningSite Reliability EngineerDatabase AdministratorQA Engineer
목록으로 돌아가기