Tuist 완벽 가이드
Xcode 프로젝트 자동 생성 도구의 모든 것. Workspace, Project, Target, Module의 관계부터 Static/Dynamic Framework까지, Tuist가 어떻게 동작하는지 완벽히 이해한다.
기록 없음
한줄 요약
Tuist는 Swift 코드(Manifest)로 Xcode 프로젝트를 정의하고, tuist generate 명령으로 .xcodeproj와 .xcworkspace를 자동 생성하는 도구다.
Xcode 프로젝트 파일(.pbxproj)은 충돌이 잦고 리뷰가 어렵다. Tuist를 쓰면 프로젝트 구조를 Swift 코드로 관리하여 Git 충돌을 줄이고, 모듈화된 대규모 프로젝트를 체계적으로 관리할 수 있다.
핵심 개념 4가지
Tuist를 이해하려면 이 4가지 개념의 계층 관계를 먼저 파악해야 한다.
Workspace.swift에서 정의한다.
Project.swift에서 정의한다.
| 단위 | 설명 | import 개수 |
|---|---|---|
| Package | 패키지 (저장소 단위) | 1개 이상 |
| Product | 패키지가 제공하는 라이브러리 | 1개 |
| Target | 빌드 단위 | 1개 |
| Module | import 단위 | 1개 |
1 Package ≥ 1 Product = 1 Target = 1 Module = 1 import예: RxSwift 패키지 →
import RxSwift, import RxCocoa, import RxRelay (여러 Product)
계층 구조
위 계층도에서 Module은 타겟이 "의존하는" 모듈이지, 타겟이 "생성하는" 모듈이 아니다.
BaseUtil, FeatureHome은 각각 별도 타겟이며, 빌드 시 각자 1개의 모듈을 생성한다.
Tuist 동작 흐름
tuist generate 명령어를 실행하면 어떤 일이 일어나는가?
sequenceDiagram
participant Dev as 개발자
participant CLI as Tuist CLI
participant MF as Manifest 파일들
participant XC as Xcode 프로젝트
Note over Dev,CLI: 외부 SPM 의존성 있으면
Dev->>CLI: tuist install
CLI-->>CLI: 외부 패키지 다운로드
Dev->>CLI: tuist generate
CLI->>MF: 1. Manifest 파싱 (Workspace, Project, Package, Helpers)
Note over CLI: 2. 의존성 그래프 구축
CLI->>XC: 3. .xcodeproj 생성
CLI->>XC: 4. .xcworkspace 생성
Note over Dev,XC: 프로젝트 열기 (선택)
Dev->>CLI: tuist open 또는 --open 플래그
CLI->>XC: Xcode로 열기
| 단계 | 설명 | 생성되는 것 |
|---|---|---|
| 사전 | tuist install — 외부 SPM 의존성 다운로드 (있을 경우) |
Tuist/Dependencies/ |
| 1 | Manifest 파싱 — Workspace.swift, Project.swift, Package.swift, Helpers 로드 | 프로젝트/타겟/의존성 정의 |
| 2 | 의존성 그래프 구축 — 모듈 간 관계 분석 | 의존성 그래프 |
| 3-4 | Xcode 프로젝트 파일 생성 | .xcodeproj, .xcworkspace |
| 선택 | tuist open 또는 --open 플래그로 Xcode 열기 |
- |
tuist generate만 실행하면 프로젝트 파일만 생성되고 Xcode는 자동으로 열리지 않는다.
Manifest 파일 상세
Tuist는 4가지 핵심 Manifest 파일로 프로젝트를 정의한다.
Workspace.swift
역할: 여러 프로젝트를 하나의 워크스페이스로 묶음
import ProjectDescription
let workspace = Workspace(
name: "MyApp",
projects: [
"Projects/MyApp", // Global 앱
"Projects/MyAppVN" // VN 앱
]
)
Project.swift
역할: 개별 프로젝트의 타겟, 스킴, 설정 정의
import ProjectDescription
import ProjectDescriptionHelpers
let project = Project(
name: "MyApp",
packages: MyAppPackages.all,
targets: MyAppProject.targets,
schemes: MyAppProject.schemes
)
Tuist/Package.swift
역할: 외부 SPM 의존성과 Static/Dynamic 정책 정의
// @tuist-ignore
import PackageDescription
#if TUIST
import ProjectDescription
let packageSettings = PackageSettings(
productTypes: [
"Alamofire": .staticFramework,
"RxSwift": .staticFramework,
]
)
#endif
let package = Package(...)
ProjectDescriptionHelpers/
역할: 재사용 가능한 설정과 Helper 함수 모듈화
// MyAppProject.swift
public struct MyAppProject {
public static var targets: [Target] {
[
makeAppTarget(),
makeTestTarget(),
makeWidgetTarget()
]
}
}
Static vs Dynamic Framework
Framework의 링킹 방식에 따라 앱의 시작 시간과 바이너리 크기가 달라진다.
- 링킹 시점: 빌드 타임
- 앱 시작: 빠름 (이미 포함됨)
- 바이너리: 앱에 코드가 복사됨
- 메모리: 앱 실행 시 함께 로드
- 크기: 앱 바이너리가 커짐
- 링킹 시점: 런타임
- 앱 시작: 느림 (dyld 로딩 필요)
- 바이너리: 별도 .framework 파일
- 메모리: 필요 시 로드 가능
- 크기: 앱과 분리됨
flowchart LR
subgraph Static["Static Linking (빌드 타임)"]
direction TB
SC["소스 코드"] --> SO["Object 파일"]
SO --> SL["Linker"]
SF["Static Library"] --> SL
SL --> SA["단일 App Binary"]
end
subgraph Dynamic["Dynamic Linking (런타임)"]
direction TB
DC["소스 코드"] --> DO["Object 파일"]
DO --> DL["Linker"]
DL --> DA["App Binary"]
DF[".framework 파일"]
DA -.->|"dyld 로딩"| DF
end
Tuist에서 설정하는 방법
// Tuist/Package.swift
let packageSettings = PackageSettings(
productTypes: [
// Static으로 설정 (권장 - 앱 시작 시간 최적화)
"Alamofire": .staticFramework,
"RxSwift": .staticFramework,
"RxCocoa": .staticFramework,
// Dynamic이 필요한 경우 (리소스 번들, 확장 공유 등)
"Firebase": .framework,
]
)
1. App Extension과 메인 앱이 코드를 공유해야 할 때
2. 프레임워크가 리소스 번들을 포함할 때
3. 바이너리 크기 제한 (200MB)에 걸릴 때
대부분의 경우 Static을 권장한다. 앱 시작 시간이 중요하기 때문이다.
의존성 관리 심화
Tuist에서 의존성을 선언하는 3가지 방법을 비교한다.
| 방식 | 용도 | 예시 코드 |
|---|---|---|
.external() |
외부 SPM 패키지 | .external(name: "Alamofire") |
.package() |
로컬 SPM 패키지의 product | .package(product: "BaseUtil") |
.target() |
같은 프로젝트 내 다른 타겟 | .target(name: "MyAppCore") |
flowchart TB
subgraph App["App Layer"]
MA["MyAppMain"]
MV["MyAppVNMain"]
end
subgraph Feature["Feature Layer"]
FH["FeatureHome"]
FL["FeatureLogin"]
FP["FeaturePayment"]
end
subgraph Core["Core Layer"]
CB["BaseUtil"]
CN["NetworkModule"]
CU["UIComponents"]
end
subgraph External["External Packages"]
AL["Alamofire"]
RX["RxSwift"]
SD["SDWebImage"]
end
MA --> FH
MA --> FL
MA --> FP
MV --> FH
MV --> FL
FH --> CB
FH --> CN
FL --> CB
FL --> CU
FP --> CN
CN --> AL
CB --> RX
CU --> SD
의존성 선언 실제 코드
// MyAppDependencies.swift
public struct MyAppDependencies {
public static var app: [TargetDependency] {
[
// 로컬 SPM 패키지
.package(product: "BaseUtil"),
.package(product: "FeatureHome"),
.package(product: "FeatureLogin"),
// 외부 SPM 패키지
.external(name: "Alamofire"),
.external(name: "RxSwift"),
// 같은 프로젝트 내 타겟
.target(name: "MyAppWidget"),
]
}
}
Q&A
tuist install은 외부 의존성(SPM 등)만 받아온다.
(⚠️ Tuist 4.x에서 tuist fetch는 tuist install로 대체됨 — fetch는 레거시)
tuist generate는 의존성 해결을 포함해 .xcodeproj와 .xcworkspace까지 생성한다.
처음 세팅할 때는 generate만 실행하면 된다.
Tuist 타겟: 해당 프로젝트 전용, 더 세밀한 Xcode 설정 가능
재사용성이 높으면 SPM, 프로젝트 전용이면 타겟으로 만든다.
tuist generate로 동일한 프로젝트를 생성하면 된다.
이렇게 하면 .pbxproj 충돌이 완전히 사라진다.
tuist cache는 의존성 모듈들을 미리 빌드해서 xcframework로 캐싱한다.
대규모 프로젝트에서 빌드 시간을 크게 줄일 수 있다.
CI/CD에서 특히 유용하다.
.external(name:): Tuist/Package.swift에 정의된 외부 SPM 패키지 참조.package(product:): 프로젝트 내 로컬 SPM 패키지의 product 참조외부냐 로컬이냐의 차이다.
Dependencies.swift 대신 Tuist/Package.swift를 사용한다.
SPM과 더 일관된 방식으로 의존성을 관리한다.