TUIST 뿌시기 3

Ios_Roy·2025년 7월 29일

TUIST뿌시기

목록 보기
3/6
post-thumbnail

07.29(화)

제가 이해하고 공부 한대로 작성한 내용이다 보니 , 정확한 정보가 아닐수도 있습니다
고쳐야 할 부분이 있다면 , 편하게 애기 해주시면 감사 하겠습니다🙏🏻

🧩 Tuist에서 Plugin 만들고 등록하기

내부 템플릿, 의존성, 유틸리티를 확장 가능한 구조로 분리하는 방법

Tuist는 기본적으로 선언형으로 프로젝트를 구성하지만,
플러그인(Plugin)을 활용하면 템플릿 재사용, 의존성 관리, 빌드 스크립트 확장 등 다양한 기능을 커스터마이징할 수 있습니다.

이번 글에서는 다음을 정리합니다:

  • Tuist Plugin의 역할과 예시
  • Plugin 디렉토리 구조 만들기
  • Tuist.swift에서 플러그인 등록하기

✅ Tuist Plugin이란?

Tuist의 플러그인은 Tuist 환경에서만 작동하는 확장 코드입니다.
대표적으로 아래와 같은 기능을 할 수 있습니다:

플러그인 유형예시 역할
ProjectTemplatePlugin모듈 템플릿 정의 (makeAppModule, makeFeatureModule)
DependencyPlugin내부 모듈 enum 정의 (.shared(.logger))
DependencyPackagePlugin외부 라이브러리 정의 (SPM 패키지 설정 등)

📁 플러그인 디렉토리 구조

Tuist 프로젝트 루트에 Plugins/ 폴더를 만들고, 아래와 같이 구조화합니다:

💡 플러그인 이름은 자유롭게 지을 수 있지만 명확한 역할로 구분하는 것이 유지보수에 좋습니다.


⚙️ Tuist.swift에서 플러그인 등록하기

Tuist는 Tuist.swift 파일을 통해 사용할 플러그인을 명시합니다.
이 파일은 프로젝트 루트에 위치해야 하며, 다음과 같이 작성할 수 있습니다:

Plugins/
├── ProjectTemplatePlugin/
│   └── ProjectDescriptionHelpers/
│       └── TemplateHelpers.swift
├── DependencyPlugin/
│   └── ProjectDescriptionHelpers/
│       └── TargetDependency+Module.swift
├── DependencyPackagePlugin/
│   ├── Package.swift
│   └── ProjectDescriptionHelpers/
│       └── Extension+TargetDependencySPM.swift
import ProjectDescription
import Foundation

let tuist = Tuist(
  project: .tuist(
    compatibleXcodeVersions: .all,
    swiftVersion: .some("6.0.0"),
    plugins: [
      .local(path: .relativeToRoot("Plugins/ProjectTemplatePlugin")),
      .local(path: .relativeToRoot("Plugins/DependencyPackagePlugin")),
      .local(path: .relativeToRoot("Plugins/DependencyPlugin")),
    ],
    generationOptions: .options(),
    installOptions: .options()
  )
)
  • plugins: 사용할 플러그인을 등록합니다.
  • .local(path:): 플러그인을 로컬 경로에서 로드합니다.
  • swiftVersion: 플러그인에서 사용할 Swift 버전을 명시합니다.

💡 Tuist.swifttuist config 명령으로 자동 생성도 가능합니다.

🗂️ Plugin 선언 파일도 필요합니다

Tuist의 각 플러그인은 반드시 자신의 루트에 Plugin.swift 파일을 포함해야 합니다.
이 파일은 플러그인의 이름을 정의하며, Tuist가 플러그인을 인식하는 데 필수입니다.

예: Plugins/DependencyPackagePlugin/Plugin.swift

@preconcurrency import ProjectDescription

let plugin = Plugin(name: "DependencyPackagePlugin")

🔗 내부 모듈 경로를 관리하는 ModulePath 정의하기

DependencyPlugin에서 계층 구조를 명확하게 표현하는 방법

Tuist로 프로젝트를 모듈화하면, 내부 의존성들을 구조적으로 그룹핑하는 것이 중요합니다.
이를 위해 우리는 DependencyPlugin 내부에 ModulePath enum을 선언하여 모듈 경로를 계층적으로 관리합니다.


📁 구조 예시

import Foundation
import ProjectDescription

public enum ModulePath {
  case Presentatios(Presentations)
  case Core(Cores)
  case Network(Networks)
  case Interface(Interfaces)
  case Domain(Domains)
  case Data(Datas)
  case Shared(Shareds)
}
  • ModulePath최상위 enum으로, 실제 폴더 구조의 루트와 1:1 대응합니다.
  • case는 내부 enum을 포함하고 있으며, 각각 서브모듈들을 CaseIterable enum으로 관리합니다.

🧱 각 모듈 계층 정의

아래는 실제 사용 예시입니다.
각 계층별 하위 모듈을 enum으로 나누고, name 속성을 통해 디렉토리명을 함께 정의합니다.


📱 앱 모듈

public extension ModulePath {
  enum App: String, CaseIterable {
    case iOS
    case iPad

    public static let name: String = "App"
  }
}

⚙️ Core 모듈

앱의 핵심 로직을 담는 내부 기반 모듈입니다.
서비스 전반에 공통으로 사용되는 기능들이 이곳에 존재합니다.

public extension ModulePath {
  enum Cores: String, CaseIterable {
    case Core

    public static let name: String = "Core"
  }
}

🌐 Network 모듈

API 통신과 관련된 모든 기능을 담당합니다.
ThirdPartys, Service, Foundations 등 네트워크 계층을 완전 분리해 구성합니다

public extension ModulePath {
  enum Networks: String, CaseIterable {
    case API
    case Networks
    case Foundations
    case Service
    case ThirdPartys

    public static let name: String = "Network"
  }
}

🗂️ Data 모듈

Repository 패턴 기반의 데이터 관리 계층입니다.
로컬/원격 데이터 저장 및 변환 로직을 분리해 구성합니다.

public extension ModulePath {
  enum Datas: String, CaseIterable {
    case Model
    case Repository

    public static let name: String = "Data"
  }
}

🧠 Domain 모듈

비즈니스 로직과 유스케이스를 담당합니다.
애플리케이션의 의도를 담는 핵심 계층이며, 프레젠테이션/데이터 계층과 느슨하게 연결됩니다.

public extension ModulePath {
  enum Domains: String, CaseIterable {
    case UseCase
    case DomainInterface

    public static let name: String = "Domain"
  }
}

🔌 Interface 모듈

외부 혹은 플랫폼별 의존성을 추상화한 계층입니다.
대표적으로 시스템 API, SDK 추상화 계층 등이 위치합니다.

public extension ModulePath {
  enum Interfaces: String, CaseIterable {
    case Interface

    public static let name: String = "Interface"
  }
}

🧩 Shared 모듈

프로젝트 전역에서 재사용 가능한 유틸, 디자인 시스템, 공통 코드 등을 정의합니다.
예: DesignSystem, ThirdParty, Utill 등

public extension ModulePath {
  enum Shareds: String, CaseIterable {
    case Shareds
    case DesignSystem
    case Utill
    case ThirdParty

    public static let name: String = "Shared"
  }
}

📁 Tuist에서 모듈 경로를 Path 확장으로 일관되게 관리하기

ProjectDescription.Path + ModulePath 조합으로 자동화/재사용성 향상

모듈이 많은 프로젝트에서는 각 모듈의 경로를 하드코딩하지 않고,
중앙에서 enum 기반으로 관리하는 것이 유지보수와 자동화에 매우 유리합니다.

이 글에서는 ModulePath와 함께 ProjectDescription.Path를 확장하여
모듈 경로를 공통화하는 구조를 소개합니다.


✅ 왜 필요한가?

Tuist에서는 의존성 추가 시 .project(...)처럼 모듈 경로를 직접 지정해야 하는 경우가 많습니다.
예를 들어:

.project(target: "Presentation", path: .relativeToRoot("Projects/Presentation"))

하지만 이렇게 문자열로 직접 작성하면:

  • ❌ 경로 오타 발생 가능
  • ❌ 중복 선언
  • ❌ CLI 자동 생성 시 동기화 어려움

→ ✅ 그래서 ProjectDescription.Path에 확장을 만들어 enum 기반으로 재사용합니다.


🔧 기본 구조

public extension ProjectDescription.Path {
  static var app: Self {
    return .relativeToRoot("Projects/\(ModulePath.App.name)")
  }
}

📦 실제 모듈별 정의

각 모듈에 대해 static var, static func 형식으로 경로를 추상화합니다.
이렇게 하면 Project.swift 등에서 경로를 일관되고 안전하게 사용할 수 있습니다.

📱 App 모듈

public extension ProjectDescription.Path {
  static var app: Self {
    return .relativeToRoot("Projects/\(ModulePath.App.name)")
  }
}

🎨 Presentation 모듈

public extension ProjectDescription.Path {
  static var Presentation: Self {
    return .relativeToRoot("Projects/\(ModulePath.Presentations.name)")
  }

  static func Presentation(implementation module: ModulePath.Presentations) -> Self {
    return .relativeToRoot("Projects/\(ModulePath.Presentations.name)/\(module.rawValue)")
  }
}

⚙️ Core 모듈

public extension ProjectDescription.Path {
  static var Core: Self {
    return .relativeToRoot("Projects/\(ModulePath.Cores.name)")
  }

  static func Core(implementation module: ModulePath.Cores) -> Self {
    return .relativeToRoot("Projects/\(ModulePath.Cores.name)/\(module.rawValue)")
  }
}

🎨 Shared 모듈

public extension ProjectDescription.Path {
  static var Shared: Self {
    return .relativeToRoot("Projects/\(ModulePath.Shareds.name)")
  }

  static func Shared(implementation module: ModulePath.Shareds) -> Self {
    return .relativeToRoot("Projects/\(ModulePath.Shareds.name)/\(module.rawValue)")
  }
}

🌐 Network 모듈

public extension ProjectDescription.Path {
  static var Networking: Self {
    return .relativeToRoot("Projects/\(ModulePath.Cores.name)/\(ModulePath.Networks.name)")
  }

  static func Network(implementation module: ModulePath.Networks) -> Self {
    return .relativeToRoot("Projects/\(ModulePath.Cores.name)/\(ModulePath.Networks.name)/\(module.rawValue)")
  }
}

🧠 Domain 모듈

public extension ProjectDescription.Path {
  static var Domain: Self {
    return .relativeToRoot("Projects/\(ModulePath.Cores.name)/\(ModulePath.Domains.name)")
  }

  static func Domain(implementation module: ModulePath.Domains) -> Self {
    return .relativeToRoot("Projects/\(ModulePath.Cores.name)/\(ModulePath.Domains.name)/\(module.rawValue)")
  }
}

🗂️ Data 모듈

public extension ProjectDescription.Path {
  static var Data: Self {
    return .relativeToRoot("Projects/\(ModulePath.Cores.name)/\(ModulePath.Datas.name)")
  }

  static func Data(implementation module: ModulePath.Datas) -> Self {
    return .relativeToRoot("Projects/\(ModulePath.Cores.name)/\(ModulePath.Datas.name)/\(module.rawValue)")
  }
}

🔌 Interface 모듈

public extension ProjectDescription.Path {
  static var Interface: Self {
    return .relativeToRoot("Projects/\(ModulePath.Cores.name)/\(ModulePath.Interfaces.name)")
  }

  static func Interface(implementation module: ModulePath.Interfaces) -> Self {
    return .relativeToRoot("Projects/\(ModulePath.Cores.name)/\(ModulePath.Interfaces.name)/\(module.rawValue)")
  }
}

🔗 Tuist에서 TargetDependency를 확장하여 의존성 선언 단순화하기

.project(...) 대신 .Shared(implements:) 등 직관적인 선언 방식 만들기

Tuist에서는 타겟 간 의존성을 추가할 때 TargetDependency.project(...) 형식을 자주 사용합니다.
하지만 이렇게 직접 경로와 타겟명을 매번 입력하면 반복적이고, 오타 가능성도 생깁니다.

그래서 우리는 TargetDependency를 직접 확장하여,
enum 기반의 모듈 정보(ModulePath)경로(Path 확장) 를 조합해
아래와 같은 선언 방식으로 간단하게 의존성을 정의할 수 있습니다:

.SPM.composableArchitecture
.Shared(implements: .DesignSystem)
.Network(implements: .Service)

🧱 확장된 TargetDependency 정의

📱 App 모듈

public extension TargetDependency {
    static var app: Self {
        return .project(target: ModulePath.App.name, path: .app)
    }

    static func app(implements module: ModulePath.App) -> Self {
        return .target(name: ModulePath.App.name + module.rawValue)
    }
}

🎨 Presentation 모듈

public extension TargetDependency {
    static func Presentation(implements module: ModulePath.Presentations) -> Self {
        return .project(target: module.rawValue, path: .Presentation(implementation: module))
    }
}

🎨 Shared 모듈

public extension TargetDependency {
    static func Shared(implements module: ModulePath.Shareds) -> Self {
        return .project(target: module.rawValue, path: .Shared(implementation: module))
    }
}

⚙️ Core 모듈

public extension TargetDependency {
    static func Core(implements module: ModulePath.Cores) -> Self {
        return .project(target: module.rawValue, path: .Core(implementation: module))
    }
}

🌐 Network 모듈

public extension TargetDependency {
    static func Network(implements module: ModulePath.Networks) -> Self {
        return .project(target: module.rawValue, path: .Network(implementation: module))
    }
}

🧠 Domain 모듈

public extension TargetDependency {
    static func Domain(implements module: ModulePath.Domains) -> Self {
        return .project(target: module.rawValue, path: .Domain(implementation: module))
    }
}

🗂️ Data 모듈

public extension TargetDependency {
    static func Data(implements module: ModulePath.Datas) -> Self {
        return .project(target: module.rawValue, path: .Data(implementation: module))
    }
}

🔌 Interface 모듈

public extension TargetDependency {
    static func Interface(implements module: ModulePath.Interfaces) -> Self {
        return .project(target: module.rawValue, path: .Interface(implementation: module))
    }
}

✨ 사용 예시

이제 Project.swift에서 아래와 같이 선언할 수 있습니다:

let project = Project(
  name: "LoginFeature",
  targets: [
    .target(
      name: "LoginFeature",
      dependencies: [
        .Shared(implements: .DesignSystem),
        .Network(implements: .Service),
        .Domain(implements: .UseCase),
        .Interface(implements: .Interface)
      ]
    )
  ]
)

🏗️ Tuist에서 Project.swift를 템플릿화하여 모듈 선언 단순화하기

makeAppModule, makeModule, makeScheme 유틸로 유지보수성과 자동화 강화

Tuist를 사용하다 보면 여러 개의 모듈을 만들고, 각 모듈마다 Project.swift를 반복해서 작성하게 됩니다.
그런데 구성 패턴은 거의 유사하죠? 🤔

그래서 저는 Project.swift 생성 로직을 템플릿 함수로 분리하여, 다음과 같이 재사용 가능한 구조로 만들었습니다:

  • makeAppModule: 앱 실행 타겟을 위한 프로젝트 템플릿
  • makeModule: 일반 프레임워크/기능 모듈 템플릿
  • makeScheme: 공통된 테스트/빌드 설정 자동화

🚀 makeAppModule 함수

앱 모듈은 다양한 환경(예: Debug, Stage, Prod 등)에 따라 별도 타겟이 필요합니다.
이를 반영하여 다음과 같이 여러 실행 타겟 + 테스트 타겟을 생성합니다:


🔧 함수 정의

static func makeAppModule(
  name: String = Environment.appName,
  bundleId: String,
  platform: Platform = .iOS,
  product: Product,
  packages: [Package] = [],
  deploymentTarget: DeploymentTargets = Environment.deploymentTarget,
  destinations: Destinations = Environment.deploymentDestination,
  settings: Settings,
  scripts: [TargetScript] = [],
  dependencies: [TargetDependency] = [],
  sources: SourceFilesList = ["Sources/**"],
  resources: ResourceFileElements? = nil,
  infoPlist: InfoPlist = .default,
  entitlements: Entitlements? = nil,
  schemes: [Scheme] = []
) -> Project

📌 주요 파라미터 설명

파라미터설명
name기본 타겟 이름 (예: "MyApp")
bundleId앱의 번들 식별자
product.app, .framework 등 타겟 유형
settingsSettings.settings(...)로 정의한 빌드 설정
dependencies외부/내부 모듈 의존성 리스트
schemes생성할 Scheme 정의 목록

🎯 생성되는 타겟

let appTarget       // 기본 앱 타겟
let appDevTarget    // Debug 환경용
let appStageTarget  // Stage 환경용
let appProdTarget   // Prod 환경용
let appTestTarget   // 유닛 테스트용

✅ 반환되는 Project 구성

return Project(
  name: name,
  packages: packages,
  settings: settings,
  targets: targets,
  schemes: schemes
)

✨ 사용 예시

let project = Project.makeAppModule(
  name: "MyApp",
  bundleId: "com.example.myapp",
  product: .app,
  settings: .settings(configurations: [...]),
  dependencies: [
    .SPM.composableArchitecture,
    .Shared(implements: .DesignSystem)
  ],
  schemes: [
    .makeScheme(target: .debug, name: "MyApp"),
    .makeScheme(target: .release, name: "MyApp-Prod")
  ]
)

🧱 makeModule 함수

일반적인 기능 모듈(예: LoginFeature, DesignSystem)을 위한 템플릿입니다.
보통 QA용 실행 타겟 + 테스트 타겟만 생성합니다.


🔧 함수 정의

static func makeModule(
  name: String = Environment.appName,
  bundleId: String,
  platform: Platform = .iOS,
  product: Product,
  packages: [Package] = [],
  deploymentTarget: DeploymentTargets = Environment.deploymentTarget,
  destinations: Destinations = Environment.deploymentDestination,
  settings: Settings,
  scripts: [TargetScript] = [],
  dependencies: [TargetDependency] = [],
  sources: SourceFilesList = ["Sources/**"],
  resources: ResourceFileElements? = nil,
  infoPlist: InfoPlist = .default,
  entitlements: Entitlements? = nil,
  schemes: [Scheme] = []
) -> Project

📌 주요 파라미터 설명

파라미터설명
name타겟 이름 (예: "LoginFeature")
bundleId번들 식별자 (예: com.example.feature)
product.framework 또는 .staticFramework
settingsSettings.settings(...) 설정 객체
dependencies.SPM, .Shared(...), .Domain(...) 등 의존성
schemes실행/테스트용 Scheme 리스트

🧩 생성되는 타겟 구성

let appTarget     // 기본 프레임워크 타겟 (name)
let appDevTarget  // QA 환경용 타겟 (name-QA)
let appTestTarget // 유닛 테스트용 타겟 (nameTests)

✨ 사용 예시

let project = Project.makeModule(
  name: "LoginFeature",
  bundleId: "com.myapp.login",
  product: .framework,
  settings: .settings(configurations: [...]),
  dependencies: [
    .Domain(implements: .UseCase),
    .Shared(implements: .DesignSystem)
  ],
  schemes: [
    .makeScheme(target: .debug, name: "LoginFeature")
  ]
)

🧪 makeScheme 함수

Project.swift에서 매번 반복적으로 작성해야 하는 Scheme 설정을
템플릿 함수로 추출해 공통화한 유틸입니다.

  • 테스트 대상과 커버리지 타겟을 자동 지정합니다.
  • 실행, 아카이브, 프로파일, 분석 등 모든 액션을 하나의 함수로 정의합니다.

🔧 함수 정의

extension Scheme {
  public static func makeScheme(target: ConfigurationName, name: String) -> Scheme {
    return Scheme.scheme(
      name: name,
      shared: true,
      buildAction: .buildAction(targets: ["\(name)"]),
      testAction: .targets(
        ["\(name)Tests"],
        configuration: target,
        options: .options(coverage: true, codeCoverageTargets: ["\(name)"])
      ),
      runAction: .runAction(configuration: target),
      archiveAction: .archiveAction(configuration: target),
      profileAction: .profileAction(configuration: target),
      analyzeAction: .analyzeAction(configuration: target)
    )
  }
}

📌 주요 특징

항목설명
name스킴 이름 (예: "LoginFeature", "MyApp-Prod")
target.debug, .releaseConfigurationName 지정
testAction"LoginFeatureTests" 대상 자동 지정 및 커버리지 활성화
codeCoverageTargets커버리지 수집 대상 타겟 설정 (예: ["LoginFeature"])
모든 Action.build, .run, .archive, .profile, .analyze 전부 포함

✨ 사용 예시

let schemes: [Scheme] = [
  .makeScheme(target: .debug, name: "LoginFeature"),
  .makeScheme(target: .release, name: "LoginFeature-Prod")
]

✅ 정리 — Tuist Plugin을 통한 확장성과 모듈 자동화 전략

Tuist에서 Plugin을 구성하면 아래와 같은 이점을 얻을 수 있습니다:

항목내용
🧩 Plugin 구조 설계ProjectTemplatePlugin, DependencyPlugin, DependencyPackagePlugin을 통해 모듈 템플릿화, 내부/외부 의존성 분리, 재사용성 극대화가 가능합니다.
📁 모듈 경로 일원화ModulePathProjectDescription.Path 확장을 통해 경로를 enum 기반으로 추상화하여 안전하고 일관된 관리가 가능합니다.
🔗 의존성 선언 간결화.project(...) 대신 .Shared(implements:), .SPM.diContainer와 같은 확장 메서드를 통해 선언을 간단하고 명확하게 표현할 수 있습니다.
🏗️ 프로젝트 템플릿화makeAppModule, makeModule, makeScheme 유틸 함수로 타겟/스킴 구성의 반복을 제거하고 자동화 기반의 구조를 구축할 수 있습니다.
⚙️ 플러그인 분리 관리각 Plugin은 독립적으로 구성되며, Tuist.swift에 등록만 해주면 환경별로 선택적으로 사용할 수 있는 유연성을 제공합니다.

👀 다음 편 예고

다음 글에서는
“TTuist Settings 설정 구성” 를 주제로 실전 예제를 소개할 예정입니다.


profile
iOS 개발자 공부하는 Roy

0개의 댓글