iOS 개발자 치고 ‘코코아팟(CocoaPods)’을 모르는 사람이 있을까. 2024년 겨울, 코코아팟이 말했다. “우리, 생각할 시간을 갖자.” 완전한 셧다운이라기보다는 ‘더 이상의 적극적 유지보수를 보장하지 않는다’에 가까운 톤이지만, 사실상의 이별이다. 십수 년간 반쯤 표준이었던 녀석이 돌연 시한부를 선언하며, 우리 Hive SDK는 눈에 그닥 차지 않던 어색한 신예와 예상보다 빨리 ‘친해지길 바라’를 찍게 되었다.
틈틈이 RnD연구, Research and Development를 진행하며 쌓인 시행착오를 다 넣자니 지나치게 내밀하고, 이 글의 예상 독자는 실무자보다는 ‘아 컴투스 뭐 하고 있나~?’ 하며 지나가는 누리꾼이다. 고민 끝에 에피타이저처럼 가벼운 소개를 하되 심심하지 않도록 몇몇 장면을 조각조각 끼워넣기로 했다. 편히 즐겨주시길 바란다.
홀로서기는 안 돼?
종속성 관리 도구란 프레임워크나 라이브러리를 내 코드에 심고, 사용하고, 관리하기 위한 도구다. 모든 코드를 혼자서 처음부터 끝까지 작성하는 개발자는 없다. 있다 하더라도 필연적으로 자신이 만든 코드를 재사용한다. Hive SDK는 게임 개발자가 게임 외적 기능을 쉽게 구현하기 위해 끌어다 쓰는 코드 덩어리를 제공한다. 이때 개발자는 Hive SDK의 원하는 기능을 원하는 버전으로 내려받게 해주는 매체를 필요로 한다. Hive SDK는 개발자 입장에서 접근성이 좋고 편리한 매체를 통해 제품을 배포하고자 한다. 종속성 관리 도구의 효용과 그 배포 최적화의 중요성이 여기서 나온다.
다소간의 오개념을 감수하고 비유하자면, 종속성 관리 도구란 다시 말해 밀키트 체인점 같은 거다. 제품 원본 파일을 직접 제공하는 건 고객이 우리 농장에 찾아오면 검정 봉다리에 담아주겠다는 말과 같다. 번거롭고, 신뢰할 수 없고, 표준화·규격화 되어 있지 않아 적용하기에도 불편하다.
코코아팟, SPM, 그리고 나
그럼 그냥 둘 다 적용하면 되지 왜 이렇게 구구절절이냐 싶을 테다. 그게, 쉬운 일이 아니다. 코코아팟과 SPM은 같은 종속성 관리 도구지만, 성향이 많이 다르다. 밀키트 체인점 비유에 기반하여 설명하자면 이렇다.
시한부 연인, 코코아팟(CocoaPods)은 노련하다. 우리 가게가 노점상이어도 가맹점으로 받아준다. 코코아팟의 인프라가 있으면 웬만한 환경에서 포장 픽업이 가능하다. 명세서podspec 작성이 상대적으로 유연하며, 분말static이든 국물dynamic이든 해당 밀키트에 적합한 처리를 수행해 준다. Hive SDK는 코코아팟의 공용 물류센터CocoaPods trunk를 이용하지 않기 때문에 이 물류센터의 박물관화신규 버전 업로드 불가 문제에서 자유롭지만, 유지·보수가 끊긴 시스템의 이가 언제 나갈지는 아무도 모르는 일이다. 코코아팟에서는 밀키트를 ‘팟(pod)’이라 부른다.
어색한 신예, SPM(Swift Package Manager)은 FM이다. 식품&주방가전 대기업의 공식 서비스인지라 깔끔하지만 까탈스럽다. 원산지가 낱낱이 써붙여진, 정육점을 겸하는 고깃집이나 오픈주방 반찬가게순수 Swift 오픈소스가 주력이다. 가공품바이너리 산출물을 판매하려면 규정된 표준 용기확장자에 담아야만 간신히 허가해 준다. 자사에서 몇 년 전부터 밀고 있는 차세대 제품라인의 정품 주방가전Xcode 프로젝트/워크스페이스가 아닌 Package.swift 형태에 한해 ‘딸깍(CLI, command line interface)’ 적용을 지원한다. SPM에서는 밀키트를 ‘패키지(swift package)’라 부른다.
그리고 나, Hive SDK(Hive SDK iOS Framework)는 안팎으로 사정이 참 많다. ‘뭘 좋아할지 몰라서 다 준비했어’ 스타일의 육중한 종합 선물세트인데다, 이 다재다능한 서비스를 어떻게든 대를 이어 운영한 전통 있는 가게라 히스토리가 넘쳐난다. 외주·협력업체와의 관계 유지개발자의 SNS, GitHub 피드를 염탐하며 신버전 체크하기 & 순수 자체제작 상품이 아니라면 그들의 최신 체인점 포맷에 의존해야 함는 필수이며, 와중에 배송이 까다로운 고객층Unity Engine을 위해 분말static 형태로만 제공하고, 재료 배합을 공개하기 어려운 비법소스도 많다오픈소스가 아님, 전용 스크립트가 존재. 타 지역Android과 창고를 공유하기 때문에 재고목록 중에는 섣불리 건들 수 없는 외국 향신료Swift가 아닌, 공용 코드도 있다.
SPM의 ‘정품’ 통에 꾸역꾸역 담아 판매하다가는 옆구리가 줄줄 새고, 그 깐깐한 조리 조건을 어떻게든 전부 맞춰놓으니, 이번엔 줄곧 너그럽던 코코아팟 측에서 파업을 선언한다. 그렇다고 조리개발와 판매배포 환경을 달리 할 수는 없다. 넘쳐나던 코코아팟-SPM 마이그레이션 가이드는 다 어디로 갔냐고? 있다. 많다. 그런데 우리가 먹을 건 없다.
교과서대로?
들어가기에 앞서, SPM 적용·배포 희망편을 간략히 서술하겠다.
●적용하기
보편적인 Xcode 프로젝트나 워크스페이스라면 Xcode 자체 화면(GUI, graphic user interface)으로 적용할 수 있다. 메뉴바 또는 타겟/프로젝트 설정의 종속성 추가 메뉴(메뉴명: ‘Add Package Dependencies…’)로 띄울 수 있다. 원하는 패키지의 저장소와 버전 범위를 입력하고 OK를 누르면 자동으로 패키지 resolve(설치, 해결)가 시작된다. 코코아팟으로 치면 Podfile의 save와 pod install 수행이 한 뭉치인 셈이다. 만약 Package.swift 파일 기반이라면 파일에서 직접 dependencies 인자를 수정하거나 swift package add-dependency 명령어로 추가할 수 있으나, 코코아팟을 함께 사용할 수는 없다.
●배포하기
swift package init 커맨드로 Package.swift 파일을 만든다. 소스코드를 오픈할 경우 모든 소스코드가 위치하는 상위 루트에서, 오픈하지 않을 경우 xcframework 산출물들이 위치하는 상위 루트에서 진행하면 된다. Package.swift 파일에는 패키지 이름, 개발 시 적용했던 종속성들, 바이너리 타겟명과 제품명을 작성한다. 만약 Package.swift 파일 기반이라면 개발용으로 사용 중인 Package.swift 자체가 배포용 명세 파일로서 기능하므로 별도 작업이 필요 없다.
이제 절망편을 살펴보겠다. 워낙 독특한 케이스가 많았기에 지면상·보안상 다 실을 수는 없지만 그나마 보편적인 사례를 서너 가지 골라보았다.
절대 양보할 수 없어
우리는 고래 싸움에 새우 등 터지지 않기 위해 몇 가지 필수 목표를 정했다. 일명 ‘룸메이트 규칙’이다.
●서드파티 버전 동기화
앞으로 서드파티 프레임워크들의 신규 버전은 SPM을 중심으로 배포될 가능성이 크고 우리의 종착지도 SPM이므로, 원래는 네이티브 개발 시 팟보다 패키지로 통일하여 적용하고자 했다. 그러나 일부 서드파티 프레임워크는 아직 코코아팟으로만 만날 수 있고, 개발 환경은 코코아팟에 최적화 되어 있었다. 팟을 패키지화 하는 자동화 방안을 연구했지만 외부 변수에 대한 안정성 우려로 보류되었다. 개발 환경에서 코코아팟과 SPM을 혼용하게 되었고, 서드파티 버전을 올릴 때마다 고칠 데가 중구난방이라 휴먼에러 가능성이 커졌다. 그래서 공용 JSON 파일을 생성하고, PodfileCocoaPods의 명세 파일과 Package.swiftSPM의 명세 파일에서 각각 이를 참조하도록 했다. 이 부분에서는 코코아팟과 SPM 모두 수고를 해주었다.
●바이너리 타겟으로 배포
SPM이 권장하는 일반적인 형태, 소스코드 자체를 패키지로 묶어 배포할 수는 없다. Hive SDK는 오픈소스가 아니기 때문이다. 따라서 바이너리 타겟(직전 섹션의 ‘소스코드를 오픈하지 않을 경우’ 예시코드 참조)으로 정의하는 것밖에는 선택지가 없다. 더군다나 SPM의 위생 조건을 충족시키기에 우리 소스코드는 순혈 Swift도 아닐 뿐더러 정형화된 파일구조도 아니다. SPM은 Swift, Objective-C, C++ 등의 코드가 한 바구니에 혼재하는 꼴을 두고 보지 못하고 여지없이 새빨간 에러를 토해내리라. 아니나다를까, SPM은 이럴 바엔 무리해서라도 바이너리 타겟을 갖겠다고 답했다. 어떻게 무리했는지는 후술하겠다.
●바이너리 에셋 공유
코코아팟으로 배포한 밀키트와 SPM으로 배포한 밀키트는 완벽히 동일해야 한다. 이 공장에서 생산한 레시피를 가지고 저 공장에서 똑같이 만드는 걸로는 부족하다. 만약의 사태를 대비하고 고객에게 균일한 품질을 보장하기 위해서는 가능한, 아니 반드시 같은 내용물을 유통처만 달리 해서 제공해야 한다. 서로 공유할 원본 바이너리 산출물은 선발주자인 코코아팟이 담당하게 되었다. 이를 위해 코코아팟이 어떤 희생을 감수했는지는 마찬가지로 후술하겠다.
삐걱삐걱 동거 생활
●역할분담과 컨닝페이퍼
코코아팟과 SPM은 각각 어떤 수고를 했는가? 팟을 패키지화 하는 안이 보류되었으므로, 코코아팟으로만 제공되고 있거나 SPM에서의 동작이 충분히 검증되지 못한 서드파티의 경우 이에 의존하는 우리 제품 또한 우선은 코코아팟으로만 배포하기로 했다. SPM이 자신 없는 건 코코아팟이 전담하기로 한 셈이다.
공용 JSON은 ‘버전 상수’를 공유하기 위해 만들었다. 로컬패키지화 또는 환경변수화 하면 더 좋았겠지만, Package.swift는 지독하게 선언적인 특수파일이라 다른 파일에 정의된 심볼을 컴파일타임에 인식하지 못한다. 코코아팟이 별도 루비파일(.rb)을 참조할 수 있다는 점과 대조적이다. 어쩔 수 없이, 런타임 실행이 조금 걸리더라도 각각 Package.swift 파일 내에서, 그리고 Podfile이 참조하는 루비파일 내에서 JSON에 정의된 ‘서드파티 버전값’을 파싱하도록 하였다.
●한 번 더 포장하기
SPM은 얼마나 무리를 했는가? 바이너리 타겟으로 정의하면 종속성 정보 주입이 불가능하다. 그리고 우리 Hive SDK는 static(정적) 형상이라 소스 뭉치(.xcframework)와 리소스 번들(.bundle)이 따로다. Product.xcframework가 ProductResource.bundle을 필요로 한다는 걸 일러줄 수 없는 것이다. 외부 종속성의 필요 여부는 말할 것도 없다.
SPM은 멀쩡한 가방에 들어있던 짐을 소분한 뒤, 메모란이 있는 지퍼백에 다시 넣어 이중포장을 했다. 일반적인 타겟 WrapperProduct를 만들고, 여기에 바이너리 타겟 Product와 그 바이너리 타겟이 필요로 하는 리소스 타겟 ProductResource가 필요하다고 명시한다.
이때 부작용이 2가지 생긴다. 하나는 ‘리소스는 바이너리 타겟화 할 수 없다’이고, 하나는 ‘고객인 개발자도 당분간 팟과 패키지를 혼용하므로, import 구문을 import Product에서 import WrapperProduct로 고치라고 강제할 수 없다’는 문제다. 전자는 현실과 타협했는데, 이어질 ‘코코아팟의 희생’에서 상술하겠다. 후자는 이중포장용 지퍼백의 더미파일에 @_exported import Product 구문Swift의 비공식 기능이지만 널리 사용되어 deprecation 우려가 거의 없다을 작성함으로써, Product 팟 없이 WrapperProduct 패키지만 import 하더라도 import Product만으로 기존과 같이 동작하게끔 했다.
●난 순살만 취급해
코코아팟은 어떤 희생을 감수했는가? 사실 산출물을 100% 공유하지는 못했다. 위에서 언급한 ‘리소스는 바이너리 타겟화 할 수 없다’는 문제 탓에 리소스에 한해서는 ‘원본의 복사본’을 가져가기로 타협한 것이다. SPM은 소스코드 산출물은 바이너리 타겟으로서 코코아팟의 xcframework를 참조, 리소스 산출물은 코코아팟의 bundle을 다운로드 받아 자신의 일반 타겟의 일부로서 끼워넣었다.
한편, 코코아팟은 나름의 기준으로 여러 제품의 xcframework와 bundle을 한데 묶어 압축한 형태로 에셋Asset을 관리하고 있었다. 여하간, SPM이 “나의 바이너리 타겟이 되려면 정해진 확장자에 단일 산출물이어야 해! 뼈 바를 줄 몰라!”라며 떼를 쓰는 바람에 코코아팟은 잘 정리되어 있던 에셋 목록을 도로 쪼개 개별 zip으로 저장하게 되었다. 단일 산출물만 존재하는 zip을 소화하기 힘든 건 코코아팟도 매한가지지만, ‘resolve’ 시 커스텀 처리가 거의 불가능한 SPM과 달리 코코아팟은 ‘prepare_command’라는 여유가 있었기 때문이다. 하여 Hive SDK의 각 제품 podspec은 아래와 같은 선처리 구문을 추가로 갖게 되었다.
SPM의 밀키트, 패키지에 들어있는 리소스의 빌드 산출물에는 ‘{패키지이름}_’ 형태의 prefix가 붙는다. 따라서 Hive SDK의 각 제품에서 리소스 번들을 찾을 때, ‘ProductResource.bundle’이 없으면 ‘Hive_ProductResource.bundle’도 탐색하도록 정규식을 수정해야 했다. 그러지 않으면 원하는 리소스가 ‘Hive_ProductResource.bundle’ 안에 떡하니 존재하는데도 ‘ProductResource.bundle’ 유무만 찾아보고는 크래시를 뱉어버린다. (아직은 코코아팟이 우리 제품의 표준 적용 방법이므로, ProductResource.bundle을 먼저 찾는다.)
빌드 산출물 경로 예시
●이 플래그, 로컬에서만 되지롱
로컬에서만 허용되는 unsafe flag…
일반적인 Xcode 프로젝트(.xcodeproj)에서는 자연스레 입력하던 옵션들을 Package.swift에서는 대단히 지저분한 군더더기인 양 ‘unsafeFlags’라는 이름으로 묶어 쓰게 해 두었는데, 이 옵션이 포함된 제품은 ‘로컬 패키지’ 및 ‘swift-tools-versionSwift 언어 버전과는 다른 개념임에 주의하자이 6.2 이상’인 경우에만 resolve 및 import 가능하다.
Hive SDK의 플러그인 제품은 Objective-C나 C++ 베이스라서 아래와 같은 명세가 꼭 필요했는데, 거의 다 구현한 마당에 이를 발견하여 뒤늦게 추가작업을 진행했다. RnD 당시 기준으로 swift-tools-version 6.2는 최신에 가까웠기 때문에 함부로 버전업 하기 어려웠으므로 플래그를 제거하는 수밖에 없었다. Umbrella 헤더 및 이들을 명시하는 각 모듈맵 파일을 제품별로 추가하였고, 이로써 ‘@import’ 속성 키워드 대신 ‘#import’ 구문을 사용해도 되도록 하였다. (원본 소스코드를 공개할 수는 없지만 키워드가 참고가 되길 바란다.)
.target( name: "MyProduct", ... cxxSettings: [ .unsafeFlags([{C++, 모듈 인식 관련 플래그들}]) ], linkerSettings: [ .unsafeFlags([{Objective-C 관련 플래그들}]) ]),
다음을 기약하며
To be continued
지금까지 뭔가 많은 걸 해낸 마냥 말했지만, 실은 스타트를 끊은 것에 불과하다. 앞으로 더 하고 싶은 일도, 개선할 점도 산더미다. 몇 가지만 추려보자면 이렇다.
첫째, static(정적) & dynamic(동적) 프레임워크 동시 배포 준비다. 언젠가는 우리도 dynamic 프레임워크를 배포해야 한다. 번들 타겟을 제품 타겟에 합쳐야 하고, 동일 제품명을 쓰려면 패키지를 분리해야 하니 저장소도 따로 써야 할 테다. 각 게임 엔진 지원을 위한 스크립트도 일정 분기를 탈 것이다. 이 과정에서 우리 Hive SDK의 개발 – 배포 – 적용지원 프로세스에 호환 문제가 없도록 네이밍과 정책을 면밀히 검토하고자 한다.
둘째, 배포 테스트 시스템 구축이다. RnD 당시 사내망 GitLab을 사용하고 있었는데, SPM은 패키지 resolve 전처리 구현이 힘들기에 네트워크 통신 헤더에 인증정보를 싣기 어려웠다. 토큰을 활용할 수 있었지만 신규 저장소 생성에도 한계가 있어 실제 원격 배포 테스트를 원 없이 수행하지 못했다. 하여 현재는 배포 및 후처리 영역이 섬세하지 못하다. 최근 공개망 서비스로 저장소를 옮기게 되었으므로 이전에 하기 힘들었던 실 배포 테스트를 양껏 해볼 생각이다.
셋째, 패키지 번들로 resolve 시간 단축하기다. 우리 Hive SDK는 파츠 탈부착이 자유로운 맥가이버다. 하지만 SPM은 개발자가 일부 파츠개별 제품만 가져오려 해도 우선 전체를 로딩해버린다. (당연히 산출물에는 포함되지 않지만 개발환경 세팅이 오래 걸린다.) ‘패키지 번들’을 사용하면 유사 도메인 제품끼리 Hive라는 정체성을 공유하면서 resolve 단위를 따로 가져갈 수 있다. 다만 패키지 자체, 즉 저장소를 쪼개야 하므로 패키지명 네이밍과 번들명 접두사 이슈에 신경 쓰고, dynamic 배포 병행 시의 저장소 수 급증 및 버전관리 부담을 최소화할 방안을 찾아야 할 것이다.
마무리
써야 할 내용은 많은데 써도 되는, 쓸모 있는 내용은 많지 않아 분량을 걱정했던 것이 무색하게 말이 길어졌다. 가볍게 방문한 분들도, 유사한 문제를 겪고 계신 분들도 이 글로부터 궁금하거나 필요했던 부분을 하나라도 찾아내셨다면 뿌듯할 것 같다. iOS 플랫폼은 쾌적하지만 제약이 많다. 최근에야 타 플랫폼으로의 확장을 시도하고 있다고는 하지만 기본적으로 특정 OS에 맞춰 설계된 언어, 시스템인 탓에 예상치 못한 애로사항을 맞닥뜨리기도 한다. 완전한 대안 없이 기존 도구/심볼이 폐기된다거나 매끄러운 생태계를 위해 커스텀을 막는 등의 상황 말이다.
그럼에도 우리는 iOS를 사랑하고, 무슨 ‘억까불합리한 상황을 일컫는 신조어‘가 있더라도 개발자로서 해결 방안을 강구해야 한다. 코코아팟과 SPM의 과도기를 험하게 겪으며 숱한 역경을 헤쳐나가야 했지만 그만큼 관성적으로만 사용하던 각 종속성 관리 도구들의 특징을 더 잘 이해할 수 있었다. 게다가 덕분에 이렇게 경험을 나눌 기회가 되어 기쁜 마음이다. 모쪼록 긴 글의 마무리를 읽어주심에 감사의 말씀 드리며 이만 글을 마친다.
이주은 기자
iOS 앱이 아닌 SDK, 즉 서비스가 아니라 서비스를 위한 서비스를 개발하면서 가장 힘든 점은 앱 개발에 비해 훨씬 무거운 버전/환경 호환성 부담과 배포 방식의 영향력입니다. ‘배포’라는 크고 긴 과제를 수행하며 막막하기도 했지만 배운 것도 나눌 것도 많아 보람차기도 합니다. 초심대로, 가진 지식을 실현하고 나누는 개발자가 되겠습니다.