Tuist 로 가는 여정 Part2 — Tuist 에서 오픈소스 라이브러리 관리하기

Hyeongseok Park
29CM TEAM
Published in
13 min readJan 18, 2023

안녕하세요? 29CM iOS 엔지니어 박형석입니다.

이전 글에서는 XcodeGen 에서 Tuist 로 전환하는 내용을 소개드렸는데요. 2부인 이번 글에서는 Tuist 에서 오픈소스 라이브러리를 관리하는 방법을 소개드리려고 합니다.

단순히 라이브러리 의존성을 주입하는 과정보다 생산성 향상, 특히 빌드 속도 향상을 위해 Tuist 에서는 외부 라이브러리 캐싱을 어떻게 사용하는지, 또 어떻게 전환했는지 중점적으로 공유드리려 합니다.

일반적으로 iOS 개발팀에서는 오픈소스 라이브러리를 세 가지 방식으로 관리하고 있습니다. CocoaPods, SPM, Carthage 와 같은 의존성 관리 도구를 사용하는데, 간혹 필요에 따라 XCFramework 를 만들어 사용하고 커스텀이 필요한 경우에는 코드를 복사 및 수정해서 워크스페이스 내부에서 관리하기도 합니다.

저희 29CM iOS 팀에서는 CocoaPods 과 XCFramework 를 잘 사용하고 있었기 때문에 Tuist 전환시 “어떤 의존성 관리 도구를, 어떻게 사용해야 하는가?” 와 “XCFramework 를 XcodeGen 처럼 잘 사용할 수 있는 방법은 없을까?” 를 중점적으로 고려해야 했습니다. 이리저리 사전 테스트를 진행한 결과 다음과 같았습니다.

  • CocoaPods 사용을 지양한다. 대책을 마련해야 한다!
  • XCFramework 는 깔끔하게 지원한다. 다른 모듈처럼 사용할 수 있는 환경을 만들자!

왜 저희 팀이 위 두 가지를 고려하게 되었는지, 최종적으로 저희는 Tuist 위에서 오픈소스를 어떻게 관리하게 되었는지 아래에서 소개드리겠습니다.

CocoaPods 에서 SPM 및 Carthage 로 전환하기

말씀드렸던 것처럼 Tuist 로 전환하기 전 저희 프로젝트는 CocoaPods 으로 외부 라이브러리 의존성을 관리하고 있었습니다. CocoaPods 은 자체적으로 Prebuild Framework 를 사용할 수 없기 때문에 Grab 에서 만든 CocoaPods-Binary-Cache 를 도입, 필요한 부분은 개선 및 자동화해서 사용하고 있었습니다.

💡 기존 저희 팀이 CocoaPods 을 사용할 때 의존성 캐싱을 어떻게 관리했는지 확인하고 싶으시면, 우성님의 글을 참고하시면 더 많은 도움을 받으실 수 있습니다.

CocoaPods 을 떠나보내며

Tuist 도입을 논의할 때 가장 큰 장점 중 하나로 내외부 모듈을 함께 관리할 수 있다는 것을 뽑았습니다. 거기다 3.x 부터는 오픈소스 라이브러리 의존성 캐싱을 관리할 수 있는 기능까지 있어 팀이 원하는 기술 수준이 모두 갖추어졌죠. 하지만 청천벽력 같은 소식이 있었습니다.

Tuist 2.x 문서의 Adding External Dependencies 에 해당하는 내용입니다.
Tuist 3.x 문서의 Adding External Dependencies 에 해당하는 내용입니다.
CocoaPods 을 지원하기 위한 리소스가 있다면 제공하겠다는 건데, 22년 4월 이후로 감감 무소식

요지는 의존성 캐싱 기능에 CocoaPods 는 지원하지 않고, 원한다면 Tuist 로 생성한 프로젝트 위에서 pod install 을 할 수 있다는 내용입니다. 이는 CocoaPods 을 사용하고 있던 저희 팀이 기대하는 사용성이 아니었습니다. 어떻게 대응할지 심사숙고 끝에 결국 의존성 관리 도구를 전면 교체하기로 결정했습니다.

CocoaPods 제거하기

대부분의 오픈소스는 CocoaPods 을 기본으로 지원합니다. 그리고 필요한 경우 SPM, Carthage 를 추가로 지원합니다. 그래서 저희가 기존에 사용하고 있었던 오픈소스 라이브러리 전체를 SPM 으로 변경할 수 밖에 없었습니다. 그래서 Carthage 를 사용하거나 코드를 직접 사용, 혹은 라이브러리를 제거하는 방식으로 진행해야 했습니다. 이 과정은 다음과 같은 우선순위를 가지고 진행했습니다.

  • 정말 필요한가?

앱에서 굳이 사용하고 있지 않은 라이브러리, 중복해서 사용하고 있는 라이브러리, 모듈화 과정에서 의존성 문제로 제거해야 하는 라이브러리 등이 있었고 이 경우 굳이 유지할 필요가 없었기 때문에 삭제 및 정리하는 방식으로 진행했습니다.

  • SPM 으로 대체 가능한가?

그 중 살아남은 라이브러리는 다음 우선순위인 SPM 을 사용해 관리하도록 했습니다. Carthage 는 잘 관리되지 않는 툴이고 Xcode 버전, M1 이슈, 독립적인 추가 설정 필요와 같은 이슈가 있기 때문에, 주기적으로 관리되는 1st Party 인 SPM 을 가장 우선순위가 높은 대체재로 선택했습니다.

  • Carthage 로 대체 가능한가?

그럼에도 Carthage 로 운영해야 하는 라이브러리들이 있었습니다. SPM 을 지원하지 않거나 SPM 으로 설정했을 때 이슈가 있었던 라이브러리입니다. 저희에게 대표적으로는 FlexLayout 이 있었고, 해당 라이브러리는 현재 Carthage 로 관리하고 있습니다. (FlexLayout 레포에 SPM 대응 관련 내용이 있습니다만, 설정의 복잡도를 높히기보다는 Carthage 를 택했습니다)

Tuist 에서 Dependencies.swift 로 관리하기

이제 실제로 저희가 어떻게 적용했는지를 코드 베이스로 설명드리겠습니다. 말씀드렸다시피, Tuist 3.x 부터는 외부 라이브러리를 한 곳에서 관리할 수 있는 기능과 함께 의존성 캐싱 기능을 함께 제공하고 있습니다. 이 작업은 Dependencies.swift 라는 파일에서 일어납니다.

tuist edit를 실행하면, Manifest 의 하위 폴더에 위와 같이 Dependencies.swift 파일을 생성해서 관리합니다.

import ProjectDescription
import ProjectDescriptionHelpers

let dependencies = Dependencies(
carthage: CarthageDependencies([]),
swiftPackageManager: SwiftPackageManagerDependencies([]),
platforms: [.iOS]
)

Tuist 는 Dependencies.swift 에 Dependencies 객체를 생성해서 외부 의존성을 관리합니다. 초기화 로직에 CarthageDependencies 와 SwiftPackageManagerDependencies 를 생성해서 주입합니다.

private extension Package {
private static func remote(repo: String, version: Version) -> Package {
return Package.remote(url: "https://github.com/\(repo).git", requirement: .exact(version))
}
}

public extension Package {
static let PinLayout = Package.remote(repo: "layoutBox/PinLayout", version: "1.10.2")
}

let dependencies = Dependencies(
// ...
swiftPackageManager: SwiftPackageManagerDependencies([
// UI
Package.PinLayout,
],
productTypes: [
"PinLayout": .framework,
]),
// ...
)

저희는 좀 더 쉽게 관리할 수 있도록 Package Extension 에 Sugar Code 를 작성했습니다. 또 특정 버전을 사용해 라이브러리 내부에서 일어나는 이슈를 미연에 방지하도록 했습니다. (그만큼 안정적인 버전을 선택해야 합니다) 위 코드는 SPM 을 예로 들었지만, Carthage 도 비슷하게 작성합니다.

의존성을 주입하는 곳 아래에 추가된 productTypes 에 주목해 주세요. 저를 골탕 먹였던 옵션입니다 😅 처음 SPM 을 도입하고 예상치 못한 warning 과 버그가 있었는데요. 외부 의존성의 Mach-O Type 이 static 인 경우 발생하는 버그들이었습니다.

CocoaPods 의 경우 의존성을 주입할 때 기본값으로 Mach-O Type 을 dynamic 으로 설정합니다. 하지만 SPM 은 static 으로 설정하죠. 때문에 각각 다른 모듈에서 해당 라이브러리를 import 할 때, 복사된 코드로 인해 의도치 않은 사이드 이펙트(혹은 오류)가 발생할 수 있습니다.(특히 내부적으로 Singleton 을 사용하는 경우)

productType 은 이런 상황을 대비해 Mach-O Type 을 결정할 수 있습니다. 저희는 필요한 경우에만 .dynamic 옵션을 주도록 해서 런타임시의 성능을 최적화하도록 했습니다.

extension TargetDependency {
public enum spm {
public static let PinLayout = Self.external("PinLayout")
}
}

let targets = Project.makeFrameworkTargets(
name: "AppCore_UI",
// ...
dependencies: [
.os.UIKit,
.spm.PinLayout,
.carthage.FlexLayout,
.workspace.shared.foundation,
.workspace.app.core.resource,
// ...
]
)

let project = Project(
name: "AppCore_UI",
organizationName: "Musinsa Inc.",
settings: .default,
targets: targets
)

마지막으로 별도의 파일에 TargetDependency 를 확장해 spm 타입을 만들고 외부 의존성을 관리할 수 있는 Sugar 를 작성했습니다. 위 AppCore_UI 모듈의 dependencies 를 확인해 보시면 저희가 어떻게 사용했는지 확인하실 수 있습니다.

위 UI 모듈의 dependencies 목록을 보시면, 해당 모듈에 필요한 여러 의존성이 한 눈에 관리되는 것을 보실 수 있습니다. OS, SPM, Carthage, Modular Architecture 를 구성하는 다른 모듈들이 한번에 나열 및 정리가 되어 있습니다.

Tuist 에서 XCFramework 로 관리하기

Tuist 에서도 pre-compiled XCFramework 로 오픈소스를 관리할 수 있습니다. 특별한 작업없이 바로 사용할 수 있고 전환 중에도 특별한 트러블 슈팅이 없었습니다.

Tuist 3.x 에서 문서에도 사용법이 기재되어 있습니다.
// TargetDependency+XCFramework.swift
extension TargetDependency {
public enum xcframeworks {
// Firebase
/// FirebaseAnalytics
public static let FirebaseAnalytics = TargetDependency.xcframework(
path: .relativeToRoot("Frameworks/Firebase/FirebaseAnalytics.xcframework")
)
//...
}
}

위에서도 언급했던 것처럼 저희는 모듈에 필요한 의존성이 한 눈에 보이는 것을 지향하기 때문에 별도의 파일을 만들어 다음과 같은 슈가를 작성해서 관리하도록 했습니다. SPM & Carthage 와 마찬가지로 TargetDependency 을 extension 해서 xcframeworks 타입을 만들어 관리합니다.

XCFramework 의 경우 경로 설정을 해주는게 중요 포인트인데, 저희는 Root 디렉토리의 Framework 라는 디렉토리에 XCFramework 를 관리하고 있어 위와 같이 경로를 설정해주었습니다.

let targets = Project.makeFrameworkTargets(
name: "App29CM",
// ...
dependencies: [
.os.UIKit,
.spm.PinLayout,
.carthage.FlexLayout,
.xcframeworks.FirebaseAnalytics, // 이렇게 추가할 수 있도록 합니다.
.workspace.shared.foundation,
.workspace.app.core.resource,
// ...
]
)

위 작업들이 모여 모든 의존성을 이렇게 한번에 관리할 수 있도록 하는 것이 목적입니다. 제대로 의존성이 주입되었는지 tuist generate 및 테스트를 통해 확인합니다.

마치며

XcodeGen 을 사용했을 때는 모듈 의존성과 오픈소스 라이브러리 의존성을 한 눈에 파악하기 어려웠습니다. 새로운 모듈을 만들어가는데 있어서 yml 을 작성하는 일, 한 눈에 파악하기 어려운 의존성 등은 휴먼 에러를 높이고 리소스를 낭비하는 원인이었죠. 이런 면에서 Tuist 로 전환하는 것은 여러모로 저희 팀에 긍정적인 영향을 주었습니다.

이렇게 두 번에 걸친 전환기를 소개해 드렸는데요. 보기 쉽게 소개드리려 하다보니 충분치 않은 내용이 있었을지 모르겠습니다 ^^; 궁금하신 점은 질문주시면 확인하는 대로 답변 드리겠습니다.

Tuist 전환에 관심이 있는 iOS 팀에게 조금이라도 도움이 되길 바라며 전환기를 마무리하겠습니다.

읽어주셔서 감사합니다.

[함께 성장할 동료를 찾습니다]

29CM (무신사) 는 3년 연속 거래액 2배의 성장을 이루었습니다.

빠르게 성장을 해나가면서도 더 큰 성장에 대비하기 위해 Modular Architecture 를 꾸준히 확장해 나가며 Tuist 를 도입하는 등 팀에 필요한 여러 기술적인 시도를 진행하고 있습니다.

함께 성장하고 유저 가치를 만들어낼 동료 개발자분들을 찾습니다.
많은 지원 부탁드립니다!

🚀 29CM 채용 페이지 : https://www.29cmcareers.co.kr/

--

--