[아키텍처] Modular(2)

한지민·2022년 4월 9일
0

아키텍처

목록 보기
7/7
post-thumbnail

Swift Package의 한계

이전 게시글에서 Swift Package를 통해 모듈로 분리해서 구현한다는 목표는 성공했지만,
각 모듈의 실행을 위해선 별도의 Xcode 프로젝트를 통해야한다는 문제점이 남아있다.

이를 해결하기 위해 Swift Package를 사용하는 대신
Tuist를 사용해 여러개의 프로젝트를 갖는 구조로 변경해보고자 한다.

프로젝트 관리 도구

프로젝트 관리 편의성과 Xcode 프로젝트 파일 가지는 문제점을 해결하기 위해 사용한다.
다음은 xcode 프로젝트 파일이 git에 포함되면 야기되는 문제이다.

  • xcodeproj 파일에 포함되는 불필요한 로컬 정보
  • pbxproj 파일에서 발생되는 충돌

이같은 문제를 사전에 예방하기 위해 코드적으로 관리할 수 있게 해주는 것이 프로젝트 관리 도구이다.

대표적으로 XcodeGen, Tuist가 있습니다.

구현

Tuist 설치

Tuist에 대한 전체 문서는 이곳을 확인해주세요.

다음 스크립트를 통해 Tuist를 설치 할 수 있다.

curl -Ls https://install.tuist.io | bash

프로젝트 생성

원하는 위치의 폴더에서 다음 명령어를 사용하면
기본 파일들이 생성된다

tuist init --platform ios

여기서 Tuist폴더를 제외하고 새롭게 구성해 다음과 같이 만든다.

프로젝트 수정

Project.swift 파일을 직접 수정해도 되지만
tuist에서 관련 내용들을 xcode를 통해 편집할 수 있도록 기능을 지원해준다.

tuist edit

Project+Templates.swift

이 파일은 각 Project.swift에서 사용하기 위한 template를 미리 정의할 수 있다.
여기선 app, coreModule, Module 3가지 타입에 대한 template을 미리 정의한다.

    public static func makeAppTargets(name: String, platform: Platform, dependencies: [TargetDependency]) -> [Target] {
        let mainTarget = Target(
            name: name,
            platform: platform,
            product: .app,
            bundleId: "hanchi.\(name)",
            deploymentTarget: .iOS(targetVersion: "15.0", devices: .iphone),
            infoPlist: .file(path: .relativeToRoot("Projects/\(name)/Info.plist")),
            sources: ["Sources/**"],
            resources: ["Resources/**"],
            dependencies: dependencies
        )
        
        let testTarget = Target(
            name: "\(name)Tests",
            platform: platform,
            product: .unitTests,
            bundleId: "hanchi.\(name)Tests",
            infoPlist: .default,
            sources: ["Tests/**"],
            dependencies: [
                .target(name: "\(name)")
            ])
        return [mainTarget, testTarget]
    }
    
    public static func makeCoreModuleTargets(name: String, platform: Platform, dependencies: [TargetDependency]) -> [Target] {
        let sources = Target(
            name: name,
            platform: platform,
            product: .framework,
            bundleId: "hanchi.\(name)",
            deploymentTarget: .iOS(targetVersion: "15.0", devices: .iphone),
            infoPlist: .default,
            sources: ["Sources/**"],
            resources: ["Resources/**"],
            dependencies: dependencies
        )
        
        let tests = Target(
            name: "\(name)Tests",
            platform: platform,
            product: .unitTests,
            bundleId: "hanchi.\(name)Tests",
            infoPlist: .default,
            sources: ["Tests/**"],
            resources: [],
            dependencies: [.target(name: name)]
        )
        return [sources, tests]
    }

    public static func makeModuleTargets(name: String, platform: Platform, dependencies: [TargetDependency]) -> [Target] {
        let mainTarget = Target(
            name: name,
            platform: platform,
            product: .app,
            bundleId: "hanchi.\(name)",
            deploymentTarget: .iOS(targetVersion: "15.0", devices: .iphone),
            infoPlist: .file(path: .relativeToRoot("Projects/Feature/\(name)/Info.plist")),
            sources: ["Sources/**"],
            resources: ["Resources/**"],
            dependencies: dependencies
        )
        
        let sources = Target(
            name: "\(name)Framework",
            platform: platform,
            product: .framework,
            bundleId: "hanchi.\(name)Framework",
            deploymentTarget: .iOS(targetVersion: "15.0", devices: .iphone),
            infoPlist: .default,
            sources: ["Sources/**"],
            resources: ["Resources/**"],
            dependencies: dependencies
        )
        
        let tests = Target(
            name: "\(name)Tests",
            platform: platform,
            product: .unitTests,
            bundleId: "hanchi.\(name)Tests",
            infoPlist: .default,
            sources: ["Tests/**"],
            resources: [],
            dependencies: [.target(name: name)]
        )
        return [mainTarget, sources, tests]
    }

여기서 기능 모듈의 Target은 app, framework, unitTests 3가지에 대한 product 를 지원하도록 설정했다.

Project.swift

Tuist에서 생성할 프로젝트의 구조를 정의하는 파일이다.
기본적인 형태는 다음과 같다.

Project(
    name: "",
    organizationName: "",
    options: .options(),
    packages: [],
    settings: .settings(),
    targets: [],
    schemes: [],
    fileHeaderTemplate: nil,
    additionalFiles: [],
    resourceSynthesizers: []
)

여기서 중요한 요소는 Settings, Targets, Schemes 3가지 이다.

구성 요소에 대해 보다 자세한 내용은 이곳을 확인해주세요.

각 프로젝트에 맞게 Project.swift를 구성하고,
APP을 담당할 GithubSearchTuistApp/Project.swift 를 구성하면 다음과 같다.

Dependencies.swift

Tuist에서 외부 종속성 관리를 위해 Dependencies를 통해 carthage와 spm을 지원한다.

다음과 같이 필요한 종속성을 추가해준다.

tuist fetch

위 명령어를 실행하면 다음과 같이 Dependencies 폴더에 관련 정보를 생성한다.

Workspace.swift

xcworkspace 생성을 위한 파일이다.

기본적인 형태는 다음과 같다.

Workspace(
    name: "",
    projects: []
)

Resources, Sources, Tests

각 프로젝트 폴더 하위에 Resources, Sources, Tests 폴더를 생성한다.
각각 리소스, 소스코드, 테스트 코드를 담을 폴더로 명칭은 Targets 생성할때 참조하는 경로에 따라 변경할 수 있다.

다음과 같이 프로젝트에 필요한 정보들을 각 폴더에 추가한다.

xcodeproj 생성

다음 명령어를 통해 위에 구현해둔 정보를 기반으로 Xcode 프로젝트를 생성한다

tuist generate

각 프로젝트에 필요한 정보를 담은 Derived 폴더와 xcworkspace, xcodeproj 파일이 생성된다.

결과

전체 코드는 깃허브에 있습니다.

profile
IOS Developer

0개의 댓글