← 홈으로 돌아가기
iOS Development Guide

Tuist 완벽 가이드

Xcode 프로젝트 자동 생성 도구의 모든 것. Workspace, Project, Target, Module의 관계부터 Static/Dynamic Framework까지, Tuist가 어떻게 동작하는지 완벽히 이해한다.

Tuist 4.x 기준 실무 프로젝트 기반
학습 날짜

기록 없음

한줄 요약

Tuist는 Swift 코드(Manifest)로 Xcode 프로젝트를 정의하고, tuist generate 명령으로 .xcodeproj와 .xcworkspace를 자동 생성하는 도구다.

왜 필요한가?

Xcode 프로젝트 파일(.pbxproj)은 충돌이 잦고 리뷰가 어렵다. Tuist를 쓰면 프로젝트 구조를 Swift 코드로 관리하여 Git 충돌을 줄이고, 모듈화된 대규모 프로젝트를 체계적으로 관리할 수 있다.

핵심 개념 4가지

Tuist를 이해하려면 이 4가지 개념의 계층 관계를 먼저 파악해야 한다.

W
Workspace
여러 Project를 묶는 최상위 컨테이너. Workspace.swift에서 정의한다.
P
Project
하나의 .xcodeproj에 해당. 여러 Target을 포함한다. Project.swift에서 정의한다.
T
Target
실제 빌드 단위. App, Test, Framework, Widget 등이 각각 하나의 Target이다.
M
Module (Package)
재사용 가능한 코드 묶음. 로컬 SPM 패키지 또는 외부 의존성으로 존재한다.
⚠️ Package ≠ 1 Module
단위설명import 개수
Package패키지 (저장소 단위)1개 이상
Product패키지가 제공하는 라이브러리1개
Target빌드 단위1개
Moduleimport 단위1개
1 Package ≥ 1 Product = 1 Target = 1 Module = 1 import

예: RxSwift 패키지 → import RxSwift, import RxCocoa, import RxRelay (여러 Product)

계층 구조

Workspace (MyApp.xcworkspace)
Project: MyApp
Target: MyAppMain
Target: MyAppTests
Target: MyAppWidget
Project: MyAppVN
Target: MyAppVNMain
→ depends: BaseUtil
→ depends: FeatureHome
⚠️ 1 타겟 = 1 모듈
위 계층도에서 Module은 타겟이 "의존하는" 모듈이지, 타겟이 "생성하는" 모듈이 아니다.
BaseUtil, FeatureHome은 각각 별도 타겟이며, 빌드 시 각자 1개의 모듈을 생성한다.

Tuist 동작 흐름

tuist generate 명령어를 실행하면 어떤 일이 일어나는가?

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의 링킹 방식에 따라 앱의 시작 시간과 바이너리 크기가 달라진다.

S Static Framework
  • 링킹 시점: 빌드 타임
  • 앱 시작: 빠름 (이미 포함됨)
  • 바이너리: 앱에 코드가 복사됨
  • 메모리: 앱 실행 시 함께 로드
  • 크기: 앱 바이너리가 커짐
D Dynamic 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, ] )
언제 Dynamic을 써야 하는가?

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

Q tuist generate와 tuist install의 차이는?
A tuist install은 외부 의존성(SPM 등)만 받아온다. (⚠️ Tuist 4.x에서 tuist fetchtuist install로 대체됨 — fetch는 레거시) tuist generate는 의존성 해결을 포함해 .xcodeproj와 .xcworkspace까지 생성한다. 처음 세팅할 때는 generate만 실행하면 된다.
Q 로컬 SPM 패키지 vs Tuist 타겟, 뭘 써야 하나?
A 로컬 SPM: 독립적으로 테스트/배포 가능, 다른 프로젝트에서 재사용 가능
Tuist 타겟: 해당 프로젝트 전용, 더 세밀한 Xcode 설정 가능
재사용성이 높으면 SPM, 프로젝트 전용이면 타겟으로 만든다.
Q 왜 .xcodeproj를 gitignore에 넣나?
A Tuist가 Manifest에서 프로젝트를 생성하므로, 생성된 .xcodeproj를 버전 관리할 필요가 없다. 각 개발자가 tuist generate로 동일한 프로젝트를 생성하면 된다. 이렇게 하면 .pbxproj 충돌이 완전히 사라진다.
Q Tuist Cache는 언제 쓰나?
A tuist cache는 의존성 모듈들을 미리 빌드해서 xcframework로 캐싱한다. 대규모 프로젝트에서 빌드 시간을 크게 줄일 수 있다. CI/CD에서 특히 유용하다.
Q .external() vs .package()는 정확히 뭐가 다른가?
A .external(name:): Tuist/Package.swift에 정의된 외부 SPM 패키지 참조
.package(product:): 프로젝트 내 로컬 SPM 패키지의 product 참조
외부냐 로컬이냐의 차이다.
Q Tuist 4.x에서 바뀐 점은?
A Tuist 4에서 외부 의존성 관리 방식이 바뀌었다. Dependencies.swift 대신 Tuist/Package.swift를 사용한다. SPM과 더 일관된 방식으로 의존성을 관리한다.

참고 자료