Tuist Plugin 만들기 + Settings 설정까지 완전 정복!

2026. 4. 5. 13:51·iOS

이 글을 쓰게 된 계기

안녕하세요! iOS 운영진 서원지입니다 ㅋㅋㅋ

Tuist로 모듈화 하다 보면 이런 고민이 생겨요:

"매번 비슷한 코드 반복하기 싫은데... 템플릿화 못 하나?" "환경별로 설정 다르게 하고 싶은데 어떻게 하지?"

그래서 오늘은 Tuist Plugin 만들기랑 Settings 설정을 한 방에 정리해볼게요! 😎

⚠️ 제가 공부하면서 정리한 내용이라 틀린 부분 있을 수 있어요! 잘못된 부분 있으면 편하게 알려주세요 🙏


Part 1: Tuist Plugin이란?

Tuist Plugin은 Tuist 환경에서만 작동하는 확장 코드예요.

이걸로 뭘 할 수 있냐면:

플러그인 유형역할

ProjectTemplatePlugin모듈 템플릿 정의 (makeAppModule, makeModule)

DependencyPlugin 내부 모듈 enum 정의 (.Shared(.DesignSystem))
DependencyPackagePlugin 외부 라이브러리 정의 (SPM 설정)

플러그인 디렉토리 구조

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

💡 플러그인 이름은 자유롭게! 근데 역할별로 명확하게 나누는 게 유지보수에 좋아요.


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

프로젝트 루트에 Tuist.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()
    )
)

Plugin.swift도 필요해요!

각 플러그인 폴더에 Plugin.swift 파일 필수!

// Plugins/DependencyPackagePlugin/Plugin.swift
@preconcurrency import ProjectDescription

let plugin = Plugin(name: "DependencyPackagePlugin")

이거 없으면 Tuist가 플러그인 인식 못 해요 ㅠㅠ


ModulePath로 모듈 경로 관리하기

모듈이 많아지면 경로 관리가 진짜 귀찮아요...

그래서 ModulePath enum으로 계층 구조를 명확하게!

import Foundation
import ProjectDescription

public enum ModulePath {
    case Presentations(Presentations)
    case Core(Cores)
    case Network(Networks)
    case Interface(Interfaces)
    case Domain(Domains)
    case Data(Datas)
    case Shared(Shareds)
}

각 모듈 계층 정의

// App 모듈
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 모듈
public extension ModulePath {
    enum Networks: String, CaseIterable {
        case API
        case Networks
        case Foundations
        case Service
        case ThirdPartys

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

// Data 모듈
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"
    }
}

// Shared 모듈
public extension ModulePath {
    enum Shareds: String, CaseIterable {
        case Shareds
        case DesignSystem
        case Utill
        case ThirdParty

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

📍 Path 확장으로 경로 자동화

매번 이렇게 쓰기 싫잖아요?

// ❌ 이렇게 하면 오타나기 쉬움
.relativeToRoot("Projects/Shared/DesignSystem")

Path Extension 만들기!

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

    // Shared
    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
    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
    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
    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)")
    }
}

TargetDependency 확장으로 의존성 간결하게!

// ❌ 이렇게 쓰면 길고 귀찮음
.project(target: "DesignSystem", path: .relativeToRoot("Projects/Shared/DesignSystem"))

// ✅ 이렇게 쓰면 깔끔!
.Shared(implements: .DesignSystem)

Extension 만들기

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

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

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

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

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

사용 예시

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

진짜 깔끔해요! 😍


🏗️ Project 템플릿 함수 만들기

makeAppModule - 앱 타겟용

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

생성되는 타겟:

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

makeModule - 일반 모듈용

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

makeScheme - 스킴 템플릿

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)
        )
    }
}

사용 예시

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")
    ]
)

🛠 Part 2: Settings 설정 완전 정복

여기서부터가 진짜 중요한 부분이에요!

환경별로 설정 다르게 하고 싶을 때 Settings.swift를 활용합니다.

commonSettings 함수

환경별로 중복되는 설정을 재사용하기 위한 유틸 함수:

private static func commonSettings(
    appName: String,
    displayName: String,
    provisioningProfile: String,
    setSkipInstall: Bool
) -> SettingsDictionary {
    return SettingsDictionary()
        .setProductName(appName)
        .setCFBundleDisplayName(displayName)
        .setOtherLdFlags("-ObjC -all_load")
        .setDebugInformationFormat("dwarf-with-dsym")
        .setProvisioningProfileSpecifier(provisioningProfile)
        .setSkipInstall(setSkipInstall)
}

설정 항목 설명

메서드설명

setProductName실행 파일 이름 (PRODUCT_NAME)

setCFBundleDisplayName 홈 화면에 표시될 앱 이름
setOtherLdFlags Objective-C 런타임 로딩 설정
setDebugInformationFormat .dSYM 생성 (크래시 추적용)
setProvisioningProfileSpecifier 서명에 사용할 프로파일
setSkipInstall 아카이브 대상 포함 여부

📋 appMainSetting - 앱 전용 빌드 설정

public static let appMainSetting: Settings = .settings(
    base: SettingsDictionary()
        .setProductName(Project.Environment.appName)
        .setCFBundleDisplayName(Project.Environment.appName)
        .setMarketingVersion(.appVersion())
        .setASAuthenticationServicesEnabled()       // Apple 로그인
        .setPushNotificationsEnabled()              // 푸시 알림
        .setEnableBackgroundModes()                 // 백그라운드 모드
        .setArchs()
        .setOtherLdFlags()
        .setCurrentProjectVersion(.appBuildVersion())
        .setCodeSignIdentity()
        .setCodeSignStyle()
        .setSwiftVersion("6.0")
        .setVersioningSystem()
        .setProvisioningProfileSpecifier("match Development \(Project.Environment.bundlePrefix)")
        .setDevelopmentTeam(Project.Environment.organizationTeamId)
        .setCFBundleDevelopmentRegion()
        .setDebugInformationFormat(),

    configurations: [
        // 🔧 Debug
        .debug(
            name: .debug,
            settings: commonSettings(
                appName: Project.Environment.appName,
                displayName: Project.Environment.appName,
                provisioningProfile: "match Development \(Project.Environment.bundlePrefix)",
                setSkipInstall: false
            ),
            xcconfig: .relativeToRoot("./Config/dev.xcconfig")
        ),

        // 🧪 Stage (QA용)
        .debug(
            name: "Stage",
            settings: commonSettings(
                appName: Project.Environment.appStageName,
                displayName: Project.Environment.appName,
                provisioningProfile: "match Development \(Project.Environment.bundlePrefix)",
                setSkipInstall: false
            ),
            xcconfig: .relativeToRoot("./Config/qa.xcconfig")
        ),

        // 📦 Release
        .release(
            name: .release,
            settings: commonSettings(
                appName: Project.Environment.appName,
                displayName: Project.Environment.appName,
                provisioningProfile: "match AppStore \(Project.Environment.bundlePrefix)",
                setSkipInstall: false
            ),
            xcconfig: .relativeToRoot("./Config/realse.xcconfig")
        ),

        // 🚀 Prod (실제 배포)
        .release(
            name: "Prod",
            settings: commonSettings(
                appName: Project.Environment.appProdName,
                displayName: Project.Environment.appName,
                provisioningProfile: "match AppStore \(Project.Environment.bundlePrefix)",
                setSkipInstall: false
            ),
            xcconfig: .relativeToRoot("./Config/realse.xcconfig")
        )
    ],
    defaultSettings: .recommended
)

📊 base 설정 항목 정리

항목설명

setProductName빌드 타겟의 실행 파일 이름

setCFBundleDisplayName 디바이스에 표시되는 앱 이름
setMarketingVersion 사용자에게 보이는 앱 버전
setASAuthenticationServicesEnabled Apple 로그인 활성화
setPushNotificationsEnabled 푸시 알림 권한
setEnableBackgroundModes 백그라운드 fetch, location 등
setArchs 아키텍처 설정 (arm64)
setCurrentProjectVersion 빌드 넘버
setCodeSignIdentity 서명 인증서
setCodeSignStyle Automatic or Manual
setSwiftVersion Swift 버전 고정
setDevelopmentTeam Apple Developer Team ID
setDebugInformationFormat .dSYM 생성 여부

🏷 Configuration별 구성

구성용도xcconfig

.debug개발용, 디버깅dev.xcconfig

"Stage" QA/테스트용 qa.xcconfig
.release 내부 릴리즈용 release.xcconfig
"Prod" 실제 App Store 배포 release.xcconfig

💡 Stage, Prod는 Tuist 기본 enum이 아니라 문자열로 직접 지정해야 해요!


💡 활용 팁

Prod와 Release를 분리하는 이유

  • CI 파이프라인 분리: release는 내부 테스트, prod는 실제 배포
  • 설정 분기: 앱 이름, 아이콘, API 엔드포인트 등 다르게 가능

xcconfig 활용

# dev.xcconfig
API_BASE_URL = https://dev-api.myapp.com
FEATURE_FLAG_DEBUG = YES

# release.xcconfig  
API_BASE_URL = https://api.myapp.com
FEATURE_FLAG_DEBUG = NO

💡 민감한 정보는 .gitignore에 추가하거나 암호화된 저장소에서 관리하세요!


✅ 전체 정리

Plugin 정리

항목내용

Plugin 구조ProjectTemplatePlugin, DependencyPlugin, DependencyPackagePlugin

ModulePath enum 기반 모듈 경로 관리
Path 확장 .Shared(implementation:) 등으로 경로 자동화
TargetDependency 확장 .Shared(implements:) 등으로 의존성 간결화
템플릿 함수 makeAppModule, makeModule, makeScheme

Settings 정리

항목내용

공통 설정 추출commonSettings() 함수로 중복 제거

코드 기반 설정 Git으로 추적 가능
환경별 분리 Debug, Stage, Release, Prod
xcconfig 연동 민감한 설정 외부 파일로 관리

마무리

Tuist Plugin이랑 Settings 설정, 처음엔 좀 복잡해 보이는데...

한 번 세팅해두면 진짜 편해요!

특히:

  1. ModulePath + Path Extension = 경로 오타 방지
  2. TargetDependency Extension = 의존성 선언 깔끔
  3. makeModule 템플릿 = 새 모듈 추가 쉬움
  4. Settings 분리 = 환경별 빌드 설정 명확

'iOS' 카테고리의 다른 글

[iOS] Tuist를 쓰면서 Asset 코드 생성은 직접 만들기로 한 이유  (0) 2026.03.28
Tuist 모듈 자동화 CLI 자동화 만들기  (0) 2026.02.25
Tuist 모듈화에 빠져보기  (0) 2026.01.24
[iOS] 당연하게 쓰던 메서드에게 뒤통수 맞아보기  (0) 2026.01.06
'iOS' 카테고리의 다른 글
  • [iOS] Tuist를 쓰면서 Asset 코드 생성은 직접 만들기로 한 이유
  • Tuist 모듈 자동화 CLI 자동화 만들기
  • Tuist 모듈화에 빠져보기
  • [iOS] 당연하게 쓰던 메서드에게 뒤통수 맞아보기
dynamic-ddd
dynamic-ddd
역할의 경계를 넘어 유저에게 도달하는 프로덕트를 완성해온 메이커들의 커뮤니티입니다.
  • 인기 글

  • dynamic-ddd
    dynamic-ddd 님의 블로그
    dynamic-ddd
  • 전체
    오늘
    어제
    • 분류 전체보기 (9)
      • iOS (5)
      • Android (2)
      • Design (2)
  • 공지사항

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
dynamic-ddd
Tuist Plugin 만들기 + Settings 설정까지 완전 정복!
상단으로

티스토리툴바