1편에서 간단하게 Tuist 사용법에 대해 알아봤습니다.

Tuist에 대해 잘 모르신다면 https://cheonsong.tistory.com/14 1편을 보고 오시면 도움이 됩니다!

이번에는 Tuist를 활용하여 프로젝트를 모듈화 하는 방법에 대해 포스팅 하겠습니다.

저는 Clean Architecture을 적용해 모듈화를 진행했으며 간단하게 Domain, Presentation, Data, App, Design 프로젝트로 구성했습니다.

 실직적으로 App을 실행하는 프로젝트는 App 프로젝트이며, 나머지 프로젝트는 Framework로 구성할 예정입니다. 그리고 모든 프로젝트를 하나의 Workspace로 포함해 프로젝트 구성 완성할 예정입니다.

 

모듈화 할 프로젝트 구분하기

위 사진처럼 Manifests하위에 Projects폴더를 만들고 모듈화 할 프로젝트로 폴더를 구성해줍니다.

모듈 정리하기

모듈 이름, 경로등을 직접 하드코딩하는것보단 저는 열거형을 이용해 캡슐화 했습니다.

ProjectDescriptionHelpers 안에 ProjectName이라는 Swift파일을 생성 후 아래 내용을 추가해줍니다.

import ProjectDescription

public enum Module {
    case app
    // Repository|DataStore
    case data
    // Presentation
    case presentation
    // Domain
    case domain
    // Design|UI
    case designSystem
}

extension Module {
    public var name: String {
        switch self {
        case .app:
            return "App"
        case .data:
            return "Data"
        case .presentation:
            return "Presentation"
        case .domain:
            return "Domain"
        case .designSystem:
            return "DesignSystem"
        }
    }
    
    public var path: ProjectDescription.Path {
        return .relativeToRoot("Projects/" + self.name)
    }
    
    public var project: TargetDependency {
        return .project(target: self.name, path: self.path)
    }
}

extension Module: CaseIterable { }

모듈을 케이스로 정의 후 name, path, project 연산 프로퍼티를 작성해줍니다.

Project+Templete 구현

모듈관련해서는 작성이 끝났고 이제 프로젝트를 구성하기 위해 템플릿 파일을 작성해야합니다.

이 템플릿 파일에 작성한것을 바탕으로 프로젝트가 생성됩니다.

저희는 App 프로젝트는 Application으로 나머지 프로젝트는 Framework으로 구성할것이기 때문에 Templete파일에는 app과 framework프로젝트를 생성하는 코드를 작성하겠습니다.

project

App, Framework 템플릿 작성에 앞서 project 자체를 생성하는 템플릿을 작성하겠습니다.

프로젝트를 생성하는 생성자입니다. 이중에 필요한것만 골라 템플릿을 작성했습니다.

public static func project(
        name: String,
        product: Product,
        bundleID: String,
        schemes: [Scheme] = [],
        dependencies: [TargetDependency] = [],
        resources: ProjectDescription.ResourceFileElements? = nil
    ) -> Project {
        return Project(
            name: name,
            targets: [
                Target(
                    name: name,
                    platform: .iOS,
                    product: product,
                    bundleId: bundleID,
                    deploymentTarget: .iOS(targetVersion: iosVersion, devices: [.iphone, .ipad]),
                    infoPlist: .file(path: .relativeToRoot("Supporting Files/Info.plist")),
                    sources: ["Sources/**"],
                    resources: resources,
                    dependencies: dependencies
                ),
                Target(
                    name: "\(name)Tests",
                    platform: .iOS,
                    product: .unitTests,
                    bundleId: bundleID,
                    deploymentTarget: .iOS(targetVersion: iosVersion, devices: [.iphone, .ipad]),
                    infoPlist: .file(path: .relativeToRoot("Supporting Files/Info.plist")),
                    sources: "Tests/**",
                    dependencies: [
                        .target(name: "\(name)")
                    ]
                )
            ],
            schemes: schemes
        )
    }

몇가지만 추려서 설명하겠습니다.

프로젝트의 이름을 설정하고 타겟을 지정합니다. 여기서 타겟을 생성할때 대부분의 세팅이 들어가게 됩니다.

product는 타겟의 Product Type 을 지정합니다.  app, library, framework, test, appExtension, watch2App 등 많은 타입이 있습니다.

deploymentTarget은 앱의 최소 타겟버전, 가능 디바이스 등을 설정합니다.

infoPlist는 default로 설정 시 프로젝트마다 infoPlist를 생성해줍니다. 그런데 저는 하나의 infoPlist를 사용하고자 하나의 plist를 바라보도록 경로를 설정했습니다.

sources 는 소스코드가 들어강 경로를 작성합니다. 저는 프로젝트의 Sources 라는 폴더로 경로를 작성했습니다.

Tests타겟의 경우에는 Tests라는 폴더로 경로를 추가했습니다.

dependencies는 외부에서 주입할 Framework, Library등을 작성합니다.

App

public static func app(
        name: String,
        dependencies: [TargetDependency] = [],
        resources: ProjectDescription.ResourceFileElements? = nil
    ) -> Project {
        return self.project(
            name: name,
            product: .app,
            bundleID: bundleID + "\(name)",
            dependencies: dependencies,
            resources: resources
        )
    }

Framework

public static func framework(name: String,
                                 dependencies: [TargetDependency] = [],
                                 resources: ProjectDescription.ResourceFileElements? = nil
    ) -> Project {
        return .project(name: name,
                        product: .framework,
                        bundleID: bundleID + ".\(name)",
                        dependencies: dependencies,
                        resources: resources)
    }

위 두 코드의 차이점은 product를 app으로 하느냐, framework로 하느냐 차이입니다.

주의! Project+Templates파일 안에 작성하는 템플릿은 Project Extension 에 작성해야합니다.

Resources

public extension ProjectDescription.ResourceFileElements {

    static let `default`: ProjectDescription.ResourceFileElements = ["Resources/**"]

}

리소스가 필요한 프로젝트가 있을것이고, 필요하지 않은 프로젝트가 있기 때문에 Extension으로 경로를 지정해주고 Resource가 필요한 프로젝트는 프로젝트를 생성할 때 .default로 경로를 설정해줍니다.

프로젝트별로 Project.swift파일 설정하기

App

App 프로젝트는 기본적으로 어플리케이션을 실행하는 프로젝트이므로 모든 Framework를 포함하고 있어야합니다.

let project = Project.app(name: Module.app.name,
                          dependencies: [
                            Module.domain,
                            Module.data,
                            Module.presentation,
                            Module.designSystem
                          ].map(\.project),
                          resources: .default)

dependencies는 TargetDependency 타입을 넣어줘야합니다. 저희가 ProjectName파일에 정의했던 Module별 project변수는 TargetDependency를 반환합니다.

App 프로젝트는 strings 파일등을 resource로 가지고 있어야하므로 리소스폴더가 필요합니다 그래서 .default로 설정해줍니다.

Framework

Framework 프로젝트는 .app이 아니라 .framework로 생성합니다.

let project = Project.framework(name: Module.data.name,
                                dependencies: [Module.domain.project])

Data Project를 예시로 보면 .framework로 프로젝트를 생성하고 CleanArchitecture를 기반으로 모듈화를 진행하기 때문에 dependencies에 Domain프로젝트를 주입했습니다. 또한 Resource는 필요 없기때문에 작성하지 않습니다.

프로젝트별 경로 폴더 추가하기

템플릿에 소스코드는 Sources에 테스트코드는 Tests에 리소스파일은 Resources로 경로를 설정했습니다.

이 경로에 실제 폴더가 존재하지 않는다면 프로젝트는 생성되지 않습니다.

각각 프로젝트 폴더에 Sources, Tests, Resources(있다면) 추가해줍니다.

워크스페이스 설정하기

저희는 총 다섯개의 프로젝트를 설정했고 이 프로젝트들을 하나의 워크스페이스로 묶어 구성할 예정입니다.

프로젝트폴더와 같은 계층에 Workspace라는 swift파일을 생성해줍니다.

let workspace = Workspace(name: "Projects",
                          projects: Module.allCases.map(\.path))

 Module Enum을 CaseIterable로 Extension했기때문에 allCases를 사용했습니다

프로젝트 생성하기

자 이제 모든 설정을 완료했습니다! 이제 터미널에서 tuist generate를 통해 프로젝트를 생성하면

프로젝트 폴더 별로 프로젝트가 생성되고 상위 폴더에 워크스페이스가 생성됐습니다~~

마치며

Tuist를 통해 모듈화를 진행했는데 프로젝트 설정을 직접 해줘야하기 때문에 XcodeProject에 대한 기본적인 지식이 어느정도 필요합니다. 

하지만 템플릿만 한번 잘 만들어놓으면 무궁무진하게 활용이 가능하니 여러분들도 직접 프로젝트에 맞춰 설정을 해보시면 도움이 많이 될껍니다! 또한 모듈간의 의존성을 손쉽게 설정할 수 있는 부분이 저는 가장 좋았습니다. 다음 포스팅에서는 SPM, CocoaPods을 이용해 외부 라이브러리를 어떻게 적용하는지에 대해 포스팅 해보겠습니다.

해당 코드는 제 깃허브에 공개돼있습니다.

https://github.com/cheonsong/TuistTemplete

 

GitHub - cheonsong/TuistTemplete: Clean Architecture With Tuist

Clean Architecture With Tuist. Contribute to cheonsong/TuistTemplete development by creating an account on GitHub.

github.com

 

복사했습니다!