Xcode 27, ld64 제거와 고유 모듈 이름 요구
Apple은 한 문장으로 링커 하나를 퇴역시켰습니다. “ld64 링커가 제거되었으며 -ld_classic 옵션은 더 이상 지원되지 않습니다.”1 그런데 이제 사라진 이 플래그는 애초에 Apple이 개발자들에게 추가하라고 안내했던 플래그입니다. Xcode 15의 릴리스 노트가 링커 버그 두 건의 우회책으로 -Wl,-ld_classic을 제시했기 때문입니다.3
핵심 요약
- Xcode 27은 ld64를 제거하고
-ld_classic을 더 이상 받아들이지 않습니다.1 Apple은 Xcode 15 노트에서 약한 심볼 크래시와 LTO 임포트 버그의 해법으로 이 플래그를 처방했고, Xcode 16에서 지원 중단을 예고했으며, Xcode 26에서는 아무 언급도 하지 않았습니다.3410 - 두 번째 파손은 Swift 컴파일러 쪽에 있습니다. Apple이 의존성 스캔을 하나의 공유 작업으로 묶으면서 “단일 Swift 의존성 스캔 작업에서 도달 가능한 모든 Clang 모듈은 고유한 모듈 이름을 가져야 한다”는 조건이 붙었습니다.2 Apple은 두 군데에서 표현을 흐립니다. 스캔이 오류를 “보고할 수 있다”고 했고, 이전 스캐너가 이름 중복을 “허용했을 수 있다”고 했습니다.2
- 두 변경 모두 배포 타깃이나 SDK 선택이 아니라 툴체인 업그레이드 시점에 터지며, 런타임 요소는 어느 쪽에도 없습니다. 노출되는 대상은 벤더링된 바이너리, CocoaPods, 또는 Swift/Objective-C/C++가 뒤섞인 대형 코드베이스입니다.
- Apple이 쓴 “할 수 있다”는 표현은 조심스러운 문장 다듬기가 아닙니다. Xcode 26.6에서 같은 이름을 선언한 모듈 맵 두 개를 하나의 검색 경로에 올려두고 스캐너를 20번 돌렸더니, 9번은 SIGSEGV로 크래시했고 5번은 abort로 끝났으며 6번은 정상 종료했습니다.8
- Apple이 직접 지목한 사례인 “SDK 모듈을 다시 선언하는 벤더링 모듈 맵”은 지금은 아무 소리 없이 컴파일되면서 진짜 모듈을 가려버립니다. 제가 만든
SQLite3벤더링 shim은 종료 코드 0으로 타입 검사를 통과했고,sqlite3_open은 존재하지 않는 심볼이 되었습니다.8 - 감사한 프로젝트 7개 전체에서
-ld_classic은 0건,OTHER_LDFLAGS는 모든 프로젝트에서 설정되지 않은 상태였으며, 모듈 맵 391개 중 이름이 겹치는 경우도 0건이었습니다. 어느 저장소에도 사람이 직접 작성한 모듈 맵이 없기 때문입니다.9
두 항목 모두 Swift 6.4 및 27 계열 SDK와 함께 Xcode 27 beta 4 릴리스 노트에 실려 있습니다.11 베타 문서라는 점을 감안하고 읽으시기 바랍니다.
Apple이 추가하라고 했던 그 플래그
이야기는 링커 재작성에서 시작합니다. Xcode 15는 새 링커를 발표하면서 “모든 macOS, iOS, tvOS, visionOS 바이너리”의 기본값으로 삼았고, 퇴역 조건을 한 절로 못박았습니다. “클래식 링커는 -ld64로 명시적으로 요청할 수 있으며, 향후 릴리스에서 제거될 예정입니다.”3
새 링커에는 버그가 따라왔고, Apple은 같은 페이지에서 탈출구를 두 번 문서화했습니다. 첫 번째 Known Issue는 이렇습니다. “약한 정의를 가진 심볼을 사용하는 바이너리는 iOS 14/macOS 12 이하에서 런타임에 크래시합니다. 약한 심볼을 광범위하게 사용하는 C++ 프로젝트가 주로 영향을 받습니다.” Apple이 제시한 우회책은 배포 타깃을 올리거나 “OTHER_LDFLAGS 빌드 설정에 -Wl,-ld_classic을 추가”하는 것이었습니다.3 두 번째는 LTO 오브젝트 파일에서 약한 심볼 임포트가 약하지 않은 것으로 링크되는 문제를 다루면서, 같은 설정에 “-Wl,-weak_reference_mismatches,weak 또는 -Wl,-ld_classic“을 넣으라고 안내했습니다.3
두 버그 모두 C++ 프로젝트를 가장 심하게 때렸고, 그래서 지금까지 이 플래그를 달고 있는 쪽이 어디인지도 설명됩니다. 크래시를 멈춰준 설정을 다시 들여다보는 사람은 없습니다.
Xcode 16은 한 문장으로 예고했습니다. “-ld_classic 링커 옵션은 지원이 중단되었으며 향후 릴리스에서 제거될 예정입니다.”4 그 뒤로는 침묵이었습니다. Xcode 26 릴리스 노트에서 ld_classic, ld64, 클래식 링커를 모두 찾아봤지만 아무것도 나오지 않았습니다.10
현재 툴체인은 이 플래그를 여전히 받아들이고, 여전히 불평합니다. Xcode 26.6에서 간단한 C 프로그램을 이 플래그로 링크해 봤습니다.7
xcrun clang hello.c -o hello -Wl,-ld_classic
ld: warning: -ld_classic is deprecated and will be removed in a future release
종료 상태는 0입니다. 바이너리는 링크됩니다. -Wl,-ld64로 바꿔도 -ld_classic을 지목하는 똑같은 경고가 나오므로, Apple이 사용한 두 표기 모두 오늘날 같은 코드 경로로 흘러갑니다.7 Apple의 Xcode 27 노트는 -ld_classic만 언급하고 있고, -ld64도 같은 방식으로 실패하는지는 확인할 수 없었습니다.
“제거”라는 단어를 복잡하게 만드는 사실이 하나 있습니다. Xcode 26.6의 링커에 xcrun ld -v로 신원을 물어보면, 어떤 아키텍처를 위임하는지 알려줍니다.7
@(#)PROGRAM:ld PROJECT:ld-1267
BUILD 16:38:58 Jun 8 2026
configured to support archs: armv6 armv7 armv7s arm64 arm64e arm64_32 i386 x86_64 x86_64h armv6m armv7k armv7m armv7em armv8m.main armv8.1m.main
will use ld-classic for: armv6 armv7 armv7s i386 armv6m armv7k armv7m armv7em
ld-classic은 툴체인 안에 자체 man 페이지까지 갖춘 실제 바이너리로 존재하고, 링커는 여덟 개 아키텍처를 여전히 여기로 넘깁니다.7 그중 현재 앱용 SDK에 살아남은 것은 하나도 없습니다. iPhoneOS 26.5는 arm64e와 arm64만 선언하고, watchOS SDK가 arm64_32를 추가할 뿐입니다.7 목록에 남은 것은 32비트 ARM, i386, 그리고 Cortex-M 임베디드 타깃입니다. 이들이 SDK에 없다는 사실을 두고 앱 개발자에게는 이번 제거가 안전하다고 읽는 것은 Apple의 주장이 아니라 제 추론입니다.
grep을 믿지 않고 플래그를 찾는 법
-ld_classic은 OTHER_LDFLAGS에 들어 있으므로, 파일에서 문자열을 찾는 대신 빌드 시스템에 최종적으로 어떤 값으로 해석되는지 물어보십시오.
xcodebuild -showBuildSettings \
-project YourApp.xcodeproj \
-configuration Release -sdk iphoneos 2>/dev/null \
| grep -E "OTHER_LDFLAGS|SUPPORTED_PLATFORMS"
Reps 프로젝트에 실행하면 한 줄이 돌아옵니다.9
SUPPORTED_PLATFORMS = appletvos appletvsimulator iphoneos iphonesimulator macosx
OTHER_LDFLAGS는 아예 나타나지 않는데, 그게 바로 답입니다. 설정이 비어 있으니 이 경로로 링크 단계에 전달되는 링커 플래그도 없습니다. 해석된 빌드 설정에서의 부재는 텍스트 검색에서의 부재가 주지 못하는 정보를 담고 있습니다. 플랫폼을 확인할 때도 SUPPORTED_PLATFORMS를 보아야 하며, *_DEPLOYMENT_TARGET은 보지 마십시오. Xcode는 실제로 존재하는 대상인지와 무관하게 그 값을 기록합니다.
이번 감사에서 함정 세 개가 가짜 ‘깨끗함’을 만들어냈고, 세 경우 모두 아무것도 출력하지 않은 채 성공적으로 종료합니다. timeout은 기본 macOS에 존재하지 않기 때문에 xcodebuild를 이 명령으로 감싸면 종료 코드 127과 빈 출력이 돌아오는데, 이는 링커 플래그가 없는 프로젝트처럼 보입니다. zsh에서는 따옴표 없는 --include=*.pbxproj가 grep에 닿기 전에 글롭 확장되어, 결과 0건을 보고하는 대신 “no matches found”로 명령이 죽습니다. 그리고 find는 시작 지점으로 준 심볼릭 링크를 따라가지 않는데, xcrun --sdk iphoneos --show-sdk-path가 바로 심볼릭 링크를 돌려주기 때문에 문제가 됩니다. find "$SDK" -name '*.modulemap'은 아무것도 찾지 못하지만, find -H "$SDK"는 파일 266개를 찾아냅니다.9 패턴에는 따옴표를 씌우고, -H를 넘기고, 0이라는 결과를 믿기 전에 그 명령이 있다고 확신하는 대상을 실제로 잡아내는지 확인하십시오.
직접 관리하는 타깃은 이 플래그를 project.pbxproj나 .xcconfig에 담아 둡니다. CocoaPods 프로젝트라면 post_install 훅을 통해 플래그가 주입될 수도 있는데, 이 경로는 개발자가 편집하는 어떤 파일에도 기록되지 않습니다. 제가 감사한 프로젝트군에는 둘 다 없어서 마지막 경로는 검증하지 못한 채 남았습니다.9
모듈 이름 규칙, 그리고 Apple이 남긴 두 개의 여지
두 번째 파손은 성능 개선으로 위장한 채, Deprecations가 아니라 New Features 항목에 실려 도착합니다. Apple의 Swift 컴파일러 항목 전문은 다음과 같습니다.
Swift 의존성 스캐너는 단일 의존성 스캔 작업 중 Clang 모듈을 조회할 때 중복된 준비 작업과 헤더 검색을 피하도록 최적화되어, 스캔 성능이 크게 향상되었습니다. 이 변경의 결과로, 단일 Swift 의존성 스캔 작업에서 도달 가능한 모든 Clang 모듈은 고유한 모듈 이름을 가져야 합니다. 같은 스캔에서 보이는 두 모듈 맵이 동일한 이름의 Clang 모듈을 선언하면, 스캔이 오류를 보고할 수 있습니다. 이전에는 스캐너가 이름 중복을 허용했을 수 있습니다. 가장 흔한 경우는 헤더 검색 경로상의 두 곳 이상에서 같은 Clang 모듈 이름을 제공하는 프로젝트나 SDK, 그리고 SDK 모듈을 다시 선언하는 module.modulemap을 포함해 배포되는 벤더링된 서드파티 소스입니다.2
여기서 네 가지를 갈라서 볼 필요가 있습니다. 첫째, Apple은 요구 범위를 디스크 전체가 아니라 하나의 스캔이 도달할 수 있는 범위로 한정합니다. 둘째, 결과를 “오류를 보고할 수 있습니다”로 서술하고, 과거 동작에 대해서도 같은 방식으로 여지를 남깁니다. 셋째, 문제를 일으키는 두 형태를 명시합니다. 헤더 검색 경로상 두 위치에서 제공되는 하나의 모듈 이름, 그리고 SDK 모듈을 다시 선언하는 벤더링된 module.modulemap입니다. 넷째, 방아쇠는 툴체인입니다. 배포 타깃이나 SDK 버전을 언급하는 대목이 전혀 없기 때문입니다.
규칙 자체는 이번 최적화보다 오래되었습니다. Clang의 모듈 맵 언어 문서는 이를 분명히 적어 두었습니다. “각 모듈은 단 하나의 정의를 가져야 한다.”6 문서가 끝내 말하지 않는 것은 이 규칙을 어겼을 때 벌어지는 일이고, 그 침묵에는 그럴 만한 이유가 있었습니다. 툴체인의 답은 하나의 동작이 아니라 여러 개이기 때문입니다.
이름 충돌이 실제로 일으키는 일
Apple이 남긴 애매한 표현 때문에 실패를 직접 보고 싶어졌고, 그래서 가장 작은 재현을 만들었습니다. 디렉터리 두 개에 각각 같은 모듈을 선언하는 module.modulemap을 넣었습니다.8
A/module.modulemap B/module.modulemap
module Widget { module Widget {
header "widget.h" header "widget.h"
export * export *
} }
두 디렉터리를 모두 검색 경로에 올린 상태에서 단순 타입 검사를 돌리면, 20번 중 20번 똑같은 방식으로 실패합니다.8
B/module.modulemap:1:8: error: redefinition of module 'Widget'
1 | module Widget {
| `- error: redefinition of module 'Widget'
A/module.modulemap:1:8: note: previously defined here
빌드 로그에서 찾아야 할 문자열은 redefinition of module이고, Xcode 26.6의 clang은 이미 이 메시지를 냅니다. 검색 경로 하나를 빼면 같은 컴파일이 성공하는데, 이는 방아쇠가 디스크상의 존재 여부가 아니라 하나의 컴파일에서 동시에 보이는지 여부임을 확인해 줍니다.8
의존성 스캐너는 다르게 동작하고, 그 차이가 바로 Apple이 “할 수 있다”라고 쓴 이유 전부입니다. 같은 모듈 맵 두 개를 swiftc -scan-dependencies로 20번 통과시키자 결과가 세 갈래로 갈렸습니다. 9번은 SIGSEGV 크래시, 5번은 abort, 6번은 깨끗한 성공이었습니다.8 크래시한 실행의 스택 추적은 performParallelClangModuleLookup을 거치는데, 이는 Apple이 교체했다고 설명한 병렬 조회에서의 경쟁 상태와 맞아떨어집니다. 이 비결정성을 그 경쟁 상태 탓으로 돌리는 것은 Apple의 진술이 아니라 스택 추적에 대한 제 해석입니다.
Apple이 지목한 두 번째 사례는 조용한 쪽입니다. iPhoneOS SDK에 실재하는 모듈인 SQLite3를 선언하는 벤더링 모듈 맵을 작성해 검색 경로에 올리고, 그것을 대상으로 컴파일했습니다.8
Vendor/module.modulemap
module SQLite3 {
header "shim.h"
export *
}
컴파일은 성공했습니다. 종료 코드 0, 경고 없음, note 없음, 어떤 종류의 진단도 없었습니다. 그다음 같은 구성에서 sqlite3_open을 요청해 봤습니다.8
error: cannot find 'sqlite3_open' in scope
벤더링 디렉터리를 검색 경로에서 빼면 동일한 파일이 컴파일됩니다. 벤더링 모듈 맵이 SDK의 SQLite3를 완전히 가려버렸는데도 툴체인은 아무 말이 없었습니다. 즉 오늘날 벤더링 재선언 사례는 요란하게 실패하지 않습니다. 임포트했다고 믿었던 모듈에서 API 하나가 사라진 형태로 실패합니다. Apple의 노트는 Xcode 27의 스캔이 여기서 “오류를 보고할 수 있다”고 말하는데, 그렇게 되면 조용한 가림이 빌드 실패로 바뀝니다.
제 빌드 환경은 Xcode 26.6(빌드 17F113)이므로, 위의 모든 진단은 이전 툴체인에서 나온 것입니다.78 Xcode 27의 오류 문구는 보고할 수 없고, 지어내지도 않았습니다. 다만 진단 장치도, grep으로 찾을 문자열도, Apple이 이제 없애겠다고 말한 그 관용도 모두 오늘 존재합니다.
중복 모듈 이름 감사하기
선언된 모듈 이름을 열거하는 일은 한 줄짜리 grep처럼 보이지만, 모듈 맵 언어의 두 가지 구문이 잘못된 답을 만들어냅니다.
extern module Foo "Foo.modulemap"은 정의가 아니라 전방 참조인데, Apple의 SDK는 하나의 모듈 맵에서 이 구문을 79번 사용합니다.7 순진한 스캔은 이 참조와 실제 정의를 Foo에 대한 선언 두 개로 셉니다. 둘째로 module Darwin.C { ... }는 점이 들어간 모듈 ID로 다른 곳에서 선언된 모듈을 확장합니다. SDK 모듈 맵 13개가 module Darwin.something 형태로 시작하는데, 첫 구성 요소를 최상위 선언으로 읽으면 이 13개가 실제 Darwin 선언과 뭉뚱그려져 파일 14개짜리 충돌 하나로 보고됩니다.7 두 오탐 모두 제 첫 초안에 등장했습니다.
살아남은 명령은 주석을 처리하고, 중첩된 explicit module 서브모듈이 걸리지 않도록 중괄호 깊이를 추적하며, 공백이 들어간 경로도 다룹니다.
find -H "${1:-.}" \( -name '*.modulemap' -o -name 'module.map' \) \
-not -path '*/build/*' -not -path '*/.build/*' \
-not -path '*/DerivedData*/*' -print0 |
while IFS= read -r -d '' map; do
awk -v f="$map" '
{ line = $0; sub(/\/\/.*/, "", line)
if (depth == 0 && line !~ /extern[[:space:]]+module/ &&
match(line, /(^|[[:space:]])module[[:space:]]+[A-Za-z_][A-Za-z0-9_.]*/)) {
n = substr(line, RSTART, RLENGTH); sub(/.*module[[:space:]]+/, "", n)
if (n !~ /\./) print n "\t" f
}
for (i = 1; i <= length(line); i++) {
c = substr(line, i, 1)
if (c == "{") depth++; else if (c == "}") depth--
}
}' "$map"
done | sort -u > /tmp/modnames.txt
cut -f1 /tmp/modnames.txt | uniq -d | while read -r n; do
echo "duplicate: $n"
awk -F'\t' -v n="$n" '$1==n {print " " $2}' /tmp/modnames.txt
done
확보할 수 있는 가장 강력한 음성 대조군은 Apple 자신의 SDK입니다. 툴체인이 동작하려면 애초에 SDK가 이 규칙을 만족해야 하기 때문입니다. iPhoneOS 26.5 SDK를 대상으로 실행하면 usr/include 아래에서 최상위 선언 1,099개, 프레임워크 모듈 맵 전체에서 205개를 찾아내며, 양쪽 모두 중복은 0건입니다.7 충돌하도록 만든 테스트 픽스처를 대상으로 하면 중복된 이름과 해당 파일 두 개를 정확히 지목합니다.8
제외 조건이 결정적입니다. -not -path '*/build/*'는 .build/를 제외하지 못합니다. 패턴이 디렉터리 이름과 글자 그대로 일치해야 하는데, SwiftPM은 생성된 모듈 맵을 .build 안에 수십 개씩 써 넣습니다. .build를 남겨 두자 프로젝트 7개를 통틀어 유일한 “중복”이 나왔습니다. GrappleCore가 파일 6개, GrappleRender가 3개였고, 전부 SwiftPM 출력물이었으며, 서로 다른 빌드 루트에 걸친 Swift 타깃 두 개에서 나온 것이었습니다.9 이 9개는 바이트 단위로 동일하지도 않은데, 그 이유가 시사적입니다. 각 파일은 같은 모듈 이름을 자기 빌드 루트에서 생성된 헤더의 절대 경로로 감싸고 있어서, 문자열 하나만 다르고 의미 있는 차이는 전혀 없습니다. 이것들을 충돌이라고 부르는 감사는 진짜 문제에 닿기도 전에 신뢰를 다 써버립니다.
Xcode는 더 명확한 형태로 같은 오탐을 만들어냅니다. 하나의 DerivedData 트리 안에서 각 Swift 패키지의 생성 모듈 맵을 GeneratedModuleMaps-iphonesimulator/와 해당 타깃의 중간 산출물 디렉터리에 두 번 쓰는데, 두 번 모두 상대 헤더 경로를 사용합니다. 어떤 프로젝트의 트리에서는 이름 15개가 각각 두 번씩 나타났고, 15쌍 전부 바이트 단위로 동일했습니다.9 감사 범위는 빌드 산출물이 아니라 소스로 한정하십시오.
프로젝트 일곱 개에 실제로 들어 있던 것
Xcode 26.6 환경에서 Xcode 프로젝트 7개를 대상으로 두 감사를 모두 돌렸습니다. 정직한 결과는 완전한 무소득입니다.9
| 프로젝트 | Swift 파일 | Objective-C / C++ / C | 의존성 | -ld_classic |
소스 모듈 맵 |
|---|---|---|---|---|---|
| Reps | 77 | 0 | 로컬 SPM 2개 | 0 | 0 |
| Return | 57 | 0 | 없음 | 0 | 0 |
| Banana List | 55 | 0 | 없음 | 0 | 0 |
| Ace Citizenship | 26 | 0 | 외부 의존성 없음 | 0 | 0 |
| Water | 34 | 0(.metal 2개) |
없음 | 0 | 0 |
| ResumeGeni | 71 | 0 | 원격 SPM 3개, 핀 9개 | 0 | 0 |
| Yawara | 143 | 0 | 로컬 SPM 2개 | 0 | 0 |
| 합계 | 463 | 0 | SPM만 사용 | 0 | 0 |
OTHER_LDFLAGS는 7개 프로젝트 모두에서 설정되지 않았고, 프로젝트군 어디에도 .xcconfig 파일이 없으며, CocoaPods도 Carthage도 없고 벤더링된 .framework나 .xcframework도 없습니다.9 모듈 이름 감사는 모듈 맵 391개를 조사했습니다. 프로젝트 디렉터리 안에 204개, Xcode GUI 빌드가 떨어지는 공유 DerivedData에 187개였고, 전부 빌드 산출물이었습니다.9 사람이 직접 작성한 모듈 맵을 가진 저장소는 단 하나도 없습니다.
두 개의 0은 원인이 같고, 여기서 가져갈 만한 발견은 바로 그 원인입니다. 노출되는 대상은 언어가 뒤섞인 프로젝트인데, 이들은 그렇지 않다는 것입니다. Swift 파일 463개를 통틀어 Objective-C도, C++도, C 소스도 0개이며, Swift가 아닌 컴파일 대상 코드는 Water의 Metal 셰이더 두 개뿐입니다.9 애초에 위험에 놓인 적 없는 코드베이스에서 나온 무소득 결과는 위험 자체에 대해서는 약한 증거지만, 누가 감사를 해야 하는지에 대해서는 강한 증거입니다.
0이라는 숫자보다 중요한 사실이 두 가지 있습니다. 해석된 패키지 가운데 사람이 직접 작성한 모듈 맵은 swift-crypto의 것뿐인데, CCryptoBoringSSL, CCryptoBoringSSLShims, CXKCP, CXKCPShims라는 서로 다른 C shim 네 개를 제공합니다.9 그리고 이 프로젝트군이 선언하는 이름 중 SDK의 Clang 모듈 네임스페이스에 등장하는 것은 하나도 없었습니다. iPhoneOS 26.5 SDK에서 명령이 찾아낸 최상위 이름 1,318개 전체와 대조한 결과입니다.9 Supabase를 통해 들어오는 일반적인 이름들조차 겹치지 않습니다. SDK는 Foundation, UIKit, SQLite3라는 이름의 Clang 모듈을 선언하지만, Crypto나 Storage, Auth라는 이름의 모듈은 선언하지 않습니다.
발밑에서 올라가는 C++ 배포 하한선
인접한 변경 하나가 -ld_classic을 달고 있는 바로 그 C++ 프로젝트들을 때립니다. Apple은 macOS만 콕 집어 하한선을 올렸습니다. “C++ 표준 라이브러리가 지원하는 macOS 최소 배포 타깃이 11.0으로 상향되었습니다.”5
같은 항목은 탈출구를 제시하면서 바로 다음 문장에서 그 탈출구의 제거 시점까지 못박습니다. libc++는 엄격 약순서가 아닌 비교자에 대해 std::map과 std::set의 lower_bound, upper_bound 결과를 변경했고, _LIBCPP_ENABLE_LEGACY_TREE_LOWER_UPPER_BOUND를 정의하면 “이 연산들의 기존 구현으로 되돌아갑니다”라고 안내합니다. 그리고 이어집니다. “이 탈출구는 곧 나올 릴리스(아마도 다음 릴리스)에서 제거될 예정입니다.”5 관련 변경으로 multimap::find와 multiset::find가 같은 값 중 첫 번째 요소를 반드시 반환하지는 않게 되었는데, Apple은 이 동작이 libc++에서는 늘 제공되긴 했으나 “표준이 보장한 적은 없었다”고 적고 있으며, 이 변경에는 opt-out이 아예 없습니다.5 이 매크로는 해결책이 아니라 마이그레이션 과제로 취급하십시오.
자주 묻는 질문
제 프로젝트에는 왜 -ld_classic이 들어 있나요?
거의 확실하게 Apple의 Xcode 15 릴리스 노트가 그렇게 처방했기 때문입니다. 그 노트의 Known Issues 두 건이 OTHER_LDFLAGS에 -Wl,-ld_classic을 추가하라고 권했습니다. 하나는 약하게 정의된 심볼을 쓰는 바이너리가 iOS 14 및 macOS 12 이하에서 런타임에 크래시하던 문제로, Apple은 “주로 C++ 프로젝트에 영향을 준다”고 적었습니다. 다른 하나는 LTO 오브젝트 파일에서 약한 심볼 임포트가 약하지 않은 것으로 링크되던 문제입니다.3 Apple은 Xcode 16에서 이 옵션의 지원을 중단했고, Xcode 27에서는 그 뒤에 있던 링커 자체를 제거했습니다.14 플래그가 남아 있다면, 대체 수단을 찾기 전에 원래 버그가 지금도 재현되는지부터 확인하십시오.
프로젝트에서 중복된 Clang 모듈 이름은 어떻게 찾나요?
모든 모듈 맵에서 최상위 모듈 선언을 열거하고, 두 파일에 걸쳐 등장하는 이름을 찾고, 결과에서 빌드 디렉터리를 걷어내십시오. 이 작업이 단순 grep으로 끝나지 않는 이유는 세 가지입니다. extern module Foo "path"는 정의가 아니라 참조이고, module Foo.Bar는 Foo를 선언하는 것이 아니라 다른 곳에서 선언된 모듈을 확장하며, 중첩된 explicit module 서브모듈은 최상위 이름과 충돌하지 않습니다.6 .build, build, DerivedData는 명시적으로 제외하십시오. -not -path '*/build/*'는 .build를 놓치고, SwiftPM과 Xcode 모두 생성된 모듈 맵을 일상적으로 중복 생성하기 때문입니다.9
Xcode 27에서는 오류가 어떤 모습인가요?
저는 답할 수 없고, Xcode 26에서 빌드하는 그 누구도 답할 수 없습니다. 제 장비는 Xcode 26.6(빌드 17F113)을 돌리고 있어서 Xcode 27 출력은 단 한 줄도 보고하지 않습니다.78 같은 이름을 선언한 모듈 맵 두 개에 대해 Xcode 26.6이 내놓는 것은 error: redefinition of module 'Widget'과 note: previously defined here이며, 단순 타입 검사에서는 매번 나옵니다.8 Xcode 27에 대한 Apple의 표현은 스캔이 “오류를 보고할 수 있다”는 것이므로, 누군가 추측해 낸 문자열이 아니라 redefinition of module로 로그를 검색하십시오.
고유 모듈 이름 관련 실패가 배포 타깃에 좌우되나요?
아닙니다. Apple은 이 요구 사항을 단일 Swift 의존성 스캔 작업을 기준으로 서술하며, 해당 항목에서 OS 버전이나 SDK, 배포 타깃을 언급하지 않습니다.2 링커 변경도 툴체인에서의 제거라는 점에서 같은 방식으로 읽힙니다.1 둘 다 같은 주기의 SDK 기반 요구 사항이 아니라 @State 매크로와 마찬가지로 Xcode 27에서의 첫 빌드에 들이닥칩니다.
핵심 정리
iOS 개발자를 위해:
- -ld_classic을 grep하는 대신 xcodebuild -showBuildSettings -configuration Release -sdk iphoneos로 OTHER_LDFLAGS를 조회하십시오. 해석된 설정에 없다는 것은 설정되지 않았다는 뜻이지만, 텍스트 검색에 없다는 것은 아무 뜻도 아닙니다.9
- 빌드 로그에서는 지어낸 Xcode 27 오류 문구가 아니라, Xcode 26.6이 이미 내보내는 진단 문자열인 redefinition of module을 검색하십시오.8
벤더링된 C·C++ 의존성을 가진 팀을 위해:
- 벤더링된 module.modulemap 파일부터 SDK 네임스페이스와 대조해 감사하십시오. Apple이 직접 지목한 사례이고, 오늘날에는 조용히 실패합니다. 제 SQLite3 벤더링 shim은 종료 코드 0으로 컴파일되면서 sqlite3_open을 사라지게 만들었습니다.8
- 감사 범위를 소스로 한정하고 .build, build, DerivedData를 제외하십시오. 모듈 맵 391개에서 중복처럼 보였던 것은 전부 단일 Swift 타깃을 위해 생성된 빌드 산출물이었습니다.9
릴리스 담당자를 위해: - 두 변경 모두 툴체인이 방아쇠라고 보고, SDK 마이그레이션이 아니라 Xcode 27로 하는 첫 빌드 일정에 배치하십시오. 어느 쪽도 배포 타깃을 참조하지 않고 런타임 요소도 없습니다.12 - Apple이 남긴 애매한 표현을 티켓에 그대로 옮겨 두십시오. 스캔은 오류를 “보고할 수 있다”고 했고, 하나의 충돌을 20번 돌렸더니 결과가 세 갈래로 갈렸습니다. 빌드가 한 번 통과했다는 사실은 아무것도 증명하지 못합니다.28
27 주기는 매번 다른 지점에서 무너집니다. 런치 스크린 키는 제출을 막고, 씬 생명주기 의무화는 실행을 막으며, @State 매크로는 소스 수준에서 빌드를 막습니다. 링커 제거와 모듈 이름 규칙은 그보다 한 층 아래, 아무도 일부러 손대지 않는 툴체인 부분에서 빌드를 막습니다. 시리즈 전체는 Apple 생태계 시리즈에 모여 있습니다.
참고 자료
-
Apple, Xcode 27 Release Notes, Linking 절, Deprecations in Xcode 27 Beta(radar 165165518). 제거 사실의 출처이며 전문은 다음과 같습니다. “ld64 링커가 제거되었으며
-ld_classic옵션은 더 이상 지원되지 않습니다.” HTML 페이지가 JavaScript를 통해 내용을 렌더링하기 때문에, 2026년 7월 26일 Apple 문서 JSON 기준으로 확인했습니다. 그 시점의 페이지 제목은 “Xcode 27 Beta 4 Release Notes”입니다. ↩↩↩↩↩ -
Apple, Xcode 27 Release Notes, Swift Compiler 절, New Features in Xcode 27 Beta(radar 136303612). 의존성 스캐너 항목의 출처로, 본문에 전문을 그대로 인용했습니다. 두 곳의 유보적 표현(“스캔이 오류를 보고할 수 있습니다”, “이전에는 스캐너가 이름 중복을 허용했을 수 있습니다”)과 명시된 두 사례를 모두 포함합니다. 2026년 7월 26일 Apple 문서 JSON 기준으로 확인했습니다. 이 항목이 Deprecations나 Known Issues가 아니라 New Features 아래에 실려 있다는 점도 함께 기록해 둡니다. ↩↩↩↩↩↩
-
Apple, Xcode 15 Release Notes, Linking 절. New Features(radar 108915312)는 다음 문장의 출처입니다. “정적 링크 속도를 크게 높이기 위해 새 링커를 작성했습니다. 이 링커는 모든 macOS, iOS, tvOS, visionOS 바이너리와 ‘Mergeable Libraries’ 기능을 사용하는 모든 경우에 기본값입니다. 클래식 링커는 -ld64로 명시적으로 요청할 수 있으며, 향후 릴리스에서 제거될 예정입니다.” Known Issues는 이 플래그를 권한 두 우회책의 출처입니다. radar 114813650(FB13097713) “약한 정의를 가진 심볼을 사용하는 바이너리는 iOS 14/macOS 12 이하에서 런타임에 크래시합니다. 약한 심볼을 광범위하게 사용하는 C++ 프로젝트가 주로 영향을 받습니다.”에는 배포 타깃을 올리거나 “
OTHER_LDFLAGS빌드 설정에-Wl,-ld_classic을 추가”하라는 우회책이 붙었고, radar 115521975(FB13171424) “LTO 오브젝트 파일에서 사용될 때 약한 심볼 임포트가 약하지 않은 임포트로 링크됩니다.”에는 “OTHER_LDFLAGS빌드 설정에-Wl,-weak_reference_mismatches,weak또는-Wl,-ld_classic옵션을 추가하십시오.”라는 우회책이 붙었습니다. 2026년 7월 26일 Apple 문서 JSON 기준으로 확인했습니다. ↩↩↩↩↩↩ -
Apple, Xcode 16 Release Notes, Linking 절, Deprecations(radar 128502299): “
-ld_classic링커 옵션은 지원이 중단되었으며 향후 릴리스에서 제거될 예정입니다.” 2026년 7월 26일 Apple 문서 JSON 기준으로 확인했습니다. ↩↩↩ -
Apple, Xcode 27 Release Notes, C++ Standard Library 절, Deprecations in Xcode 27 Beta. Apple은 블록 전체에 대해 radar 번호를 하나만(178191050) 표기하는데, 각 항목 옆이 아니라 마지막 항목 뒤에 붙여 두었습니다. “C++ 표준 라이브러리가 지원하는 macOS 최소 배포 타깃이 11.0으로 상향되었습니다.”의 출처이자,
multi{map,set}::find변경(“find가 첫 번째 요소를 반환한다는 데 기대는 코드는 깨지게 되며, 대신lower_bound나equal_range를 사용해야 합니다”)의 출처이며, 탈출구와 그 만료 시점의 출처이기도 합니다. “경우에 따라 우회가 까다로울 수 있으므로 이번 릴리스에서는 탈출구를 제공합니다._LIBCPP_ENABLE_LEGACY_TREE_LOWER_UPPER_BOUND를 정의하면 이 연산들의 기존 구현으로 되돌아갑니다. 이 탈출구는 곧 나올 릴리스(아마도 다음 릴리스)에서 제거될 예정입니다.” 2026년 7월 26일 Apple 문서 JSON 기준으로 확인했습니다. ↩↩↩ -
Clang 팀, Clang Modules documentation, Module Map Language. “각 모듈은 단 하나의 정의를 가져야 한다”의 출처이자, 모듈 ID 규칙과
extern module선언의 출처이고, “explicit한정자는 서브모듈, 즉 다른 모듈 안에 중첩된 모듈에만 적용할 수 있습니다”의 출처이며, 모듈 맵을module.modulemap이라는 파일 이름으로 탐색하되 호환성을 위해module.map도 함께 찾는다는 내용의 출처입니다. Apple 개발자 문서가 아니라 모듈 맵 언어에 대한 툴체인 자체의 레퍼런스로서 인용했습니다. Apple의 clang은 이 구현에서 파생되었습니다. ↩↩ -
필자가 2026년 7월 26일 macOS 26.5.2(빌드 25F84), Xcode 26.6(빌드 17F113), Apple clang 21.0.0 환경에서 직접 테스트했습니다. 명령 출력은 그대로 옮겼습니다.
xcrun clang hello.c -o hello -Wl,-ld_classic은 “ld: warning: -ld_classic is deprecated and will be removed in a future release”를 출력하고 0으로 종료하며,-Wl,-ld64로 바꿔도-ld_classic을 지목하는 동일한 경고가 나옵니다.xcrun ld -v는PROJECT:ld-1267과 위에 인용한 아키텍처 목록을 보고합니다.ld-classic은/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld-classic에 man 페이지와 함께 존재합니다. SDK 아키텍처는SDKSettings.plist에서 확인했습니다. iPhoneOS 26.5는arm64e와arm64를, WatchOS 26.5는arm64,arm64e,arm64_32를 선언합니다. SDK 모듈 맵 집계(usr/include전체에서 최상위 선언 1,099개,System/Library/Frameworks전체에서 205개, 양쪽 모두 중복 0건)는 위에 공개한 명령을 iPhoneOS 26.5 SDK에 실행해 얻은 결과입니다.extern module선언 79개는usr/include/module.modulemap에 있고, 같은 디렉터리의 모듈 맵 13개가 점이 들어간module Darwin.*형태로 시작합니다(bank,Darwin_C,Darwin_Mach,Darwin_Mach_machine,Darwin_machine,Darwin_POSIX,Darwin_sys,device,mach_debug,net,netinet,netinet6,uuid). 첫 구성 요소만 읽는 방식은 이 13개를Darwin.modulemap의 실제Darwin선언과 묶어 파일 14개짜리 충돌 하나로 처리합니다. 사용한 장비에 Xcode 27이 설치되어 있지 않았기 때문에 Xcode 27에서의 동작은 테스트하지 않았습니다. ↩↩↩↩↩↩↩↩↩↩ -
필자가 2026년 7월 26일 같은 장비와 툴체인에서 재현했습니다. 디렉터리 두 개에 각각
module Widget을 선언하는module.modulemap을 두고, 두 디렉터리를 모두 검색 경로에 올린 상태로swiftc로 컴파일했습니다.-typecheck는 20번 중 20번error: redefinition of module 'Widget'과note: previously defined here를 출력하고 1로 종료했으며,-I경로 하나를 빼자 같은 파일이 0으로 종료하며 컴파일되었습니다. 같은 입력에 대한-scan-dependencies20회 실행은 시그널 11(SIGSEGV) 종료 9회, 시그널 6(SIGABRT) 종료 5회, 상태 0 종료 6회를 냈고, 크래시한 실행의 스택 추적은swift::ModuleDependencyScanner::performParallelClangModuleLookup을 거쳤습니다. 별도로, iPhoneOS 26.5 SDK의 모듈인SQLite3를 선언하는 벤더링 모듈 맵을 검색 경로에 올린 경우 아무 진단 없이 0으로 종료하며 타입 검사를 통과했고, 같은 구성에서 SDK의sqlite3_open을 호출하는 파일은 “error: cannot find ‘sqlite3_open’ in scope”로 실패했으며, 벤더링 디렉터리를 검색 경로에서 제거하자 문제없이 컴파일되었습니다. 공개한 명령을 검증하기 위해 픽스처에는 공백이 포함된 경로도 넣었습니다. 그 명령은 파일 사이의 이름을 비교하므로, 하나의 모듈 맵 안에서 두 번 선언된 이름은 설계상 범위 밖입니다. 이 글 어디에서도 Xcode 27 출력은 보고하지 않습니다. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
필자가 2026년 7월 26일 macOS 26.5.2 및 Xcode 26.6(빌드 17F113) 환경에서 Xcode 프로젝트 7개(Reps, Return, Banana List, Ace Citizenship, Water, ResumeGeni, Yawara)를 감사했습니다. 어떤 작업 트리에서도
-ld_classic과ld64는 0회 등장하고,OTHER_LDFLAGS는 모든 프로젝트에서 설정되지 않았으며, 프로젝트군 어디에도.xcconfig파일, Podfile, Carthage, 벤더링된.framework나.xcframework가 없습니다. 검색 범위는 현재 작업 트리로만 한정했고 git 이력이나 보관된 빌드 로그는 포함하지 않았으므로, 결과는 “한 번도 쓴 적 없음”이 아니라 “지금은 없음”입니다. Reps에 대해 인용한SUPPORTED_PLATFORMS줄은xcodebuild -showBuildSettings -configuration Release -sdk iphoneos에서 나온 것입니다. 모듈 맵 총계는 조사 대상 391개이며, 7개 프로젝트 디렉터리 아래에 204개, 공유~/Library/Developer/Xcode/DerivedData아래 이 7개 프로젝트 자체의 트리에 187개였고(공유 디렉터리 전체에는 다른 프로젝트에 속한 것이 훨씬 많습니다), 전부 빌드 산출물이었으며, 7개 저장소 어디에도 사람이 직접 작성했거나 벤더링된 모듈 맵은 없었습니다. 중복처럼 보인 것들은 내용 해시로 확인했으며, 두 생성기의 동작은 서로 다릅니다. 감사 대상이었던 ResumeGeni 트리(프로젝트 내부의build/DerivedData)에서 Xcode가 만든 이름 15쌍은GeneratedModuleMaps-iphonesimulator/아래에 한 번, 해당 타깃의 중간 산출물 아래에 한 번 쓰였고, Xcode가 상대 헤더 경로를 내보내기 때문에 15쌍 모두 바이트 단위로 동일했습니다. 이 수치는 프로젝트 단위가 아니라 트리 단위입니다. 같은 앱의 공유~/Library/Developer/Xcode/DerivedData아래 트리에는 16개, 그Index.noindex변형에는 22개가 있습니다. 일반화되는 것은 숫자가 아니라 메커니즘입니다. SwiftPM의 사본은 다릅니다. Yawara의.build디렉터리들 아래GrappleCore6개와GrappleRender3개 파일은 각각 서로 다른 해시 6개와 3개를 가지며 크기도 171바이트에서 185바이트 사이인데, 각 파일이 자기 빌드 루트 안에 생성된-Swift.h의 절대 경로를 품고 있을 뿐 나머지는 동일하기 때문입니다. 공개한 명령을 iPhoneOS 26.5 SDK 전체에 실행하면 서로 다른 최상위 Clang 모듈 이름 1,318개가 나오는데(그중 1,099개는usr/include아래, 205개는System/Library/Frameworks아래), 프로젝트군이 선언한 이름 가운데 이 목록에 등장하는 것은 없으며 교집합은 공집합입니다. 같은 스캔은 SDK 전체에서 중복 0건을 보고하는데, 이것이 바로 이 규칙이 요구하는 음성 대조군입니다.CryptoKit이 그 목록에 없는 이유는 Clang 모듈 맵 없이 Swift 전용 프레임워크로 배포되기 때문입니다. 따라서Crypto충돌이 없다는 사실은CryptoKit이 그 이름을 차지하고 있어서가 아니라 SDK가 그런 이름의 Clang 모듈을 선언하지 않기 때문입니다. 해석된 의존성 그래프에서 사람이 직접 작성한 모듈 맵을 제공하는 것은 swift-crypto 4.4.0뿐입니다(CCryptoBoringSSL,CCryptoBoringSSLShims,CXKCP,CXKCPShims, 각각 선언 하나씩).SymbolKit의 호스트 도구 변형과 타깃 변형은 7개 앱이 아니라 공유 로컬 패키지인 941Kit에 있습니다. 소스 집계에서는 Python 가상 환경을 제외했는데, 이 보정은 실제로 중요했습니다. 순진하게 세었을 때 Reps에 C 파일과 헤더 파일이 있는 것으로 집계되었지만, 전부.venv와site-packages안에 있었고 어떤 Xcode 타깃도 컴파일하지 않는 파일이었습니다. Water는 로컬 빌드 트리가 없어서, 모듈 맵 0개라는 수치는 검증된 깨끗한 빌드 그래프가 아니라 빌드된 적 없는 프로젝트를 반영합니다. 가짜 ‘깨끗함’을 만든 함정 세 가지는 각각 직접 확인했습니다.timeout은 기본 macOS에 없으며 127로 종료합니다. zsh는 따옴표 없는--include=*.pbxproj를 확장한 뒤 “no matches found”로 중단합니다.xcrun --sdk iphoneos --show-sdk-path는 심볼릭 링크를 반환하는데,-H없는find는 모듈 맵 0개를 보고하는 반면find -H는 266개를 보고합니다. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple, Xcode 26 Release Notes. 2026년 7월 26일에
ld_classic,ld64, 그리고 클래식 링커를 설명하는 항목을 모두 검색했지만 아무것도 나오지 않았습니다. 즉 Xcode 16의 지원 중단 예고와 Xcode 27의 제거 사이에 이를 다시 언급한 대목은 없습니다. ↩↩ -
Apple, Xcode 27 Release Notes, Overview: “Xcode 27 beta 4에는 Swift 6.4와 iOS 27, iPadOS 27, tvOS 27, watchOS 27, macOS 27, visionOS 27용 SDK가 포함됩니다.” 같은 노트는 Intel Deprecation(radar 162138432) 항목에서 “Xcode 27은 Apple 실리콘 Mac에서만 설치되고 실행됩니다”라고 기록하고 있습니다. 2026년 7월 26일 확인했습니다. ↩