이 글을 쓰게 된 계기
안녕하세요! 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 설정, 처음엔 좀 복잡해 보이는데...
한 번 세팅해두면 진짜 편해요!
특히:
- ModulePath + Path Extension = 경로 오타 방지
- TargetDependency Extension = 의존성 선언 깔끔
- makeModule 템플릿 = 새 모듈 추가 쉬움
- 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 |
