[iOS] Tuist를 정복해보자!!

목차


  1. Tuist란? 설치해보자
  2. Static/Dynamic Framework란?
  3. ✅ Tuist를 정복해보자!!

1. Dependencies

프로젝트가 성장함에 따라 여러 타겟으로 분리되고 복잡한 의존성 그래프가 형성됩니다.
이러한 상황에서 명시적이고 정적인 의존성 그래프를 지향하여 쉽게 검증하고 최적화할 수 있도록 돕습니다.


  • Local dependencies
    • 다양한 유형의 로컬 의존성을 지원합니다.
      • Target, Project, Framework, Library, XCFramework, SDK, XCTest 등을 의존성으로 설정할 수 있습니다.
      • 또한 조건부 의존성 설정을 통해 특정 상황에서만 의존성이 적용되도록 할 수 있습니다.
    • ONEstore에서는 Tuist/ProjectDescriptionHelpers 디렉토리에 Dependencies+Extension.swift 파일을 추가하여 관리하고 있습니다.
      import ProjectDescription
      
      extension TargetDependency {
         public enum OnestoreProject {
            public enum AppCore { fileprivate static let GROUP_NAME = "AppCore" }
            public enum Network { fileprivate static let GROUP_NAME = "Network" }
         }
         
         private static func moduleProject(groupName:String, _ name:String) -> TargetDependency {
            return TargetDependency.project(target: name, 
                                             path: .relativeToRoot("\(Project.rootDirectory)/\(groupName)/\(name)"), 
                                             condition: .none)
         }
      }
      
      public extension TargetDependency.OnestoreProject.AppCore {
            /**
            원스토어 앱 코어
            - dependency:
               .external(name: "KeychainAccess", condition: .none),
               .OnestoreProject.Network.NetworkApi
            */
            static let ONEstoreCore = TargetDependency.moduleProject(groupName: Self.GROUP_NAME, "ONEstoreCore")
            
            /**
            원스토어 앱 네비게이션 Interface
            - dependency:
               .OnestoreProject.Network.NetworkModel,
               .external(name: "Factory", condition: .none)
            */
            static let ONEstoreNavigation = TargetDependency.moduleProject(groupName: Self.GROUP_NAME, "ONEstoreNavigation")
      }
      
      public extension TargetDependency.OnestoreProject.Network {
            
            /**
            네트워크 모듈의 코어
            - dependency:
               .external(name: "CryptoSwift", condition: .none),
               .external(name: "Factory", condition: .none),
            */
            static let NetworkCore = TargetDependency.moduleProject(groupName: Self.GROUP_NAME, "NetworkCore")
            
            /**
            Network Api의 Response 모델 구성
            - dependency: 없음
            */
            static let NetworkModel = TargetDependency.moduleProject(groupName: Self.GROUP_NAME, "NetworkModel")
            
            /**
            Network Api
            - dependency:
               .OnestoreProject.Network.NetworkModel,
            */
            static let NetworkApi = TargetDependency.moduleProject(groupName: Self.GROUP_NAME, "NetworkApi")
      }
      
    • 아래는 로컬 의존성을 가진 NetworkApi Framework의 Project.swift 파일의 내용입니다.
      import ProjectDescription
      import ProjectDescriptionHelpers
      
      /// Project 생성
      /// - Parameters:
      ///   - name: Framework의 이름을 지정합니다.
      ///   - sources: 소스 코드 파일의 위치를 지정합니다. 
      ///              Sources/**는 Sources 디렉토리 내의 모든 파일을 포함합니다.
      ///   - dependencies: 타겟의 의존성을 설정합니다.
      ///   - environmentVariables: 환경변수를 설정합니다.
      let project = Project(
         name: "NetworkApi",
         targets: [
               .target(
                  name: "NetworkApi",
                  destinations: .iOS,
                  product: .framework,
                  bundleId: "com.onestore.NetworkApi",
                  deploymentTargets: .iOS("17.4"),
                  infoPlist: .file(path: "NetworkApi.plist"),
                  sources: ["Sources/**"],
                  dependencies: [
                     .OnestoreProject.Network.NetworkCore,
                     .OnestoreProject.Network.CCSModel,
                  ],
                  environmentVariables: [
                     "IDEPreferLogStreaming" : "YES",
                     "IDELogRedirectionPolicy" : "oslogToStdio",
                     "OS_ACTIVITY_MODE" : "disable",
                  ]
               ),
         ]
      )
      

  • External dependencies
    • Swift Package Manager(SPM)를 통해 의존성 관리를 합니다.
    • 다음 코드를 추가하여 Tuist 디렉토리의 Package.swift 파일 내에서 외부 의존성을 선언합니다.
      import PackageDescription
      
      /// 패키지의 product type을 변경하여 static/dynamic framework으로 설정하여 패키지 간의 빌드 설정 충돌을 해결할 수 있습니다.
      /// 외부 의존성 관리를 더욱 유연하게 만들어 프로젝트 통합 과정에서 발생할 수 있는 여러 문제들을 해결할 수 있습니다.
      let packageSettings = PackageSettings(
         productTypes: [
               "Factory" : .framework,
               "FBLPromises" : .framework,
               "GoogleUtilities-Environment" : .framework,
               "third-party-IsAppEncrypted" : .framework,
               "GoogleUtilities-Logger" : .framework,
               "GoogleUtilities-UserDefaults" : .framework,
         ]
      )
      
      let package = Package(
         name: "ONEstore",
         dependencies: [
            .package(url: "https://github.com/google/GoogleSignIn-iOS.git", from: "8.0.0"),
            .package(url: "https://github.com/firebase/firebase-ios-sdk.git", from: "11.1.0"),
            .package(url: "https://github.com/hmlongco/Factory.git", from: "2.3.2"),
            .package(url: "https://github.com/Kitura/Swift-JWT.git", from: "4.0.0"),
            .package(url: "https://github.com/google/GoogleUtilities.git", from: "8.0.2"),
            
            .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.8.3"),
            .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", from: "4.2.2"),
            .package(url: "https://github.com/airbnb/lottie-ios.git", from: "4.5.0"),
         ]
      )
      
    • Package.swift 파일은 외부 의존성을 선언하는 인터페이스입니다.

  • 설정을 마쳤으면 다음 명령어로 의존성을 해결하고 프로젝트에 가져옵니다.
    $ tuist install
    
  • 이 방식을 통해 프로젝트의 의존성 관리에 대한 더 많은 제어력과 유연성을 얻을 수 있습니다.

2. Workspace

Tuist에서 Workspace는 여러 프로젝트를 관리하고 구성하는 데 사용됩니다.
Workspace 파일을 통해 워크스페이스를 정의할 수 있습니다.


  • ONEstoreGlobal Project의 Workspace.swift 파일 내용입니다.
    import ProjectDescription
    import ProjectDescriptionHelpers
    
    /// Workspace 생성
    /// - Parameters:
    ///   - name: 워크스페이스의 이름을 지정합니다.
    ///           이 이름으로 .xcworkspace 파일이 생성됩니다.
    ///   - projects: 워크스페이스에 포함될 프로젝트들의 경로를 배열 혹은 최상위 루트로 지정합니다.
    ///               배열의 경로나 하위 디렉토리에 있는 Project.swift파일을 인식합니다.
    ///   - fileHeaderTemplate: 새로 생성되는 파일의 헤더에 들어갈 내용을 지정합니다.
    ///                         이 옵션을 사용하여 저작권 정보나 라이선스 문구를 자동으로 추가할 수 있습니다
    let workspace = Workspace(
       name: "ONEstore Workspace",
       projects: [ "Projects/**" ],
       fileHeaderTemplate: Workspace.fileHeaderTemplate
    )
    

  • 위 설정을 토대로 워크스페이스의 기본 구조는 다음과 같습니다.
    ONEstore/ // 루트 디렉토리
    ├── Workspace.swift
    ├── Projects/
    │   ├── A/
    │   │   └── Project.swift  
    │   ├── B/
    │   │   └── Project.swift  
    │   └── ...
    └── Tuist/
       ├── ProjectDescriptionHelpers/
       │   ├── Dependencies+Extension.swift
       │   └── Workspace+Extension.swift
       └── Package.swift
    

  • fileHeaderTemplate 내용이 추가된 Workspace+Extension.swift 파일 내용입니다.
    import ProjectDescription
    
    public extension Workspace {
    
       /// 새로운 파일을 생성할 때 자동으로 추가되는 주석이나 코드를 커스터마이즈하는데 사용됩니다.
       /// https://help.apple.com/xcode/mac/11.4/#/dev7fe737ce0 참고
       static let fileHeaderTemplate:FileHeaderTemplate = 
          .string(
                """
                Created ___DATE___
                /*------------------------------------------------------------------------------
                * PROJECT : ___PROJECTNAME___
                * NAME    : ___FILEBASENAME___
                * DESC    :
                * AUTHOR  : ___FULLUSERNAME___
                * Copyright ___YEAR___ ONEstore All rights reserved
                *------------------------------------------------------------------------------*/
                """
          )
    }
    
  • Workspace.swift 파일은 보통 프로젝트의 루트 디렉토리에 위치합니다.
  • 이 파일을 통해 여러 프로젝트를 하나의 워크스페이스로 묶어 관리할 수 있습니다.
  • Tuist는 이 설정을 바탕으로 .xcworkspace 파일을 생성합니다.
  • 이렇게 구성된 워크스페이스를 통해 여러 모듈이나 프로젝트를 효율적으로 관리하고, 의존성을 쉽게 설정할 수 있습니다.

3. Project

  • 프로젝트의 기본 구조는 다음과 같습니다.
    ONEstore/Projects/ONEstoreApp/ // 프로젝트 디렉토리
                      ├── Project.swift
                      ├── Sources/
                      │   ├── .swift
                      │   └── AppDelegate.swift
                      └── Resources/
                         ├── .assets
                         └── .png
    

  • Project.swift 파일은 프로젝트의 설정을 정의합니다.
    ONEstoreGlobal App의 Project.swift 파일 내용입니다.
    import ProjectDescription
    
    let baseDependencies:[TargetDependency] = [
       // Pakcage.swift 에 추가된 외부 의존성
       .external(name: "FirebaseAnalytics", condition: .none), 
       .external(name: "FirebaseMessaging", condition: .none),
       .external(name: "FirebaseCrashlytics", condition: .none),
       .external(name: "SwiftJWT", condition: .none),
       .external(name: "GoogleSignIn", condition: .none)
    
       // Dependencies+Extension 에서 관리되고 있는 로컬 의존성
       .OnestoreProject.AppCore.ONEstoreCore,
       .OnestoreProject.AppCore.NetworkApi
    ]
    
    /// Project 생성
    /// - Parameters:
    ///   - name: 프로젝트의 이름을 지정합니다.
    ///   - options: 프로젝트 옵션을 설정합니다.
    ///              여기서는 자동 스키마 생성을 비활성화했습니다.
    ///   - targets: 프로젝트의 타겟들을 정의합니다.
    ///              각 타겟은 이름, 플랫폼, 제품 유형, 번들 ID, Info.plist 설정, 소스 파일 경로, 리소스 파일 경로, 의존성 등을 지정합니다.
    ///   - sources: 소스 코드 파일의 위치를 지정합니다. 
    ///              Sources/**는 Sources 디렉토리 내의 모든 파일을 포함합니다.
    ///   - resources: 리소스 파일의 위치를 지정합니다.
    ///                Resources/**는 Resources 디렉토리 내의 모든 파일을 포함합니다.
    ///   - dependencies: 타겟의 의존성을 설정합니다.
    let project = Project(
       name: "ONEstore",
       options: .options(
          automaticSchemesOptions: .disabled
       ),
       targets: [
          Target(
                name: "ONEstore_QA",
                platform: .iOS,
                product: .app,
                bundleId: "com.onestore.ios.qa",
                infoPlist: .default,
                sources: ["Sources/"],
                resources: ["Resources/"],
                dependencies: baseDependencies
          ),
          Target(
                name: "ONEstore_Release",
                platform: .iOS,
                product: .app,
                bundleId: "com.onestore.ios",
                infoPlist: .default,
                sources: ["Sources/"],
                resources: ["Resources/"],
                dependencies: baseDependencies
          )
       ]
    )
    
    • 소스 파일과 리소스 파일은 지정된 디렉토리(Sources, Resources)에 올바르게 위치해야 합니다.
    • 필요에 따라 추가적인 타겟(예: 프레임워크, 익스텐션 등)을 정의할 수 있습니다.
    • infoPlist 설정을 통해 커스텀 Info.plist 파일을 사용하거나 동적으로 생성할 수 있습니다.

    • Tuist는 이 설정을 바탕으로 Project는 .xcodeproj을 생성합니다.

프로젝트 설정이 완료되면, 루트 디렉토리에서 다음 명령어를 실행하여 Xcode 프로젝트를 생성합니다.

$ tuist generate

Workspace.swift 파일과 Project.swift 파일들을 기반으로 Xcode 워크스페이스를 생성하여 실행합니다.

마치며

  • iOS 개발 환경에서 프로젝트 관리와 의존성 처리는 종종 복잡하고 까다로운 과제가 됩니다.
    이러한 상황에서 Tuist는 개발자들에게 강력하고 유연한 해결책을 제시합니다.
  • Tuist의 XcodeProj 기반 통합 방식은 Swift 패키지 관리에 새로운 차원의 제어력을 제공하며, 특히 중대형 프로젝트에서 그 진가를 발휘합니다.
  • 로컬 의존성, 외부 의존성 세부 설정을 정밀하게 조정할 수 있어, 프로젝트의 특정 요구사항에 맞춤 대응이 가능해집니다.
    이러한 기능들은 단순히 의존성 문제를 해결하는 것을 넘어, 프로젝트의 전반적인 구조와 워크플로우를 개선하는 데 기여합니다.

결과적으로 Tuist는 iOS 개발자들에게 더 효율적이고 안정적인 프로젝트 관리 경험을 제공하며, 복잡한 개발 과정을 보다 순조롭게 만들어줍니다.