Tuist 의존성 관리 · Static/Dynamic 링킹 완벽 가이드
Tuist가 외부 패키지를 어떻게 관리하는지, Static/Dynamic Framework의 링크/임베드 차이, Xcode GUI에서의 의존성 추가 위치, pbxproj 충돌 문제까지 실무 중심 정리.
기록 없음
1. Tuist 프로젝트 네비게이터 구조
Xcode 네비게이터에서 두 가지 다른 의존성 섹션이 보인다:
| 섹션 | 아이콘 | 관리 주체 | 내용 |
|---|---|---|---|
| Dependencies 폴더 | 📊 xcodeproj | Tuist | xcframework 포함 패키지 (Firebase, Amplitude, Google 등) |
| Package Dependencies | 📦 package | Xcode SPM | 소스 기반 SPM (Alamofire, RxSwift, 로컬 패키지 등) |
왜 나뉘어 있나?
| 패키지 | 배포 형태 | Tuist 처리 |
|---|---|---|
| Firebase, Google, Facebook, Amplitude | 바이너리 (xcframework) | xcodeproj로 변환 → Dependencies 폴더 |
| Alamofire, RxSwift, Kingfisher | 소스 코드 | 네이티브 SPM 유지 → Package Dependencies |
productTypes 설정(staticFramework/framework)은 빌드 산출물 타입만 지정하는 것이고, xcodeproj 변환 여부와는 관계없다.
2. Tuist의 전체 역할
xcframework → xcodeproj 변환은 Tuist가 하는 여러 일 중 하나일 뿐. 프로젝트 생성 자동화가 본질이다.
| 역할 | 설명 |
|---|---|
| 프로젝트 생성 (핵심) | xcodeproj/xcworkspace를 코드로 생성 |
| 빌드 설정 관리 | 20개+ Configuration을 코드로 선언 (Debug, Release, Qa, vn 등) |
| 의존성 선언 통합 | 로컬 + 외부 패키지를 한 곳에서 관리 |
| 로컬 모듈 구조화 | 50개+ 로컬 패키지 관계를 manifest로 관리 |
| xcframework 처리 | 바이너리 패키지를 xcodeproj로 변환 |
3. Static vs Dynamic Framework
productTypes 설정의 의미
// Tuist/Package.swift
let packageSettings = PackageSettings(
productTypes: [
"Alamofire": .staticFramework, // Static으로 빌드
"LineSDK": .framework, // Dynamic으로 빌드
]
)
| Static Framework | Dynamic Framework | |
|---|---|---|
| 링크 타임 | 코드가 앱 바이너리에 복사 | 참조만 기록 (코드 복사 X) |
| 런타임 | 별도 로드 없음 (이미 앱 안에 있음) | dyld가 별도 로드 |
| 앱 번들 위치 | 실행파일 안에 포함 | Frameworks/ 폴더에 별도 존재 |
| 앱 바이너리 크기 | 커짐 (코드 포함) | 작음 |
| 앱 시작 속도 | 빠름 | 약간 느림 (로드 필요) |
"링커가 참조를 기록"의 의미
Static (코드 복사)
링크 타임:
Alamofire ──복사──→ MyApp 실행파일
(코드가 실행파일 안에)
런타임:
MyApp 실행파일 하나만 있으면 됨
Dynamic (참조 기록)
링크 타임:
LineSDK ──참조──→ MyApp 실행파일 헤더:
"LineSDK.framework 필요"
런타임:
dyld가 Frameworks/LineSDK.framework 로드
실행파일 헤더에 기록된 정보 확인:
$ otool -L MyApp
MyApp:
@rpath/LineSDK.framework/LineSDK ← "이 프레임워크 필요해"
/usr/lib/libSystem.B.dylib ← "시스템 라이브러리 필요해"
4. Xcode GUI에서 의존성 추가 위치
General 탭
Frameworks, Libraries, and Embedded Content
├── + 버튼으로 추가
└── Embed 옵션 선택
- Do Not Embed
- Embed & Sign
- Embed Without Signing
Build Phases 탭
Link Binary With Libraries ← 링크 (컴파일 타임)
Embed Frameworks ← 복사 (Dynamic만 해당)
관계 정리
| 위치 | 역할 | 언제 |
|---|---|---|
| Frameworks, Libraries... | 통합 UI (추가하면 아래 두 곳에 자동 반영) | - |
| Link Binary With Libraries | 링커에게 "이 라이브러리 링크해" | 빌드 타임 |
| Embed Frameworks | 앱 번들에 .framework 복사 | 빌드 후 |
| Embed 옵션 | Link Binary With Libraries | Embed Frameworks |
|---|---|---|
| Do Not Embed | ✅ 자동 추가 | ❌ |
| Embed & Sign | ✅ 자동 추가 | ✅ 자동 추가 |
| Embed Without Signing | ✅ 자동 추가 | ✅ 자동 추가 |
Link Binary With Libraries에 뭐가 들어가나?
Static과 Dynamic 둘 다 들어간다.
Link Binary With Libraries
├── Alamofire.framework ← Static
├── RxSwift.framework ← Static
├── LineSDK.framework ← Dynamic
└── libz.tbd ← System library
| Link Binary With Libraries | Embed Frameworks | |
|---|---|---|
| Static Framework | ✅ | ❌ |
| Dynamic Framework | ✅ | ✅ |
Link = "이 라이브러리 사용한다" 선언 (둘 다 필요)
Embed = 앱 번들에 파일 복사 (Dynamic만 필요)
5. pbxproj 충돌 문제
.xcodeproj 구조
MStove.xcodeproj/
├── project.pbxproj ← 이 파일이 문제
├── xcshareddata/
└── xcuserdata/
pbxproj 내용 (일부)
/* Begin PBXFrameworksBuildPhase section */
3E9F8A2B1C8B4D5E00123456 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
files = (
3E9F8A2C1C8B4D5E00789ABC /* Alamofire.framework */,
3E9F8A2D1C8B4D5E00DEF012 /* RxSwift.framework */,
);
};
/* End PBXFrameworksBuildPhase section */
충돌 시나리오
팀원 A: Kingfisher 추가 → pbxproj에 UUID 추가
팀원 B: Lottie 추가 → pbxproj에 다른 UUID 추가
↓
동시에 PR
↓
pbxproj CONFLICT 💥
- UUID 기반:
3E9F8A2B1C8B4D5E00123456이런 식 - 순서 변경: Xcode가 임의로 순서 바꿈
- 한 줄 변경 → 수십 줄 diff
- 수동 머지 거의 불가능
Tuist의 해결책
팀원 A: MStoveAppDependencies.swift에 Kingfisher 추가
팀원 B: MStoveAppDependencies.swift에 Lottie 추가
↓
일반 Swift 코드라 머지 쉬움
↓
tuist generate → pbxproj 재생성
pbxproj를 커밋 안 하거나, 생성된 걸로 덮어씀 → 충돌 없음
6. 의존성 선언 통합의 가치
Tuist 없이 (순수 Xcode)
타겟이 10개라면:
MStove → Xcode GUI에서 Alamofire 추가
MStoveVN → Xcode GUI에서 Alamofire 추가
QA_MStove → Xcode GUI에서 Alamofire 추가
QA_MStove_VN → Xcode GUI에서 Alamofire 추가
DEV_MStove → Xcode GUI에서 Alamofire 추가
... (10번 반복)
- 새 의존성 추가 시 10번 반복 작업
- 하나 빠뜨리면 빌드 에러
- 버전 불일치 위험
.xcodeproj파일 conflict (팀 작업 시)
Tuist 사용
// 한 번만 선언
private let mstoveSharedAppDependencies: [TargetDependency] = [
.external(name: "Alamofire"),
.external(name: "RxSwift"),
// ...
]
// 모든 타겟이 공유
public let mstoveAppDependencies = mstoveSharedAppDependencies + [...]
public let mstoveVNAppDependencies = mstoveSharedAppDependencies + [...]
→ tuist generate 하면 모든 타겟에 자동 적용
7. Configuration 중복 문제와 해결
현재 프로젝트에서 같은 Configuration 목록이 여러 파일에 중복되어 있다:
MStoveAppProject.swift
public let mstoveProjectSettings = Settings.settings(configurations: [
.debug(name: "Debug"),
.debug(name: "Debug_Dev"),
.debug(name: "Debug_Qa"),
// ... 20개
])
Package.swift
baseSettings: Settings.settings(configurations: [
.debug(name: "Debug"),
.debug(name: "Debug_Dev"),
.debug(name: "Debug_Qa"),
// ... 동일한 20개
])
개선 가능한 구조
// Tuist/ProjectDescriptionHelpers/Configurations.swift
public let allConfigurations: [Configuration] = [
.debug(name: "Debug"),
.debug(name: "Debug_Dev"),
// ... 한 곳에서 정의
]
// Package.swift (#if TUIST 블록 안에서)
import ProjectDescriptionHelpers
let packageSettings = PackageSettings(
baseSettings: Settings.settings(configurations: allConfigurations)
)
// MStoveAppProject.swift
public let mstoveProjectSettings = Settings.settings(
configurations: allConfigurations
)
#if TUIST 블록 안에서는 ProjectDescriptionHelpers import 가능하므로, 한 곳 정의 → 모든 곳 참조 가능하다.
Q&A
아니다. xcframework → xcodeproj 변환은 Tuist가 하는 여러 일 중 하나일 뿐.
Tuist의 핵심 역할은 프로젝트 생성 자동화:
- xcodeproj/xcworkspace 코드로 생성
- 빌드 설정 (Configuration) 통합 관리
- 의존성 선언 통합
- 로컬 모듈 구조화
아니다. productTypes는 빌드 산출물 타입만 지정하는 것.
xcodeproj 변환 여부는 패키지 배포 방식에 따라 결정:
- 바이너리(xcframework) 포함 패키지 → xcodeproj로 변환
- 소스 코드만 있는 패키지 → 네이티브 SPM 유지
맞다.
- Dependencies 폴더 (xcodeproj 아이콘) → Tuist가 SPM을 xcodeproj로 변환
- Package Dependencies (package 아이콘) → Xcode 네이티브 SPM 영역
Tuist는 Package Dependencies의 패키지들을 선언만 하고, 실제 fetch/resolve/build는 Xcode SPM이 담당.
맞다. General 탭의 "Frameworks, Libraries, and Embedded Content"에서 추가하면:
- Link Binary With Libraries → 자동 추가
- Embed Frameworks → Embed 옵션 선택에 따라 자동 추가
Build Phases는 직접 건드릴 필요 없다.
아니다. Static과 Dynamic 둘 다 들어간다.
Link는 "이 라이브러리 사용한다" 선언 — 둘 다 필요.
- Static: 링커가 코드를 실행파일에 복사
- Dynamic: 링커가 참조를 기록 (런타임에 로드할 위치)
Embed Frameworks는 Dynamic만 필요 (앱 번들에 파일 복사).