안녕하세요. 29CM iOS 개발자 김동환, 오유원입니다. 최근 29CM 어플리케이션에는 큰 변화가 있었습니다. 서비스의 초창기부터 오랜 기간 같은 형태를 유지하고 있던 메인 화면이 새롭게 개편되었는데요, 새로운 29CM 메인 화면에는 저희 iOS팀의 일하는 방식이 전부 녹아들어가 있습니다.
이번 글에서는 저희가 어떻게 메인 화면을 개편했는지 A 부터 Z 까지 소개해드리고자 합니다.
AS-IS / TO-BE
29CM 의 메인 화면에는 화면의 절반 이상을 차지하는 배너가 있습니다. 29CM 특유의 감도에 있어 많은 부분을 차지하고 있는 배너 구좌이지만 기존에는 배너가 카테고리 별로 분류되어 있지 않아 각각의 고객에게 더 적합한 컨텐츠를 제공하는데 있어 운영적인 어려움이 있었습니다.
저희 스쿼드에서는 이러한 문제 상황을 개선하고자 배너의 위계를 재설계하여 카테고리 하위의 컴포넌트가 되도록 만들었고, 이를 통해 더 다양한 카테고리의 배너 컨텐츠를 제공하여 이 개편을 기점으로 29CM를 패션 플랫폼에서 라이프 스타일 플랫폼으로 보다 더 확장하고자 했습니다.
실제로 메인 화면 개편을 완료한 시점 이후부터 하루 평균 약 24 개 정도의 배너가 고객에게 제공되고 있고, 기존 메인 화면에서의 약 14–16 개의 배너 컨텐츠보다 다양한 컨텐츠를 제공하고 있습니다.
새로운 메인 화면 구조 설계
29CM iOS 메인 화면은 전체 컴포넌트를 담는 상위 ViewController 와, 피드를 표시하는 하위 ViewController 로 구성되어 있습니다. 상위 ViewController 는 각 카테고리 탭을 표시하고 있고, 사용자가 선택한 탭의 이벤트를 방출합니다.
선택한 탭의 정보는 하위 ViewController 의 Reactor 로 이어지고, 이 Reactor 에서 API 통신을 거쳐 fetch 해 온 선택된 탭의 data 가 각 섹션의 데이터로 구성됩니다. 이후, 이 섹션들이 ViewController 에서 각 컴포넌트로 렌더링되어 화면에 표시되도록 설계되어 있습니다.
초기에 저희는 홈 메인을 단일 화면으로 구성하고 단일 ViewController 에서 여러 섹션들로 나누어 각 섹션에 해당하는 데이터를 ViewController 에서 렌더링 하는 방식을 고안했습니다.
이전 메인 화면에서는 배너가 최상위에 위치하면서도 하위에 여러 화면이 있는 구조로 인해 설계가 상당히 복잡하게 되어 있었기 때문에, 새 화면에서는 앞서 존재했었던 설계의 복잡성을 최소화하려는 의도가 있었습니다.
하지만 이후 메인 화면에서 웹 뷰를 표시할 가능성이 있는 등 더 유연한 설계가 필요한 기획이 있는 것으로 확인했기에 비즈니스 차원의 니즈로 인해 ViewController 를 계층적으로 설계하는 것에 대한 논의를 개발을 본격적으로 들어가기 앞서 iOS 팀 내부에서 시작하게 되었습니다.
설계에 대해 충분한 피드백을 받은 뒤 먼저 계층적인 설계 쪽으로 방향을 정하고, 이를 빠른 PoC(Proof of Concept)를 통해서 이 방향이 저희의 스펙에 맞는 방향이라는 것을 먼저 검증한 뒤 본격적으로 개발을 진행할 수 있었습니다.
저희 팀은 빠른 개발을 위해 Modular Architecture 를 채택하고 있어서, 홈 피쳐 모듈에 새롭게 추가되는 컴포넌트들을 개발하는 것과 동시에 기존에 있던 많은 레거시 로직들을 모듈로 이동하는 모듈화 과정도 같이 진행했습니다.
그리고 저희는 Feature Flag 를 도입하고 있기에, 하나의 메인 브랜치에 새로운 메인 화면 개발이 점진적으로 진행되는 동안에 저희의 고객들은 기존 화면을 정상적으로 사용할 수 있도록 하였습니다.
배너 요구사항 분석하기
이번 개선작업에서 가장 큰 변화는 배너였는데요, 배너가 스크롤될 때 크기와 텍스트 위치가 변경되는 형태로 29CM에서는 한번도 사용하지 않았던 UI 입니다.
배너를 구현했던 과정에 대해 소개드리겠습니다.
구현하기에 앞서 요구사항을 정리해보았습니다.
- 좌/우 Cell 은 특정 사이즈로 scale 이 변경
- 스크롤을 넘길 때 다음 Cell 이 가운데에 오는 과정에 텍스트의 위치, alpha 값 변경
- 자동 스크롤
스크롤에 따라 레이아웃이 변경되는걸 볼 수 있는데요, 이를 구현하기 위해서는 UICollectionViewLayout 을 오버라이드할 필요가 있었습니다.
UICollectionView 는 UICollectionViewLayout 을 기반으로 뷰를 그립니다. 레이아웃이 변경될 때마다 CollectionView 는 레이아웃에 Cell 에 대한 사이즈 및 속성들에 대한 정보를 요청하고, CollectionViewLayout 을 통해 받은 정보를 통해 변경된 레이아웃을 그립니다.
커스텀한 레이아웃을 그리기 위해서는 아래의 함수들을 이해해야 했습니다.
- prepare(): 레이아웃 연산이 발생할때 마다 호출되는 함수로, CollectionView의 사이즈, item의 위치 등을 결정하기 위한 계산을 수행합니다.
- shouldInvalidateLayout(): CollectionVIew 의 bound 가 변경될 때 레이아웃 업데이트 여부를 반환합니다.
- layoutAttributesForElements(): 화면에 보이는 Cell 의 레이아웃 속성을 UICollectionViewLayoutAttributes 배열로 반환합니다.
배너 셀 레이아웃 구현
이제 설계를 진행할 차례입니다. 디자인을 보면 UICollectionView 의 중심에서 멀어질수록 Cell 의 크기가 작아지고, 임계점을 지나면 Cell 의 사이즈가 계속해서 유지됩니다.
maxDistance = collectionView.frame.width / 2
distance = abs(collectionViewCenterX - cellCenterX)
// Cell 의 중심축으로 부터 CollectionView의 중심축까지의 거리를 비율로 계산
ratio = (maxDistance - distance) / maxDistance
scale = ratio * (1 - minScale) + minScale // 적용될 Cell 의 스케일
정리해보면 가운데 있는 Cell 이 CollectionView의 중심축을 기준으로 maxDistance 에서 이동한 offset 만큼 스케일이 변경되는걸 알 수 있습니다. 계산된 ratio 를 활용해 스크롤 애니메이션도 구현해주었습니다.
하지만 minimumLineSpacing 이 의도한 값대로 나오지 않는데요, 이는 Cell 의 스케일이 적용되지 않은 사이즈를 기준으로 Cell 사이 간격이 계산되기 때문입니다.
스케일이 적용된만큼 수치를 보정해주면 Cell 간 간격이 의도한대로 나오는걸 확인할 수 있습니다.
배너 자동 스크롤 구현
마지막 요구사항이었던 자동 스크롤이 남았습니다. 배너의 경우 주요한 지표로 보고 있어 신뢰성이 중요한데요, 배너의 노출 여부 외에 IDFA 수집 팝업/인앱메시지 등이 노출되고 있다면 자동스크롤을 멈추도록 해야했습니다.
단순 타이머가 아닌 Rx 관점으로 특정한 조건에 해당하는 경우에만 스크롤이 되도록 해야했기 때문에 복잡한 스트림을 단순화 하기 위해 정리를 했습니다
배너가 초기화되고, 메인화면이 유저에게 노출되는지 여부를 판단하기 위해서 window 를 체크하는 등 많은 스트림이 필요한 상태였으나 스트림을 최대한 단순화 하기 위해 여러 상태들을 크게 4개의 스트림으로 묶었습니다.
Future Action
현재 개편된 메인 화면의 카테고리 탭은 로컬 데이터로 저장되어 표시되고 있고, 백그라운드 컬러 등의 UI 도 iOS 프로젝트 단에서 하드 코딩 되어 있습니다. 그래서 현재는 세일즈 쪽이나 각 스쿼드에서 상황이나 조건에 따라 카테고리의 순서나 각 탭의 UI 를 유연하게 표시하려고 하는 경우에 바로바로 대응하기가 어려운 상황입니다.
예컨대 저희가 앞으로 특정 프로모션이나 기획전을 진행하려고 할 때, 해당 프로모션 전용 탭을 추가하거나 특정 탭을 피쳐링하거나 하는 니즈가 있을 때 이를 원격으로 바로 설정할 수 있다면 보다 적시에, 빠르게 운영을 할 수 있게 됩니다.
그래서 저희 스쿼드는 Future Action 으로 Server Driven UI 를 통해 카테고리 탭을 구성하는 방향을 염두해 두고 있습니다. 카테고리 탭과 각 탭의 UI 를 서버에서 내려주는 Data 만으로 구성하도록 하여, 비즈니스 사이드에서 다양한 카테고리 탭 구성에 대한 요구사항이 있는 경우 바로 유연하게 대처하기 위함입니다.
동시에 저희 iOS 팀에서는 이번 메인 화면 개편 작업과 동시에 모든 메인 화면 코드를 홈 피처 모듈로 분리했기 때문에 앞으로 메인 화면에서의 추가 개발을 이전보다 더욱 빠른 속도로 해나갈 수 있으리라 기대하고 있습니다.
홈 개편에 대한 회고
29CM의 스쿼드와 기술 조직에서는 이번 메인 화면 개편 프로젝트와 같이 규모가 큰 프로젝트 이후에는 진행했던 프로젝트에 대한 회고를 하는 문화가 있는데요, 이번 프로젝트가 완료된 이후에도 스쿼드와 iOS 팀에서 각각 메인 화면 개편 프로젝트에 대한 회고를 진행했습니다.
회고에서 저희가 이번 프로젝트에서 미흡했던 점과 잘 했던 점, 다음에 다시 이러한 큰 프로젝트가 있을 경우를 대비해서 어떤 점을 개선할 수 있을 지에 대해 여러 이야기를 나누었는데 이번 회고에서는 마지막 배포까지 긴박하게 돌아갔던 이번 프로젝트에서 도움이 되었던 협업 문화에 대한 얘기를 많이 나누었습니다.
저희 iOS 팀에서는 작은 PR 로 협업하는 방식을 지향하고 있는데 이는 각 PR 의 단위가 커질수록 리뷰어의 피로도가 올라가 리뷰를 하기 어려워지고 PR 리뷰의 핀포인트를 잡기 어려워지기 때문입니다.
이번 프로젝트의 첫 배포까지 저희가 작성했던 PR 은 104개로 꽤 많은 PR을 작성하게 되었는데, iOS 구성원 각자가 각 목적조직에서 맡은 업무를 진행하면서도 이 PR들을 같이 리뷰하며 협업할 수 있었던 건 팀이 micro PR 기반으로 협업하는 프로세스 덕이라는 의견이 많았습니다.
micro PR 은 오랜 기간에 걸쳐 저희가 일하는 방식으로 자리 잡았기에 이번 프로젝트의 작업자들은 작업물들을 최대한 작은 PR 로 나누려 노력했었고, 크래시나 성능 이슈를 방지할 수 있도록 구성원들은 적극적으로 PR 리뷰에 참여해 주셨습니다.
그 결과 릴리즈 직전 긴박하게 돌아하던 마무리 과정 속에서도 각 작업들에 대해 충분한 리뷰를 받고 수정을 함으로써 새롭게 개발한 메인 화면에서 예상치 못한 신규 크래시나 이슈가 발생하지 않았고 성능 이슈 또한 발생하지 않을 수 있었습니다.micro PR을 포함해 저희 팀의 개발 문화와 관련해선 앞서 작성된 ‘Trunk-based development, Feature Flag, micro PR 와 함께 주 2회 배포하기’ 글을 참고해 주시면 감사하겠습니다.
마치며
이번 메인 화면 개편 프로젝트는 나름대로 규모가 있있었던 만큼 막바지 크런치 기간엔 모든 모바일팀 구성원이 프로젝트 마무리를 서포트 해 주셨습니다.
개발, 이벤트 적재, 이슈 대응, 리뷰, 테스트 등 배포 과정에서 모든 구성원이 각자가 할 수 있는 작업을 찾아 적극적으로 협업했고, 그 결과 이번 프로젝트를 개발 기한 안에 완수할 수 있었습니다.
이 글에서 소개드린 내용이 저희와 같이 규모 있는 프로젝트를 진행하시거나 혹은 개발 문화/인프라를 구축하시는 데에 도움이 되시길 바라면서 이번 글을 마치겠습니다.
함께 성장할 동료를 찾습니다
29CM (무신사) 는 3년 연속 거래액 2배의 성장을 이루었습니다.
더 나은 방향을 가기위해 노력하는, 위에서 소개드린 내용에 관심이 많고 좋은 조직문화를 함께 가꾸어나가길 원하는 동료 개발자분들을 찾습니다.
특히 저희 iOS 팀은 비즈니스/플랫폼 영역에서 홈 개편, 컨텐츠 개인화, 새로운 포맷의 컨텐츠 개발, QA 자동화 프로세스, 자동화 테스트 구축, 지속적인 모듈화를 통한 microFeature Architecture 구축, 감도 높은 사용성을 위한 웹뷰 고도화, 디버깅 인프라 개선 등 여러 도전 과제들과 함께 진행할 iOS 엔지니어를 모시고 있습니다.
저희 팀에 대해 궁금한 점이 있으시면 iosdev@29cm.com 으로 메일을 남겨주세요. 가벼운 티타임도 가능합니다!
저희 iOS 포지션을 비롯해 많은 지원 부탁드립니다 :)