Tuist 학습 Q&A 정리
pbxproj, Workspace, Project, Target, Scheme, Configuration, dSYM, Symbol Table, .swiftmodule, dyld 등 Tuist와 Xcode 프로젝트 구조에 대한 Q&A 정리.
기록 없음
1. pbxproj 파일
Q: pbxproj는 XML 파일인가?
아니요. OpenStep plist 형식 (NeXTSTEP 시대 유산)
// .pbxproj 예시 (OpenStep plist 형식)
{
archiveVersion = 1;
classes = { };
objectVersion = 56;
objects = {
13B07F961A680F5B00A75B9A /* AppDelegate.swift */ = {
isa = PBXFileReference;
lastKnownFileType = sourcecode.swift;
path = AppDelegate.swift;
};
};
rootObject = 83CBB9F71A601CBA00E9B192;
}
| 형식 | 특징 |
|---|---|
| OpenStep plist | =와 ; 사용, NeXTSTEP 시대 유산 |
| XML plist | <key>, <string> 태그 사용 |
| Binary plist | 바이너리 형식, 사람이 읽기 불가 |
Q: pbxproj가 Git 충돌이 잦은 이유?
1. 객체 ID가 랜덤 해시값
// 개발자 A가 LoginView.swift 추가
A1B2C3D4E5F6G7H8 /* LoginView.swift */ = { ... };
// 개발자 B가 같은 LoginView.swift 추가
9X8Y7Z6W5V4U3T2S /* LoginView.swift */ = { ... };
// → 머지 시 충돌 발생
2. 파일 하나 추가 시 여러 섹션 변경
파일 1개 추가 → 4개 섹션 변경:
- PBXFileReference (파일 참조)
- PBXBuildFile (빌드 대상)
- PBXGroup (폴더 구조)
- PBXSourcesBuildPhase (컴파일 순서)
3. Xcode 버전마다 정렬 순서 다름
// Xcode 14
children = (AppDelegate.swift, HomeView.swift, LoginView.swift);
// Xcode 15 (아무것도 안 바꿔도)
children = (HomeView.swift, LoginView.swift, AppDelegate.swift);
Q: pbxproj는 언제 생겼나?
| 연도 | 사건 |
|---|---|
| 1988 | NeXT에서 Project Builder IDE 출시 |
| 1997 | Apple이 NeXT 인수 |
| 2003 | Xcode 1.0 출시, pbxproj 형식 유지 |
| 현재 | 36년 된 레거시 형식 계속 사용 중 |
P = Project, B = Builder, X = Xcode (또는 NeXTSTEP의 X)
2. Workspace, Project, Target 관계
Q: project.pbxproj가 "핵심 설정 파일"이란 무슨 의미?
MyApp.xcodeproj/ ← 폴더 (패키지)
├── project.pbxproj ← 핵심 설정 파일
├── project.xcworkspace/ ← 단일 프로젝트용 워크스페이스
└── xcuserdata/ ← 사용자별 설정
| 설정 | 예시 |
|---|---|
| 파일 목록 | 어떤 .swift 파일들이 포함되는지 |
| 폴더 구조 | Xcode 네비게이터 계층 |
| 타겟 정의 | App, Test, Widget 등 |
| 빌드 설정 | Debug/Release, 컴파일러 플래그 |
| 의존성 | 링크할 프레임워크/라이브러리 |
Q: .xcodeproj vs .xcworkspace 차이?
| .xcodeproj | .xcworkspace | |
|---|---|---|
| 정체 | 단일 프로젝트 | 여러 프로젝트 묶음 |
| 용도 | 앱 하나만 개발 | 앱 + SPM + 여러 프로젝트 |
| 외부 모듈 방식 | 뭘 열어야 하나 |
|---|---|
| SPM | .xcodeproj OK |
| CocoaPods | .xcworkspace 필수 |
| Tuist | .xcworkspace 필수 |
Q: 여러 프로젝트 = 여러 타겟?
아니요, 다른 개념.
Workspace (.xcworkspace)
│
├── Project A (.xcodeproj)
│ ├── Target: App
│ ├── Target: Tests
│ └── Target: Widget
│
└── Project B (.xcodeproj)
├── Target: Framework
└── Target: FrameworkTests
| 개념 | 설명 |
|---|---|
| Target | 빌드 결과물 1개 |
| Project | 타겟들의 묶음 |
| Workspace | 프로젝트들의 묶음 |
Q: 타겟의 "빌드 결과물이 하나"란?
Target: MyApp → 빌드 → MyApp.app
Target: MyTests → 빌드 → MyTests.xctest
Target: MyWidget → 빌드 → MyWidget.appex
| Target 종류 | 빌드 결과물 | 확장자 |
|---|---|---|
| App | 실행 가능한 앱 | .app |
| Unit Test | 테스트 번들 | .xctest |
| Widget Extension | 위젯 | .appex |
| Framework | 프레임워크 | .framework |
| Static Library | 정적 라이브러리 | .a |
Q: 프로젝트 빌드하면 모든 타겟이 빌드되나?
아니요. 프로젝트가 아니라 Scheme을 빌드하는 것.
프로젝트 빌드 (X) → 스킴 빌드 (O)
↓
스킴이 지정한 타겟만 빌드
3. Scheme (스킴)
Q: Scheme이 뭐야?
"어떤 타겟을, 어떤 설정으로, 어떻게 실행할지" 정의한 것
Scheme: "MyApp-Dev"
│
├── Build → 어떤 타겟을 빌드할지
├── Run → 어떤 타겟을 실행할지 + Configuration
├── Test → 어떤 테스트 타겟을 돌릴지
├── Profile → Instruments 프로파일링 설정
├── Analyze → 정적 분석 설정
└── Archive → 앱스토어 배포용 빌드 설정
Q: 왜 Scheme이 여러 개 필요한가?
같은 타겟, 다른 설정으로 실행하기 위해:
| Scheme | Configuration | API 서버 |
|---|---|---|
| MyApp-Dev | Debug_Dev | dev-api.com |
| MyApp-QA | Debug_QA | qa-api.com |
| MyApp-Prod | Release_Prod | api.com |
Q: Scheme vs Target 차이?
| Target | Scheme | |
|---|---|---|
| 정의 | 빌드 결과물 정의 | 빌드 방법 정의 |
| 내용 | 소스, 리소스, 의존성 | 타겟 + Configuration + 실행 옵션 |
| 결과 | .app, .framework 등 | 없음 (실행 설정일 뿐) |
| 비유 | 요리 레시피 | 주문서 |
Target 없이 Scheme? ❌ 불가능
Scheme 없이 Target? ❌ 불가능
Target 1개 + Scheme 여러개? ✅ 가능 (환경별 실행)
4. Configuration (빌드 설정)
Q: Configuration이 뭐야?
"어떤 옵션으로 컴파일할지" 미리 정의한 설정 묶음
| Configuration | 용도 | 특징 |
|---|---|---|
| Debug | 개발 중 | 최적화 ❌, 디버깅 ✅ |
| Release | 배포용 | 최적화 ✅, 디버깅 ❌ |
| Debug | Release | |
|---|---|---|
| 최적화 | -Onone | -O |
| 속도 | 느림 | 빠름 |
| 디버깅 | 가능 | 어려움 |
| 빌드 시간 | 빠름 | 느림 |
Q: 커스텀 Configuration 예시?
Configurations:
├── Debug_Dev ← 개발 서버 + 디버그
├── Debug_QA ← QA 서버 + 디버그
├── Debug_Staging ← 스테이징 서버 + 디버그
├── Release_Prod ← 프로덕션 서버 + 릴리즈
└── Release_QA ← QA 서버 + 릴리즈
코드에서 분기:
#if DEBUG
print("디버그 모드")
#else
print("릴리즈 모드")
#endif
Q: 전체 구조 정리?
Project
│
├── Target: MyApp
│ └── 소스, 리소스, 의존성 정의
│
├── Configurations (빌드 설정 세트)
│ ├── Debug_Dev
│ ├── Debug_QA
│ └── Release_Prod
│
└── Schemes (실행 조합)
├── MyApp-Dev = MyApp + Debug_Dev
├── MyApp-QA = MyApp + Debug_QA
└── MyApp-Prod = MyApp + Release_Prod
비유
Target = 요리 (파스타)
Configuration = 조리법 (매운맛 / 순한맛)
Scheme = 주문서 (파스타 + 매운맛 + 포장)
5. dSYM과 빌드 스크립트
Q: dSYM이 뭐야?
Debug Symbol 파일 = 메모리 주소 ↔ 코드 위치 매핑
dSYM 없이:
0 MyApp 0x0000000102a3f4c8
1 MyApp 0x0000000102a3f1a0
dSYM 있으면:
0 MyApp HomeViewController.swift:142 loadData()
1 MyApp NetworkManager.swift:89 fetchUser()
Q: dSYM 업로드는 런타임에 하나?
아니요. 빌드 타임에 함.
Xcode Build Phases 순서:
│
├── 1. Compile Sources (.swift → .o)
├── 2. Link Binary (.o → 실행파일 + dSYM 생성)
├── 3. Copy Bundle Resources
└── 4. Run Script (Firebase dSYM 업로드) ← 여기
모든 게 빌드 타임 안에서 일어남 (런타임 아님)
Q: dSYM 업로드 흐름?
Q: 빌드 스크립트 종류?
| 스크립트 | 실행 시점 | 역할 |
|---|---|---|
| SwiftLint | 빌드 전 | 코드 스타일 검사 |
| R.swift | 빌드 전 | 리소스 타입 세이프 코드 생성 |
| Firebase Crashlytics | 빌드 후 | dSYM 업로드 |
6. dyld와 Dynamic Framework 로딩
Q: 앱 실행 시 dyld가 미리 코드를 읽어오는가?
Static Framework (정적):
Dynamic Framework (동적):
Q: 실제로 코드를 "읽어오는" 시점은?
dyld가 하는 것:
- Framework 파일을 가상 메모리에 매핑 (mmap)
- 심볼 주소 연결 (바인딩)
실제 코드 로딩:
- 가상 메모리 주소만 할당됨
- 실제 물리 메모리 로딩은 "Page Fault" 시점
- 즉, 해당 코드를 처음 실행할 때 OS가 로드
정리
dyld: 앱 시작 시 주소 매핑 + 심볼 바인딩
실제 코드 로딩: 해당 코드 처음 실행 시 (Lazy Loading)
7. Symbol Table vs .swiftmodule
Q: Symbol Table이 뭐야?
함수 이름 ↔ 주소 매핑 (링커/디버거용)
Symbol Table (오브젝트 파일 / 바이너리 안에 존재)
┌────────────────────────────────────────┐
│ Name │ Address │
├────────────────────────────────────────┤
│ _$s7MyClass4loadyyF │ 0x1000 │ ← load() 함수
│ _$s7MyClass4saveyyF │ 0x1200 │ ← save() 함수
│ _$s7MyClassCfd │ 0x1400 │ ← deinit
└────────────────────────────────────────┘
용도:
- 링커: 다른 모듈에서 호출 시 주소 연결
- 디버거: 크래시 주소 → 함수 이름 변환 (dSYM)
Q: .swiftmodule이 뭐야?
모듈의 공개 인터페이스 (컴파일러용)
MyModule.swiftmodule 내용 (바이너리 형식):
┌────────────────────────────────────────┐
│ public class MyClass { │
│ public func load() -> Data │
│ public func save(_ data: Data) │
│ } │
│ │
│ public struct Config { │
│ public var timeout: Int │
│ public var retryCount: Int │
│ } │
│ │
│ Generic 정보, Associated Type 등... │
└────────────────────────────────────────┘
용도:
- 컴파일러: import 시 타입 체크, 자동완성
- 함수 시그니처, 제네릭 정보, 프로토콜 준수 등
Q: Symbol Table vs .swiftmodule 비교?
| Symbol Table | .swiftmodule | |
|---|---|---|
| 위치 | .o, .a, .framework 바이너리 안 | 별도 파일 (.swiftmodule) |
| 내용 | 이름 ↔ 주소 매핑 | 타입, 함수 시그니처, 제네릭 |
| 용도 | 링킹, 디버깅 | 컴파일 (타입 체크) |
| 사용 시점 | 링크 타임, 런타임 (디버깅) | 컴파일 타임 |
| 사용자 | 링커 (ld), 디버거 (lldb) | 컴파일러 (swiftc) |
Q: 전체 흐름은?
정리
.swiftmodule: 컴파일 시 "이 함수 있어?" 확인 (타입 체크)
Symbol Table: 링크 시 "이 함수 주소가 뭐야?" 연결
8. dyld의 정확한 역할
Q: dyld가 런타임에 함수이름과 주소를 매핑하는 건가?
아니요. dyld는 이미 있는 Symbol Table을 보고 실제 메모리 주소로 연결해주는 것.
Static Framework (dyld 관여 안 함)
Dynamic Framework (dyld가 처리)
그림으로 정리
핵심 비교
| 빌드 타임 | 런타임 (dyld) | |
|---|---|---|
| Symbol Table | 링커가 생성 (이름↔주소) | 읽기만 함 |
| 주소 바인딩 | Static: 완료 | Dynamic: dyld가 수행 |
| 함수 이름 | 바이너리에 저장됨 | dyld가 새로 매핑 안 함 |
결론
dyld가 하는 것:
✅ Dynamic Framework를 메모리에 로드
✅ 로드된 위치 기반으로 주소 바인딩 (? → 0x5100)
❌ 함수 이름 ↔ 주소 테이블 새로 생성 (이미 있음)
dyld는 "함수 이름과 주소를 매핑"하는 게 아니라, 이미 있는 Symbol Table을 보고 실제 메모리 주소로 연결해주는 것.
9. Tuist의 SPM 통합 & 캐싱 (2026-06-20)
SPM 통합 — "패키지가 아니라 sub project/framework로"
Xcode 기본은 SPM 패키지를 native package로 통합(Xcode 내부 블랙박스). Tuist는 다르게, 각 SPM 패키지를 별도 Xcode 프로젝트/framework 타겟으로 생성해 의존하게 만든다.
| 일반 Xcode SPM | Tuist | |
|---|---|---|
| 통합 방식 | Xcode가 native package로 | 패키지를 Xcode 프로젝트/타겟으로 생성 |
| 빌드 산물 | package product | framework |
| 누가 관리 | Xcode 내부(블랙박스) | Tuist가 명시적 그래프로 |
왜? Tuist가 빌드를 통제해야 ① 바이너리 캐싱을 외부 의존성에도 적용하고 ② 내 모듈도 외부도 framework로 일관 관리할 수 있기 때문.
SPM도 타겟으로 관리한다 (Tuist만의 개념 아님)
SPM도 Package.swift에 target이 있다 (Package > Product > Target). 차이는 SPM=라이브러리 빌드 모델(표현력 제한) vs Tuist=Xcode 프로젝트 생성 모델(풀 표현력: 앱·확장·스킴·설정). 둘 다 타겟 단위.
⭐ 증분 빌드 vs 바이너리 캐시 — 다른 층위
SPM/Xcode도 증분 빌드는 한다(바뀐 모듈만 재컴파일, DerivedData 재사용). 그런데 그건 Tuist 바이너리 캐시와 다르다.
| 증분 빌드 (SPM/Xcode) | Tuist 바이너리 캐시 | |
|---|---|---|
| 바뀐 모듈만 재컴파일 | ✅ | ✅ |
| 식별 방식 | 타임스탬프·파일 변경 | 콘텐츠 해시 |
| 저장 | DerivedData(로컬·휘발성) | 별도 캐시(로컬/원격) |
| 클린 빌드 후 | ❌ 사라짐 → 전부 재컴파일 | ✅ 살아남아 재사용 |
| 새 머신/CI/팀 공유 | ❌ 처음부터 | ✅ (원격이면) 받아 재사용 |
→ 일상 개발(DerivedData 유지)에선 SPM 증분 빌드로 충분. Tuist 바이너리 캐시가 빛나는 건 클린 빌드·CI·새 머신·팀 공유처럼 증분 캐시가 무력화되는 상황이다.
⚠️ 용어: DerivedData 재사용을 "바이너리 캐싱"이라 부르면 어폐. 그건 증분 빌드 캐시이고, "바이너리 캐시"는 해시 기반·영속·공유형(Tuist/Bazel)을 지칭. SPM은 binaryTarget(XCFramework 의존)은 지원하지만 자동 바이너리 캐시 시스템은 없다.
로컬 vs 원격 캐시 — 서버 필요 여부
| 원격 서버 필요? | 용도 | |
|---|---|---|
로컬 캐시 (~/.cache/tuist) | ❌ 불필요 | 내 머신 빌드 재사용 |
| 원격 캐시 (Tuist 서버/S3 등) | ✅ 필요 | 팀·CI 공유 |
공유는 git이 아니라 별도 캐시 스토리지로(바이너리를 git에 커밋 X). CI는 Jenkins 같은 도구로 매번 클린 빌드 → 느린데, 원격 캐시를 붙이면 미리 빌드된 framework를 받아 가속. (CI = 자주 통합+자동 빌드·테스트 관행 / Jenkins = 그걸 실행하는 도구)
⚠️ 흔한 오해: "Tuist + SPM = 항상 xcframework?"
아니다. tuist cache를 실행해야만 xcframework로 저장된다.
| 상황 | SPM 저장 형태 | DerivedData 삭제 시 |
|---|---|---|
| Xcode 기본 | 소스 | 다시 빌드 |
| Tuist (cache 안 함) | 소스 (타겟으로 관리) | 다시 빌드 |
Tuist + tuist cache | .xcframework | 캐시에서 복원 ✅ |
즉, Tuist를 쓴다고 자동으로 바이너리 캐시가 생기는 게 아니라, tuist cache 명령을 명시적으로 실행해야 SPM 의존성이 xcframework로 미리 빌드되어 저장된다.
관련 문서
10. 빌드 결과물 · 캐시 · Framework 위치 Q&A (2026-06-29)
Q. 컴파일 결과물은 어디에 저장?
DerivedData에 저장됨:
~/Library/Developer/Xcode/DerivedData/
└── MyApp-xxxxx/
└── Build/Products/
├── Debug-iphonesimulator/
│ ├── Alamofire.framework ← 의존성 빌드 결과
│ └── MyApp.app ← 앱 빌드 결과
└── Release-iphoneos/
타겟 단위로 관리 — 각 타겟이 각자의 빌드 결과물 생성.
Q. DerivedData 삭제해도 컴파일 결과물 유지?
❌ 아니다. DerivedData 삭제 → 컴파일 결과물 전부 삭제 → 처음부터 다시 빌드.
| 상황 | DerivedData 삭제 시 |
|---|---|
| Xcode 기본 | 다시 컴파일 |
| Tuist (cache 안 함) | 다시 컴파일 |
| Tuist + tuist cache | ~/.tuist/Cache/에서 xcframework 복원 ✅ |
Q. SourcePackages/artifacts vs checkouts 차이?
| 폴더 | 내용 | 언제 생김 |
|---|---|---|
| checkouts/ | 소스 코드 (.swift) | 일반 SPM 의존성 |
| artifacts/ | 바이너리 (.xcframework) | binaryTarget 의존성 |
Q. Tuist/Dependencies/에 Package.swift만 있는데?
실제 소스 코드는 여기 없음!
Tuist/Dependencies/SwiftPackageManager/— 의존성 목록 정의Tuist/Dependencies/Lockfiles/— 버전 고정 (Package.resolved)tuist install시 소스는 별도 위치에 다운로드 (.build/또는 시스템 캐시)
Q. tuist cache는 항상 xcframework로 복원?
✅ 맞다. tuist cache는 의존성을 xcframework로 미리 빌드해서 저장. 복원 시 xcframework 그대로 연결 (재컴파일 X).
Q. Debug-iphoneos/ 폴더에 Static/Dynamic 둘 다 있나?
✅ 맞다. 빌드 결과 폴더에는 타입 상관없이 모든 .framework가 존재.
구분 방법:
file SomeLib.framework/SomeLib # Static: "current ar archive" # Dynamic: "Mach-O 64-bit dynamically linked shared library"
Q. .app/Frameworks/에 있는 것 = Dynamic만?
✅ 맞다.
| 위치 | 포함되는 것 |
|---|---|
Debug-iphoneos/ | Static + Dynamic 모두 |
.app/Frameworks/ | Dynamic만 복사됨 |
.app/MyApp (실행파일) | Static은 여기에 포함 |
Debug-iphoneos/
├── StaticA.framework ← Static
├── DynamicB.framework ← Dynamic
└── MyApp.app/
├── MyApp ← 실행파일 (StaticA 코드 포함)
└── Frameworks/
└── DynamicB.framework ← Dynamic만 여기로
Q. Static Framework는 앱 실행 파일에 포함?
✅ 맞다. Static은 링킹 시 앱 실행 파일에 코드가 복사됨. Dynamic은 .app/Frameworks/에 별도 파일로 존재.
11. 심화 Q&A — DerivedData · checkouts · artifacts · tuist cache (2026-06-29)
Q. DerivedData 폴더명이 MStove-abcdef123 이상한 이유?
해시값 = 프로젝트 경로 기반. 같은 이름 프로젝트 충돌 방지용.
~/Projects/AppA/MStove.xcodeproj → MStove-abc123/ ~/Projects/AppB/MStove.xcodeproj → MStove-def456/
Q. checkouts은 항상 SPM 소스코드만?
✅ 맞다. SPM 의존성의 git에서 클론한 소스 코드만 저장.
Q. artifacts는 Xcode가 빌드한 결과물?
❌ 아니다. artifacts는 이미 빌드된 상태로 배포되는 바이너리 (binaryTarget).
| 폴더 | 내용 | 누가 만듦 |
|---|---|---|
| checkouts/ | 소스 코드 | git clone |
| artifacts/ | 미리 빌드된 바이너리 | 라이브러리 배포자 |
| Build/Products/ | Xcode가 빌드한 결과물 | Xcode |
Q. tuist install은 소스를 어디에 저장?
SourcePackages/checkouts 아님. Tuist는 자체 경로 사용:
~/.tuist/Dependencies/ 또는 프로젝트/.build/checkouts/
Q. tuist cache 기준, 저장 위치, static/dynamic은?
기준: 프로젝트 의존성 그래프에서 캐시 가능한 모든 타겟.
저장 위치:
~/.tuist/Cache/
└── [hash]/
├── Alamofire.xcframework
└── RxSwift.xcframework
static/dynamic: xcframework는 "컨테이너"일 뿐 — 안에 static 또는 dynamic 담을 수 있음. Tuist 설정에서 선택 가능.
Q. .framework가 Static/Dynamic 둘 다 될 수 있나?
✅ 맞다.
file MyLib.framework/MyLib # Static: "current ar archive random library" # Dynamic: "Mach-O 64-bit dynamically linked shared library"
Q. Library vs Framework 차이?
| Library | Framework | |
|---|---|---|
| 정체 | 컴파일된 코드만 | 코드 + 헤더 + 리소스 + 메타데이터 |
| Static | .a (archive) | .framework (static) |
| Dynamic | .dylib | .framework (dynamic) |
| 구조 | 단일 파일 | 폴더 (번들) |
Library (.a): libMyLib.a ← .o 파일들 묶은 아카이브 Framework (.framework): MyLib.framework/ ├── MyLib ← 바이너리 (static/dynamic) ├── Headers/ ← 헤더 파일 ├── Modules/ ← Swift module ├── Resources/ ← 이미지, xib 등 └── Info.plist ← 메타데이터
요약: Framework = Library + 헤더 + 리소스 + 메타데이터 (구조화된 번들)
12. Package.swift 심화 Q&A — Target · Product · binaryTarget (2026-06-29)
Q. 1 Product = 1 Target = 1 Module?
기본적으로 맞다. 단, 1 Product가 여러 Target을 묶을 수도 있다:
.library(
name: "MyProduct",
targets: ["TargetA", "TargetB"] // ← 2개 타겟 묶음
)
이 경우 import MyProduct하면 TargetA, TargetB 코드 둘 다 사용 가능.
Q. binaryTarget을 왜 만드나? 패키지 내 타겟끼리 의존 가능?
소스 코드 없이 xcframework만 있을 때 binaryTarget을 쓴다:
.binaryTarget(
name: "SGSAuthGoogle",
path: "SGSSDK/SGSAuthGoogle.xcframework" // 로컬 xcframework
)
같은 패키지 내 타겟끼리 의존 가능:
targets: [
.target(
name: "MStoveSDKWrapper",
dependencies: ["SGSAuthGoogle"] // ← 같은 패키지의 binaryTarget
),
.binaryTarget(name: "SGSAuthGoogle", path: "..."),
]
| 상황 | 사용할 것 |
|---|---|
| 소스 코드 있음 | .target() |
| 미리 빌드된 xcframework | .binaryTarget() |
Q. .zip 하나에 xcframework 여러 개?
1 zip = 1 xcframework. 하지만 전이 의존성이 풀리면서 여러 개 생긴다:
artifacts/에 생기는 것들: ├── SGSAuthGoogle.xcframework ← 직접 선언 ├── GoogleSignIn.xcframework ← SGSAuthGoogle이 의존 ├── GTMSessionFetcher.xcframework ← GoogleSignIn이 의존 └── AppAuth.xcframework ← 전이 의존성
Q. xcframework를 패키지로 감싸서 배포하는 이유?
직접 binaryTarget도 가능하지만, 버전 관리를 위해 패키지로 감싼다:
| 방식 | 장점 | 단점 |
|---|---|---|
.binaryTarget(path:) | 단순, 로컬 파일 | 버전 관리 어려움 |
.binaryTarget(url:) | 원격 다운로드 | 버전 태그 없음 |
.package(url:exact:) | git 태그로 버전 관리 | Package.swift 필요 |
Q. dependencies에서 문자열 vs .product() 차이?
dependencies: [
"MStoveSDKWrapperInterface", // 같은 패키지 내 타겟
.product(name: "SGSBase", package: "base"), // 다른 패키지, 이름 다름
"SGSAuthGoogle", // 같은 패키지 내 binaryTarget
]
| 상황 | 문법 |
|---|---|
| 같은 패키지 내 타겟 | "TargetName" |
| 다른 패키지, 이름 같음 | "ProductName" (가능) |
| 다른 패키지, 이름 다름 | .product(name:package:) (필수) |
Q. 외부 패키지도 이름 같으면 문자열만 가능?
조건부로 가능. SPM이 추론할 수 있을 때만:
// 패키지명 = 프로덕트명 같으면
.package(url: ".../Alamofire", from: "5.0.0")
dependencies: ["Alamofire"] // ✅ 가능
// 패키지명 ≠ 프로덕트명 다르면
.package(url: ".../auth-line", exact: "2.9.0") // 패키지명: auth-line
// 프로덕트명: SGSAuthLine (다름!)
dependencies: [
"SGSAuthLine" // ❌ 안 됨
.product(name: "SGSAuthLine", package: "auth-line") // ✅ 필요
]
| 상황 | 문자열 가능? |
|---|---|
| 패키지명 = 프로덕트명 | ✅ 가능 |
| 패키지명 ≠ 프로덕트명 | ❌ .product() 필수 |
| 프로덕트가 여러 개 | ❌ .product() 필수 |
Q. binaryTarget은 Package.dependencies에 없어도 사용 가능?
✅ 가능하다. 두 가지 dependencies를 구분:
let package = Package(
dependencies: [
// 패키지 레벨: 외부 패키지 가져오기
.package(url: ".../auth-line", exact: "2.9.0"),
],
targets: [
.target(
name: "MStoveSDKWrapper",
dependencies: [
// 타겟 레벨: 이 타겟이 의존하는 것
"SGSAuthGoogle", // ← 같은 패키지 내 binaryTarget (외부 아님)
]
),
.binaryTarget(
name: "SGSAuthGoogle", // ← targets 배열에서 정의
path: "SGSSDK/SGSAuthGoogle.xcframework"
),
]
)
| 위치 | 역할 | 내용 |
|---|---|---|
Package.dependencies | 외부 패키지 가져오기 | .package(url:...) |
targets 배열 | 이 패키지 내 타겟 정의 | .target(), .binaryTarget() |
Target.dependencies | 이 타겟이 의존하는 것 | 같은 패키지 타겟 + 외부 프로덕트 |
binaryTarget은 targets 배열에서 정의하면, 같은 패키지 내 다른 타겟이 의존 가능.
Q. auth-google 주석인데 binaryTarget은 어떻게 사용?
// 주석 처리됨 (원격 패키지 안 씀)
// .package(url: "https://.../auth-google", exact: "2.8.2"),
// 대신 로컬 xcframework 직접 사용
.binaryTarget(
name: "SGSAuthGoogle",
path: "SGSSDK/SGSAuthGoogle.xcframework"
)
원격 패키지 대신 로컬 xcframework를 직접 사용. 이유: 버전 충돌, 커스텀 빌드, 빌드 속도 등.
Q. 원격 패키지 binaryTarget vs 로컬 binaryTarget 차이?
SGSPush (원격 패키지 경유): ┌─────────────────────────────────────────────┐ │ MStoveSDKWrapper/Package.swift │ │ dependencies: [.package(url: ".../push")] │ │ ↓ │ │ push 패키지의 Package.swift │ │ targets: [.binaryTarget(url: "...zip")] │ │ ↓ │ │ 원격에서 SGSPush.xcframework.zip 다운로드 │ └─────────────────────────────────────────────┘ SGSAuthGoogle (로컬 직접 참조): ┌─────────────────────────────────────────────┐ │ MStoveSDKWrapper/Package.swift │ │ targets: [.binaryTarget(path: "SGSSDK/")] │ │ ↓ │ │ 로컬 SGSAuthGoogle.xcframework 직접 사용 │ └─────────────────────────────────────────────┘
| 방식 | 경로 | 장점 |
|---|---|---|
| 원격 패키지 | Package.dependencies → 패키지 → binaryTarget(url:) | 버전 태그 관리 |
| 로컬 직접 | targets → binaryTarget(path:) | 단순, 오프라인 가능 |
Q. 앱에 포함되는 건 xcframework? framework?
xcframework 안에 있는 framework. xcframework는 컨테이너일 뿐:
SGSPush.xcframework/ ← 컨테이너 (빌드 후 사라짐) ├── ios-arm64/ │ └── SGSPush.framework/ ← 실제로 사용되는 것 │ └── SGSPush (바이너리) ├── ios-arm64_x86_64-simulator/ │ └── SGSPush.framework/ └── Info.plist
Static인 경우
빌드: 1. xcframework에서 현재 플랫폼 framework 선택 2. framework 코드 → 앱 실행파일에 복사 (링킹) 결과: MyApp.app/ └── MyApp ← SGSPush 코드가 여기 포함됨 (xcframework, framework 파일 자체는 앱에 없음)
Dynamic인 경우
빌드:
1. xcframework에서 현재 플랫폼 framework 선택
2. framework → .app/Frameworks/에 복사
결과:
MyApp.app/
├── MyApp ← "SGSPush 필요해" 참조만
└── Frameworks/
└── SGSPush.framework/ ← framework가 복사됨 (xcframework 아님!)
런타임:
dyld가 SGSPush.framework 로드
Q. 버전 충돌 시 왜 하나만 포함돼?
framework 파일 이름이 같아서. 타겟 이름 때문이 아님:
.app/Frameworks/ └── base.framework/ ← 폴더 이름 base 2.8.0 → base.framework base 2.9.0 → base.framework → 이름 같아서 하나만 존재 가능
| 상황 | 결과 |
|---|---|
| SGSAuthGoogle이 base 2.8.0 기준 빌드 (Dynamic) | base의 login(id:) 심볼 기대 |
| 앱이 base 2.9.0 포함 | .app/Frameworks/base.framework (2.9.0) |
| 런타임 | login(id:) 없음 → 💥 크래시 |
같은 이름 framework는 하나만 존재 → 버전 불일치 시 심볼 못 찾아 크래시.
Q. xcodeproj 변환 = 컴파일/링킹?
❌ 아니다. 완전히 다른 단계.
| 단계 | 하는 일 | 결과물 |
|---|---|---|
tuist generate | 프로젝트 설정 파일 생성 | .xcodeproj, .xcworkspace |
xcodebuild (⌘B) | 컴파일 + 링킹 | .app, .framework |
1. tuist install → 의존성 다운로드 2. tuist generate → .xcodeproj 생성 (컴파일 X) 3. xcodebuild / ⌘B → 컴파일 + 링킹 (여기서 빌드)
Q. tuist generate하면 자동으로 tuist cache 실행?
❌ 아니다. 별도 명령어.
| 명령어 | 하는 일 |
|---|---|
tuist install | 의존성 다운로드 |
tuist cache | 의존성을 xcframework로 미리 빌드 |
tuist generate | .xcodeproj 생성 (cache 있으면 사용) |
tuist generate --no-cache | cache 무시, 소스로만 |
cache 없이 generate: Alamofire → 소스 타겟 → 빌드 시 컴파일 cache 사용: tuist cache → ~/.tuist/Cache/에 Alamofire.xcframework tuist generate → xcframework 연결 → 컴파일 스킵
Q. tuist cache의 xcframework, 빌드하면 Static/Dynamic 어디로?
xcframework도 빌드(링킹) 시 Static/Dynamic 규칙 그대로 적용:
tuist cache 저장:
~/.tuist/Cache/
├── Alamofire.xcframework/ ← 안에 static 바이너리
└── RxSwift.xcframework/ ← 안에 dynamic 바이너리
빌드 (⌘B) 후:
MyApp.app/
├── MyApp ← 실행 파일 (Alamofire 코드 포함됨)
└── Frameworks/
└── RxSwift.framework/ ← Dynamic만 여기로 복사
| xcframework 내용 | 링킹 시 | 최종 위치 |
|---|---|---|
| Static 바이너리 | 앱 실행 파일에 병합 | MyApp.app/MyApp 안에 포함 |
| Dynamic 바이너리 | 별도 파일로 복사 | MyApp.app/Frameworks/ |
xcframework는 "컨테이너"일 뿐 — 안에 static이면 실행 파일에, dynamic이면 Frameworks/에. tuist cache든 소스 컴파일이든 이 규칙은 동일.
13. tuist generate 흐름 심화 Q&A (2026-06-29)
Q. cache 있으면 tuist install 안 해도 돼?
❌ 아니다. install과 cache는 다른 단계:
| 명령어 | 하는 일 | 캐시 영향 |
|---|---|---|
tuist install | 소스/바이너리 다운로드 | ❌ 없음 (항상 다운로드) |
tuist cache | 소스 → xcframework 빌드 | ✅ 여기서 캐시 생성 |
tuist generate | .xcodeproj 생성 | ✅ 캐시 있으면 xcframework 참조 |
⌘B | 컴파일 + 링킹 | ✅ 캐시 있으면 링킹만 |
캐시는 "다운로드"가 아니라 "컴파일"을 스킵하는 것.
Q. "소스 타겟으로 프로젝트 생성"이 무슨 말?
tuist generate가 의존성을 어떻게 참조하느냐의 차이:
캐시 없이 generate:
MyApp.xcworkspace
├── MyApp.xcodeproj
│ └── Targets: MyApp
├── Alamofire.xcodeproj ← 별도 프로젝트
│ └── Targets: Alamofire ← Xcode Target (소스 컴파일 필요)
└── RxSwift.xcodeproj
└── Targets: RxSwift ← Xcode Target (소스 컴파일 필요)
캐시 있으면 generate:
MyApp.xcodeproj ← 프로젝트 하나
└── Targets: MyApp
└── Link Binary: Alamofire.xcframework, RxSwift.xcframework
"소스 타겟" = 의존성마다 Xcode Target이 생성되어 빌드 시 소스 컴파일 필요
Q. tuist generate가 컴파일도 해?
❌ 안 한다. generate는 프로젝트 파일만 생성:
| 단계 | 하는 일 | 컴파일? |
|---|---|---|
tuist generate | 의존성 그래프 분석, .xcodeproj 생성 | ❌ |
⌘B (xcodebuild) | .swift → .o → 실행파일 | ✅ |
Q. "Link Binary With Libraries"가 뭐야?
Xcode Build Phases의 한 단계. 링커에게 "이 라이브러리들과 연결해라"고 지정하는 곳:
Build Phases (타겟 → Build Phases 탭): ├── Dependencies ├── Compile Sources ← .swift → .o 컴파일 ├── Link Binary With Libraries ← 여기! 링크할 라이브러리 지정 ├── Copy Bundle Resources ← 리소스 복사 └── Embed Frameworks ← Dynamic Framework 앱에 포함
빌드 흐름에서의 역할:
1. Compile Sources .swift → .o (오브젝트 파일) 2. Link Binary With Libraries ← 여기 .o + 라이브러리들 → 실행 파일 "Alamofire의 함수 주소 연결해줘" 3. Embed Frameworks (Dynamic만) Dynamic Framework를 .app/Frameworks/에 복사
| 타입 | Link Binary With Libraries | Embed Frameworks |
|---|---|---|
| Static | ✅ 추가 | ❌ 불필요 (코드가 실행파일에 병합) |
| Dynamic | ✅ 추가 | ✅ 추가 (런타임에 로드해야 하니까) |
Q. Xcode 프로젝트의 Frameworks는 Dynamic만?
❌ 아니다. 두 가지 "Frameworks"를 구분해야 함:
| 위치 | 내용 |
|---|---|
| Xcode "Link Binary With Libraries" | Static + Dynamic 둘 다 참조 |
런타임 .app/Frameworks/ | Dynamic만 복사됨 |
Xcode 프로젝트 (빌드 설정):
└── Link Binary With Libraries:
├── Alamofire.xcframework ← Static도 여기 참조
└── RxSwift.xcframework ← Dynamic도 여기 참조
빌드 후 .app:
MyApp.app/
├── MyApp ← Static은 여기에 포함
└── Frameworks/ ← Dynamic만 여기에 복사
└── RxSwift.framework
Q. 의존성 있으면 xcworkspace가 무조건 생겨?
❌ 항상은 아니다.
| 상황 | 생성되는 것 |
|---|---|
| 의존성 없음 | .xcodeproj만 |
| 의존성 있음 (캐시 없음) | .xcworkspace + 의존성별 .xcodeproj |
| 의존성 있음 (캐시 있음) | .xcodeproj만 (xcframework 참조) |
여러 프로젝트가 필요하면 workspace, 하나면 project만.
14. Tuist Project.swift vs SPM Package.swift (2026-06-29)
Q. Project.swift와 Package.swift 차이?
| Tuist Project.swift | SPM Package.swift | |
|---|---|---|
| 목적 | Xcode 프로젝트 생성 | Swift 패키지 정의 |
| 패키지 선언 | packages: | dependencies: |
| 결과물 | .xcodeproj / .xcworkspace | 패키지 자체 |
| 타겟 정의 | Target(...) | .target(...) |
Q. packages:에 로컬/원격 패키지 둘 다 가능?
✅ 둘 다 가능.
// Tuist Project.swift
packages: [
.local(path: "../Packages/MStoveBaseUtil"), // 로컬 SPM 패키지
.remote(url: "https://github.com/Alamofire", ...), // 원격 SPM 패키지
]
// SPM Package.swift
dependencies: [
.package(path: "../MStoveBaseUtil"), // 로컬
.package(url: "https://github.com/Alamofire", ...), // 원격
]
Q. .package(product:)는 패키지? 프로덕트?
프로덕트를 사용하겠다는 것. 패키지 ≠ 프로덕트:
packages: [
.local(path: "../MStoveBaseUtil"), // ← 패키지 선언
]
targets: [
Target(
dependencies: [
.package(product: "MStoveBaseUtil"), // ← 패키지의 "프로덕트" 사용
]
)
]
- 패키지 = 컨테이너 (Package.swift 가진 폴더)
- 프로덕트 = 패키지가 외부에 노출하는 것 (library, executable)
Q. .package(product:)는 "외부 패키지" 전용?
❌ "외부"가 아니라 "packages:에 선언된 패키지" 전용. 로컬이든 원격이든:
packages: [
.local(path: "../MStoveBaseUtil"), // 로컬 패키지
.remote(url: "https://...", ...), // 원격 패키지
]
// 둘 다 .package(product:)로 사용
dependencies: [
.package(product: "MStoveBaseUtil"), // 로컬이든
.package(product: "Alamofire"), // 원격이든
]
Q. Tuist 의존성 종류 정리
| 문법 | 대상 | 예시 |
|---|---|---|
.target(name:) | 같은 Project.swift 내 타겟 | .target(name: "MyFramework") |
.project(target:path:) | 다른 Tuist Project.swift의 타겟 | .project(target: "FeatureA", path: "../Features") |
.package(product:) | packages:에 선언된 SPM 패키지 (로컬/원격) | .package(product: "Alamofire") |
.external(name:) | Dependencies.swift에 정의된 것 (Tuist 3.x+) | .external(name: "RxSwift") |
Q. import는 Product? Module?
Module을 import한다. Product가 아니라:
Package
└── Product (외부 노출용)
└── Target (빌드 단위)
└── Module (import 대상)
// 의존성 선언 (Product 이름)
.package(product: "SGSAuth")
// 코드에서 사용 (Module 이름 = Target 이름)
import SGSAuth
보통 Product 이름 = Target 이름 = Module 이름이라 헷갈리지만, 실제 import하는 건 Module.
Q. 의존성 그래프가 뭐야?
누가 누구를 의존하는지 관계를 파악하는 것:
MStoveSDKWrapper
├── MStoveBaseUtil
├── SGSAuth
│ ├── SGSBase ← SGSAuth가 의존
│ └── SGSLog
└── SGSPush
└── SGSBase ← 중복 (한 번만 빌드)
용도: 빌드 순서 결정, 중복 제거, 버전 충돌 감지
Q. Tuist/Package.swift vs Project.swift packages 차이?
| Tuist/Package.swift | Project.swift packages: | |
|---|---|---|
| 내용 | 외부 의존성 (GitHub 등) | 내부 모듈 (우리 코드) |
| 사용법 | .external(name:) | .package(product:) |
| 캐싱 | ✅ 대상 (버전 고정) | ❌ 대상 아님 (자주 바뀜) |
// Tuist/Package.swift — 외부 의존성
dependencies: [
.package(url: "https://github.com/Alamofire/Alamofire", ...),
]
// Project.swift packages: — 내부 모듈
packages: [
.local(path: "App/MStoveBaseUI"),
.local(path: "Feature/MStoveV3Home"),
]
// Target dependencies에서
dependencies: [
.external(name: "Alamofire"), // ← Tuist/Package.swift
.package(product: "MStoveBaseUI"), // ← 로컬 패키지
]
Q. productTypes가 뭐야?
외부 의존성의 Static/Dynamic을 결정:
// Tuist/Package.swift
let packageSettings = PackageSettings(
productTypes: [
"Alamofire": .staticFramework, // Static으로 빌드
"LineSDK": .framework, // Dynamic으로 빌드
"FirebaseAnalytics": .staticFramework,
]
)
이 설정 없으면 Tuist가 기본값으로 결정.
Q. "소스 타겟" vs "xcframework 연결" 차이?
캐시 없을 때 (소스 타겟):
MyApp.xcworkspace/
├── MyApp.xcodeproj
├── Alamofire.xcodeproj ← 소스 타겟 (컴파일 필요)
│ └── Sources/*.swift
└── RxSwift.xcodeproj ← 소스 타겟 (컴파일 필요)
└── Sources/*.swift
→ ⌘B 하면 Alamofire, RxSwift 소스 전부 컴파일
캐시 있을 때 (xcframework 연결):
MyApp.xcodeproj/
└── Link:
├── Alamofire.xcframework ← 미리 빌드됨
└── RxSwift.xcframework ← 미리 빌드됨
→ ⌘B 하면 링킹만 (컴파일 스킵)
"소스 타겟" = 소스 코드(.swift)를 포함하는 Xcode Target. 빌드 시 컴파일 필요.
Q. 내부 모듈은 캐싱 안 해?
✅ 맞다. 내부 모듈은 항상 소스 타겟:
| 외부 의존성 | 내부 모듈 | |
|---|---|---|
| 변경 빈도 | 거의 안 바뀜 (버전 고정) | 매일 바뀜 (개발 중) |
| 캐시 효과 | ✅ 높음 | ❌ 낮음 (바꿀 때마다 무효화) |
| generate 결과 | xcframework 또는 소스 타겟 | 항상 소스 타겟 |
tuist generate 결과:
MyApp.xcworkspace/
├── Alamofire.xcframework ← 외부 (캐시됨, 컴파일 X)
├── RxSwift.xcframework ← 외부 (캐시됨, 컴파일 X)
│
├── MStoveV3Home.xcodeproj ← 내부 (소스 타겟, 매번 컴파일)
│ └── Sources/*.swift
└── MStoveNetwork.xcodeproj ← 내부 (소스 타겟, 매번 컴파일)
└── Sources/*.swift
Q. 전체 흐름 정리
1. Tuist/Package.swift → 외부 의존성 URL + 버전 선언 → productTypes로 Static/Dynamic 지정 2. tuist install → 외부 의존성 소스 다운로드 3. tuist cache (선택) → 외부 의존성을 xcframework로 빌드 → ~/.tuist/Cache/에 저장 4. tuist generate → .xcodeproj 생성 → 외부: 캐시 있으면 xcframework, 없으면 소스 타겟 → 내부: 항상 소스 타겟 5. Target dependencies에서 → .external(name:) = Tuist/Package.swift → .package(product:) = 로컬 패키지
Q. 왜 ".external()"이라고 부르나?
Project.swift 기준으로 "외부"라는 뜻:
프로젝트 구조:
├── Tuist/
│ └── Package.swift ← Project.swift "외부"에 정의
│
└── Projects/
└── ABCApp/
└── Project.swift ← 여기 기준
└── packages: ← Project.swift "내부"에 정의
.external()= "Project.swift 밖 (Tuist/ 폴더)에서 정의된 것".package()= "Project.swift 안 packages:에서 정의된 것"
Q. tuist install은 Project.swift packages:도 다운로드하나?
❌ 안 합니다:
| 명령어 | Tuist/Package.swift | Project.swift packages: |
|---|---|---|
tuist install | ✅ 다운로드 | ❌ 무시 |
tuist generate | 연결만 | ✅ 이때 resolve/다운로드 |
// Project.swift에 원격 패키지가 있다면
packages: [
.remote(url: "https://github.com/SomeLib/SomeLib", ...),
]
tuist install // ← SomeLib 다운로드 안 됨
tuist generate // ← 이때 SPM이 resolve해서 다운로드
Q. resolve가 뭐야? 언제 하나?
버전 호환성 계산 + 의존성 그래프 확정:
| resolve 시점 | generate 시 | |
|---|---|---|
| Tuist/Package.swift | tuist install | 연결만 (이미 끝남) |
| Project.swift packages: | tuist generate | resolve + 연결 |
tuist install (Tuist/Package.swift 대상)
├── 1. Package.swift 읽기
├── 2. 버전 resolve (의존성 그래프 계산)
├── 3. 소스 다운로드 → Tuist/.build/checkouts/
└── 4. Package.resolved 생성 (버전 잠금)
tuist generate
└── Tuist/Package.swift 것들은 이미 resolve 끝남
→ 그냥 연결만
Q. 왜 Tuist/Package.swift를 권장하나?
resolve가 "느림 × 반복"이라서:
tuist generate 할 때마다 (Project.swift packages: 원격): 1. 원격 서버에 연결 (네트워크) 2. 패키지 manifest 확인 (Package.swift 읽기) 3. 버전 호환성 계산 (의존성 그래프) 4. 소스 다운로드 (없거나 변경 시) 5. .xcodeproj 생성 → 매번 1~4 체크해야 함
| Project.swift packages: (원격) | Tuist/Package.swift | |
|---|---|---|
generate 시 | 매번 resolve 체크 | 이미 완료됨 |
| 빌드 시 | 소스 컴파일 | xcframework 링킹만 |
| 캐싱 | ❌ | ✅ |
| 체감 속도 | 느림 (30초~1분) | 빠름 (5초) |
결론: 한 번 resolve/build 해두고 재사용 → Tuist/Package.swift + cache 권장
15. Scheme · Configuration · dSYM (2026-06-30)
Q. Scheme 구조가 뭐야?
Scheme.scheme(
name: "QA_ABCApp", // 1. 스킴 이름
buildAction: .buildAction(targets: [...]), // 2. 빌드할 타겟
runAction: .runAction(configuration: "Debug_Qa"), // 3. 실행(⌘R) 시 Configuration
archiveAction: .archiveAction(configuration: "Release_Qa") // 4. 아카이브 시 Configuration
)
┌─────────────────────────────────────────────────────────┐ │ Scheme: QA_ABCApp ← name │ ├─────────────────────────────────────────────────────────┤ │ Build │ │ └── Target: QA_ABCApp ← targets │ ├─────────────────────────────────────────────────────────┤ │ Run (⌘R) │ │ └── Configuration: Debug_Qa ← runAction │ ├─────────────────────────────────────────────────────────┤ │ Archive (배포용 빌드) │ │ └── Configuration: Release_Qa ← archiveAction │ └─────────────────────────────────────────────────────────┘
Q. Debug vs Release Configuration 차이?
| 설정 | Debug | Release |
|---|---|---|
| Optimization Level | -Onone (없음) | -O (최적화) |
| Debug Symbols | ✅ 포함 | ❌ 제거 (dSYM 별도) |
| Assertions | ✅ 활성화 | ❌ 제거 |
| Testability | ✅ 테스트 가능 | ❌ |
| Code Signing | 개발자 인증서 | 배포 인증서 |
// Assertions 예시 assert(user != nil, "유저 없음") // Debug: 크래시 (버그 발견) // Release: 이 줄 자체가 사라짐 // Optimization 예시 // Debug: 코드 그대로 실행 → 브레이크포인트 정확히 멈춤 // Release: 컴파일러가 코드 재배치 → 브레이크포인트 이상하게 멈춤
Q. 환경별 Configuration은 왜 나누나?
| 환경 | 실행 시 (개발) | 아카이브 시 (배포) |
|---|---|---|
| Production | Debug | Release |
| QA | Debug_Qa | Release_Qa |
| Sandbox | Debug_Sandbox | Release_Sandbox |
// Debug_Qa vs Release_Qa 공통 API_BASE_URL = "https://qa-api.example.com" // Debug_Qa만 ENABLE_LOGGING = true SHOW_DEBUG_MENU = true // Release_Qa만 ENABLE_LOGGING = false SHOW_DEBUG_MENU = false
Q. dSYM이 뭐야? Firebase Crashlytics랑 무슨 관계?
dSYM = 주소 ↔ 함수명 매핑 테이블. 크래시 로그를 읽을 수 있게 해줌:
크래시 발생 시 실제 기록:
0x1000a4f2c ← 메모리 주소만 있음
0x1000a3b18
0x1000a2104
↓ dSYM으로 symbolicate
Firebase Crashlytics 표시:
UserService.swift:42 fetchUser()
LoginViewController.swift:128 loginButtonTapped()
AppDelegate.swift:31 application(_:didFinishLaunching:)
Q. dSYM 없으면 어떻게 되나?
| dSYM 없음 | dSYM 있음 |
|---|---|
Crashed: com.app.main 0x1000a4f2c 0x1000a3b18 → 뭐가 문제인지 모름 |
Crashed: com.app.main UserService.fetchUser():42 LoginViewController.loginButtonTapped():128 → 어디서 터졌는지 바로 앎 |
Q. dSYM 흐름 정리
1. Release 빌드 시 ├── 앱 바이너리 (주소만, 심볼 제거됨) └── dSYM 파일 (주소 ↔ 함수명 매핑) 2. 앱스토어 배포 └── 앱 바이너리만 올라감 3. Firebase에 dSYM 업로드 └── 나중에 크래시 해석용 4. 유저 크래시 발생 └── 주소만 Firebase로 전송: "0x1000a4f2c에서 죽음" 5. Firebase가 dSYM으로 변환 (symbolicate) └── "UserService.swift:42 fetchUser()에서 죽음"
핵심: Release 빌드는 심볼 제거 → dSYM 별도 보관 → 크래시 시 symbolicate
16. Framework · Library · 링킹 흐름 (2026-06-30)
Q. Static/Dynamic Framework 결과물 확장자?
둘 다 .framework:
| 타입 | 확장자 | 특징 |
|---|---|---|
| Static Library | .a | 바이너리만 (헤더 별도) |
| Dynamic Library | .dylib | 바이너리만 (헤더 별도) |
| Static Framework | .framework | 바이너리 + 헤더 + 리소스 + 메타데이터 |
| Dynamic Framework | .framework | 바이너리 + 헤더 + 리소스 + 메타데이터 |
# .framework 안의 바이너리로 구분 file MyFramework.framework/MyFramework # Static이면: current ar archive random library # Dynamic이면: Mach-O 64-bit dynamically linked shared library
핵심: .framework는 껍데기(컨테이너), 안의 바이너리가 static/dynamic 결정
Q. Static vs Dynamic 앱 번들 구조?
Static:
MyApp.app/
└── MyApp ← 실행파일 (Alamofire 코드 포함됨)
Dynamic:
MyApp.app/
├── MyApp ← 실행파일 (참조만: "@rpath/Alamofire")
└── Frameworks/
└── Alamofire.framework ← 별도 파일로 존재
Q. Dynamic Framework 링킹 흐름?
1. 컴파일 타임
└── .swiftmodule 읽음
→ 타입 체킹 ✅
→ 주소? ❌ 없음
2. 링크 타임
└── 실행파일에 기록:
"Alamofire.request 심볼 필요"
"@rpath/Alamofire.framework에서 찾아라"
→ 주소? ❌ 아직 모름 (placeholder)
3. 런타임 (앱 실행)
└── dyld가:
1) .app/Frameworks/Alamofire.framework 로드
2) 메모리에 올림
3) 심볼 테이블 읽음
4) "request" → 0x1000a4f2c 바인딩
→ 주소? ✅ 이제 알음
| 단계 | 사용하는 것 | 하는 일 |
|---|---|---|
| 컴파일 | .swiftmodule | 타입 체킹, 문법 검사 |
| 링크 | .tbd / 심볼 목록 | "이 심볼 필요해" 기록 |
| 런타임 | .framework 바이너리 | 심볼 → 주소 바인딩 |
차이: Static은 링크 타임에 주소 확정, Dynamic은 런타임에 dyld가 바인딩
Q. ProjectDescriptionHelpers는 사용자 정의 폴더명?
❌ Tuist 공식 기능 (이름 고정):
Tuist/
├── Config.swift
├── Package.swift
└── ProjectDescriptionHelpers/ ← Tuist가 이 이름을 인식
└── *.swift ← 자동으로 모듈로 만들어줌
// Tuist/ProjectDescriptionHelpers/MyHelpers.swift
public func makeAppTarget() -> Target { ... }
// Projects/MyApp/Project.swift
import ProjectDescriptionHelpers // ← Tuist가 자동으로 import 가능하게 해줌
let project = Project(
targets: [makeAppTarget()]
)
폴더 이름 바꾸면 Tuist가 인식 못함. 이름 고정, 내용만 사용자 정의.
Q. Tuist 의존성 문법 — .target() vs .package()?
"어디서 가져오느냐" 기준:
| 문법 | 대상 |
|---|---|
.target(name:) | 같은 Project.swift 내 타겟 |
.project(target:path:) | 다른 Tuist 프로젝트의 타겟 |
.package(product:) | packages:에 선언된 SPM 패키지의 프로덕트 |
.external(name:) | Tuist/Package.swift의 프로덕트 |
// Project.swift
let project = Project(
packages: [
.local(path: "../MyLibrary"), // SPM 패키지
],
targets: [
Target(
name: "MyApp",
dependencies: [
.target(name: "MyFramework"), // 같은 Project.swift의 타겟
.package(product: "MyLibrary"), // packages:의 SPM 프로덕트
]
),
Target(name: "MyFramework", ...),
]
)
핵심: SPM 패키지의 "타겟"을 직접 가져오는 건 없음. 항상 프로덕트를 통해 가져옴.
핵심 요약
| 개념 | 정의 |
|---|---|
| pbxproj | 36년 된 OpenStep plist 형식의 프로젝트 설정 파일 |
| Target | 빌드 결과물 1개 (App, Test, Widget 등) |
| Project | 타겟들의 묶음 (.xcodeproj) |
| Workspace | 프로젝트들의 묶음 (.xcworkspace) |
| Scheme | 타겟 + Configuration 조합 (실행 방법) |
| Configuration | 빌드 옵션 세트 (Debug, Release 등) |
| dSYM | 크래시 로그를 읽을 수 있게 해주는 디버그 심볼 |
| Symbol Table | 함수 이름 ↔ 주소 매핑 (링커/디버거용) |
| .swiftmodule | 모듈 공개 인터페이스 (컴파일러용) |
| dyld | 런타임에 Dynamic Framework 로드 및 주소 바인딩 |
Tuist 한 줄 요약
Tuist = Swift 코드로 위 모든 것을 정의하고
tuist generate로 .xcodeproj/.xcworkspace 자동 생성