관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] 4. 모듈화 개념 - Tuist로 프로젝트 관리 방법 본문

iOS 응용 (swift)

[iOS - swift] 4. 모듈화 개념 - Tuist로 프로젝트 관리 방법

jake-kim 2022. 6. 15. 01:06

Tuist로 모듈화 최신 포스팅 글 목록 > https://ios-development.tistory.com/1303


1. 모듈화 개념 - Library vs Framework (static library, dynamic library, static framework, dynamic framework)

2. 모듈화 개념 - Binary File 개념 (Mach-O, CPU Architectures, Universal binary, lipo command)

3. 모듈화 개념 - XCFramework 생성, 사용 방법

4. 모듈화 개념 - Tuist로 프로젝트 관리 방법

 

cf) tuist로 모듈화하는 더 구체적인 방법은 tuist로 모듈화 하기 포스팅 글 참고

 

* tuist를 사용하기전에 알아야하는 Xcode의 target, project, workspace 차이의 개념 먼저 이 포스팅에서 먼저 확인

Tuist로 만든 workspace, project 구조

모듈화의 이점

  • 빌드 속도 향상
    • 모듈화로 나누어져 있으면, 빌드 시 변경된 부분만 빌드
  • 모듈간 결합도는 낮추고, 응집도를 높이는 형태
  • .pbxproj에 UUID의 conflict를 줄일 수 있는 장점

Tuist

  • Xcode 프로젝트의 구조를 관리하는 커멘드 라인 툴
  • 비슷한 툴은 XcodeGen이 있지만, XcodeGen은 .yml파일로 관리를 하고 tuist는 swift언어로 프로젝트 관리를 하므로 tuist 사용을 추천
  • 모듈화에 앞서서 Tuist로 프로젝트의 구성을 Tuist로 관리해놓으면 모듈 구조 역시도 관리하는데 편리하여, 모듈화에 Tuist 사용할것

Tuist를 안써서 관리할 경우 예시

  • 모듈화를 Target단위로 관리하는 프로젝트라고 가정
  • Target에 Utils와 CommonUI를 가지고 있는 프로젝트로 구성하기 위해 아래 Target에서 +버튼을 클릭하여 Framework 생성

  • 생성된것을 확인

  • 모듈화 적용 전 문제점
    • Utils, CommonUI가 한 프로젝트의 Target으로 관리되고 있으므로, Utils와 CommonUI를 프로젝트로 빼고 마치, pod install하면 생겨나는 Pods 프로젝트처럼 변경할 것
    • 타겟이 아닌 프로젝트로 관리하게 되면, MyApp이라는 프로젝트 안에서 나와 독립적인 프로젝트로 만드는 것 (결합도는 낮추고 응집도는 높이기)
  • 기대 효과
    • 빌드 속도 향상
    • .pbxproj 를 별도로 쓰기 때문에 협업 간 conflict 감소

Tuist 사용하여 모듈화하기

  • Tuist 설치
curl -Ls https://install.tuist.io | bash
  • Tuist로 iOS 프로젝트 생성
// cd root_project
mkdir MyApp
cd MyApp
tuist init --platform ios

생성된 4개의 파일

  • tree 명령어로 tree가 설치되어 있지 않다면 설치 brew install tree
tree .
  • 자동으로 예제 파일이 생성 (아래 tuist edit에서 삭제할것)
    • Plugins
    • Targets아래에 MyAppKit, MyAppUI
.
├── Plugins
│   └── MyApp
│       ├── Package.swift
│       ├── Plugin.swift
│       ├── ProjectDescriptionHelpers
│       │   └── LocalHelper.swift
│       └── Sources
│           └── tuist-my-cli
│               └── main.swift
├── Project.swift
├── Targets
│   ├── MyApp
│   │   ├── Resources
│   │   │   └── LaunchScreen.storyboard
│   │   ├── Sources
│   │   │   └── AppDelegate.swift
│   │   └── Tests
│   │       └── AppTests.swift
│   ├── MyAppKit
│   │   ├── Sources
│   │   │   └── MyAppKit.swift
│   │   └── Tests
│   │       └── MyAppKitTests.swift
│   └── MyAppUI
│       ├── Sources
│       │   └── MyAppUI.swift
│       └── Tests
│           └── MyAppUITests.swift
└── Tuist
    ├── Config.swift
    └── ProjectDescriptionHelpers
        └── Project+Templates.swift

18 directories, 14 files
  • Project.swift를 수정하는 명령어 Tuist Edit 실행
tuist edit

tuist edit 명령어 실행 시 Manifests 프로젝트가 오픈

  • Project.swift 파일 확인
    • 친절하게 예제 그림까지 존재
    • App이 있으면 Kit과 UI를 분리해둔 상태
import ProjectDescription
import ProjectDescriptionHelpers
import MyPlugin

/*
                +-------------+
                |             |
                |     App     | Contains Structure App target and Structure unit-test target
                |             |
         +------+-------------+-------+
         |         depends on         |
         |                            |
 +----v-----+                   +-----v-----+
 |          |                   |           |
 |   Kit    |                   |     UI    |   Two independent frameworks to share code and start modularising your app
 |          |                   |           |
 +----------+                   +-----------+

 */

// MARK: - Project

// Local plugin loaded
let localHelper = LocalHelper(name: "MyPlugin")

// Creates our project using a helper function defined in ProjectDescriptionHelpers
let project = Project.app(name: "MyApp",
                          platform: .iOS,
                          additionalTargets: ["MyAppKit", "MyAppUI"])
  • Project.app은 extension으로 예제로 만들어놓은 함수
    • 아래에서 이 함수를 참고하여 내가 원하는 프로젝트 구조로 만들것
// Project+Templates

extension Project {
    /// Helper function to create the Project for this ExampleApp
    public static func app(name: String, platform: Platform, additionalTargets: [String]) -> Project {
        var targets = makeAppTargets(name: name,
        ...
  • Project.swift 파일을 원하는 환경으로 변경
import ProjectDescription

let projectName = "MyApp"
let bundleID = "com.jake.sample.MyApp"
let iOSTargetVersion = "13.0"

let project = Project(
  name: projectName,
  organizationName: "jake",
  packages: [], // SPM 사용 시 입력 ".package(url: "https://github.com/ReactiveX/RxSwift.git", from: "6.5.0")"
  settings: nil // configuration으로 관리할 경우 아래처럼 path정보 입력
//  settings: .settings(configurations: [
//    .debug(name: "Debug", xcconfig: .relativeToRoot("Confiugrations/\(projectName)-Debog.xcconfg")),
//    .debug(name: "Release", xcconfig: "Confiugrations/\(projectName)-Release.xcconfg"),
//  ]),
  ,
  targets: [
    Target(name: projectName,
           platform: .iOS,
           product: .staticFramework, // Static Framework, Dynamic Framework
           bundleId: bundleID,
           deploymentTarget: .iOS(targetVersion: iOSTargetVersion, devices: [.iphone, .ipad]),
           infoPlist: .default,
           sources: ["Targets/\(projectName)/Sources/**"],
           resources: [],
           dependencies: [] // tuist generate할 경우 pod install이 자동으로 실행
          )
  ],
  schemes: [
    Scheme(name: "\(projectName)-Debug"),
    Scheme(name: "\(projectName)-Release")
  ],
  fileHeaderTemplate: nil,
  additionalFiles: [],
  resourceSynthesizers: []
)
  • Terminal에서 편집 종료 Ctrl + C를 입력

  • 아래 명령어를 실행하여 Project.swift내용 적용
tuist generate
  • 아래와 같은 오류 발생할 경우 --verbose를 이용하여 디버깅
// 오류
Manifest not found at path /Users/...

// 디버깅
$ tuist generate --verbose
  •  결과
    • 자동으로 프로젝트 생성
    • import MyAppKit, import MyAppUI는 예제로 생겨난 것 (삭제할것)

  • 프로젝트 구조를 보면 MyApp하위에 MyApp Framework가 존재하는 형태

  • Scheme을 보면 명시해준대로 MyApp-Debug, MyApp-Release가 생성된 것을 확인

Workspace 추가하여 다수의 Project 관리 방법

  • Workspace가 있고, Projects 하위에 다수의 Project(MyApp, MyFramework)가 존재하는 형태

완성

  • Tuist로 구조를 잡다가 보면 휴먼 에러를 일으킬 수 있는 부분이 여러곳 존재하기 때문에(tuist generate 오류), 순서대로 따라서 진행
  • 예제로 사용할 프로젝트 초기화
mkdir Mine
cd Mine
tuist init --platform ios
tuist edit

(앞으로 파일 생성할때 Targets을 설정하는게 나오는데 이 부분은 default로 그냥 두어도 자동으로 알맞게 지정되니 바꾸지 않고 디폴트로 둘것)

  • Manifests하위에 Projects폴더를 놓아서 관리하고, Tuist 폴더 하위에는 Tuist 관련 템플릿이나 관련 정보만 사용
    • 기존에는 위 사진처럼 Project하나만 두고 Project가 최상위가 되도록 정의되어 있지만, Workspace를 추가하여 관리
    • Workspace는 Manifests 바로 하위에 (= Projects 폴더랑 같은 레벨) 생성

생성된 5개의 폴더 및 swift파일

  • 여기서 각 프로젝트 MyApp, MyFramework의 하위에서 Sources라는 폴더를 또 만들기
    • 이 부분은 편집이 종료되고 나서 다시 tuist edit하면 Xcode navigator에서 사라져 있으므로 주의할것

  • Tests 폴더도 추가 (이 폴더도 Sources 폴더와 마찬가지로, 해당 편집을 종료하면 navigator에서는 안보이므로 주의)

편집 모드를 종료하면 위처럼 Sources 폴더가 사라짐

  • Project+Templates 코드를 아래처럼 변경
import ProjectDescription

extension Project {
  public static func app(
    name: String,
    platform: Platform,
    bundleID: String,
    dependencies: [TargetDependency] = []
  ) -> Project {
    return self.project(
      name: name,
      product: .app,
      bundleID: bundleID,
      platform: platform,
      dependencies: dependencies,
      infoPlist: [
        "CFBundleShortVersionString": "1.0",
        "CFBundleVersion": "1"
      ]
    )
  }
  
  public static func framework(
    name: String,
    platform: Platform,
    bundleID: String,
    dependencies: [TargetDependency] = []
  ) -> Project {
    return self.project(
      name: name,
      product: .framework,
      bundleID: bundleID,
      platform: platform,
      dependencies: dependencies
    )
  }
  
  public static func project(
    name: String,
    product: Product,
    bundleID: String,
    platform: Platform,
    dependencies: [TargetDependency] = [],
    infoPlist: [String: InfoPlist.Value] = [:]
  ) -> Project {
    return Project(
      name: name,
      targets: [
        Target(
          name: name,
          platform: platform,
          product: product,
          bundleId: bundleID,
          infoPlist: .extendingDefault(with: infoPlist),
          sources: ["Sources/**"],
          resources: [],
          dependencies: dependencies
        ),
        Target(
          name: "\(name)Tests",
          platform: platform,
          product: .unitTests,
          bundleId: bundleID,
          infoPlist: .default,
          sources: "Tests/**",
          dependencies: [
            .target(name: "\(name)")
          ]
        )
      ]
    )
  }
}
  • 빌드 > 성공 > 터미널에서 CTRL + C를 눌러 편집 종료 후 generate
tuist generate

완성

cf) Tuist를 사용하면 Dependency가 Circular형태면 사전에 generate 오류가 발생하기 때문에 dependency 관리도 용이 

cf) 아래 helper메소드에서 Project 인자에 Scheme을 넣어서 Scheme 관리도 가능

public static func project(
    name: String,
    product: Product,
    bundleID: String,
    platform: Platform,
    dependencies: [TargetDependency] = [],
    infoPlist: [String: InfoPlist.Value] = [:]
  ) -> Project {
    return Project(
      name: name,
      targets: [
        Target(
          name: name,
          platform: platform,
          product: product,
          bundleId: bundleID,
          infoPlist: .extendingDefault(with: infoPlist),
          sources: ["Sources/**"],
          resources: [],
          dependencies: dependencies
        ),
        Target(
          name: "\(name)Tests",
          platform: platform,
          product: .unitTests,
          bundleId: bundleID,
          infoPlist: .default,
          sources: "Tests/**",
          dependencies: [
            .target(name: "\(name)")
          ]
        )
      ],
      schemes: [
        Scheme(
          name: "\(name)-Debug",
          shared: true,
          buildAction: BuildAction(targets: ["\(name)"])
        ),
        Scheme(
          name: "\(name)-Release",
          shared: true,
          buildAction: BuildAction(targets: ["\(name)"])
        )
      ]
    )
  }

* 전체 코드: https://github.com/JK0369/ExTuist

 

* 참고

https://minsone.github.io/mac/ios/ios-project-generate-with-tuist-1

https://docs.tuist.io/manifests/project

https://developer.apple.com/library/archive/featuredarticles/XcodeConcepts/Concept-Targets.html

https://tuist.io/

Comments