Intro
최근 사이드 프로젝트에 합류하면서 깃허브 초대를 받았는데, 해당 프로젝트는 Tuist를 사용하고 있었다.
그동안은 소규모 팀 프로젝트 위주에다가 팀원들이(나도 포함) Xcode라는 것 자체에도 익숙해하지 않았던 경우가 많았으므로, Tuist뿐만아니라, fastlane 뭐 등등, 사용하지않고, 바닐라로다가 어느정도의 불편함은 감수하며 진행을 해왔다. 그렇게 미루다 Tuist에 대한 학습을 미뤄왔었는데, 이번 기회에 Tuist가 무엇인지 제대로 파악하고 전체적인 청사진을 그려보고자 합니다!

엄청 간단하게 설명한다면, xcodeproj나 xcworkspace가 하는 일을 대신해주는 도구입니다! 홈페이지를 보시면은 모듈 캐싱에다가 빌드시간 감소 등 더 다양한 장점들이 많은데, 일단 제가 가장 크게 와닿은 부분은 아무래도 Tuist를 통해서 proj파일을 Swift파일로 관리해줄 수 있다는점 정도일 것입니다!

iOS로 팀 프로젝트를 해보신분들은 공감하실텐데, xcodeproj파일이 진짜 끔찍할정도로 관리가 귀찮습니다..
<<<<<<< HEAD
2A3F5C1A2A3F5C1A /* LoginView.swift in Sources */ = {
=======
3B4G6D2B3B4G6D2B /* HomeView.swift in Sources */ = {
>>>>>>> feature/home
.xcodeproj 안의 project.pbxproj 파일 충돌
만약 팀원A가 LoginView.swift를, 팀원B가 HomeView.swift를 동시에 추가하면 이렇게 충돌이 발생하는 것이다. 이런 경우에는 뭐 둘다 추가해주고 업로드하면되겠지만, 규모가 거대해지고, 다양한 파일을 정리하고 수정하다보면 한명이 책임을 떠안게 될 수도 있을 것입니다..!

그와중에또 이게 그냥 swift코드가 아니라, 바이너리 형태라 일일히 수동으로 다루기가 까다롭다는 문제가 있다..
그래서 등장한게 Tuist일텐데, 서론이 길었으니 빠르게 나아가보겠슴다.
Tuist는 Swift 코드로 Xcode Project를 생성/유지보수하거나 상호작용하는 CLI툴이다. (Tuist Readme 의역..)
Xcode에서 GUI(말이 어려울 수 있는데, 그냥 Xcode에서 직접 프로젝트 설정을 컨트롤하는 것을 의미한다.)로 클릭하며 설정하던 것들을 Project.swift라는 Swift 파일에 코드로 작성하면, tuist generate 를 통해서 .xcodeproj를 생성해줍니다! 그러니까 더이상 git에 올려서 고통을 받을 이유가 없고, Project.swift만 잘 관리를 해주면되는 것임다. 바이너리 파일이 아니어서 관리도 편하고 분산되어있지도 않고 ..!
지금 현재 우리 프로젝트에서는 proj파일 관리가 단순하다는 이점만 가져가고 있기 때문에 tuist grpah를 했을때, 다음과 같이 단순한 트리가 나옴다.

앱 타겟이 이것뿐이라.. 그런것인데, 그래서 이상적인 다른블로그의 graph를 가져와봤슴다.

그래프 출처 - https://zuminternet.github.io/iOS-tuist-module/
다시 돌아와서 결국 현재 프로젝트가 그래프가 저렇게 나온 이유는 Project.swift에서 찾을 수 있슴다.
바로 아래 첨부되어있다..! 중복이 될 수 있으니 여기에는 생략을 하겠다..!
보시면 타겟이 딱 두개 뿐이라 그런것임다.
targets: [
.target(name: "프로젝트이름", ...), // 타겟 1번
.target(name: "프로젝트이름Tests", ...), // 타겟 2번
]
모듈로 분리까지는 해주지않은 상태라 잘 나온게 맞다. 이거보면서 모듈로 분리를 해주고 싶다는 생각이 들었는데, 다음 포스팅때 기회가 되면 진행을 해보는 것도 되게 좋은 공부가 될 것 같슴다..
기존 방식
.xcodeproj 파일을 직접 git에서 관리project.pbxproj 변경 → 충돌 발생Tuist 방식
Project.swift만 git에서 관리.xcodeproj는 .gitignore에 추가하고 로컬에서 매번 tuist generate로 동일하게 생긴 Proj파일 생성Tuist를 적용하면 다음과 같은 디렉토리가 완성되는데,,
Projects/
├── Project.swift // 사실상 info.list
├── Tuist.swift // Tuist 전역 설정 (Cloud 연동 등)
├── mise.toml // Tuist 버전 고정
├── Tuist/
│ └── Package.swift // 외부 SPM 패키지 목록
└── [AppName] // 여기부터는 그냥 앱
├── Sources/
├── Resources/
└── Tests/
| 파일 | 역할 |
|---|---|
Project.swift | 타겟, 번들 ID, Deployment Target, Info.plist, 의존성 정의 |
Tuist.swift | Tuist Cloud 연결 등 전역 옵션 설정 |
Tuist/Package.swift | 외부 라이브러리(SPM) 의존성 목록 |
mise.toml | 팀 전체가 사용할 Tuist 버전 고정 |
하나씩 파일을 훑으면서 이어나가보자!
현재 프로젝트 설정을 공유하자면..!
mise파일은 Tuist의 버전을 고정시켜주는 역할이라고 보시면된다. 당연히 버전차이로 인한 문제들을 사전에 방지하는 것이다.
[tools]
tuist = "4.43.2"
import ProjectDescription
let project = Project(
name: "Projects",
options: .options(
defaultKnownRegions: ["ko"],
developmentRegion: "ko"
),
targets: [
.target(
name: "프로젝트이름",
destinations: .iOS,
product: .app,
bundleId: "org.medical.프로젝트이름",
deploymentTargets: .iOS("17.0"),
infoPlist: .extendingDefault(
with: [
"UIUserInterfaceStyle": "Light",
"UISupportedInterfaceOrientations": [
"UIInterfaceOrientationPortrait",
],
"UILaunchScreen": [
"UIColorName": "",
"UIImageName": "",
],
"UIAppFonts": [
"GmarketSansBold.otf",
"GmarketSansMedium.otf",
"SCDream5.otf",
"SCDream6.otf",
"SCDream7.otf",
],
"UIBackgroundModes": ["remote-notification"],
]
),
sources: ["프로젝트이름/Sources/**"],
resources: [
"프로젝트이름/Resources/**",
"프로젝트이름/Resources/**/*.xcdatamodeld",
],
entitlements: "프로젝트이름/프로젝트이름.entitlements",
dependencies: []
),
.target(
name: "프로젝트이름Tests",
destinations: .iOS,
product: .unitTests,
bundleId: "org.프로젝트이름.프로젝트이름Tests",
infoPlist: .default,
sources: ["프로젝트이름/Tests/**"],
resources: [],
dependencies: [.target(name: "프로젝트이름")]
),
]
)
deploymentTargets: .iOS("17.0") — Xcode General 탭에서 설정하던 iOS배포 타겟 설정infoPlist: .extendingDefault(with: [...]) — Info.plist 항목을 코드로 직접 관리. infolist에 지정해주던 폰트도 여기서 처리할 수 있다. UIUserInterfaceStyle: "Light" — 다크모드 지원 안 함UIBackgroundModes: ["remote-notification"] — 백그라운드에서 푸시 알림 수신 가능 설정// swift-tools-version: 6.0
import PackageDescription
#if TUIST
import struct ProjectDescription.PackageSettings
let packageSettings = PackageSettings(
productTypes: [:] //기본값 사용
)
#endif
let package = Package(
name: "Projects",
dependencies: [
// 현재 외부 라이브러리가 없는 상태
]
)
현재 프로젝트는 외부 라이브러리 없이 기본 프레임워크로만 구성이 되어있다. 추후 외부 패키지가 필요하면 이 파일의 dependencies 배열에 추가하면 된다.
// 나중에 추가가 된다면 이렇게 붙을것이다.
dependencies: [
.package(url: "https://github.com/Alamofire/Alamofire", from: "5.0.0"),
]
공식 사이트 참고 - https://docs.tuist.dev/en/guides/install-tuist
mise install tuist
git clone https://github.com/your-team/your-project.git // 저장소 클론따고..!
cd Projects //접근해서
tuist generate // 이제 이렇게하면 xcodeproj파일이 생성된다.
.gitignore아까 tuist쓰니까 이제 git으로 proj파일을 관리안한다고 말씀을 드렸는데요..!
그래서 이제 gitignore를 통해서 가차없이 제거해주면 됩니다!

# Tuist로 생성되는 파일들을 추가해주면됨!!
*.xcodeproj
*.xcworkspace
!*.xcworkspace/contents.xcworkspacedata
살짝 알아보니까 이번 프로젝트에 적용이 되긴되어있지만 장점을 극히 일부만 가져가고 있구나..! 하는 생각이 들었다.
모듈로 나누지도않았고, 외부 패키지를 쓰고있는것도 아니어서, 사실상 정말 proj파일 충돌만을 피하고 있구나 정도..?
그래서 이번에 보니까 다양한 명령어들이 있고, 좀 더 깊이파면은 드라마틱한 성과를 낼수가 있겠구나..! 좀 더 알아보고 싶은 욕심이 생긴 것 같다.
아직은! 이정도로만 정리할 수 있을 것같은데, 더 공부를 하면서 업그레이드시켜서 빌드단축 + 모듈화를 통한 프로젝트 구조 가시화 같은 드라마틱한 성과를 눈으로 확인해보고싶다.
Xcode 프로젝트 설정을 코드로 관리해서- proj 파일 충돌을 해결해주는 도구