2025 KWDC 톺아보기
소개
FE conf 내용 정리
- 한글과 사과밭
- actor boundary를 넘어서
- SharePlay, 어떻게 쓰죠?
- Array로부터 이해하는 Swift의 성능: 동기, 진화, 그리고 미래
- 셰이더 몰라도 괜찮아, Metal 파이프라인은 이렇게 생겼어
- PassKit / ID Verifier로 검증 프로세스 구축하기
- Swift Testing으로 작성한 테스트 코드를 어떻게 찾고 수행할 수 있을까?
- Apple의 컨테이너화 프레임워크 이해하기 - 기초부터 시작하기
- AI as Apple & AI as a Developer
- 사려 깊은 공간 디자인: 세포에서 원자까지 생물학 탐구
- 디자이너가 말하는 디자인 시스템
한글과 사과밭
한글은 초성(ㄱ, ㄴ, …), 중성(ㅏ, ㅑ, …), **종성(ㄱ, ㄹ, …)**을 조합하여 문자를 형성합니다. 유니코드에서는 이를 표현하는 두 가지 방법이 있습니다.
- NFC (Normalization Form C): ‘가’를 하나의 완성형 코드 포인트(U+AC00)로 저장
- NFD (Normalization Form D): ‘가’를 초성(U+1100) + 중성(U+1161)으로 분해해 저장
이 차이는 운영체제에서 파일명 충돌 문제로 이어집니다.
- macOS: 기본적으로 NFD 사용 → ‘가’는 두 글자로 저장됨
- Windows/Linux: NFC 사용 → ‘가’는 하나의 코드로 저장됨
따라서 Dropbox, Google Drive 등 클라우드 동기화에서 같은 이름의 파일이 중복 생성되거나, 문자열 비교가 실패하는 경우가 발생합니다. Python, Swift 등 대부분 언어에서 제공하는 normalize("NFC", text)
를 사용하면 문제를 예방할 수 있습니다.
actor boundary를 넘어서
Swift는 5.5부터 async/await과 actor 모델을 도입했습니다. Swift 6에서는 이 모델을 더욱 엄격히 적용하여, 레거시 스레드 기반 API(DispatchQueue, NSThread 등)와 충돌할 수 있습니다.
Actor 경계 문제
actor는 상태를 격리해 동시 접근 문제를 방지하지만, 레거시 API는 이를 고려하지 않고 동기 호출을 수행합니다. 따라서 actor 메서드를 직접 호출하면 Swift 6에서 컴파일 오류가 발생합니다.
해결 방법 비교
- 커스텀 Executor: actor 실행을 특정 스케줄러(예: DispatchQueue)와 연결해 고성능 제어 가능하지만, 복잡한 구현이 필요합니다.
- DispatchQueue → Task 브리지:
DispatchQueue.async { Task { await actor.method() } }
방식으로 actor isolation을 안전하게 유지합니다. 일반 앱 개발에서 가장 권장됩니다. - 락 기반 접근: NSLock이나 pthread_mutex를 활용해 상태를 보호합니다. 기존 코드를 최소 수정으로 유지할 수 있지만, Swift 동시성 철학에는 맞지 않으며 권장되지 않습니다.
SharePlay, 어떻게 쓰죠?
SharePlay는 FaceTime 세션 중 앱의 콘텐츠(영상, 음악, 문서 등)를 동기화하는 기능이며, GroupActivities는 이를 지원하는 프레임워크입니다.
적용 방법
- Xcode 타깃의 Signing & Capabilities에 Group Activities(또는 “Group Activity”)를 추가(→ 필요한 엔타이틀(entitlement)과 프로비저닝 업데이트).
- 앱에 GroupActivity 타입(프로토콜 준수) 정의 → prepareForActivation() / activate() 흐름 구현.
- GroupSession(sessions async sequence)을 수신하여 join() 하고 세션을 관리.
- 커스텀 데이터 동기화는 GroupSessionMessenger 사용.
- 미디어 동기화(AVPlayer) 필요하면 AVPlayerPlaybackCoordinator / AVPlayerPlaybackCoordinator.coordinateWithSession 사용.
자주 발생하는 이슈 & 해결법
-
엔타이틀먼트/프로비저닝 오류 (code signing / provisioning)
- 증상: 빌드/배포 시 “provisioning profile doesn’t include …” 또는 ITMS 에러. 원인: Group Activities capability를 추가한 뒤 프로비저닝 파일/앱 ID가 갱신되지 않았거나 수동 서명 설정 불일치.
- 해결: Xcode에서 Group Activities 추가 → Apple Developer (Identifiers)에서 해당 App ID에 capability 허용 확인 → 프로비저닝 프로파일 재생성(또는 자동 관리 사용) → Clean & rebuild. (실제 사례/해결 스레드 참고).
-
사용자 단말/설정 문제 (SharePlay 자체가 꺼져 있거나 OS 버전 불일치)
- 증상: 초대 UI가 뜨지 않거나 동작이 아예 안 됨.
- 해결: 참가자 모두 OS 최소 버전(SharePlay 도입 이후 버전, FaceTime 연동 등) 충족 확인·업데이트, FaceTime → SharePlay 설정이 활성화되어 있는지 확인. 또한 네트워크(인터넷) 연결 안정성 확인.
-
동기화/재생 제어가 제대로 안 됨 (플레이백 비동기화, 일시정지/재생 불일치)
- 원인: AVPlayer와 GroupSession 연결 누락 또는 AVPlaybackCoordinator 미사용.
- 해결: GroupSession이 join()된 후 AVPlayerPlaybackCoordinator(또는 AVPlayer의 관련 API)를 사용해 플레이어를 세션에 연결하여 재생/일시정지/seek를 조율. WWDC 샘플/문서에 따라 playbackCoordinator.coordinateWithSession(...) 식으로 연결.
-
커스텀 AVPlayer/SwiftUI 통합 문제 (플레이어가 @State나 바인딩 문제로 재생 제어 누락)
- 증상: 커스텀 플레이어 뷰에서 원격 제어(다른 참가자 액션)가 반영되지 않음.
- 해결: AVPlayer 인스턴스 생명주기(예: @State var player: AVPlayer?)와 GroupSession/Coordinator를 올바른 컨텍스트(메인 스레드/적절한 Actor)에서 연결. Apple 샘플과 StackOverflow 패턴을 참고.
-
컨텐츠/권한(DRM, 구독) 관련 제약
- 증상: Apple Music, 일부 유료/DRM 콘텐츠는 SharePlay로 공유 불가 또는 제한.
- 해결: 서비스 별 정책 확인(일부 미디어는 참가자가 구독자여야 하거나 지역 제한 있음). 컨텐츠가 DRM/서버 제한을 가지면 SharePlay 경험 설계 시 예외 처리(대체 콘텐츠, 초대 거부 메시지 등) 필요.
-
시스템 없이 앱만으로 바로 시작하고 싶을 때 (FaceTime이 없거나 호출 없이 시작)
- SharePlay은 전통적으로 FaceTime 연결을 통해 시작되지만, 앱 내부에서 직접 시작(share sheet / group activity sharing controller) 할 수 있는 방법도 제공됨(문서·기술노트 참조). 즉 “앱에서 바로 시작” 흐름 구현 가능.
샘플 코드
import GroupActivities
import AVKit
// 1) Activity 정의
struct VideoActivity: GroupActivity, Codable {
let url: URL
static var activityIdentifier: String { "com.example.app.video" }
var metadata: GroupActivityMetadata {
var m = GroupActivityMetadata()
m.title = "Watch together"
m.type = .watchTogether
m.fallbackURL = url
return m
}
}
// 2) 사용자가 '공유'를 누를 때: prepareForActivation() -> activate() 흐름
func startSharePlay(with url: URL) async {
let activity = VideoActivity(url: url)
switch await activity.prepareForActivation() {
case .activationDisabled:
// FaceTime 없음 또는 시스템이 로컬 재생 권장
return
case .activationPreferred:
do {
// activate() 또는 prepareForActivation 후 sessions()로 세션 수신
try await activity.activate()
} catch {
print("활성화 실패:", error)
}
case .canceled:
return
}
}
// 3) 세션 수신/관리 (앱의 Coordinator 등에서)
Task {
for await session in VideoActivity.sessions() {
// session: GroupSession<VideoActivity>
await session.join() // join -> 네트워크/메시지 연결이 시작됨
// 메시지(커스텀 동기화)를 위해 Messenger 사용
let messenger = GroupSessionMessenger(session: session)
// messenger.send(...) / for await messages(of: MyMsg.self) { ... }
// AVPlayer 동기화
let player = AVPlayer(url: session.activity.url)
let coordinator = AVPlayerPlaybackCoordinator()
coordinator.coordinate(player, with: session) // 또는 적절한 API 사용
// UI에 player 연결 등...
}
}