본문 바로가기

iOS

(iOS) 유니플로거 리팩토링(1) XCFramework

올해도 애플에게 친구비 납입..... 올해도 친구 해줘서 고마워 애플..

매년 친구비 나가는것도 아까운데 앱이라도 다시 출시해야지....

2020년 말 YAPP 동아리를 통해 개발한 유니플로거라는 앱이 있다. 조깅하면서 쓰레기도 줍는 플로깅(Plogging)이란 활동을 조금 더 즐겁게 하도록 도와주는 취지로 개발되었지만, 서버 비용문제로 아쉽게 서비스를 종료했던 프로젝트인데.....

 

취직 후 개인프로젝트를 더 이상 진행하지 않다가 개인 공부 목적으로 무언가는 해야겠다 싶고.. 그렇다고 뭔가 새로 앱을 개발할만한 아이디어는 떠오르지 않아 이 프로젝트를 한번 리팩토링 해보고자 한다. 

 

가능한 지금껏 iOS를 개발하면서 궁금했던 점들이 많았는데 그 중에서 특히 아래의 항목들을 공부하자는 취지에서 유니플로거 프로젝트를 활용해 볼 예정이다.

- 모듈화: 앱 내에서 사용되는 기능들을 모듈로 분리해보기, 빌드 속도 개선해 보기

- RIBs: 아키텍쳐 변경 VIP(Clean Swift) -> RIBs(Router, Interactor, Builder)

- Tuist: 당장 사용하지 않겠지만, 프로젝트 구성 이해하기를 위한 목적으로 정리하기

- FlexLayout: 오토레이아웃 기반으로 작성된 UI를 Manual Layout으로 변경하기(FlexLayout 및 PinLayout 활용)

 

일단은 간단하게 위처럼 적어놨지만, 그냥 리팩터링 해보면서 그때그때 필요한 부분들을 정리해보려고 한다.


XCFramework

회사에서 클린 빌드 이후에도 빠르게 빌드할 수 있도록 카르타고(Carthage)를 이용해 사용되는 프레임워크들을 빌드하여 XCFramework로 만든 후 프로젝트에 추가하여 사용하고 있는 것을 보았다. 기존 알고 있던 개념으로 framework 파일은 단일 아키텍처를 지원하고, xcframework는 여러 아키텍쳐를 한 번에 지원할 수 있도록 설계된 것으로 알고 있다.

 

iPhone, Mac, Apple Watch 등 기기의 종류, 출시 연도에 따라 다른 아키텍쳐를 사용하고 있다. 예전에는 intel 프로세서가 탑재된 맥을 대부분 사용하였으나, 최근에는 M1(애플 실리콘)을 탑재한 맥 또는 아이패드를 이용해 개발을 하고 있으므로 이를 위해 사용되는 프레임워크들은 필요한 만큼 많은 아키텍쳐를 지원해야 한다.

간단하게 정리해 보자면 다음과 같은 아키텍쳐가 있다.

- iOS: armv7, arm64, arm64e, x86_64

- MacOS: x86_64, arm64

- WatchOS: armv7k, arm64_32, arm64, x86_64

 

실제 기기, 기기에 대한 시뮬레이터, 특정 아키텍쳐가 탑재된 맥에서의 시뮬레이터 등에 따라 다른 아키텍쳐를 지원해야 한다. 

기존 프로젝트에서 사용중이던 Podfile

기존 사용하고 있던 의존성은 위와 같고, SnapKit을 PinLayout, FlexLayout으로, 나머지는 필요한 것만 유지한 채로 xcframework로 변환해보고자 한다.

의존성이 최대한 걸려있지 않은(빌드하고자 하는 프레임워크가 의존하고 있는 게 없거나 적은)것부터 만들어보자.

Then

1. 클론 받기

git clone https://github.com/devxoul/Then.git

2. 빌드하기

빌드를 시도하려고 보니 Sources/Then/Then.swift 파일 하나만 있다.... 다른 프레임워크들은 프로젝트의 형태로 제공되어 아키텍쳐별 빌드 및 XCFramework로 합쳐주면 되지만 이건 라이브러리 또는 프레임워크 프로젝트를 별도로 생성해서 빌드해야 한다. 먼저 라이브러리 프로젝트를 만들어서 시도해 보자.

근데 라이브러리도 XCFramework처럼 멀티 아키텍쳐를 지원할 수 있나...? 하고 찾아보니 fat library가 있다고 한다. 요건 이따가 좀 더 살펴보고..  Then이라는 이름으로 Static Library를 만들어보자

Static Library 생성

프로젝트를 생성하면 프로젝트 이름과 동일한 이름으로 소스파일이 생성이 되는데, 여기에 Then 소스 내용을 그대로 넣어보자.

소스코드 추가 및 아키텍쳐 선택

일단 사용해야 할 아키텍쳐는 iOS 시뮬레이터 및 iOS 실 기기이므로 각각 선택해서 빌드해야 하지만.. Mac OS도 추가로 빌드해 주자. 그전에 라이브러리 빌드를 Release모드로 바꾸는 것을 잊지 말자.

Build Configuration을 Release로 변경

Build Configiratopm 변경 후 빌드까지 끝냈다면 아래처럼 3개의 라이브러리가 생성된 것을 확인할 수 있다.

빌드된 라이브러리가 3개이지만, xcfrmaeowk처럼 하나의 라이브러리로 만들어 import할 수 있게 만들려면 fat library를 만들어야 한다. 

lipo 명령어를 이용하여 세 라이브러리를 하나의 fat library로 합쳐보자.

$ lipo -create Release-iphoneos/libThen.a Release-iphonesimulator/libThen.a Release-maccatalyst/libThen.a -output libThen.a

// fatal error: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/lipo: Release-iphoneos/libThen.a and Release-iphonesimulator/libThen.a have the same architectures (arm64) and can't be in the same fat output file

위와 같은 오류로 라이브러리를 합칠 수 없다고 한다.. 원인을 살펴보니 iPhone Simulator와 실제 iOS 기기 모두 arm64 아키텍처를 지원하기 때문에, 충돌이 생겨 문제가 발생하는 것이다. mac catalyst도 마찬가지로 x86_64 및 arm64 아키텍쳐를 지원하므로 결국 3개의 라이브러리는 합쳐질 수 없다.

 

해결 방법으로 특정 타깃 빌드 시 EXCLUDED_ARCHS에서 불필요한 아키텍쳐를 제외하는 방법이 있으나... 그렇게 해서 fat library 생성이 성공했다고 하더라도 아래와 같은 문제로 빌드할 수 없다.

swift module에 특정 아키텍쳐를 지원하지 않아 로제타로 빌드할 것인지를 물어봄.

fat library 생성 후 각각 빌드된 swiftmodule파일을 import하도록 해도 이미 EXCLUDED_ARCHS에서 제외된 아키텍쳐를 빌드하려고 시도한다면 위와 같은 에러를 출력한다. 

 

결국 fat library를 생성하는 것은 포기하고, 프레임워크 프로젝트를 생성하여 xcframework를 생성하도록 해야겠다.

라이브러리 생성때와 비슷하게 프레임워크 프로젝트를 생성 후 동일하게 버전 세팅(여기서는 11.0으로 세팅함) 및 Build Configuration을 Release로 변경해 주자.

추가로 Framework생성 시 기본 Mach-O 타입이 Dynamic Library로 되어있는데 굳이 다이나믹일 필요가 없으니 Static Libray로 변경해 주자.

아까와 마찬가지로 3개의 프레임워크가 생성된 것을 확인할 수 있다.

이제 이 3개의 프레임워크를 XCFramework로 합쳐보자.

xcodebuild -create-xcframework \
-framework './Release-iphoneos/Then.framework' \
-framework './Release-iphonesimulator/Then.framework' \
-framework './Release-maccatalyst/Then.framework' \
-output './Then.xcframework'

// No 'swiftinterface' files found within '/Users/ever/Library/Developer/Xcode/DerivedData/Then-eyqyewxlvvdmlfbzgklwlgipsxdt/Build/Products/Release-iphoneos/Then.framework/Modules/Then.swiftmodule'.

swiftinterface파일이 없어서 빌드가 안된다고 한다. 빌드 시 한 가지 빠뜨렸던 게 Build Libraries for Distribution 플래그를 세팅해주지 않았다. 해당 값을 Yes로 세팅 후 다시 빌드해 보면...

xcodebuild -create-xcframework \
-framework './Release-iphoneos/Then.framework' \
-framework './Release-iphonesimulator/Then.framework' \
-framework './Release-maccatalyst/Then.framework' \
-output './Then.xcframework'

// xcframework successfully written out to: /Users/ever/Library/Developer/Xcode/DerivedData/Then-eyqyewxlvvdmlfbzgklwlgipsxdt/Build/Products/Then.xcframework

성공적으로 xcframework를 생성했다. 이제 샘플 앱 프로젝트에서 Then을 추가해 사용해 보자.

역시 한 번에 성공하는 일이 없다 ㅎㅎ.  Swift의 모듈 네임과 인터페이스 네임인 Then이 겹쳐서 생기는 문제라고 한다. 다른 글을 참고해 보니 겹치지 않도록 swiftinterface 파일에서 모두 겹치는 Then을 지워버리는 방법이 있다고 한다. (안전한 방법은 아닐 거라고 생각하지만... 사실 CocoaPod이나 SwiftPM에서는 어떻게 이런 문제를 해결했는지 궁금하다.) 

일단 시도라도 해보자

$ find . -name "*.swiftinterface" -exec sed -i -e 's/Then\.//g' {} \;

위 명령어를 통해 'Then.'을 모두 지워주도록 한 후 빌드해 보면 

성공적으로 빌드 및 런치 하는 것을 확인할 수 있다.

하 지 만... 실 기기에서는 빌드까지만 성공하고 런치 되지 않는다..

실 기기에서는 빌드가 실패한다... 두 번째 방법으로 모듈 이름과 인터페이스 이름을 다르게 하여 시도해 보면. (ThenFramework로 프로젝트 생성)

Build 및 Launch 성공

Then 하나만 의존성으로 추가하는데 너무 많은 시간을 써버려서.... 나머지 의존성에 대한 xcframework 생성의 Carthage의 도움을 이용해 생성했다.

 

Carthage

카르타고라고 읽는다고 한다. 취지는 Cocoapods사용 시 클린빌드를 수행하면 의존성을 다시 모두 빌드해야 하는 불편함 및 불필요한 시간 낭비를 줄여주기 위해 framework 또는 xcframework로 생성하여 프로젝트에 추가할 수 있도록 도와준다.

 

Cartfile을 아래와 같이 생성한 후 xcframework를 생성하도록 해보자.

github "Uber/RIBs"
github "Alamofire/Alamofire"
github "layoutBox/PinLayout"
github "layoutBox/FlexLayout"
$ carthage update --platform iOS --use-xcframeworks

역시 한 번에 성공하면 재미가 없지... FlexLayout에서 빌드가 실패한 것 같다. 

로그를 살펴보니 PinLayout의 TestProjects에서 프로젝트파일을 열 수 없어서 문제가 있는 것 같다. 마침 민소네님 레포를 살펴보니 TestProjects 디렉터리를 제거해 주면 된다고 쓰여있어서 제거 후 다시 빌드를 시도해 보았다.

다행히 FlexLayout 및 PinLayout까지는 빌드가 성공했지만.. RIBs 빌드가 실패했다.

빌드 로그를 보니 RxSwift import가 실패해서 빌드가 실패한 것 같다.

Checkout 된 RIBs 프로젝트를 열고 직접 빌드를 해봐도 RxSwift를 import 하지 못해 빌드를 못하고 있었다.

의존하고 있는 RxRelay 및 RxSwift는 이미 카르타고로 빌드 후 xcframework가 생성되어 있기 때문에 위의 두 프레임워크를 대체해 준 후 빌드를 시도해 보자.

위와 같이 xcframework를 추가해 준 후 다시 카르타고로 빌드를 수행해 보면...

RIBs까지 빌드가 성공했다. 각각의 빌드를 위해 필요한 의존성까지 포함해 빌드된 xcframework를 살펴보면 아래와 같다.

이렇게 빌드된 프레임워크들을 프로젝트에 추가해 주면 별도의 빌드 세팅 없이 import하여 사용할 수 있다.

 

마지막으로 Cocoapods로 의존성을 관리할 때와 Carthage로 의존성을 추가했을 때의 클린 빌드 속도를 비교해 보자

Carthage로 의존성을 추가했을 경우 2.5 seconds
Cocoapods으로 의존성을 추가했을 경우 6.3 seconds

사용되는 프레임워크가 많지 않아 시간의 차이는 크지 않지만, Carthage의 경우 서명 및 복사만 하는 반면 Cocoapods는 컴파일 과정이 포함된 것을 빌드 로그를 통해 확인할 수 있다. 프로젝트의 규모가 커지고 의존성이 계속 추가될수록 이런 차이는 심해질 것이라고 생각한다.

 


여기까지 기존 의존성 관리 방식을 교체하는 것만으로도 꽤 많은 시간이 들었다. 하지만 직접 의존성을 빌드하고 추가해 보면서 평소에 궁금한 점들을 해소하고 배울 수 있어서 좋았다. 다음 글에서는 다시 앱을 리펙토링 하면서 생기는 고민들 및 해결과정을 적어보려고 한다.

 

References

- https://stackoverflow.com/questions/64022291/ios-14-lipo-error-while-creating-library-for-both-device-and-simulator

- https://medium.com/@hongseongho/xcframework%EC%97%90%EC%84%9C%EB%8A%94-%EB%AA%A8%EB%93%88%EB%AA%85%EA%B3%BC-%ED%81%B4%EB%9E%98%EC%8A%A4%EB%AA%85%EC%9D%84-%EA%B0%99%EC%9D%B4-%EC%93%B8-%EC%88%98-%EC%97%86%EB%8A%94-%EB%B2%84%EA%B7%B8-6339b433c45e

- https://github.com/minsOne/iOSApplicationTemplate

- https://github.com/Carthage/Carthage#nested-dependencies

- https://ios-development.tistory.com/1005

- https://developer.apple.com/documentation/xcode/build-settings-reference

- https://medium.com/strava-engineering/convert-a-universal-fat-framework-to-an-xcframework-39e33b7bd861