Tuist_How? - 모듈 분리편

김재형_LittleTale·2025년 6월 17일

Tuist

목록 보기
2/5
post-thumbnail

들어가기에 앞서

오랜만에 인사 드립니다
예비군에 CMC 리드일과 사이드 프로젝트 일정이 계속 겹쳐서
이제야...! Tuist How 편을 작성할 수 있을 수 있는것 같아요!
이번시간에는 어떻게 하면 Tuist로 프로젝트를 마이그레이션 할 수 있는가를
알아보고자 합니다!!!

마이그레이션..?

보통의 블로그 글 같은 경우에는
새로 프로젝트를 파서 Tuist 를 설명하드라구요
너무 흔해지니까 기존 사이드 프로젝트를 Tuist로 전환하여 몸소 느낄 수 있도록
글을 작성하는게 읽는이들이 이해가 더 잘 되지 않을까 생각이 들어서
마이그레이션을 하는 작업으로 진행해 보도록 하겠습니다.

준비사항

  1. 기존 프로젝트
  2. mise Setting
  3. tuist Docs 꼭 읽어보기
  4. Version Check ( 이글은 현재 최신버전인 4.51.1 버전 기준 )

자 들어가보실까요?

Tuist Docs - Migrate

마이그레이션 시작

일단 문서를 읽으면서 따라하셔서 만들어보시면
되긴 됩니다.
근데 저희가(?) 원하는 멀티 모듈 형태이기 보다
싱글 모듈의 느낌이 좀더 강한 간단한 형태에요
저희가 원하는건 이제 작은 모듈들이 모여 하나의 모듈 형태를 원한다고 가정하고
진행합니다.

1단계 기존 프로젝트를 복붙하세요

기존 프로젝트에서 바로 작업하면 문제 발생때 피곤해 질수 있습니다.

2단계 Tuist 파일을 생성합니다.

touch Tuist.swift

빈 파일을 생성후 아래처럼 코드를 구성하겠습니다.

import ProjectDescription

let tuist = Tuist(
    project: TuistProject.tuist(
        plugins: [
            .local(
                path: .relativeToRoot("Plugins/TuistExtensions")
            )
        ]
    )
)

자 여기서 Plugins/TuistExtensions는 제가 만든 플러그인이니 일단은 빼고
진행하시길 바랍니다.

3단계 Package.Swift 생성하기

mkdir Tuist
cd tuist
touch Package.swift

해당 파일에 기존 프로젝트에서 사용하고 있는 라이브러리를 정의할 겁니다.
예전 버전에선 Config() 를 통해 구성하였던 부분이니 참고 바랍니다.

// swift-tools-version: 5.9
import PackageDescription

#if TUIST
    import ProjectDescription
    import TuistExtensions

    let packageSettings = PackageSettings(
        // Customize the product types for specific package product
        // Default is .staticFramework
        // productTypes: ["Alamofire": .framework,]
        productTypes: getProjectPackageSetting,
        baseSettings: .appSettings
    )
#endif

let package = Package(
    name: "Goolbitg-iOS-Lib",
    dependencies: [
        .package(url: "https://github.com/onevcat/Kingfisher", .upToNextMajor(from: "8.1.3")),
        .package(url: "https://github.com/pointfreeco/swift-composable-architecture.git", exact: "1.17.1"),
        .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.10.2")),
        
		...
    ]
)

// MARK: Plugin 코드 입니다.

import Foundation
import ProjectDescription

public var getProjectPackageSetting: [String: Product] {
    let result = projectProductTypes.merging(tcaDynamics) { current, _ in
        return current
    }
    return result
}

private let projectProductTypes: [String: Product] = [
    "TCACoordinators" : .framework,
    "SwiftyBeaver": .framework,
    "Kingfisher" : .framework,
    "PopupView" : .framework
]

여기서 정말 중요한 부분은 대략 두가지 정도입니다.

  1. // swift-tools-version: 5.9 을 지우시면 클납니다.

    tuist 내부적으로 해당값을 통해 작업을 하기 때문에
    작성하지 않으면 Tool버전이 낮다고 하거나 높다고 하여 문제가 발생할 수 있습니다.

  2. 정적 프레임 워크를 동적 프레임 워크로 변환

    getProjectPackageSetting 을 보시면 .framework 로 설정하는 코드가
    보일거에요 여러 보듈에서 자주 쓰이는 라이브러리를 동적 프레임워크로 하겠다고
    설정하는 거다 라고 생각하셔도 무방합니다.

4단계 본인이 정한 모듈로 분리

다 보여드릴 순 없으니 홈탭을 피처로 분리한 부분을 공유 드립니다.
만약 전체 코드가 궁금합니다 하시면 마지막에서
깃허브 링크를 남겨 놓겠습니다.

import TuistExtensions
import ProjectDescription

let homeFremeWork = Project.create(
    config: FrameworkConfig(
        name: Module.feature(.Home).frameWorkName,
        deploymentTargets: AppConfig.deployTarget,
        dependencies: [
            Module.feature(.Common).projectTarget,
            .tca,
            .tcaCoordinator,
            .popupView
        ],
        sources: [
            "Sources/**"
        ]
    )
)

// MARK: Plugin 부분
extension Project {
    
    public static func create(config: ProjectConfigProtocol, subAppName: String? = nil) -> Project {
        var targets: [Target] = []
        switch config.product {
        case .app:
            targets = [
                .target( // 단일 타켓 여러 스킴
                    name: subAppName ?? "App",
                    destinations: AppConfig.destinations,
                    product: config.product,
                    bundleId: config.bundleId ?? "$(PRODUCT_BUNDLE_IDENTIFIER)",
                    deploymentTargets: config.deploymentTargets,
                    infoPlist: .file(
                        path: Path.onesPlistName()
                    ),
                    sources: config.sources,
                    resources: config.resources,
                    entitlements: appEntitlementsPath,
                    dependencies: config.dependencies
                )
            ]
            let name = subAppName ?? config.name
            
            return Project(
                name: name,
                packages: config.packages,
                settings: .appSettings,
                targets: targets + config.customTargets,
                schemes: Scheme.schemes(name: name, root: subAppName == nil),
                resourceSynthesizers: [
                    .custom(name: "Assets", parser: .assets, extensions: ["xcassets"]),
                    .custom(name: "Fonts", parser: .fonts, extensions: ["otf"]),
                ]
            )
        case .framework:
            targets = [
                .target(
                    name: config.name,
                    destinations: AppConfig.destinations,
                    product: config.product,
                    bundleId: "com.frameWork.\(config.name)",
                    deploymentTargets: config.deploymentTargets,
                    sources: config.sources,
                    resources: config.resources,
                    scripts: config.scripts,
                    dependencies: config.dependencies
                )
            ]
            
            return Project(
                name: config.name,
                packages: config.packages,
                settings: .appSettings,
                targets: targets + config.customTargets,
                schemes: config.schemes,
                resourceSynthesizers: [
                    .custom(name: "Assets", parser: .assets, extensions: ["xcassets"]),
                    .custom(name: "Fonts", parser: .fonts, extensions: ["otf"]),
                ]
            )
        default:
            fatalError()
        }
    }
}

각 피처들과 Domain 이나 Data 등의 모듈등은 해당 Create 함수를 통해 설계하였습니다.

5단계 Workspace.swift 를 생성합니다.

📦 왜 필요할까?

Tuist를 쓰면 모듈화를 많이 하게 되는데, 각 모듈은 Project.swift로 관리하죠.
근데 빌드하거나 Xcode에서 볼 때는 이걸 하나로 묶어줘야 하죠.
그걸 도와주는 게 Workspace.swift 입니다.

import TuistExtensions
import ProjectDescription
import Foundation

let workSpace = Workspace(
    name: "앱이름이에용",
    projects: (
        [
            AppConfig.appPath,
            DomainConfig.path,
        ] + Module.modules.map(
            \.path
        ) + DemoApps.allCases.map(
            \.onlyTargetPath
        )
    )
)

저는 위와 같이 구성하였어요!
Tuist-Docs-Workspace

자 이제 4단계를 계속 하나하나 확장해 가면서
앱을 마이그레이션 해주면 끝입니다!

6단계 App Project 세팅

Root 가 될 프로젝트를 생성합니다.

let project = Project.create(
    config: ProjectConfig(
        name: AppConfig.noTuistAppName,
        product: .app,
        deploymentTargets: AppConfig.deployTarget,
        schemes: [], // SchemeMode.getSchemes(targetName: "App", path: "App"),
        dependencies: Module.features.map(\.projectTarget) + [
            Module.utils.projectTarget,
            Module.Data.projectTarget,
            .tca,
            .tcaCoordinator,
            .firebaseCore,
            .firebaseMessaging,
            .popupView
        ],
        resources: [
            .glob(
                pattern: .relativeToRoot("AppSettingFiles/AppResources/**"),
                excluding: [],
                tags: [],
                inclusionCondition: nil
            )
        ]
        , sources: "Sources/**"
    )
)

형태는 아래 사진처럼 보통 구성 될거에요

Github Code

상당히 이부분은 블로그에는 다 담기가 애매합니다.
특히 플러그인을 구성하여 작업을 했었어서
그렇다고 플러그인에 만든 코드를 다 설명하는 것도 문제라
링크를 남겨 놓겠습니다.

https://github.com/Little-tale/Goolbitg-iOS

마무리 하며

자 이번 편은 모듈을 분리하는 작업을 진행 하였습니다...!
짝짝짝
한 이정도 작업을 하게 되면 한 80퍼센트 한거구요
다음편은 Scheme + Xcconfig 분리 하는 방법!
을 진행하고 마무리 지을려고 합니다.

참고로 아마 TCA를 사용하고 계신다면
Xcconfig 와 스킴을 분리하고 나서부터 프리뷰 이슈가 나오게 됩니다.
그것 또한 어떻게 고치는지 알아가 보는 시간도 같이 가져 보려고 합니다.

다음시간 뵙겠습니다. 감사합니당

profile
IOS 개발자 새싹이, 작은 이야기로부터

0개의 댓글