Tuist로 Xcode 프로젝트 설정하기

Tuist로 프로젝트 설정하기!

Tuist 를 사용하게 된 계기

아카데미 내에서 프로젝트를 하는 동안, pbxproj 파일의 충돌은 계속해서 일어났다. 이것을 해결하기 위해 git attributes 파일도 작성해봤지만, 여전히 충돌은 계속해서 일어났고, develop 브랜치에 rebase 혹은 pull 을 땡기면 github 가 자동으로 충돌을 머지해주는 것 같았지만, 종종 pbxproj 파일이 깨지는 경우가 발생했고 그 때마다 머지 전 커밋으로 돌아가 conflict 를 직접 해결하거나, 아니면 눈알 빠지게 pbxproj 파일을 훑어보며 잘못 합쳐진 부분을 직접 수정해야 했다...

문제는 이런 사태로 인해 시간이 너무 많이 소요된다는 점이다. 프로젝트가 매우 작은 초기 단계에서도 이 정도인데, 계속해서 유지보수를 할 때는 얼마나 많은 시간을 소요해야 할까... 너무 스트레스 받은 나머지 swift 파일로 프로젝트를 관리하는 tuist 를 도입하기로 결정했다.

Tuist 를 사용하면서 느낀 장점

일단 pbxproj 파일을 아예 삭제해버릴 수 있으므로, 관련된 충돌이 아예 사라지게 된다. 아직까지 팀원들과 협업하면서 사용한 적은 없지만, 너무 행복할 것 같다. swift 코드로 프로젝트를 관리할 수 있으니, 자동 완성의 힘을 빌려 익숙하지 않더라도 프로젝트 설정을 쉽게 할 수 있었다.

Tuist 를 사용하면 모듈화도 쉽게 할 수 있고, 알아서 모듈 간 cyclic dependency 도 탐지해준다고는 들었으나, 아직까지 모듈화에 대해 공부할 시기는 아니라고 판단해서 계속해서 공부하면서 차차 적용해보면 될 것 같다.

Tuist 를 적용해보면서 느낀 어려운 점

일단 pbxproj 파일이 아예 사라지고, tuist 를 사용해 프로젝트를 생성해야 하므로, github action 의 호스트 컴퓨터에도 tuist 를 설치해줘야 한다. 기존에 github action 으로 테스트를 자동으로 실행하고 있었는데, 프로젝트를 tuist 로 변경헀으므로 tuist test 커맨드를 이용해 action 에서 테스트를 실행하려고 했다.

그러나 tuist test 커맨드를 실행하면 계속해서 swiftlint 관련된 에러가 발생했는데, shell script 관련된 지식이 별로 없기 때문에... Build configuration 에 대해 공부하는 계기가 되었고 tuist test 용 build configuration 을 만들어 lint 비활성화에 성공했다.

또한 github action 에서 tuist action을 사용해 test 를 실행하니까, 자꾸 에러가 발생한다. 검색해보니 tuist 3.1~~ 버전에서 계속해서 에러가 일어난다고 하는데, (https://github.com/tuist/tuist-action/issues/7) 따라서 강제로 tuist 3.0.1 버전을 지정해서 사용할 수 밖에 없었다... (tuist 버전을 강제로 고정하는 방법은 아래 링크 참조)

https://docs.tuist.io/guides/version-management/#local

infoPlist 파일 설정하기

infoPlist 파일은 카카오 API에 관련된 URL Scheme, 또한 SceneDelegate 에 관한 데이터를 포함하며, 따라서 기본 infoPlist 파일을 확장하는 방식으로 작성했다.

let infoPlist: InfoPlist = .extendingDefault(with: [
    "CFBundleURLTypes": .array([.dictionary([
        "CFBundleTypeRole": .string("Editor"),
        "CFBundleURLSchemes": .array([.string("kakaoeb92b48052cc747b19537d2ed3f9f8a2")])
    ])]),
    "LSApplicationQueriesSchemes": .array([.string("kakaokompassauth"), .string("kakaolink")]),
    "UIApplicationSceneManifest": .dictionary([
        "UIApplicationSupportsMultipleScenes": .boolean(false),
        "UISceneConfigurations": .dictionary([
            "UIWindowSceneSessionRoleApplication": .array([
                .dictionary([
                    "UISceneConfigurationName": .string("Default Configuration"),
                    "UISceneDelegateClassName": .string("$(PRODUCT_MODULE_NAME).SceneDelegate"),
                    "UISceneStoryboardFile": .string("Main")
                ])
            ])
        ])
    ])
])

AppTarget 설정하기

let appTarget: Target = Project.target(name: "Gom4ziz",
                                       bundleId: "team.gom4ziz.Ziz4gom",
                                       deploymentTarget: .iOS(targetVersion: iOSTargetVersion, devices: [.iphone]),
                                       infoPlist: infoPlist,
                                       platform: .iOS,
                                       product: .app,
                                       // 1
                                       sources: ["Gom4ziz/Source/**"],
                                       // 2
                                       resources: ["Gom4ziz/Resource/**"],
                                       entitlements: "Gom4ziz/File/Gom4ziz.entitlements",
                                       // 3
                                       scripts: [.pre(script: swiftlint, name: "SwiftLint")],
                                       // 4
                                       dependencies: [.package(product: "FirebaseAuth"),
                                                      .package(product: "FirebaseFirestore"),
                                                      .package(product: "KeyChainWrapper"),
                                                      .package(product: "FirebaseFirestoreSwift"),
                                                      .package(product: "RxSwift"),
                                                      .package(product: "RxRelay"),
                                                      .package(product: "RxCocoa"),
                                                      .package(product: "RxKakaoSDKAuth"),
                                                      .package(product: "RxKakaoSDKCommon"),
                                                      .package(product: "RxKakaoSDKShare"),
                                                      .package(product: "RxKakaoSDKUser")],
                                       // 5
                                       additionalFiles: [".swiftlint.yml"])
  1. App Product 를 생성하는 App Target 의 모든 swift 파일들은 "Gom4ziz/Source" 디렉토리 내부에 존재한다.
  2. 또한 모든 리소스 파일들(에셋, GoogleServiceInfoplist)은 "Gom4ziz/Resource" 디렉토리 내부에 존재한다.
  3. App target 은 빌드 시 swiftlint로 검사해야 하기 때문에, swiftlint run script 를 추가해준다.
  4. SPM package 의존성을 추가함
  5. lint yml 파일을 추가함

Test Target 설정하기

let testTarget: Target = Project.target(name: "Gom4zizTests",
                                        deploymentTarget: .iOS(targetVersion: iOSTargetVersion, devices: [.iphone]),
                                        platform: .iOS,
                                        product: .unitTests,
                                        sources: ["Gom4zizTests/Source/**"],
                                        resources: [],
                                        // 1
                                        dependencies: [.target(name: "Gom4ziz"),
                                                       .package(product: "RxTest"),
                                                       .package(product: "RxBlocking")])
  1. 기본적으로 test target 은 호스팅할 애플리케이션 (target) 인 Gom4ziz 에 대한 의존성을 가진다. 또한 Test 용 패키지인 RxTest 와 RxBlocking 패키지에 의존성을 가진다.

Build configuration 설정하기 (Tuist test 에서 swiftlint 에러를 해결하기 위해)

let settings: Settings = Settings.settings(configurations: [
    .debug(name: .init(stringLiteral: "Test"), settings: [
        "ENABLE_LINT": "NO"
    ]),
    .debug(name: .init(stringLiteral: "Dev"), settings: [
        "ENABLE_LINT": "YES",
    ]),
    .release(name: .init(stringLiteral: "Release"), settings: [
        "ENABLE_LINT": "NO"
    ])
])

3가지의 build configuration 을 생성했다. Dev 용으로 쓰일 Dev 세팅과, Tuist test command에서 사용할 Test 세팅, 그리고 릴리즈 용에서 사용할 release configuration. 모든 설정은 User-defined 속성인 "ENABLE_LINT" 속성을 가지고 있는데, 위 속성을 run script 또는 ProcessInfo 에서 접근할 수 있다. 본인은 "Tuist test" 커맨드를 실행했을 때 자꾸 lint 관련된 에러가 발생했기 때문에, 만약 ENABLE_LINT 속성이 "NO" 일 때는 lint script 를 실행하지 않기 위해 관련 속성을 추가했다.

수정된 swiftlint run script

    // 추가된 script 로, build configuration 에서 ENABLE LINT 속성이 NO 일 때는 script를 종료한다!
    if [ "${ENABLE_LINT}" = "NO" ] ; then
        echo "LINT DISABLED!"
        exit
    fi
    echo "${ENABLE_LINT}"
    echo "EXECUTE LINT"
    if test -d "/opt/homebrew/bin/"; then
            PATH="/opt/homebrew/bin/:${PATH}"
        fi

        export PATH

        if which swiftlint > /dev/null; then
            swiftlint
        else
            echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
        fi

Project 설정하기

let project = Project(
    name: projectName,
    organizationName: nil,
    // 1
    options: .options(automaticSchemesOptions: .disabled),
    // 2
    packages: [
        .remote(url: "https://github.com/firebase/firebase-ios-sdk", requirement: .upToNextMajor(from: "9.0.0")),
        .remote(url: "https://github.com/ReactiveX/RxSwift", requirement: .upToNextMajor(from: "6.0.0")),
        .remote(url: "https://github.com/kakao/kakao-ios-sdk-rx", requirement: .branch("master")),
        .remote(url: "https://github.com/HoJongE/KeychainPackage.git", requirement: .upToNextMajor(from: "1.0.0"))
    ],
    // 3
    settings: settings,
    // 4
    targets: [
        appTarget,
        testTarget
    ],
    schemes: [
        // 5
        Scheme(name: "\(projectName)-Dev",
               shared: true,
               buildAction: .buildAction(targets: ["Gom4ziz"]),
               testAction: .targets(["Gom4zizTests"], configuration: .configuration("Dev"), options: .options(coverage: true)),
               runAction: .runAction(configuration: .configuration("Dev")),
               archiveAction: .archiveAction(configuration: .configuration("Dev")),
               profileAction: .profileAction(configuration: .configuration("Dev")),
               analyzeAction: .analyzeAction(configuration: .configuration("Dev"))),
        // 6
        Scheme(name: "\(projectName)-Release",
               shared: true,
               buildAction: .buildAction(targets: ["Gom4ziz"]),
               testAction: .targets(["Gom4zizTests"], configuration: .configuration("Release"), options: .options(coverage: true)),
               runAction: .runAction(configuration: .configuration("Release")),
               archiveAction: .archiveAction(configuration: .configuration("Release")),
               profileAction: .profileAction(configuration: .configuration("Release")),
               analyzeAction: .analyzeAction(configuration: .configuration("Release"))),
        // 7
        Scheme(name: "\(projectName)-Test",
               shared: true,
               buildAction: .buildAction(targets: ["Gom4ziz"]),
               testAction: .targets(["Gom4zizTests"], configuration: .configuration("Test"), options: .options(coverage: true)),
               runAction: .runAction(configuration: .configuration("Test")),
               archiveAction: .archiveAction(configuration: .configuration("Test")),
               profileAction: .profileAction(configuration: .configuration("Test")),
               analyzeAction: .analyzeAction(configuration: .configuration("Test"))),
    ],
    fileHeaderTemplate: nil,
    additionalFiles: [],
    resourceSynthesizers: []
    )
  1. scheme을 따로 생성할 것이기 때문에, 자동 생성 옵션을 끔
  2. Kakao SDK, Firebase SDK, Keychain Package 에 대한 의존성을 추가함
  3. Build setting 을 설정 (위에서 생성함)
  4. Project 가 가질 target 을 설정 (App Product 를 생성하는 appTarget 과, appTarget 을 테스트할 testTarget을 추가)
  5. Dev Scheme 을 생성. Dev Scheme 은 테스트, 런, 아카이브, 프로파일, 애널라이즈 액션을 실행할 때, Dev build setting 을 사용한다 (ENABLE_LINT 가 YES 인 설정).
  6. Release용 Scheme 을 생성. Release build setting 을 사용한다.
  7. Test 용 Scheme 을 생성. Test build setting 을 사용한다.

References

https://okanghoon.medium.com/xcode-프로젝트-관리를-위한-tuist-알아보기-6a92950780be