1년 동안의 iOS 모듈화 진행기 - 2. Component 모듈

안녕하세요! 앱 개발팀의 레이몬드입니다. 2024년에 진행했던 모듈화 여정을 1년 동안의 iOS 모듈화 진행기로 공유했었는데요, 이번에는 그 여정의 다음 단계인 Component 모듈에 대해 이야기해보려 합니다.

지난 글에서는 모놀리식 구조에서 출발해 Core 레이어를 분리하고, 의존성을 정리해 나가는 과정을 다뤘습니다. 이번 글에서는 그 위에 쌓인 다음 단계, 즉 공통 UI 컴포넌트를 모듈로 분리하고, 스토리북·스냅샷 테스트까지 엮어 “관리 가능한 컴포넌트 체계”를 만든 과정을 공유하려고 합니다.

단순히 모듈을 나누는 것을 넘어서서,

  • 왜 Component 모듈이 필요했는지,
  • 어떻게 분리했고,
  • 서버 드리븐 환경에서의 가시성 문제를 어떻게 줄였는지,
  • 컴포넌트 변경 시 QA 범위를 어떻게 “눈으로 볼 수 있게” 만들었는지

까지 정리해 보겠습니다.

1. 왜 Component 모듈이 필요했을까?

1-1. 피처 모듈 분리의 필수 관문

제 개인적인 모듈화의 궁극적인 목표는 피처 모듈 분리와 데모앱을 통한 생산성 향상입니다. 하지만 현실에서는 피처 모듈들이 공통 UI 컴포넌트에 강하게 의존하고 있었습니다.

지그재그 앱에는 다음과 같은 공통 UI 컴포넌트들이 다수 존재했습니다.

  • 상품 카드를 포함한 다양한 형태의 캐러셀
  • 여러 지면에서 재사용되는 헤더 뷰
  • 다양한 타입의 배너

01.png

이런 컴포넌트들이 여러 피처에서 얽혀서 사용되고 있었고, 이 상태에서는 피처 모듈을 독립적으로 분리하기가 어려웠습니다. 결과적으로 우리는 다음과 같은 결론에 도달했습니다.

피처 모듈을 온전히 분리하려면, 먼저 공통 UI 컴포넌트들을 독립된 Component 모듈로 분리해야 한다.

03.png

여러 지면에서 동일한 컴포넌트를 공유하고 있기 때문에, 이 레이어를 먼저 정리하지 않으면 피처 단에서의 모듈 분리는 항상 어딘가 걸려버리곤 했습니다.

1-2. 서버 드리븐 환경에서의 가시성 부족

지그재그 앱의 주요 지면은 서버 드리븐 방식으로 운영되고 있습니다.

  • 서버에서 내려주는 데이터에 따라 어떤 컴포넌트가 어떤 순서로 배치될지가 결정되고
  • 클라이언트는 그 스펙에 맞춰 UI를 그립니다.

이 구조는 유연성을 주는 동시에, 다음과 같은 한계를 만들었습니다.

  • 앱에 어떤 컴포넌트들이 존재하는지 한눈에 볼 수 있는 체계가 없음
  • “이거 예전에 비슷한 거 본 것 같은데…” 싶은데, 실제로 있는지 확인하려면
    • 특정 사람에게 물어보거나,
    • 서버 스펙과 앱 코드를 함께 뒤져봐야 했습니다.

이로 인해 실제로 이런 문제가 반복해서 발생했습니다.

  • 이미 유사한 컴포넌트가 존재했지만, 그 존재를 몰라서 거의 동일한 컴포넌트가 중복 생성
  • “이 컴포넌트 재사용하면 되지 않나요?”를 아는 사람이 일부에 한정되어 있어, 확인만으로도 큰 커뮤니케이션 비용이 발생

04.png

결과적으로, 서버 드리븐 구조 위에서 컴포넌트 카탈로그/스토리북의 부재가 비용으로 드러나고 있었습니다.

1-3. 불명확한 QA 범위

서버 드리븐 컴포넌트들은 내부적으로 공통 View나 공통 헤더·캐러셀을 공유하는 경우가 많아서 다음과 같은 상황도 자주 발생했습니다.

05.png

문제는 크게 세 가지였습니다.

  • 특정 컴포넌트를 수정했을 때 다른 컴포넌트까지 영향을 받는 경우가 잦음
  • 어떤 컴포넌트까지 영향이 가는지, 그리고 그 컴포넌트가 어떤 지면에서 노출되는지를 한눈에 파악하기 어려움
  • 결과적으로 QA 범위를 명확하게 정의하기 힘들어짐

“수정은 했는데, 정확히 어디까지 검증해야 하는가?”가 항상 남는 질문이었습니다.

2. Component 모듈을 어떻게 분리했나

2-1. 기본 구조: UICollectionViewCell + CellViewModel

지그재그 앱의 서버 드리븐 컴포넌트들은 대부분 다음과 같은 구조를 가지고 있었습니다.

06.png

2-2. Core 레이어 선행 분리의 효과

여기서 큰 도움이 되었던 것은, 이미 ZCore 모듈로 모델·Repository를 분리해 둔 상태였다는 점입니다.

과거 모듈화 여정에서 우리는 다음을 먼저 진행했습니다.

  • 공용 도메인 모델, Repository, LogService 등을 Core 레이어(ZCore)로 분리
  • 이로 인해 컴포넌트 레이어에서 참조하는 의존성 상당수가 이미 모듈화된 상태였습니다.

그래서 Component 모듈 분리는 크게 아래와 같은 작업으로 정리할 수 있었습니다.

  1. 관련 파일들을 ZComponent 모듈로 이동
  2. 필요한 타입에 public 접근 제어자 추가
  3. 사용하는 쪽에 import ZComponent 추가

물론 실제로는 파일 수가 많아 수고가 적지는 않았지만, “Core 분리가 선행된 상태” 덕분에 의존성 정리에 대한 불확실성은 많이 줄어든 상태에서 작업을 시작할 수 있었습니다.

2-3. 커밋 전략: 리뷰어 친화적으로 쪼개기

모듈 분리를 하다 보면, 단순히 파일을 옮기고 public을 붙였을 뿐인데도 수백 줄의 diff가 생기곤 합니다. 이 상태에서 로직 변경까지 섞이면 리뷰어 입장에서 중요한 변경을 놓치기 쉬웠습니다.

그래서 다음과 같이 커밋을 나누는 전략을 사용했습니다.

  1. 의존성 분리 커밋
    • 모듈로 분리하고자 하는 대상(컴포넌트, 클래스)과 엮인 의존성 코드를 먼저 분리
    • 커밋 메시지로 “의존성 정리”임을 명시
  2. 파일 이동 커밋
    • 대상 파일만 모듈로 이동
    • Git이 rename/move를 인식하도록 변경 최소화
    • 리뷰어는 “이 커밋은 그냥 이동이구나” 하고 빠르게 넘어갈 수 있음
  3. public + import 정리 커밋
    • 이동된 파일에 필요한 public 추가
    • 사용처에 import ZComponent 추가
    • 커밋 메시지로 명시해서 “접근 제어자/임포트 정리 커밋”임을 알 수 있게 함
  4. 로직 변경 커밋
    • 실제 로직 변경은 별도의 커밋으로 분리
    • PR 설명에 “여기가 진짜 로직 변경 포인트”라고 명시

이렇게 나누니 리뷰어 입장에서 “어디를 집중해서 봐야 하는지”가 훨씬 명확해졌고, 결과적으로 리뷰 속도와 품질이 함께 개선되었습니다.

3. Component 스토리북: 서버 드리븐 환경의 가시성 높이기

앞서 말한 두 번째 문제, 즉 서버 드리븐 환경에서의 가시성 부족을 해결하기 위해 우리는 ZComponent 모듈에 **데모 타겟(스토리북 앱)**을 만들었습니다.

07.png

3-1. 목표

Component 스토리북의 목표는 명확했습니다.

  1. ZComponent 모듈에 어떤 컴포넌트들이 있는지 한눈에 파악할 수 있을 것
  2. 각 컴포넌트의 variation(상태, 옵션 조합)을 손쉽게 살펴볼 수 있을 것

3-2. 구현 방식

1) 데모 타겟 추가

ZComponent 모듈에 데모용 앱 타겟을 추가했습니다.

  • 데모앱은 ZComponent 모듈을 import하여 컴포넌트만으로 화면을 구성
  • 서버와는 완전히 독립된 형태로, 순수하게 “컴포넌트의 모습”만 보여주는 앱입니다.

2) JSON 기반 Mock 데이터

각 컴포넌트가 바인딩하는 모델 데이터를 JSON 파일로 케이스별 저장했습니다.

  • 예: GoodsGroupCell_default.json, GoodsGroupCell_discounted.json
  • 데모 앱에서는 이 JSON을 로딩해 모델로 디코딩한 뒤, 해당 컴포넌트에 바인딩

이를 통해 다음이 가능해졌습니다.

  • 상태/옵션 조합이 많은 컴포넌트도 모든 케이스를 빠르게 순회
  • 서버 응답 스펙과 무관하게, 순수하게 UI만 검증

3) 팀 전체의 컴포넌트 카탈로그로

이 데모앱은 iOS 개발자뿐만 아니라, 팀 전체의 “컴포넌트 카탈로그” 역할을 하게 되었습니다.

  • iOS 개발자
    • 새 기능을 만들 때, 먼저 데모앱을 열어 이미 있는 컴포넌트로 구현 가능한지 확인
  • 서버 개발자
    • 서버에서 어떤 타입을 내려줘야 어떤 컴포넌트가 그려지는지, 데모앱으로 스펙을 구체화
  • 디자이너
    • 현재 앱이 실제로 어떤 컴포넌트를 지원하는지, 어떤 variation이 있는지 화면으로 확인

이전에는 “누군가의 머릿속”에만 있던 정보가, 이제는 누구나 실행해볼 수 있는 앱으로 정리되었다는 점이 가장 큰 변화였습니다.

4. Component 스냅샷 테스트: QA 영향 범위를 시각화하기

세 번째 문제였던 QA 범위 파악의 어려움을 해결하기 위해, 우리는 Component 모듈에 스냅샷 테스트를 도입했습니다.

4-1. 스냅샷 테스트의 목표

Component 스냅샷 테스트의 목적은 명확합니다.

  1. Component 모듈 변경 시 다른 컴포넌트에 미치는 영향도를 파악
  2. 그 영향도를 한눈에 보기 쉽게 정리하는 것

4-2. 구현: uber/ios-snapshot-test-case + CI/CD

우리는 uber/ios-snapshot-test-case 라이브러리를 기반으로 스냅샷 테스트를 구현했습니다.

08.png

작동 방식은 다음과 같습니다.

09.png

덕분에 PR에 다음과 같은 대화가 가능해졌습니다.

10.png

PR 작성자와 코드 리뷰어는 실제 UI 기준으로 어떤 컴포넌트들이 영향을 받았는지를 PR 화면에서 바로 확인할 수 있게 되었고 QA에게 바로 전달할 수 있게 되었습니다.

정리하며: 우리가 해결하고 싶었던 세 가지 문제

Component 모듈, 데모앱, 스냅샷 테스트까지의 흐름을 통해, 처음에 이야기했던 세 가지 문제를 다음과 같이 정리할 수 있습니다.

  1. Feature 모듈 분리 전, 필수 요건
    • 여러 지면에서 공통으로 쓰이는 UI 컴포넌트들을 ZComponent 모듈로 분리함으로써, 향후 피처 모듈 분리를 위한 기반을 마련했습니다.
  2. 서버 드리븐 환경에서의 가시성 부족
    • ZComponent 데모앱(스토리북)을 통해, 어떤 컴포넌트가 존재하고 어떤 variation이 있는지 한눈에 확인 가능한 카탈로그를 만들었습니다.
    • 이제는 “비슷한 컴포넌트 또 만들 뻔했다”는 상황을 많이 줄일 수 있게 되었습니다.
  3. 영향범위·QA 범위 파악의 어려움
    • Component 스냅샷 테스트를 도입하고, CI/CD와 연계해 PR 코멘트로 결과를 남기면서, UI 기준의 영향 범위를 시각적으로 공유할 수 있게 되었습니다.
    • “이번 변경이 어디까지 영향을 미쳤는지”를 말이 아닌 이미지로 설명할 수 있게 된 것이 큰 차이였습니다.

모듈화 여정은 여전히 진행 중이고, 완벽한 정답을 찾았다고 보기는 어렵습니다. 다만 이번 Component 모듈 작업을 통해,

  • 피처 모듈 분리를 위한 발판을 만들고,
  • 서버 드리븐 환경에서의 컴포넌트 가시성을 높이고,
  • 영향 범위를 이전보다 빠르게 파악할 수 있게 되었다는 점은 분명한 성과였습니다.

이 글이 비슷한 고민을 하고 있는 팀들에게 “이런 접근도 가능하구나” 정도의 참고가 되면 좋겠습니다.



comments powered by Disqus