기존 프로젝트에 Tuist 4를 도입했던 과정을 기록합니다.
Tuist3에서 Tuist4로 넘어가는 과정에 마이그레이션을 진행하느라 자료도 충분치 않았고, 여러 기본 개념들에 빈틈이 많았던 상황이라 그 과정에서 개인적으로도 여러모로 많이 학습했습니다.
현재는 Tuist로의 전환을 마치고 µFEATURES 도입을 위해 (또는 현재 프로젝트에 fit한 구조를 찾는 과정) 공통 모듈들을 분리하는 작업을 진행 중에 있는데요. Tuist를 도입한 배경, 마이그레이션 과정, 모듈들을 분리하면서 만났던 이슈들 그리고 모듈화 진행 과정까지 하나씩 정리해보려 합니다.
이번 글에서는 Tuist를 도입한 배경과 도입 후 느낀 장점들에 대해 소개합니다.
상황
Tuist의 도입을 처음 고려하게 된 이유는 프로젝트 모듈화에 대한 니즈였습니다.
1. 의존 관계 강제
기존 프로젝트는 단일 프로젝트에 모든 코드가 들어가있는 구조입니다. 우버에서 만든 RIBs 아키텍쳐를 사용하고 있어 어느정도 코드가 잘 정리되어 있고 의존성 방향들이 지켜지고 있었습니다. 하지만 여전히 더럽혀진 의존 관계의 코드들이 존재하고 심지어 증가하고 있습니다. 팀원들이 늘어가는 상황에서 잘못된 의존 관계 접근을 컴파일러 레벨에서 방지할 필요성이 있었습니다.
2. 빌드 속도 개선
또한 SwiftUI로 UI 개발을 시작함에 따라 프리뷰 빌드의 수요가 있었습니다. 단일 프로젝트에 모든 코드가 들어가 있기에, 프리뷰 노출을 위해 모든 파일에 대한 빌드가 이뤄질 수 밖에 없는데요. 긴 빌드 시간으로 인해 사실상 프리뷰를 보고 UI를 개발하는 것이 생산성을 더 낮추는 상황이었습니다. 모듈을 분리하여 단축된 빌드 시간의 확보가 필요했습니다.
목적
모듈화를 고민하는 과정에서 우리의 프로젝트에 필요한 것은 아래와 같았습니다.
- 구성원 모두가 쉽게 모듈을 생성 / 관리할 수 있는 구조
- 빌드 속도 개선
- 의존성 관리 용이
- 유지보수 용이
- 미래에도 지속적으로 관리되는 솔루션
SPM vs Tuist
팀 내에서 관리하는 또 다른 프로젝트에서는 SPM을 이용한 모듈 분리 및 관리가 이뤄지고 있었는데요. 왜 SPM을 선택하지 않고 Tuist를 선택하게 되었는지 소개하려 합니다. (Bazel과 XcodeGen도 선택지가 될 수 있었지만 러닝 커브, 편의성 등의 이유로 고려 대상에서 제외하였습니다.)
SPM은 Apple의 first party라는 점에서 정말 매력적인 선택지입니다. 솔루션이 관리되지 않아 버려질 가능성이 매우 적고 Xcode에 이미 통합되어 별도의 설치가 필요하지 않습니다.
다만 실제로 사용해보며 느낀 점은 Xcode Integration이 아직까지는 좋은 개발자 경험을 주지 못한다고 느꼈습니다. 의존성 그래프가 무효화되거나 컴파일 되지 않거나 캐시 관련 문제가 간헐적으로 발생했습니다. 이는 특히 프리뷰 빌드를 진행하는 과정에서 두드러지게 발생했습니다. 부가적으로 네트워크가 없거나 (또는 사내 VPN 사용이 어렵거나) 불안정한 환경에서 개발이 아예 차단되는 문제도 있었습니다.
Tuist, XcodeGen 등의 솔루션이 프로젝트 관리에 초점을 둔 것에 비해 SPM은 의존성 관리에 초점을 두고 있기 때문에, 아직까지 Xcode Integration 과정에서 편의성을 제공하지 못하는 느낌을 받았습니다.
의외로 SPM을 사용 시 빌드 시간도 미세하게 개발자 경험을 저해하는 요인 중 하나였는데요. 다른 블로그 포스팅들의 실험 결과에서 SPM 사용 시 의존성이 복잡해질 수록 빌드를 위해 더 많은 시간이 소요된다는 것을 유추할 수 있었습니다.
이에 반해 Tuist는 프로젝트와 그 의존성 그래프를 일관되게 유지합니다. 명령어를 통한 프로젝트 생성 시점에 이러한 내용들이 모두 결정되기에 인덱싱과 빌드 시스템 등이 효율적으로 동작할 것으로 기대되었습니다.
뿐만 아니라 Tuist에 잠금 효과(lock-in Effect)가 거의 없다고 생각되었는데요. 내일이라도 Tuist를 걷어내고 싶다면, 기존 구조에 Package.swift 추가, .gitignore에서 .xcodeProj를 제거 등의 과정으로 손쉽게 SPM 전환이 가능합니다.
또한 많은 편의적인 기능이 매력적이었습니다. 모든 Manifest가 Swift로 작성되기 때문에 이해 및 관리가 용이하고, scaffold를 지원하여 누구나 손쉽게 모듈을 추가하도록 지원할 수 있습니다.
이러한 이유로 우리 프로젝트에서 필요로 하는 목적에 부합한다고 여겼습니다. 그리고 SPM 보다 더 나은 개발자 경험을 가져다 줄 것을 기대했습니다.
도입 후
마지막으로 도입 이후에 느꼈던 장점에 대해 정리해보려 합니다.
1. SPM 관련 문제 해결
프로젝트 의존성 관리 툴로 cocoapod을 사용하고 있었던 터라 직접적인 비교는 아니지만, 의존성이나 캐시가 꼬여 컴파일이 되지 않는 문제는 거의 발견되지 않았습니다. 빌드 과정에 뭔가 문제가 생기더라도 단순히 tuist clean, tuist install, tuist generate 명령어를 입력하면 간단히 해결됩니다. 그렇지 않은 경우는 대부분 manifest에 문제가 있는 것이기 때문에 잘못된 의존성 관계를 확인하기도 쉬워집니다.
2. 모듈화 용이
모듈을 추가하고 빌드하기 까지 과정이 편합니다. .xcodeproj를 사용하는 프로젝트에서 모듈을 만들고 의존성을 조정하는 작업은 조금은 번거로웠습니다. Tuist에서는 Makefile에 tuist scaffold 명령어를 추가 해두어 팀 구성원 누구라도 명령어 한 줄이면 정해진 규칙 안에서 모듈을 추가할 수 있습니다. 뭔가 잘못 설정되었다면 tuist generate 단계에서 바로 알아낼 수 있습니다.
3. UI 개발 환경 개선
모듈 분리로 SwiftUI 프리뷰 지원 뿐만 아니라, UI 용 데모 앱이 추가되었습니다. Tuist에서는 아주 편하게 의존 관계를 설정하고 프로젝트의 생성을 동적으로 관리할 수 있는데요. 이러한 장점 덕분에 Demo App Target을 쉽게 추가할 수 있습니다. 덕분에 더욱 빠른 빌드 속도, UI의 독립적인 개발, UI 로직 엣지 케이스 확인 등이 편해졌습니다.
4. merge conflict
팀 내 iOS 개발자는 6명으로 xcodeproj 등 파일의 충돌에 대해 심각하게 불편함을 느끼고 해결 방안을 고민할 단계는 아니었습니다. 아직까지는 충돌을 해결하는 비용보다 프로젝트 생성 관리 툴을 도입하는 비용이 더 컸기 때문인데요. 그럼에도 Tuist 전환 이후에 충돌 해결 및 manifest 관리에 대해 편리함을 느끼고 있습니다.
5. 빌드 속도 개선, 앱 크기 감소
아래 항목들은 Tuist 도입의 직접적인 영향으로 보기는 어렵습니다. Tuist를 도입하면서 현재 앱의 의존성 연결 상태(Static Linking, Dynamic Linking 등)에 대해 다시 점검할 수 있었습니다. 개인적으로는 이런 것들을 다시 점검해 볼 수 있는 것이 참 좋은 포인트인 것 같습니다.
기존 프로젝트에서는 cocoapods + dynamic linking 방식으로 서드 파티를 연결하고 있었는데요. Tuist로 전환하는 과정에서 이를 static linking으로 전환했습니다. 그 덕분에 앱 크기는 ipa 파일 기준 약 5~6MB가 감소했습니다.
또한 빌드 속도가 향상되었습니다. Tuist Cache 기능을 사용하는 경우 clean build 73 ~ 77%, rebuild에서는 35% 이상의 빌드 속도 향상이 있었습니다. Tuist Cache 기능을 사용할 경우 prebuild된 XCFramework 상태로 캐싱해 두기 때문에 측정 결과가 자연스럽습니다.
한편 Tuist Cache를 사용하지 않는 경우도 clean build 23~31%, rebuild 28% 이상의 빌드 속도 향상이 측정되었습니다. 일반적으로 Dynamic Linking의 빌드 속도가 더 빠르다는 것을 고려했을 때 이상하게 느낀 부분이었습니다. Tuist에서 SPM을 받아올 때 어떠한 최적화 과정을 수행하는가? 생각이 들어 Tuist 팀에게 물어봤는데 딱히 그런 것 같지는 않았습니다. (혹시 아시는 분 계시면 알려주세요!)
아래 빌드 시간은 동일 조건 테스트를 위해 모듈 분리 진행 전 상태를 기준으로 측정하였습니다.
[ DerivedData 제거 ]
Try | Cocopods (Dynamic Linking) | Tuist (Static Linking) | Tuist Cache 3rd Parties (XCFramework) |
1 | 69.0 Sec | 54.9 sec | 20.6 sec |
2 | 70.5 sec | 53.6 sec | 19.6 sec |
3 | 65.3 sec | 55.8 sec | 17.9 sec |
4 | 65.9 sec | 52.1 sec | 17.0 sec |
5 | 66.6 sec | 51.0 sec | 23.5 sec |
6 | 71.2 sec | 55.2 sec | 17.0 sec |
7 | 72.0 sec | 52.5 sec | 17.0 sec |
8 | 74.6 sec | 52.6 sec | 17.3 sec |
9 | 71.5 sec | 53.2 sec | 17.4 sec |
10 | 73.5 sec | 53.5 sec | 16.8 sec |
Avg | 70.01 sec | 53.44 sec | 18.41 sec |
Avg Imrpove Rate | 23.66% | 73.70% |
[ clean build (cmd + shift + k) ]
Cocopods (Dynamic Linking) | Tuist (Static Linking) | Tuist Cache 3rd Parties (XCFramework) | |
1 | 57.2 sec | 40.1 sec | 12.6 sec |
2 | 57.3 sec | 40.4 sec | 12.3 sec |
3 | 58.5 sec | 40.3 sec | 14.4 sec |
4 | 56.4 sec | 39.9 sec | 12.5 sec |
5 | 57.8 sec | 39.8 sec | 12.6 sec |
6 | 57.1 sec | 38.9 sec | 12.5 sec |
7 | 55.4 sec | 40.3 sec | 12.6 sec |
8 | 58.7 sec | 38.5 sec | 16.5 sec |
9 | 57.5 sec | 38.1 sec | 12.3 sec |
10 | 59.7 sec | 40.1 sec | 12.2 sec |
Avg | 57.56 sec | 39.64 sec | 13.05 sec |
Avg Imrpove Rate | 31.13% | 77.32% |
[ rebuild ]
rebuild 시에 특정 코드의 변경을 동일하게 수행하였습니다.
Try | Cocopods (Dynamic Linking) | Tuist (Static Linking) | Tuist Cache 3rd Parties (XCFramework) |
1 | 16.3 sec | 7.4 sec | 6.4 sec |
2 | 13.5 sec | 6.5 sec | 6.4 sec |
3 | 7.5 sec | 7.0 sec | 6.2 sec |
4 | 8.3 sec | 6.8 sec | 6.3 sec |
5 | 7.2 sec | 7.1 sec | 6.2 sec |
6 | 9.5 sec | 6.5 sec | 6.2 sec |
7 | 9.3 sec | 7.1 sec | 6.1 sec |
8 | 8.1 sec | 6.7 sec | 6.4 sec |
9 | 8.3 sec | 6.8 sec | 6.0 sec |
10 | 8.5 sec | 7.0 sec | 6.0 sec |
Avg | 9.65 sec | 6.89 sec | 6.22 sec |
Avg Imrpove Rate | 28.60% | 35.55% |
참고 - Static Linking vs Dynamic Linking
참고한 글들
https://tuist.io/blog/2020/04/28/why-tuist/
https://pepicrft.me/blog/2021/02/05/tuist-and-spm
https://medium.com/bumble-tech/scaling-ios-at-bumble-76754fa874f7
https://medium.com/bumble-tech/scaling-ios-at-bumble-239e0fa009f2
https://medium.com/bumble-tech/scaling-ios-at-bumble-6f0602682903
'iOS > Module' 카테고리의 다른 글
Tuist - tuist test (0) | 2024.11.07 |
---|---|
Static Linking vs Dynamic Linking (3) | 2024.04.21 |
Static Framework의 번들/리소스 (0) | 2024.04.21 |