Tuist 란 Apple 앱 개발을 위해 제공되는 서드파티 도구이다. iOS, macOS 등의 Apple 앱 개발을 위해서는 Xcode 라는 IDE 가 필요한데, Tuist 는 이 Xcode 프로젝트의 모든 세팅을 Swift 코드로 관리할 수 있도록 돕는다. 앱 모듈화, 코드 generation, 프로젝트 관리 등 많은 기술들을 제공한다.
Tuist 는 꽤 강력하다. Xcode 의 UI 로 프로젝트 세팅을 하는 것이 아니라, Swift 코드로 build version, target scheme, 모듈 dependency 등을 한 번 세팅해보면 그 위력을 체감할 수 있다. 풀리퀘를 날려 diff 를 확인할 때도 .pbxproj
파일의 diff 파악보다 .swift
로 적힌 diff 파악이 훨씬 쉽다.
오늘은 Tuist 로 할 수 있는 스킬들 중에서도, Feature 데모 앱 개발
에 대해 정리하려 한다.
Feature 데모 앱이란, 앱에 포함된 수많은 피쳐(기능)들 중, 특정 피쳐만을 따서 앱으로 만든 것. 간단하게 Example 앱이라고도 한다. 토스, 카카오 뱅크에서도 이 Example 앱을 적극 활용하고 있다. 예를 들어 카카오 뱅크 앱엔 송금, 자산관리, 카드, 인증, ... 수많은 기능(피쳐)들이 있는데, 이 중에서 나머지 기능들은 잠시 신경을 끄고, 송금 관련 기능만 집중해서 보여주는 Example 앱을 만들면 이게 바로 송금 Feature 데모 앱이 되는것이다.
카카오 뱅크 예시 (1)
Core 와 UI 모듈 위에 수많은 Feature 모듈이 존재하며, 그에 대응하는 다양한 Feature 데모 앱들이 시뮬레이터에 설치된 모습. (신분증 진위확인 앱, 주소 검색 앱, 상품 안내 앱 등)
카카오 뱅크 예시 (2)
예를들어, 카카오 뱅크 앱 내에서 신분증을 등록할 때 신분증에 대한 진위 확인을 하는 기능(Feature)이 있을 것이다. 이때 Feature 데모 앱이 없다면, 신분증 진위확인 피쳐를 테스트하기 위해서 앱에 로그인하고, 신분증 등록하기 전의 모든 절차를 걸쳐 신분증 진위확인 화면 까지 와야만 한다. 물론, 빌드도 신분증 진위확인을 위한 코드만 빌드하는 것이 아니라 앱 전체에 대한 모든 코드를 빌드해야한다. 위 사진은 로그인 및 다른 절차를 생략하고, 신분증 진위확인 기능만을 제시하는 Feature 데모 앱의 모습을 보여준다.
다른 예로, 토스 앱의 코드는 100만 줄이 넘으며, 모듈 개수도 100개가 넘는다고 한다. 앱의 사이즈가 커지면서 서비스 모듈과 피쳐 모듈들이 계속 늘어나게 되었을 것이다. 이럴 때 이제 Feature 데모 앱의 니즈가 생기게 된다.
토스 예시 (1)
카뱅과 마찬가지로 Service 모듈 위에 홈, 인증, 송금, 소비 등 수많은 Feature 모듈이 존재한다.
토스 예시 (2)
기기에 수많은 Feature 데모 앱들이 설치되어있고, 그 중 송금 Feature 데모 앱의 모습이다. 송금 성공 / 송금 실패 / 송금 기타 각 시나리오에서 어떤 UI 플로우를 타게되는지 쉽게 알아볼 수 있게 된다. 송금 Feature 와 전혀 의존성이 없는 모듈 (예를 들면 증권 모듈 등이 있을 것) 들은 빌드 과정에서 생략된다.
빌드 속도 개선.
예를 들어, 쏘카에서 이제 따릉이도 빌릴 수 있게 되었다고 해보자. 쏘카의 아벨은 따릉이 피쳐 개발을 맡았고, 기존 쏘카의 다른 피쳐 모듈들과 의존성이 없는 피쳐라고 하자. 이 경우 따릉이 피쳐 모듈을 생성하고, 따릉이 Feature 데모 앱을 만들면 아벨은 개발할 때 앱 전체를 빌드할 필요없이 따릉이 데모 앱만 빌드하며 개발할 수 있게 된다. 앱 전체를 빌드하는 것과 피쳐 한 개를 빌드하는 것의 빌드 속도 차이는 매우 클 것이다. 특히 UI 개발에 큰 도움을 줄 수 있는데, UIKit 특성 상 UI 코드를 수정하고 화면에서 어떻게 보이는지 확인하려면 무조건 앱을 빌드해야 하기 때문이다. 수정 한 번 확인하려고 앱 전체를 빌드하는 매우 불편한 상황을 막을 수 있게 된다.
사내 배포 및 테스트 용으로 적합.
토스 디자인 시스템(TDS)을 만드는 디자인 플랫폼 팀의 예시. 개발을 하고 나면, Example 앱을 사내에 배포할 수 있다. 복잡한 화면이나 애니메이션을 개발하다 보면 디자이너와 빌드 된 앱을 같이 확인하는 경우가 많다. 중간 중간 다른 개발 팀원에게 보여주는 경우도 많다. 그럴때마다 "OO 님, 이거 와서 봐주세요, 어때요?" 라고 할 필요가 없어진다. "Example 앱 배포했습니다!" 라고 하면 직접 사내에 배포된 배포물을 받아서 확인하면 되기 때문이다. 실제 협업하며 PO 나 디자이너들이 편리함을 느꼈다고 한다.
한마디로, Feature 데모 앱은 개발 생산성 및 협업에 도움이 된다.
개인 프로젝트 soma-weather-ios 가 마침 피쳐별로 모듈화를 해뒀었기 때문에, 이 프로젝트에 피쳐 데모 앱을 추가해본 과정을 기록하려 한다. 아래는 기존 앱을 실행한 모습이다.
간단한 날씨 앱이다. 3개 탭이 존재하고, 각각은 다른 피쳐 모듈로 나누어져 있다.
Home
: 홈 화면 모듈. 오늘의 날씨와 3시간 간격 예보를 노출.Forecast
: 날씨 예보 모듈. 일주일 날씨를 예보한다.Search
: 검색 모듈. 도시 이름을 검색하면 그 도시의 날씨를 노출.각각의 피쳐들을 분리해서 3개의 피쳐 데모 앱을 만들어보았다.
피쳐 데모 앱을 적용하기 전과 후의 모듈 구조 그래프.
기존 모듈 구조
클린 아키텍처의 구조를 따른다. 가장 상위에 앱 모듈 (SomaWeather
) 이 1개 존재하며, 여기에선 앱에 필요한 의존성을 조립한다. 앱 모듈은 모든 모듈의 존재를 알고 있으며, 각 모듈에 대한 DI 가 이루어지는 장소이다. 그 밑에 Feature 모듈 3개 (Forecast
, Home
, Search
) 가 있고, 각 피쳐에서 공통으로 사용할 코드가 들어있는 Common
과 CommonUI
모듈이 있다. Domain
모듈은 앱의 비즈니스 정체성, 즉 Usecase 와 RepositoryProtocol 이 존재한다. Data
모듈은 앱에 데이터를 제공하는 RepositoryImpl 이 존재한다.
데모 앱이 포함된 모듈 구조
기존 모듈 구조에서, 앱 모듈을 갈아끼울 수 있는 구조가 되었다. 기존에는 전체를 빌드하는 앱 모듈이 하나가 있었다면, 변경된 구조에는 앱 모듈이 4개 존재한다. 앱을 시작할 수 있는 타겟을 갈아끼울 수 있게 되었다고 생각하면 된다.
HomeDemoApp
그래프의 의존 방향 화살표를 보면 Home, Forecast, Search 중 Home 모듈 만을 의존한다. HomeDemoApp 는 Home 피쳐만 노출할 것이기 때문에 다른 피쳐 모듈들은 의존하지 않아도 된다.
SearchDemoApp
피쳐 모듈 중에서는 Search 모듈 만을 의존한다.
ForecastDemoApp
피쳐 모듈 중에서는 Forecast 모듈 만을 의존한다.
App
전체 앱 모듈. 모든 피쳐 모듈들을 의존한다.
위 모듈 구조에서 확인했듯이, Feature 데모 앱을 생성하는 방법은 어렵지 않다. 새로운 앱 모듈을 만들고, 그 앱이 의존할 모듈들을 조립하면 된다.
크게 아래와 같은 과정을 통해서 완성할 수 있다.
터미널에 Tuist edit 명령어를 수행하면 Manifests 화면을 열 수 있다. 위 사진은 Manifests 의 디렉토리 구조 변경 모습이다. 나는 DemoApps 라는 디렉토리를 새로 생성했고, 그 안에 Feature DemoApp 에 대한 프로젝트들을 모아두었다.
Tuist 로 프로젝트를 세팅하기 위해선, 하나의 모듈에 반드시 "Project" 라는 이름의 swift 파일과 Project 인스턴스를 선언해야한다. 위 사진은 Tuist 를 활용해 각각 App 모듈과 HomeDemoApp 모듈의 프로젝트를 Swift 코드로 세팅하는 모습이다. 이 부분이 바로 글의 초반에서 'Tuist 는 Swifty 한 도구다' 라고 말했던 근거 부분이 될 것 같다.
App 모듈은 Home, Forecast, Search 3 개의 피쳐 모듈을 모두 dependency 로 추가하고 있지만, HomeDemoApp 은 Home 피쳐 모듈만을 targetDependency 에 추가하는 모습이다. 다른 FeatureDemoApp 들도 마찬가지 논리로 dependency 코드를 작성해야 한다. 주의해야 할 점은 product 를 .app
으로 세팅해야한다는 점과 각 데모 앱의 bundleId
는 서로 달라야한다는 점이다.
WorkSpace.swift 에 DemoApps/**
경로를 추가하는 것도 잊지 말아야 한다. Workspace 는 모든 프로젝트를 포함하는 가장 큰 단위이다.
터미널에서 tuist generate 를 수행하면, manifests 에 작성했던 코드들을 토대로 프로젝트가 generate 된다. 위 사진은 DemoApps 들이 잘 생성되었고, 모듈별 빌드 스킴도 생긴 모습이다. Project 를 생성할때, 빌드 스킴도 함께 생성되도록 코드를 작성 해두었다.
먼저, 각 데모 앱들에 AppDelegate (or SceneDelegate) 를 추가하고, 그 안에 각 DemoApp 의 새로운 DI 코드를 작성해야한다. 일반 App 모듈과 다르게, Feature 데모 앱 모듈에서는 그 피쳐에서 필요한 모듈만을 주입해주면 된다. DI 에는 Swinject 를 활용했다.
// App 모듈의 DI 세팅 코드. 나는 SceneDelegate 에 DI 코드를 작성했다.
// Data, Domain, Home, Forecast, Search 모듈에 대한 DI 세팅.
injector.assemble([DataAssembly(),
DomainAssembly(),
HomeAssembly(),
ForecastAssembly(),
SearchAssembly()])
appCoordinator?.start()
// HomeDemoApp 모듈의 DI 세팅 코드.
// Data, Domain, Home 모듈에 대한 DI 세팅.
injector.assemble([DataAssembly(),
DomainAssembly(),
HomeAssembly()])
appCoordinator?.start()
위 코드들을 비교해보면, 피쳐 앱에는 그 피쳐에 필요한 모듈의 Assembly 만 assemble 하고 있는 모습을 볼 수 있다. 이 프로젝트에서는 화면간 이동을 Coordinator 로 하고 있었기 때문에 Coordinator 의 로직도 수정해주었고, 한가지 피쳐만 보여주는 피쳐 데모 앱에서는 하단 탭 바가 노출될 필요가 없다고 판단해 TabBarCoordinator 도 삭제했다.
이제 각 FeatureDemoApp 에 해당하는 빌드를 하기만 하면 시뮬레이터에 새로운 Feature 데모 앱이 생성된다.
기존 앱과 각 Feature 데모 앱을 실행한 모습이다. Feature 데모 앱일 경우 상단 네비게이션 바에 Feature 데모 앱이라는 것을 표시해주었으며, 하단 탭 바를 삭제한 모습도 확인할 수 있다. 이렇게 해서 각 Feature 에만 집중할 수 있는 Feature 데모 앱을 완성했다. 전체 코드는 개인 깃허브 레포에서 확인할 수 있다.
그렇담 프로젝트에 피쳐 데모 앱을 도입했을 때의 사이드 이펙트나 파생 문제는 없을까. 두 가지 경우를 생각해볼 수 있다.
Tuist Manifest 에 데모 앱 모듈을 만들어야 하고, 의존 관계를 세팅하고, AppDelegate 를 수정해야하고.. 데모 앱을 개발하는 데 공수가 든다는 것. 이는 Tuist Template 과 Scaffold 를 활용하면 어느정도 해소가 될 수 있다. Tuist 의 Scaffold 는 새로운 모듈이나 프로젝트 구조를 템플릿화 하는 시스템. 데모 앱 생성 코드와 디렉토리 구조를 템플릿화 해서 사이드 이펙트를 해소한다.
문제 상황: 다른 피쳐를 의존하게 되는 피쳐가 생기게 된다.
토스의 예를 들어보면, 홈 화면에서 소비 내역을 보여주기 위해선 소비 피쳐의 코드를 써야하고, 송금으로 연결하기 위해선 송금 피쳐의 코드를 써야하고, 계좌 상세 내역을 조회하려면 인증 피쳐의 코드를 써야한다. 이렇게 3~4 개의 모듈 관계만 봐도 머리가 아픈데, 이런 모듈이 100 개가 넘는 구조가 된다면 피쳐를 분리해서 데모 앱으로 만들기 어려워진다. 모듈끼리의 순환 참조 위험성도 생긴다. 그래서 처음에 생각했던 해결방안은, 홈 모듈과 인증 모듈이 공통으로 사용할 홈인증 Service 모듈을 생성하는 것. 하지만 모듈이 많아지면서 그에 따른 Service 모듈도 많아지기 때문에 좋은 해법이 되기는 힘들다. 토스는 결국 Micro Features Architecture 를 활용해서 해결하게 된다.
Micro Features Architecture 는 Tuist 에서 만든 아키텍처로, 5가지 구성요소로 이루어진다.
각 구성 요소
Features
: 실제 기능 구현 모듈Interface
: 해당 기능 중 외부에 공개되어야하는 인터페이스와 관련된 모듈Testing
: Interface 에 대한 Mocking 제공,Tests
: 단위 테스트를 위한 모듈Example
: Example 앱. (= 데모 앱)→ Micro Features Architecture 를 따르면 의존 관계가 위처럼 된다.
위 아키텍처를 송금 Feature 데모 앱
을 예시로 들어 구체적으로 이해해보자.
송금 기능을 보여줘야 하니까 송금 Feature 모듈
과 송금 Interface 모듈
이 필요하다. 그런데, 송금을 하려면 중간에 비밀번호 인증을 해야한다. 이때 인증 Interface 모듈
과 인증 Testing 모듈
을 조립한다.
실제 앱에서는 구현체인 인증 Feature 모듈
을 조립하겠지만, 지금은 데모앱이기 때문에 Mocking 된 Testing 모듈만 간편하게 조립해도 된다. 예를 들어, 인증의 결과가 성공인지 실패인지가 송금 Feature 모듈에서 필요한 값일 텐데, 이 결과만 목킹해서 주입하는 것. 이렇게하면 실제 인증 과정을 진행할 필요없이, 즉 인증 피쳐 모듈을 이어 붙일 필요없이 딱 송금 피쳐만 데모 앱으로 뽑을 수 있게 된다.
→ Tuist 로 송금 데모 앱을 만들기 위한 디펜던시를 조립하는 모습. 인증은 실제 피쳐모듈이 아닌 Interface 모듈과 목킹 모듈(Testing) 만을 이어붙인다.
드디어 feature 앱을 이해했습니다 감사합니다
이걸 xcodeproj로 관리하려면 머리 터지겠네요 ㅎㅎ