협업) Tuist로 .xcodeproj 머지 컨플릭트 해결하기

Havi·2021년 5월 18일
1

협업

목록 보기
1/1

민소네님 블로그 참고

도입 이유

xcode 로 개발하다보면 .xcodeproj 파일때문에 git conflict가 일어나는 일이 잦다.

또한 모듈화 하면서 모듈간의 의존성이 많아질 경우 xcode로 제어하기 힘들다.

따라서 프로젝트 관리를 효율적으로 하기 위해 tuist라는 CLI 툴을 이용해 프로젝트 관리를 하기로 했다.

tuist를 사용하면 Xcode가 프로젝트 파일을 관리하지 않기 때문에 다음과 같은 그림이 나오고 팀원이 모두 같은 프로젝트 파일을 사용할 수 있다.

따라서 git에 .xcodeproj 이나 .xcworkspace 파일을 올리지 않는다.

xcodegen또한 사용해보았지만 project파일을 .yml로 관리하지 않고 .swift로 관리하고 싶었기 때문에 tuist를 선택했다.

설치

공식 문서: https://docs.tuist.io/tutorial/get-started/

아래 명령어를 입력하면 튜이스트가 설치된다.

bash <(curl -Ls https://install.tuist.io)

공식 문서에서는 아래와 같이 만들라고 안내한다.

mkdir TestApp
cd TestApp
tuist init --platform ios

위 명령어를 입력하고 트리 구조를 보면 다음과 같이 나온다.

brew install tree # if needed
tree .

.
├── Project.swift
├── Targets
│   ├── TestApp
│   │   ├── Resources
│   │   │   └── LaunchScreen.storyboard
│   │   ├── Sources
│   │   │   └── AppDelegate.swift
│   │   └── Tests
│   │       └── AppTests.swift
│   ├── TestAppKit
│   │   ├── Sources
│   │   │   └── TestAppKit.swift
│   │   └── Tests
│   │       └── TestAppKitTests.swift
│   └── TestAppUI
│       ├── Sources
│       │   └── TestAppUI.swift
│       └── Tests
│           └── TestAppUITests.swift
└── Tuist
    ├── Config.swift
    └── ProjectDescriptionHelpers
        └── Project+Templates.swift

적용

위와 같은 멀티 모듈 구조는 처음엔 이해가 안됐었다.

따라서 먼저 기존에 만들던대로 xcode에서 새로운 프로젝트를 만들고 그 프로젝트에 tuist를 적용해보자.

tuist로 프로젝트를 만들려면 Project.swift 파일이 필요하다.

따라서 위 파일을 만들어주고 import ProjectDescription 를 해준 뒤 로컬 변수로 project 를 만들면 tuist generate에서 프로젝트를 만들어준다.

해당 Project.swift 파일을 변경하려면 tuist edit 명령어를 입력하면 된다.

만약

Couldn't find Xcode's Info.plist at /Library/Contents/Info.plist. Make sure your Xcode installation is selected by running: sudo xcode-select -s /Applications/Xcode.app

이런 에러가 발생한다면 터미널에서 시키는 대로 sudo xcode-select -s /Applications/Xcode.app 를 입력하면 된다.

이제 새로 만든 프로젝트에 다음과 같이 폴더 구조를 변경해줬다.

Derived 폴더는 tuist generate시 생성되는 아이이니까 안만들어줘도 자동으로 생성된다.

iosTemplateApp 폴더 안에 Resources와 Sources 폴더만 있으면 되니 다른 폴더들은 신경쓰지 않아도 된다.

이렇게 Resources와 Sources 폴더를 만들어 줬으면 tuist edit 를 입력해 Project.swift 파일을 변경해주자.

Project.swift 의 완성본은 다음과 같다.

import ProjectDescription

let projectName: String = "TemplateApp" /// 프로젝트 이름
let organizationName: String = "havi" /// organization 이름
let bundleName: String = "com.havi" /// 번들 앞 prefix -> 이렇게 안해줘도 됨

/// 타겟에 해당하는 스킴 외의 추가 스킴
/// configuration을 나누기 위해 추가했다.
/// 아래 settings에 추가했는데 정확하게 나오지 않아 이렇게 추가해봄
/// 이렇게 하는게 아닐 수 있음
let schemes = [
    Scheme(
        name: "\(projectName)-Debug",
        shared: true,
        buildAction: BuildAction(targets: ["\(projectName)"]),
        testAction: TestAction(
            targets: ["\(projectName)Tests"],
            configurationName: "Debug",
            coverage: true
        ),
        runAction: RunAction(configurationName: "Debug"),
        archiveAction: ArchiveAction(configurationName: "Debug"),
        profileAction: ProfileAction(configurationName: "Debug"),
        analyzeAction: AnalyzeAction(configurationName: "Debug")
    ),
    Scheme(
        name: "\(projectName)-Inhouse",
        shared: true,
        buildAction: BuildAction(targets: ["\(projectName)"]),
        testAction: TestAction(
            targets: ["\(projectName)Tests"],
            configurationName: "Inhouse",
            coverage: true
        ),
        runAction: RunAction(configurationName: "Inhouse"),
        archiveAction: ArchiveAction(configurationName: "Inhouse"),
        profileAction: ProfileAction(configurationName: "Inhouse"),
        analyzeAction: AnalyzeAction(configurationName: "Inhouse")
    ),
    Scheme(
        name: "\(projectName)-Release",
        shared: true,
        buildAction: BuildAction(targets: ["\(projectName)"]),
        testAction: TestAction(
            targets: ["\(projectName)Tests"],
            configurationName: "Release",
            coverage: true
        ),
        runAction: RunAction(configurationName: "Release"),
        archiveAction: ArchiveAction(configurationName: "Release"),
        profileAction: ProfileAction(configurationName: "Release"),
        analyzeAction: AnalyzeAction(configurationName: "Release")
    ),
]

/// Configuration을 관리하기 위해 추가
/// .relativeToRoot는 Tuist 폴더 또는 .git 이 있는 경로 중 가장 가까운 경로를 사용함. - by minsOne
/// 필요에 따라 .relativeToManifest, .relativeToCurrentFile를 사용함. - by minsOne
let settings = Settings(configurations: [
    .debug(name: "Debug"),
/// xcconfig파일로 관리할 경우 이렇게 써준다.
//		.debug(name: "Debug", xcconfig: .relativeToRoot("Configurations/\(projectName)-Debug.xcconfig")),
    .debug(name: "Inhouse"),
    .release(name: "Release")
])

/// Build Phase에서 돌릴 runscript
/// 실제로 sh파일을 Scripts폴더를 만들어 넣어줘야함
/// tuist에서도 린팅이 되는걸로 알고있지만 익숙한 swiftlint 사용
/// R.swift나 swiftgen도 빌드 스크립트 짜서 넣어줄 수 있지만
/// tuist의 derived 폴더에서 비슷한 역할을 해주고 있기 때문에 적용x
let targetActions = [
    TargetAction.pre(
        path: "Scripts/SwiftLintRunScript.sh",
        arguments: [],
        name: "SwiftLint"
    )
]

/// 앱의 타겟 설정
let targets = [
    Target(
        name: projectName, /// 타겟 이름
        platform: .iOS, /// 플랫폼
        product: .app, /// 앱인지 프레임워크인지 라이브러리인지 등
        bundleId: "\(bundleName).\(projectName)", /// 번들 아이디
        deploymentTarget: .iOS(targetVersion: "13.0", devices: [.iphone]), /// 배포 타겟 정보, 혹시 generate에서 버전정보 워닝이 뜬다면 pod파일 버전 제어, 프로젝트 deploy는 default로 설정되는 거 같다.
        infoPlist: "\(projectName)/Supporting/Info.plist", /// plist 관리 위치 - .default로도 사용 가능
        sources: "\(projectName)/Sources/**", /// 민소네님은 앞에 프로젝트 이름 안붙여주던데 붙여줘야 빌드가 되서
        resources: "\(projectName)/Resources/**", /// 리소스가 있다면 관리
        actions: targetActions, /// 빌드 스크립트 실행
        dependencies: [
            .cocoapods(path: ".") /// 코코아 팟으로 의존성 관리, tuist generate하면 pod install 자동으로 됨
        ]
    ),
    Target( /// unit test
        name: "\(projectName)Tests",
        platform: .iOS,
        product: .unitTests,
        bundleId: "\(bundleName).\(projectName)Tests",
        infoPlist: "\(projectName)Tests/Info.plist",
        sources: "\(projectName)Tests/**",
        dependencies: [
            .target(name: projectName) /// 테스트의 의존성은 실제 프로젝트에 있음
        ]
    ),
    Target( /// ui test
        name: "\(projectName)UITests",
        platform: .iOS,
        product: .uiTests,
        bundleId: "\(bundleName).\(projectName)UITests",
        infoPlist: "\(projectName)UITests/Info.plist",
        sources: "\(projectName)UITests/**",
        dependencies: [
            .target(name: projectName)
        ]
    )
]

/// 실제 프로젝트
let project = Project(
    name: projectName,
    organizationName: organizationName,
    settings: settings,
    targets: targets,
    schemes: schemes
)

구조

이 프로젝트 파일로 tuist generate를 하면 다음과 같은 폴더 구조를 가진다.

.gitignore에 derived 파일과 프로젝트 파일을 추가하면 git에는 다음과 같이 올라간다.

### Tuist derived files ###
graph.dot
Derived/

### Projects ###
*.xcodeproj
*.xcworkspace

깃헙 주소: https://github.com/hansangjin96/TuistTemplate

참고: configuration을 나누고 싶지 않은 사람은 커밋 로그를 보면 추가 안한 프로젝트 파일이 있다.
참고2: swiftLint는 pod에 추가하지 않고 brew로 깔았다.
참고3: xcode13(베타)로 프로젝트를 만들 경우 test 타겟에 info.plist가 없다?
참고4: tuist 자체적으로 리소스 관리를 해주지만, 혹시 R.swift를 쓰고 싶은 사람은 다음 링크 참고
https://medium.com/swlh/first-touches-of-using-tuist-for-xcode-project-generation-f46c630bc29b
참고5: 모듈간의 의존성이 복잡해질 경우 spm으로 써드파티를 관리하는게 효율적이라고 알고있는데 현재 사용하는 pod이 모두 spm을 100%대체 가능한지 확신이 없어 pod으로 관리함 - 확실히 아시는분은 댓글 부탁드려요

2021.08.23 수정
디펜던시 관리 SPM으로 변경
모듈 단위로 프로젝트 나눈 깃헙예제 : https://github.com/hansangjin96/HaviTemplateApp

profile
iOS Developer

0개의 댓글