ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Tuist v4 설치 및 사용 (with. 자동화)
    Tuist 2024. 7. 1. 18:52

    안녕하세요. 그린입니다 🍏

    이번 포스팅에서는 Tuist v4을 기준으로 설치하고 간단히 사용하는 방법에 대해 학습해보겠습니다 🙋🏻

     

     


    Tuist v4

    프로젝트를 관리하며 모듈화에 도움을 주는 우리 익숙한 Tuist가 3 버전대에서 4 버전대로 오면서 변화가 생겼죠.

    로고가 바뀌었을뿐만 아니라, 기존 curl이나 homebrew등을 이용해 설치하였던 Tuist가 이제는 mise를 이용해 설치하라고 권장하고 생겨났습니다😃

     

    물론, 아직까지도 homebrew 및 다양한 방법으로 설치할 수 있지만 mise를 활용하라고 권장하네요.

    mise는 마이스? 마이즈로 읽는줄 알았는데 미즈라고 읽더라구요 ㅎㅎ

     

    뿐만 아니라, 프로젝트 생성을 위한 의존성들을 관리하는것들과 타겟 및 프로젝트 생성을 위해 작성하던 코드들의 일부분도 수정이 되었습니다.

    3 버전대를 잘 사용하고 계셨던 분들이라면 4 버전대로 어렵지 않게 마이그레이션을 할 수 있을 정도로 어렵지 않습니다.

     

    그래서 오늘 해볼 학습은 Tuist 4 버전대를 기준으로 처음부터 설치하는 방법 그리고 실제 프로젝트 구성을 위해 Tuist를 사용해보는것을 톺아보겠습니다.

     


    설치하기

    우선 앞서 말했지만, Tuist v4는 mise로 설치하는것을 권장하기에 우리도 이 mise로 설치해볼께요!

     

    만약 여러분들이 기존 Tuist v3을 사용하고 계셨다면, 충돌이 날 수 있으니 아래와 같이 터미널에서 명령어를 입력하여 기존 Tuist 삭제부터 진행하는것이 좋습니다.

     

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

     

    이제 mise부터 설치해야겠죠?

     

    아래 명령어를 통해 mise를 설치합니다.

     

    $ curl https://mise.run | sh

     

    curl을 이용하죠.

     

    이렇게 mise를 최신 버전으로 설치할 수도 있고, 아래와 같이 특정 버전으로 설치할 수도 있습니다.

     

    $ curl https://mise.run | MISE_VERSION=v2024.5.16 sh

     

    그럼, mise가 정상적으로 실행되었다면 아래 경로에서 mise 버전을 체크해보면 나오는것을 확인할 수 있어요!

     

    $ ~/.local/bin/mise --version
    mise 2024.6.6

     

    mise는 설치만 하면 되는것이 아니라 실제로 각자의 환경에서 mise를 활성화 시켜줘야 합니다.

     

    각자의 터미널에 맞는 명령어를 입력해주십다!

     

    echo 'eval "$(~/.local/bin/mise activate bash)"' >> ~/.bashrc
    echo 'eval "$(~/.local/bin/mise activate zsh)"' >> ~/.zshrc
    echo '~/.local/bin/mise activate fish | source' >> ~/.config/fish/config.fish

     

    그럼 이제 mise 설치 및 활성화는 끝났으며, Tuist를 본격적으로 설치해볼 차례에요.

     

    Tuist를 설치하는 방법은 아주 간단합니다.

     

    최신 버전부터 특정 버전까지 원하는것을 입맛대로 설치할 수 있어요.

     

    mise install tuist            # Install the current version specified in .tool-versions/.mise.toml
    mise install tuist@x.y.z      # Install a specific version number
    mise install tuist@3          # Install a fuzzy version number

     

    mise로 Tuist를 설치하고 mise로 해당 버전을 use를 통해 활성화 시켜줍니다.

     

    mise use tuist@x.y.z          # Use tuist-x.y.z in the current project
    mise use tuist@latest         # Use the latest tuist in the current directory
    mise use -g tuist@x.y.z       # Use tuist-x.y.z as the global default
    mise use -g tuist@system      # Use the system's tuist as the global default

     

    즉 쉽게 말해, install로 버전 설치를 하고 use로 활성화 시켜준다라고 보면 됩니다.

     

    여기서, mise install tuist를 하게 되면 .mise.toml에 들어있는 지정된 버전을 설치한다고 하죠?

    즉, 이전 v3에서는 .tuist-version으로 같이 작업하는 사람들의 tuist 버전을 맞추기 위해 공통적인 버전 관리로 사용했는데 이제 v4에서는 지원하지 않습니다.

    그렇기에 mise에서 해당 파일이 그 기능을 해준다고 생각하면 됩니다!

    이건 실제로 어떻게 사용되는지는 아래에서 다시 같이 보시죠 ☺️

     

    결국 이렇게 mise를 통해 tuist가 설치되고 활성화 되었습니다.

     

    현재 우리 Tuist가 어떤 버전을 사용하고 싶은지 확인하고자 한다면 아래 명령어로 확인해볼 수 있습니다.

     

    tuist version
    // 4.13.0

     

    설치까지는 아주 간단하죠?

     

    근데 저는 이 일련의 설치 및 활성화 과정을 스크립트 자동화를 시켜놓았어요.

     


    bootstrap.sh

    // bootstrap.sh
    
    set -e
    
    echo "=================================================================="
    echo "Bootstrap Script"
    echo "=================================================================="
    
    ...
    해당 부분에 다양한 tool 설치 및 환경 설정 명령어가 존재합니다.
    ...
    
    # mise 설치
    if ! command -v mise &> /dev/null; then
      echo "\n[7] > Installing mise ...\n"
      curl https://mise.run | sh
    else
      echo "mise is already installed."
    fi
    
    # mise 활성화
    echo "\n[8] > Activating mise ...\n"
    if ! grep -q 'eval "$(~/.local/bin/mise activate zsh)"' ~/.zshrc; then
      echo 'eval "$(~/.local/bin/mise activate zsh)"' >> ~/.zshrc
    else
      echo "mise is already activated in ~/.zshrc"
    fi
    
    echo "\n---------------------------------"
    echo "::: Bootstrap Script Finished :::"
    echo "---------------------------------\n"

     

    먼저 이렇게 bootstrap 쉘 파일을 만들었습니다.

    여기서 mise를 설치하고 활성화 해줍니다.

    물론, 당연히도 bootstrap 파일에서 해당 mise만 설정하는것이 아니라 다른 fastlane 및 필요한 도구 설치 및 각종 초기 환경도 설정해요.

    (필요없는 부분은 생략하고 보여드렸습니다.)

     

    그리고, 이제 초기 설정을 했으면, 실제로 프로젝트 tuist를 통해 프로젝트를 구성하는 실행 파일을 구성할 수 있어요.

     


    generate.sh

    // generate.sh
    
    #!/bin/sh
    
    set -e
    
    echo "=================================================================="
    echo "Generate Script"
    echo "=================================================================="
    
    EXECUTION_DIR=${PWD}
    echo "- Executed From: ${EXECUTION_DIR}"
    
    SCRIPT_PATH=$(readlink -f "$0")
    echo "- Script Path: ${SCRIPT_PATH}"
    
    SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
    echo "- Script Directory: ${SCRIPT_DIR}"
    
    WORKSPACE_DIR="$(dirname "$(dirname "${SCRIPT_DIR}")")"
    echo "- Workspace Directory: ${WORKSPACE_DIR}"
    
    PROJECT_DIR_NAME="Projects"
    PROJECT_NAME="App"
    PROJECT_DIR="${WORKSPACE_DIR}/${PROJECT_DIR_NAME}/${PROJECT_NAME}"
    echo "- Project Directory: ${PROJECT_DIR}"
    echo "------------------------------------------------------------------"
    
    echo "\n[1] > mise install and use tuist ...\n"
    mise install tuist
    mise use tuist@4.13.0
    
    # Tuist install 실행
    echo "\n[2] > Installing Tuist ...\n"
    tuist install --path "${WORKSPACE_DIR}"
    
    # Tuist generate 실행 및 프로젝트 open
    echo "\n[3] > Generating Tuist ...\n"
    if [ "$1" = "--no-open" ]; then
        TUIST_ROOT_DIR=$PWD tuist generate --no-open
    else
        TUIST_ROOT_DIR=$PWD tuist generate
    fi
    
    echo "\n----------------------------------"
    echo "::: Generate Script Finished :::"
    echo "----------------------------------\n"

     

    이렇게 각 워크스페이스나 스크립트 경로 및 각종 상수 값을 설정하여 mise를 통해 tuist를 설치하고 활성화 합니다.

    그게 첫번째 단계에요.

     

    그 다음으로 tuist install을 통해 지정된 경로에 SPM 종속성을 설치합니다.

     

    그리고, tuist generate를 통해 우리의 프로젝트를 생성하면 끝이죠!

     

    여기서 mise install 시, 앞서 mise.toml 파일의 버전을 사용하여 설치한다고 했습니다.

     

    .mise.toml 파일은 해당 프로젝트 디렉토리의 최상위에 위치하여 저는 설정해뒀어요.

     

    // .mise.toml
    
    [tools]
    tuist = "4.13.0"

     

    이렇게 각자 팀원이 4.13.0 특정 버전으로 설치하고 활성화를 통일할 수 있죠.

     

    이제, 마지막으로 해당 쉘 스크립트를 MakeFile을 생성하여 스크립트 실행을 쉽게 할 수 있도록 넣어두면 됩니다.

    물론, MakeFile의 생성없이 해당 쉘 파일의 경로에서 sh를 이용해 직접 실행해도 됩니다.

    근데, 전 make로 하는것이 편하더라구요ㅎㅎ

     


    MakeFile

    // MakeFile
    
    .PHONY: bootstrap
    bootstrap:
      sh ./etc/script/bootstrap.sh
    
    .PHONY: generate
    generate:
      sh ./etc/script/generate.sh
    
    .PHONY: generate-no-open
    generate-no-open:
      sh ./etc/script/generate.sh --no-open

     

    이렇게 MakeFile을 구성합니다.

    이제 make bootstrap으로 초기 설치 및 설정을 해주고, 이후부터는 make generate를 사용해 Tuist를 이용한 프로젝트 생성을 해주면 됩니다.

     

    해당 프로젝트 디렉토리 최상위 경로에서요 😃

     

    그럼 이제 실제로 Tuist를 통해 프로젝트를 만들어 볼까요?

     


    프로젝트 생성 및 설정

    Tuist로 아예 베이스부터 새로운 프로젝트를 만드는 방법부터 알아보겠습니다.

     

    우선, 여러분들이 만들 프로젝트의 디렉토리부터 만들어야 하겠죠?

     

    mkdir MyApp
    cd MyApp

     

    이렇게 디렉토리가 생성되면 해당 디렉토리로 이동하여, init 명령어를 통해 프로젝트를 생성합니다.

     

    tuist init --platform ios

     

    이렇게 되면 프로젝트가 생성되는데요.

    이전 v3에서는 기본적으로 UIKit으로 생성되었지만, v4에서는 SwiftUI로 프로젝트가 생성됩니다.

     

    이렇게 프로젝트가 생성되면 이제 각자 프로젝트에 맞게 Project 파일을 다듬고 작업하면 되겠죠?

     

    v3에서 하던것처럼요!

     

    즉, 가장 기본적인 형태부터 본다면 아래와 같습니다.

     

    import ProjectDescription
    
    let project = Project(
        name: "MyApp",
        targets: [
            .target(
                name: "MyApp",
                destinations: .iOS,
                product: .app,
                bundleId: "io.tuist.MyApp",
                infoPlist: .extendingDefault(
                    with: [
                        "UILaunchScreen": [
                            "UIColorName": "",
                            "UIImageName": "",
                        ],
                    ]
                ),
                sources: ["MyApp/Sources/**"],
                resources: ["MyApp/Resources/**"],
                dependencies: []
            ),
            .target(
                name: "MyAppTests",
                destinations: .iOS,
                product: .unitTests,
                bundleId: "io.tuist.MyAppTests",
                infoPlist: .default,
                sources: ["MyApp/Tests/**"],
                resources: [],
                dependencies: [.target(name: "MyApp")]
            ),
        ]
    )

     

    이렇게 taget을 만들고 해당 타겟들을 통해 프로젝트를 만드는것이죠.

    Project.swift 파일에서요!

     

    그럼 이건 기본 사용이고, 저는 실제로 어떻게 활용하고 있는지 한번 공유드려볼까 합니다!

     


    저는 이렇게 사용하고 있어요 🙋🏻

    우선, 저는 tuist edit을 돌려보면 아래와 같이 프로젝트를 세분화되게 모듈화를 해놨으며 Tuist에서도 v3에서 사용하던 ProjectDescriptionHelpers도 사용하고 있습니다.

     

     

    제가 알기로 v3에서는 기본으로 제공되었지만, v4에서는 ProjectDescriptionHelpers는 초기 생성 시 만들어지지 않아 필요하다면 직접 만들어 사용할 수 있더라구요.

     

    그리고, 기본적으로 tuist에서 리소스에 대한 관리 및 생성을 해주지만, 커스텀하게 사용할 경우가 있어 ResourceSynthesizers를 이용해 나만의 stencil 파일을 만들고 이를 활용하고 있습니다.

     

    전체적은 구조 자체는 이렇고, 이번 포스팅에 맥락에 맞게 Project 및 target, workspace를 보겠습니다.

     

    그전에 ProjectDescriptionHelpers에서 구축한 확장 파일들부터 볼께요!

     


    Project+extensions

    import ProjectDescription
    
    extension Project {
      public static func make(
        name: String,
        organizationName: String? = nil,
        options: [Options]? = nil,
        packages: [Package] = [],
        settings: Settings? = nil,
        targets: [Target] = [],
        schemes: [Scheme] = [],
        fileHeaderTemplate: FileHeaderTemplate? = nil,
        additionalFiles: [FileElement] = [],
        resourceSynthesizers: [ResourceSynthesizer] = .default
      ) -> Project {
        return Project(
          name: name,
          organizationName: "com.green",
          options: .options(
            automaticSchemesOptions: .disabled,
            defaultKnownRegions: ["ko"],
            developmentRegion: "ko",
            textSettings: .textSettings(usesTabs: false, indentWidth: 2, tabWidth: 2)
          ),
          packages: packages,
          settings: settings,
          targets: targets,
          schemes: schemes,
          fileHeaderTemplate: fileHeaderTemplate,
          additionalFiles: additionalFiles,
          resourceSynthesizers: resourceSynthesizers
        )
      }
    }

     

    프로젝트를 생성하기 위해 쉽게 make 메서드를 확장해놨습니다.

    프로젝트 이름부터 조직 이름, 설정, 패키지, 타겟 등등 필요한것을 파라미터로 넘겨 사용하고 나머진 기본값을 유지할 수 있죠.

     


    Target+extensions

    import Foundation
    import ProjectDescription
    
    fileprivate let commonScripts: [TargetScript] = [
      .pre(
        script: """
        ROOT_DIR=\(ProcessInfo.processInfo.environment["TUIST_ROOT_DIR"] ?? "")
        
        ${ROOT_DIR}/swiftlint --config ${ROOT_DIR}/.swiftlint.yml
        
        """,
        name: "SwiftLint",
        basedOnDependencyAnalysis: false
      )
    ]
    
    extension Target {
      public static func make(
        name: String,
        destinations: Destinations = [.iPhone],
        product: Product,
        productName: String? = nil,
        bundleId: String,
        deploymentTargets: DeploymentTargets? = nil,
        infoPlist: InfoPlist? = .default,
        sources: SourceFilesList,
        resources: ResourceFileElements? = nil,
        copyFiles: [CopyFilesAction]? = nil,
        headers: Headers? = nil,
        entitlements: Entitlements? = nil,
        scripts: [TargetScript] = [],
        dependencies: [TargetDependency] = [],
        settings: Settings? = nil,
        coreDataModels: [CoreDataModel] = [],
        environmentVariables: [String: EnvironmentVariable] = [:],
        launchArguments: [LaunchArgument] = [],
        additionalFiles: [FileElement] = [],
        buildRules: [BuildRule] = [],
        mergedBinaryType: MergedBinaryType = .disabled,
        mergeable: Bool = false
      ) -> Target {
        return .target(
          name: name,
          destinations: destinations,
          product: product,
          productName: productName,
          bundleId: bundleId,
          deploymentTargets: .iOS("17.0"),
          infoPlist: infoPlist,
          sources: sources,
          resources: resources,
          copyFiles: copyFiles,
          headers: headers,
          entitlements: entitlements,
          scripts: commonScripts + scripts,
          dependencies: dependencies,
          settings: settings,
          coreDataModels: coreDataModels,
          environmentVariables: environmentVariables,
          launchArguments: launchArguments,
          additionalFiles: additionalFiles,
          buildRules: buildRules,
          mergedBinaryType: mergedBinaryType,
          mergeable: mergeable
        )
      }
    }

     

    다음으로, 타겟을 생성하는 확장된 메서드입니다.

    commonScripts를 통해 SwiftLint 같은 공통 빌드 스크립트들을 구축하고 사용할 수 있죠.

    즉, 타겟을 만들때 조금 더 편리하게 사용할 수 있는 메서드입니다.

    대부분의 기본값이 존재하죠.

     


    TargetDependency+extensions

    import Foundation
    import ProjectDescription
    
    extension TargetDependency {
      public static func external(externalDependency: ExternalDependency) -> TargetDependency {
        return .external(name: externalDependency.rawValue)
      }
      
      public static func target(name: TargetName) -> TargetDependency {
        return .target(name: name.rawValue)
      }
      
      public static func project(target: TargetName, projectPath: ProjectPath) -> TargetDependency {
        return .project(
          target: target.rawValue,
          path: .relativeToRoot(projectPath.rawValue)
        )
      }
    }
    
    public enum ProjectPath: String {
      case core = "Projects/Core"
      case designSystem = "Projects/DesignSystem"
      case coordinator = "Projects/Features/Coordinator"
      case scene = "Projects/Features/Scene"
    }
    
    public enum TargetName: String {
      case models = "Models"
      case services = "Services"
      case common = "Common"
      case coreKit = "CoreKit"
      case designSystem = "DesignSystem"
      case appCoordinator = "AppCoordinator"
      case mainCoordinator = "MainCoordinator"
      case main = "Main"
      case kakaoLogin = "KakaoLogin"
    }
    
    public enum ExternalDependency: String {
      case get = "Get"
      case composableArchitecture = "ComposableArchitecture"
      case tcaCoordinators = "TCACoordinators"
      case nuke = "Nuke"
      case lottie = "Lottie"
      case kakaoSDK = "KakaoSDK"
    }

     

    다음으로, 타겟 의존성을 조금 더 편리하게 사용부에서 쓸 수 있도록 확장해놓은 파일입니다.

    각 프로젝트나 타겟 경로와 외부 라이브러리의 의존성 이름을 enum 케이스로 관리하여 편하게 사용할 수 있도록 만들었습니다.

     

    그럼 이제 실제 타겟과 프로젝트를 만드는 코드를 볼까요?

     

    대표적으로 하나씩만 보겠습니다.

     


    Core/Project

    import ProjectDescription
    import ProjectDescriptionHelpers
    
    let project = Project.make(
      name: "Core",
      targets: [
        .make(
          name: "CoreKit",
          product: .staticLibrary,
          bundleId: "com.green.coreKit",
          sources: ["CoreKit/**"],
          dependencies: [
            .target(name: .models),
            .target(name: .services),
            .target(name: .common)
          ]
        ),
        .make(
          name: "Models",
          product: .staticLibrary,
          bundleId: "com.green.models",
          sources: ["Models/**"],
          dependencies: []
        ),
        .make(
          name: "Services",
          product: .staticLibrary,
          bundleId: "com.green.services",
          sources: ["Services/**"],
          dependencies: [
            .external(externalDependency: .get),
            .external(externalDependency: .composableArchitecture),
            .external(externalDependency: .kakaoSDK)
          ]
        ),
        .make(
          name: "Common",
          product: .staticLibrary,
          bundleId: "com.green.common",
          sources: ["Common/**"],
          dependencies: []
        )
      ]
    )

     

    핵심 로직이 되는 Core 모듈을 보면 이렇게 각 타겟들을 나눠 생성하고 이들을 가지고 하나의 프로젝트 모듈을 만든것을 볼 수 있습니다.

     

    즉, 4개의 타겟 모듈이 하나의 Core라는 프로젝트로 생성되죠.

     

    그리고, 이는 사용하는 곳에서 아래와 같이 언제든 편하게 불러 사용할 수 있죠.

    디펜던시로 말이죠!

     


    App/Project

    import ProjectDescription
    import ProjectDescriptionHelpers
    
    let project = Project.make(
      name: "App",
      targets: [
        .make(
          name: "Prod-Green",
          product: .app,
          bundleId: "com.green",
          infoPlist: .file(path: .relativeToRoot("Projects/App/Info.plist")),
          sources: ["Sources/**"],
          resources: ["Resources/**"],
          dependencies: [
            .project(target: .coreKit, projectPath: .core),
            .external(externalDependency: .composableArchitecture)
          ],
          settings: .settings(
            base: [
              "ASSETCATALOG_COMPILER_APPICON_NAME": "ProdAppIcon"
            ],
            configurations: [
              .release(name: .release, xcconfig: "./xcconfigs/Green.release.xcconfig")
            ]
          )
        )
      ],
      additionalFiles: [
        "./xcconfigs/Green.shared.xcconfig"
      ]
    )

     

    이렇게 App 프로젝트에서 해당 coreKit 모듈을 dependencies로 관리하여 사용하죠.

     

    이런식으로 우리는 타겟부터 프로젝트까지 설정할 수 있어요.

     

    마지막으로, 워크스페이스 생성을 위해 파일을 구성해줄 수 있습니다.

     


    Workspace

    import ProjectDescription
    
    let workspace = Workspace(
      name: "Green",
      projects: [
        "Projects/App",
        "Projects/Core",
        "Projects/DesignSystem",
        "Projects/Features/Scene",
        "Projects/Features/Coordinator",
      ],
      schemes: [
        .scheme(
          name: "Prod-Green",
          buildAction: .buildAction(targets: [.project(path: "./Projects/App", target: "Prod-Green")]),
          runAction: .runAction(
            configuration: .release,
            arguments: .arguments(environmentVariables: ["IDEPreferLogStreaming": "YES"])
          ),
          archiveAction: .archiveAction(configuration: .release),
          profileAction: .profileAction(configuration: .release),
          analyzeAction: .analyzeAction(configuration: .release)
        )
      ],
      generationOptions: .options(autogeneratedWorkspaceSchemes: .disabled)
    )

     

    해당 워크스페이스를 구축할 각 프로젝트들을 담고 스킴을 맞게 적절히 설정해줌으로 최상위 워크스페이스를 생성할 수 있습니다!

     


    마무리

    생각보다 쉽게 적용해볼 수 있을것 같지 않나요!?

    v3에서 v4로 오면서 저도 어려운거 아니야? 하고 걱정이 많았는데 막상 해보니 v3보다 더 편리해졌고 나아졌더라구요.

    그리고 어렵지 않았어요.

    여러분들도 Tuist v4를 시작으로 모듈화 및 프로젝트 관리를 한번 해보면 어떨까요?

     


    레퍼런스

     

    Installation | Tuist

     

    docs.tuist.io

     

     

    GitHub - jdx/mise: dev tools, env vars, task runner

    dev tools, env vars, task runner. Contribute to jdx/mise development by creating an account on GitHub.

    github.com

Designed by Tistory.