작은 네이티브 Mac 유틸리티라면 코드가 실행 파일과 함께 배포될 때 정적 라이브러리부터 시작하세요. 별도의 런타임 구성 요소가 유용하다면 동적 라이브러리를 선택하고, Apple의 번들 패키징이 필요하다면 프레임워크를 선택하세요. 이 가이드에서는 동일한 C++ 헬퍼를 두 가지 방식으로 빌드합니다.
구체적인 작업은 로컬 macOS 데스크톱 유틸리티에서 재사용 가능한 그리드 계산 기능을 패키징하는 것입니다. 이 예에서는 합성 입력값과 터미널 명령을 사용하므로 앱 프로젝트의 형식을 선택하기 전에 컴파일, 링크, 로드를 각각 확인할 수 있습니다.
실제로 무엇을 선택하는 건가요?
결정할 사항은 두 가지입니다. 코드의 링크 방식과 결과물의 패키징 방식입니다. 일반적으로 .a로 끝나는 정적 라이브러리는 오브젝트 파일의 아카이브입니다. .dylib로 끝나는 동적 라이브러리는 별도의 런타임 이미지로 유지됩니다. 프레임워크는 라이브러리와 관련 자료를 정리하는 번들입니다. Apple은 An Apple Library Primer에서 이러한 기본 형식을 설명합니다.
프레임워크에는 정적 라이브러리도 포함할 수 있습니다. Apple은 리소스를 포함한 정적 프레임워크 생성을 Xcode 15 이상에서 지원합니다. 따라서 .framework 접미사만으로는 라이브러리가 정적으로 링크되는지 동적으로 링크되는지 알 수 없습니다. Creating a static framework를 참조하세요.
소스에서 직접 빌드하는 앱 소유 헬퍼에는 다음 판단 기준을 사용하세요.
| 요구 사항 | 시작할 때 선택할 항목 | 새로 생기는 책임 |
|---|---|---|
| 함께 다시 빌드하는 실행 파일 하나와 헬퍼 | 정적 .a |
헬퍼가 바뀌면 실행 파일을 다시 링크해야 함 |
| 별도의 런타임 구성 요소로 유지되는 헬퍼 | 동적 .dylib |
라이브러리를 제공하고 런타임 검색 경로가 작동하도록 해야 함 |
| 공개 헤더와 번들 리소스를 갖춘 Xcode 구성 요소 | 프레임워크 | 링크 방식을 별도로 선택하고 타깃 포함을 구성해야 함 |
| 여러 Apple 플랫폼용 바이너리 변형 | XCFramework | 필요한 플랫폼 변형을 빌드하고 패키징해야 함 |
이는 문서화된 형식에서 도출한 실용적인 권장 사항입니다. 어느 방식이 성능 면에서 우월하다는 뜻은 아닙니다. 이 그리드 유틸리티에는 정적 아카이브가 자연스러운 출발점입니다. 클라이언트는 하나이고, 리소스 번들은 없으며, 별도의 라이브러리를 교체할 요구 사항도 없습니다.
XCFramework는 바이너리 변형을 패키징합니다. 정적 또는 동적 링크 방식의 결정을 대신하지는 않습니다. Apple은 해당 배포 형식 안에 정적 또는 동적 프레임워크와 라이브러리를 넣을 수 있도록 허용합니다. Creating a multiplatform binary framework bundle을 참조하세요.
컨테이너를 선택하기 전에 호환성을 확인하세요
파일 이름이나 CPU 아키텍처가 일치하는 것만으로는 충분하지 않습니다. macOS, Mac Catalyst, iOS는 서로 다른 빌드 플랫폼입니다. 한 플랫폼용으로 컴파일한 arm64 라이브러리를 다른 플랫폼의 실행 파일에 그대로 링크할 수 없습니다. Apple은 An Apple Library Primer에서 플랫폼과 아키텍처를 명확히 구분합니다.
이 작업에서는 다음 설정을 일관되게 유지하세요. macOS 타깃, 필요한 CPU 아키텍처, 최소 macOS 버전, 호환되는 언어 및 런타임 설정입니다. 범용 Mac 결과물을 만들려면 지원하는 각 아키텍처에 맞는 종속성 슬라이스가 필요합니다. Apple의 Building a universal macOS binary에서는 실행 파일뿐 아니라 라이브러리를 다시 빌드하는 방법도 다룹니다.
C++ 호환성도 별도로 살펴봐야 합니다. -std=c++17을 선택하면 언어 모드를 지정하는 것이며, 독립적으로 빌드한 구성 요소 사이의 바이너리 호환성을 보장하는 것은 아닙니다. LLVM은 libc++의 ABI 보장과 공급업체 구성 선택 사항을 문서화하고 있으며, 여기에는 타입 레이아웃을 바꾸는 설정도 포함됩니다. 앱 소유 종속성이라면 앱과 동일한 툴체인 및 일관된 설정으로 다시 빌드하는 것이 좋은 기본 원칙입니다. libc++’s ABI Guarantees를 참조하세요.
이 예에서는 C 호환 헤더를 통해 함수 하나를 노출하고 C++로 구현합니다. 보호된 extern \"C\" 선언은 C++ 컴파일 시 C 언어 링크를 지정합니다. 임의의 C++ 클래스를 C 인터페이스로 바꾸거나 바이너리를 플랫폼 간에 이식 가능하게 만들지는 않습니다. C++ 작업 초안은 Linkage specifications에서 이 경계를 정의합니다.
헬퍼 하나를 정적 라이브러리와 동적 라이브러리로 각각 빌드하기
Apple 개발자 도구와 macOS SDK가 설치된 Mac이 필요합니다. 전체 Xcode에는 명령줄 도구가 포함되어 있고, Apple은 독립형 Command Line Tools 패키지도 제공합니다. 둘 다 설치되어 있지 않다면 xcode-select --install을 실행하고 설치 프로그램을 완료한 뒤 계속하세요. Installing the command-line tools를 참조하세요.
아래 전체 스크립트는 새 임시 디렉터리를 만들고 합성 소스 파일만 사용합니다. 터미널에서 실행하세요. 현재 셸 환경의 아키텍처와 예시용 배포 최소 버전인 macOS 11.0을 명시적으로 대상으로 지정합니다. 단일 아키텍처 연습이므로 프로젝트에 필요한 최소 버전이 다르면 변경하세요.
예제를 검토했지만 연구 환경이 Linux이고 clang++도 없어 Mac 빌드를 실행할 수는 없었습니다. 표시된 결과는 예상 결과이며 실제 테스트에서 기록된 출력은 아닙니다.
bash <<'SH'
set -eu
work_dir=$(mktemp -d "${TMPDIR:-/tmp}/mac-library-demo.XXXXXX")
cd "$work_dir"
printf 'Example directory: %s\n' "$work_dir"
mkdir include src build
mkdir -p build/static build/dynamic/lib
cat > include/grid.h <<'EOF'
#ifndef GRID_H
#define GRID_H
#ifdef __cplusplus
extern "C" {
#endif
/* Inputs: positive dimensions whose product fits in int. */
int grid_cells(int rows, int columns);
#ifdef __cplusplus
}
#endif
#endif
EOF
cat > src/grid.cpp <<'EOF'
#include "grid.h"
int grid_cells(int rows, int columns) {
return rows * columns;
}
EOF
cat > src/main.cpp <<'EOF'
#include "grid.h"
#include <cstdio>
int main() {
const int cells = grid_cells(3, 4);
std::printf("Grid cells: %d\n", cells);
return cells == 12 ? 0 : 1;
}
EOF
sdk_path=$(xcrun --sdk macosx --show-sdk-path)
build_arch=$(uname -m)
case "$build_arch" in
arm64|x86_64) ;;
*) printf 'Unexpected architecture: %s\n' "$build_arch" >&2; exit 1 ;;
esac
xcrun --sdk macosx clang++ \
-std=c++17 -arch "$build_arch" -isysroot "$sdk_path" \
-mmacosx-version-min=11.0 -Iinclude \
-c src/grid.cpp -o build/grid.o
xcrun --sdk macosx ar rcs build/static/libGrid.a build/grid.o
xcrun --sdk macosx clang++ \
-std=c++17 -arch "$build_arch" -isysroot "$sdk_path" \
-mmacosx-version-min=11.0 -Iinclude \
src/main.cpp build/static/libGrid.a -o build/static/grid-demo
xcrun --sdk macosx clang++ \
-arch "$build_arch" -isysroot "$sdk_path" \
-mmacosx-version-min=11.0 -dynamiclib build/grid.o \
-Wl,-install_name,@rpath/libGrid.dylib \
-o build/dynamic/lib/libGrid.dylib
xcrun --sdk macosx clang++ \
-std=c++17 -arch "$build_arch" -isysroot "$sdk_path" \
-mmacosx-version-min=11.0 -Iinclude \
src/main.cpp build/dynamic/lib/libGrid.dylib \
-Wl,-rpath,@loader_path/lib -o build/dynamic/grid-demo
./build/static/grid-demo
./build/dynamic/grid-demo
xcrun --sdk macosx ar t build/static/libGrid.a
xcrun --sdk macosx otool -L build/static/grid-demo
xcrun --sdk macosx otool -L build/dynamic/grid-demo
xcrun --sdk macosx otool -l build/dynamic/grid-demo
xcrun --sdk macosx lipo -archs build/dynamic/grid-demo
SH
두 번 모두 Grid cells: 12가 출력되어야 합니다. 이 계산은 일부러 간단하게 구성했습니다. 3개 행에 4개 열을 곱합니다. 헬퍼의 명시적 입력 계약은 곱을 int에 담을 수 없는 크기를 제외합니다. 이 예제는 일반적인 입력 검증 API를 구현하지 않습니다.
정적 빌드에서 확인할 내용
-c 명령은 실행 파일을 링크하지 않고 grid.o을 생성합니다. 아카이브 명령은 이 오브젝트 파일을 libGrid.a에 패키징합니다. r, c, s 옵션은 각각 삽입/교체, 아카이브 생성, 심볼 인덱스 생성을 요청합니다. Clang은 컴파일러 명령 가이드에서 컴파일 단계를 문서화하며, LLVM은 llvm-ar — LLVM archiver에서 아카이브 작업을 설명합니다.
클라이언트는 아카이브의 명시적 경로를 사용해 링크합니다. 정적 버전과 동적 버전이 모두 있을 때 어떤 입력을 선택했는지 명확하게 알 수 있습니다. 예상 아카이브 목록에는 grid.o이 포함되고, 실행 파일의 종속성 목록에는 libGrid.a에 대한 런타임 종속성이 없어야 합니다.
여기서 정적 링크를 적용한 대상은 Grid 헬퍼입니다. 그렇다고 결과 실행 파일이 macOS 런타임 라이브러리와 무관해지는 것은 아닙니다. otool -L이 보고하는 나머지 항목도 확인하세요. Apple은 An Apple Library Primer에서 빌드 시점의 링커와 런타임 로더를 구분합니다.
동적 빌드에서 확인할 내용
-dynamiclib 명령은 별도의 라이브러리를 생성합니다. 설치 이름은 @rpath/libGrid.dylib입니다. 실행 파일은 실행 파일의 런타임 경로 항목이 인접한 lib 디렉터리를 가리키도록 @loader_path/lib를 기록합니다. Clang은 명령줄 인수 참조에서 관련 드라이버 옵션을 나열합니다.
동적 클라이언트에서 예상되는 otool -L 출력에는 @rpath/libGrid.dylib가 포함됩니다. 로드 명령 출력에는 @loader_path/lib가 있는 LC_RPATH 항목이 있어야 합니다. Apple은 Run-Path Dependent Libraries에서 설치 이름과 실행 파일 런타임 경로가 함께 작동하는 방식을 설명합니다.
이렇게 구체적인 패키징 테스트를 할 수 있습니다. build/dynamic 디렉터리 전체를 내부 구조를 유지한 채 옮긴 다음, 새 위치에서 실행 파일을 실행하세요. 실행 파일만 옮기면 Grid 종속성은 원래 위치에 남습니다. 링크에 성공했다는 것은 빌드 입력을 찾았다는 뜻이지 런타임 패키지가 완전하다는 뜻은 아닙니다.
헬퍼를 언제 프레임워크로 만들면 좋을까요?
번들 구조가 프로젝트에 도움이 될 때 프레임워크를 선택하세요. 예를 들어 구성 요소에 공개 헤더와 리소스가 필요하고, 이를 Xcode에서 함께 관리해야 하는 경우입니다. 계산만 수행하는 이 헬퍼의 경우 프레임워크를 사용해도 추가 요구 사항은 충족되지 않고 패키징 작업만 늘어납니다.
Xcode 15 이상에서 정적 프레임워크를 만드는 절차는 다음과 같이 Apple이 문서화했습니다.
File > New > Target을 선택하고 macOS를 고른 다음Framework템플릿을 사용합니다.- 새 타깃의
Build Settings를 선택하고Mach-O Type을Static Library로 변경합니다. - 구현 파일을
Compile Sources에, 리소스를Copy Bundle Resources에 추가합니다. - 내보낼 헤더를
Headers빌드 단계에 추가하고 공개 헤더를 우산 헤더에 포함합니다. - 프레임워크를 이를 사용하는 앱에 추가하고 Apple이 문서화한 임베딩 구성을 따릅니다.
리소스를 포함하는 정적 프레임워크의 경우 Apple은 Embed & Sign을 지정합니다. Xcode는 이미 링크된 메인 바이너리는 임베드 번들에서 제외하고 번들 자료는 유지합니다. 모든 정적 프레임워크를 임베딩이 필요 없는 아카이브처럼 취급하지 말고 Creating a static framework를 따르세요.
바이너리 변형을 배포한다면 XCFramework를 사용하세요. 기존의 독립형 .a 파일에 대해 Apple은 xcodebuild -create-xcframework에 전달할 -library와 -headers 입력을 문서화하고 있으며, 아카이브를 프레임워크인 것처럼 수동으로 꾸미지 말라고 권고합니다. xcodebuild에는 전체 Xcode가 필요합니다. Creating a multiplatform binary framework bundle과 Installing the command-line tools을 참조하세요.
Swift를 사용하면 선택이 어떻게 달라지나요?
Swift에서는 기본 링크 방식 결정에 모듈 소비와 진화 요구 사항이 추가됩니다. ABI 안정성, 모듈 안정성, 라이브러리 진화는 서로 다른 호환성 문제를 다루며, .framework 컨테이너가 이를 자동으로 활성화하지는 않습니다.
Swift 지침에 따르면 프레임워크와 클라이언트를 함께 빌드하고 배포하는 경우 일반적으로 라이브러리 진화를 활성화하지 않아야 합니다. 별도로 빌드하고 업데이트하는 바이너리 프레임워크라면 첫 릴리스부터 진화 지원 여부를 결정하세요. Xcode에서 BUILD_LIBRARY_FOR_DISTRIBUTION은 라이브러리 진화와 모듈 안정성을 활성화합니다. 이러한 선택이 성능 특성과 허용되는 API 변경에 미치는 영향은 Library Evolution in Swift에서 설명합니다.
이러한 보장은 문서에 명시된 플랫폼 범위 안에서만 적용하세요. Swift 문서는 Apple 플랫폼에서의 컴파일러 간 바이너리 호환성을 설명합니다. 이 약속을 Swift가 지원하는 모든 플랫폼으로 확장하거나 임의의 API 변경을 허용하는 것으로 해석하지 마세요. 이 예의 C 호환 헤더도 완전한 Swift 모듈 설정은 아닙니다.
빌드 실패를 발생 단계 순서대로 진단하기
실패한 단계를 기준으로 다음에 확인할 항목을 좁히세요.
| 증상 | 먼저 확인할 항목 | 다음 조치 |
|---|---|---|
컴파일러가 grid.h을 찾지 못함 |
include 아래에 헤더가 있는지 |
작업 디렉터리와 -Iinclude를 확인 |
| Grid 함수에 대한 정의되지 않은 참조 | 구현과 선언이 일치하는지 | 의도한 아카이브 또는 동적 라이브러리가 링크 명령에 있는지 확인 |
| 아키텍처 또는 플랫폼 불일치 | 실제 종속성 슬라이스와 빌드 플랫폼 | 클라이언트의 macOS 타깃에 맞춰 종속성을 다시 빌드 |
| 실행 파일은 링크되지만 Grid를 로드할 수 없음 | 기록된 종속성과 런타임 경로 | otool -L, otool -l, 패키징된 디렉터리 구조를 비교 |
| Intel과 Apple silicon 빌드의 동작이 다름 | 모든 종속성의 아키텍처 지원 범위 | 각 바이너리를 검사하고 의도한 아키텍처를 테스트 |
Clang의 컴파일러 명령 가이드는 컴파일과 링크를 구분합니다. Apple의 Run-Path Dependent Libraries는 런타임 확인 과정을 설명하고, Building a universal macOS binary는 lipo -archs를 사용한 아키텍처 검사를 문서화합니다. 이 구분에 따라 진단 순서를 정하면 유용합니다. 헤더 경로를 바꿔도 런타임 라이브러리 누락은 해결되지 않습니다.
이 합성 그리드 유틸리티에는 구체적인 패키징 또는 런타임 요구 사항이 생길 때까지 정적 아카이브를 사용하세요. 그런 요구 사항이 생기면 동일한 간단한 계산을 빌드 확인에 활용하고 선택한 라이브러리, 타깃 호환성, 최종 디렉터리 구조를 각각 확인하세요.
출처
- An Apple Library Primer — Apple Developer Technical Support.
- Creating a static framework — Apple.
- Creating a multiplatform binary framework bundle — Apple.
- Building a universal macOS binary — Apple.
- Installing the command-line tools — Apple.
- Run-Path Dependent Libraries — Apple Documentation Archive.
- clang — the Clang C, C++, and Objective-C compiler — LLVM Project.
- Clang command line argument reference — LLVM Project.
- llvm-ar — LLVM archiver — LLVM Project.
- libc++’s ABI Guarantees — LLVM Project.
- Linkage specifications — C++ Working Draft, rendered by eel.is.
- Library Evolution in Swift — Swift.org.