Engineering
Mergeable libraries 로 29% 빠르게 앱 실행하기
2025년 1월 7일
원문에서 보기 ↗
stockcake.com
안녕하세요. 29CM 모바일팀 iOS 개발자 김중원입니다. 이번 글에서는 앱 시작 시간을 개선하기 위해 새 기술을 도입하고 이를 정량적으로 평가하기 위한 인프라를 구축하여 명확한 성과를 확인한 내용을 공유드립니다.
29CM 모바일 앱은 높은 수준의 성능 유지를 목표로 성능 지표 설정과 정량적 측정을 위해 2분기 과제로 앱 성능 측정 및 개선을 추진했습니다. 이를 위해 Sentry 를 도입하여 성능을 체계적으로 분석했고 그 결과 iOS 앱의 Cold Start 시간이 p50(상위 50%) 기준 약 1.5초, p90(상위 10%) 기준 약 2.3초로 다소 느린 상태임을 확인했습니다. 이에 따라 성능 개선의 첫 번째 과제로 앱 시작 시간 단축에 집중하기로 했습니다.
앱 시작시간 개선을 위해 가장 먼저 찾아본 문서는 애플의 Reducing your app’s launch time 였습니다. 내용 중 저에겐 생소한 섹션이 있었는데요.

Reducing your app’s launch time
Debug 빌드 환경에서는 Dynamic libraries 로 빌드 시간을 단축할 수 있고 Release 빌드 환경에서는 Static libraries 와 유사한 앱 시작 시간을 얻을 수 있다고 합니다.
29CM iOS 프로젝트는 Tuist 에서 제시하는 The Modular Architecture (TMA) 를 적용하고 있습니다. 현재 80개가 넘는 모듈로 구성되어 있고 다른 모듈에 의존하는 interface 타겟은 Dynamic library 로 되어있습니다. Mergeable libraries 를 도입하면 앱 시작 시간이 크게 개선될 것이라는 확신을 갖게 되었습니다.
Dynamic / Static library
Static library
- 앱 빌드 시 정적 링커는 라이브러리의 복사본을 앱 실행 파일에 병합합니다.
- Static library 가 많아질수록 정적 링킹 과정에서 병합해야 할 코드가 많아져 빌드 시간이 길어지고 앱 실행 파일의 크기도 커집니다.
Dynamic library
- 앱 실행 파일은 Dynamic library 의 위치 정보를 통해 앱 시작 시 해당 라이브러리를 로드합니다. 이후 동적 링커(dyld)가 런타임에서 앱과 라이브러리를 연결합니다.
- 앱 실행 파일에는 런타임에 필요한 동적 라이브러리 목록만 포함되기 때문에 Static library를 사용할 때보다 실행 파일의 크기가 줄어듭니다.
- 앱 시작 시 앱 실행 파일과 모든 Dynamic library 파일들을 로드하고 링크하는 시간이 필요하여 앱 시작 시간을 길어지게 합니다.
Static library VS Dynamic library
- 빌드 시간에 유리한 Dynamic library
- 앱 시작 시간에 유리한 Static library
- 결국 어느 방식을 선택할지는 개발자의 판단에 달려 있었습니다.
Mergeable libraries
Xcode 15에서는 위 두 라이브러리 타입의 장점을 모두 갖춘 새로운 옵션이 추가되었습니다. 이 옵션은 Debug 빌드 환경에서는 Dynamic library 를 사용하여 빌드 시간을 단축시키고 Release 빌드 환경에서는 Static library 처럼 앱 실행 파일에 병합하여 앱 시작 시간을 개선해주는 방식입니다.

Configuring your project to use mergeable libraries
Mergeable libraries 에서 라이브러리 병합 방법
라이브러리들이 병합되는 과정을 간략히 설명드리겠습니다.
-
-make_mergeable 옵션을 설정하고 라이브러리를 빌드하면 앱 바이너리의 정적 링커에 병합을 위한 메타데이터가 기록됩니다.
-
이후 -Wl,-merge_framework 옵션이 설정된 라이브러리들은 해당 메타데이터와 라이브러리를 사용하여 최종 바이너리(앱 실행 파일)가 생성됩니다.
프로젝트에 적용
적용 방법은 Automatic, Manual 두 가지가 있습니다.
1. Automatic 으로 설정

병합하려는 타겟의 Build Settings 에서 MERGED_BINARY_TYPE 을 Automatic 으로 설정하면 됩니다. 놀랍게도 이 설정만으로 Xcode 가 Release 빌드 환경에서 Dynamic library 들을 자동으로 앱 실행 파일에 병합합니다. 병합된 라이브러리들은 앱 번들에 포함되지 않습니다.
부푼 기대를 안고 설정을 적용한 후 Release 모드로 빌드를 시도해 보았습니다.
결과는 Crash…

🤔 Dynamic library 를 찾을 수 없다는 에러가 발생했습니다. 해당 라이브러리가 앱 실행 파일에 병합되지 않은 것으로 보입니다.
병합이 되지 않은 이유는 애플의 Mergeable libraries 가이드인 Configuring your project to use mergeable libraries 문서에 설명되어 있었습니다.

Configuring your project to use mergeable libraries
간접 종속성(indirect dependency) 라이브러리에 Mergeable libraries 를 적용하려면 Manual 로 설정해야 한다고 합니다.
직접 종속성(Direct dependency)과 간접 종속성(Indirect dependency)?
Configuring your project to use mergeable libraries 문서에 설명되어 있습니다.

Configuring your project to use mergeable libraries
앱 타겟에 직접 포함되어 있는지, 또는 다른 타겟을 통해 간접적으로 링크되어 있는지 여부로 확인할 수 있습니다.
2. Manual 로 설정
Crash 문제를 해결하기 위해 Manual 로 설정해 보았습니다.
먼저 위에서 설정한 Build Settings 의 MERGED_BINARY_TYPE 을 Manual 로 설정합니다.

그리고, Mergeable libraries 로 설정하려는 타겟으로 이동하여 MERGEABLE_LIBRARY 와 MAKE_MERGEABLE 을 Yes로 설정합니다.

간접 종속성으로 연결되어 있는 라이브러리는 위의 Mergeable libraries 에서 라이브러리 병합 방법에서 언급한 대로 -Wl,-merge_framework,{라이브러리명}설정을 Other Linker Flags 에 추가해줍니다.
저희는 프로젝트를 Tuist로 구성하고 있으며, 앞서 설정한 항목들을 Tuist 코드에 반영했습니다.
이로써 모든 설정이 완료되었습니다.😁
적용된 것 확인
Debug, Release 로 빌드하여 앱 실행 파일에 어떤 변화가 있는지 확인해 보겠습니다.
라이브러리 목록 확인
먼저 빌드 결과물인 앱 번들 파일 위치로 이동합니다.


Debug 빌드

패키지 내용 보기로 진입 후 앱 타겟과 같은 이름으로 되어있는 실행파일을 볼 수 있습니다.
앱 실행 파일 자체는 57KB 로 매우 작으며 링크 정보를 가지고 있는 debug.dylib 파일이 용량을 많이 차지합니다.
앱 파일이 링크하고 있는 라이브러리 목록을 확인할 수 있는 otool 명령어를 실행해 보았습니다.
$ otool -L /path/to/binary

스크린샷에서 @rpath로 시작하는 항목들은 Dynamic library 의 경로입니다. 저희가 만든 여러 모듈들이 표시되고 있습니다.
프레임워크 파일 용량도 살펴보겠습니다. Frameworks 폴더에서 찾아볼 수 있습니다.

185KB 를 차치하고 있습니다.
Release 빌드
앱 실행파일의 용량이 Debug 보다 대략 2배 커진 것을 확인할 수 있었습니다.
앱 파일이 링크하고 있는 라이브러리 목록을 확인해보겠습니다.

저희가 만든 라이브러리들은 사라지고 오픈소스 라이브러리인 Alamofire, Kingfisher 등만 남아 있습니다.

프레임워크 파일 용량도 84KB 로 많이 줄어들었습니다.
잠깐… 라이브러리가 병합되면 삭제된다고 하지 않았나요?!
애플은 이를 “삭제”라고 표현했지만 실제로는 코드나 리소스가 전혀 없는 껍데기 파일만 남게 됩니다.
잘 병합되었습니다. 😊
성과
Mergeable libraries를 적용한 후 Sentry에서 수집된 Cold Start와 Warm Start 데이터를 확인해보았습니다.
Cold Start 와 Warm Start

- Cold Start 앱이 메모리에 없을 때 실행되는 시간입니다. 앱 최초 실행 시나 앱이 프로세스에서 제거된 후 오랜 시간이 지나거나 기타 이유로 OS가 앱을 메모리에서 내린 경우가 있습니다.
- Warm Start 앱 실행에 필요한 최소한의 정보가 메모리에 남아있는 경우 실행되는 시간입니다. 최근에 프로세스에서 제거된 경우도 Warm Start 에 포함될 수 있습니다.
Sentry 집계 결과


버전 6.39.0 이상부터 Mergeable libraries가 적용되었습니다. (Sentry 비용 절감을 위해 SampleRate 를 낮게 설정하여 수집된 데이터의 양은 크지 않습니다.)
Cold Start 는 약 400~500ms 개선되었고 Warm Start 는 약 180~250ms 개선되었습니다.
마치며
Mergeable libraries 를 적용한 뒤 Sentry 를 통해 앱 시작 시간이 눈에 띄게 개선된 것을 정량적으로 확인할 수 있었습니다. 이렇게 성능 개선 작업의 효과를 구체적으로 확인할 수 있어 앞으로도 더욱 확신을 가지고 최적화 작업을 이어갈 수 있을 것 같습니다.
여기에서 다루진 않았지만 WWDC23의 Meet Mergeable Libraries 세션에서 소개한 -no_exported_symbols 옵션을 Other Linker Flags 에 추가하여 불필요한 심볼 정보를 제거하고, 링크 시간을 단축함으로써 앱 시작 시간을 개선했습니다.
이 글이 Modular Architecture 를 사용하거나 관심이 있으신 개발자 분들께 도움이 되셨으면 좋겠습니다.
참고자료
- Meet mergeable libraries - WWDC23 - Videos - Apple Developer
- Link fast: Improve build and launch times - WWDC22 - Videos - Apple Developer
- Reducing your app's launch time | Apple Developer Documentation
- Configuring your project to use mergeable libraries | Apple Developer Documentation
함께 성장할 동료를 찾습니다
저희 29CM(무신사) 모바일팀은 고객중심의 더 나은 경험과 선택에 대한 가이드를 드리기 위해 끊임없이 고민하며 도전하고 있습니다.
저희 iOS 팀은 Tuist 4.0 기반의 The Modular Architecture(TMA) 를 기반으로 아키텍처를 만들어 가고 있으며, 여러 레이어로 분산해 피쳐 작업을 할 때 써드파티부터 로깅, 네트워킹, 서비스 등 필요한 모듈들만 의존해 빠르고 효율적으로 개발을 할 수 있는 환경을 만들어 가고 있습니다.
이에 더해 SwiftLint 캐싱과 함께 Incremental Build 시간도 최적화를 해 둔 상태에서 CI 를 통해 빌드와 변경된 파일에 대해서만 SwiftLint 검증을 하도록 함으로써 원활한 협업 환경을 구축했습니다.
이외에도 AppCenter/TestFlight 로 지속적으로 사내 Canary/RC 빌드를 배포해 프로덕트 팀 동료와 개발하는 결과물들을 같이 확인하며 제품의 완성도를 올려가는 동시에, 매주 정기배포를 통해 유저에게도 지속적으로 가치를 전달하고자 합니다.
저희 팀은 위와 같은 iOS 인프라 위에서 29CM 의 여러 비즈니스 도메인과 iOS 플랫폼을 개발해 나가고 있습니다. 도전적인 과제를 풀어나가고 함께 성장할 수 있는 동료 개발자분들을 찾습니다.
많은 관심과 지원 부탁드립니다!
🚀 무신사 채용 페이지 : https://corp.musinsa.com/ko/career/
🚀 29CM iOS 채용 페이지 : https://www.musinsacareers.com/o/129830