View in English

  • 메뉴 열기 메뉴 닫기
  • Apple Developer
검색
검색 닫기
  • Apple Developer
  • 뉴스
  • 둘러보기
  • 디자인
  • 개발
  • 배포
  • 지원
  • 계정
페이지에서만 검색

빠른 링크

5 빠른 링크

비디오

메뉴 열기 메뉴 닫기
  • 컬렉션
  • 주제
  • 전체 비디오
  • 소개

WWDC25 컬렉션으로 돌아가기

  • 소개
  • 요약
  • 자막 전문
  • 코드
  • SwiftData: 상속 및 스키마 마이그레이션 자세히 알아보기

    클래스 상속을 사용하여 데이터를 모델링하는 방법을 알아보세요. 상속 사용을 위해 쿼리를 최적화하고 앱 데이터를 원활하게 마이그레이션하는 방법을 학습합니다. 모델 그래프 구축, 효율적인 가져오기 및 쿼리 작성, 강력한 스키마 마이그레이션 구현을 위한 서브클래스 생성 방법을 확인하세요. 효율적인 변경 추적을 위해 Observable 및 영구 기록을 이용하는 방법에 대해서도 이해할 수 있습니다.

    챕터

    • 0:00 - 서론
    • 2:11 - 클래스 상속 사용하기
    • 7:39 - 마이그레이션으로 데이터 개선하기
    • 11:27 - 가져온 데이터 맞춤화하기
    • 13:54 - 데이터 변경 관찰하기
    • 18:28 - 다음 단계

    리소스

    • Adopting SwiftData for a Core Data app
    • Building rich SwiftUI text experiences
    • SwiftData
      • HD 비디오
      • SD 비디오

    관련 비디오

    WWDC25

    • SwiftUI의 새로운 기능

    WWDC24

    • SwiftData 기록으로 모델 변경 사항 추적하기
  • 비디오 검색…

    저는 Rishi Verma입니다 SwiftData 팀 엔지니어죠 “SwiftData: 상속과 스키마 마이그레이션 자세히 알아보기”에 오신 여러분을 환영합니다 iOS 17에 도입된 SwiftData로 앱 데이터를 모든 Apple 플랫폼에서 모델링하고 유지할 수 있습니다 최신 Swift 언어 기능을 활용하면 빠르고 안전하고 효율적으로 코드를 작성할 수 있죠 이 영상에서는 클래스 상속 활용을 소개하고 언제 상속을 하는 것이 좋은지 알아보려 합니다 상속을 도입하고 스키마를 발전시키면서 데이터 보존 마이그레이션 전략에 대해 이야기하고 최적의 성능을 위해 SwiftData 가져오기와 쿼리를 조정하는 방법을 살펴보겠습니다 모델 변경 사항을 로컬이나 원격으로 보는 법도 다룹니다 몇 차례의 릴리스에서 친숙한 SampleTrips를 사용했죠 SwiftUI로 작성한 앱이며 다양한 여행 계획 관리에 사용됩니다 이 앱 모델에 SwiftData를 사용하려면 프레임워크를 가져와서

    Model 매크로를 적용하기만 하면 됩니다

    그리고 앱 정의에서 WindowGroup에 modelContainer 수정자를 추가하면 전체 보기 계층에 Trip 모델이 전달됩니다 modelContainer를 연결하면 보기를 업데이트하고 Query 매크로를 활용할 수 있습니다 정적 데이터를 제거하고 보기를 다르게 채워 보겠습니다 Query 매크로로 코드를 생성하고 모델 컨테이너에서 여행을 가져옵니다

    다 됐습니다 이제 제가 만든 모든 여행이 영속되고 SwiftUI 보기에 완벽하게 맞아떨어집니다 SwiftData는 손쉽게 영속성을 제공할 뿐만 아니라 스키마의 모델링과 마이그레이션 그래프 관리 CloudKit 동기화 등 다양한 기능을 제공합니다 SwiftData에 추가된 최신 기능이 바로 클래스 상속입니다 iOS 26의 새로운 기능으로 상속을 활용한 모델 그래프를 구축할 수 있게 되었습니다 클래스 상속은 강력한 도구입니다 어떤 작업에 적합한 도구인지 알아보시죠 상속이 잘 작동하려면 모델이 자연스러운 계층 구조를 형성하며 공통 특성을 공유해야 합니다 Trip 모델에는 모든 여행에 필요한 속성인 목적지와 startDate, endDate가 있어 어디로 언제 가는지 알 수 있죠 새 Trip 하위 클래스는 모두 Trip에 정의된 이 속성들과 다른 공유 동작들을 자동으로 가지게 되죠 Trip 모델은 광범위한 도메인입니다 우리 삶에는 다양한 유형의 여행이 있죠 새 Trip 하위 클래스는 광범위한 Trip에 알맞은 자연스러운 하위 도메인이어야 합니다

    SampleTrips 앱에서는 많은 여행이 개인 여행과 출장 2개의 하위 도메인으로 나뉩니다 이렇게 여행의 하위 도메인을 표현하는 새 모델에 하위 클래스에만 적용되는 속성과 동작을 추가해 보겠습니다 개인 여행에는 그 여행을 간 이유를 나타내는 열거형을 추가하겠습니다 그리고 출장에는 일일 경비를 기록하는 속성을 추가해서 다음 출장에 얼마를 써야 할지 알아보겠습니다 SampleTrips 앱으로 더 풍부한 경험을 만들어 보세요 하위 클래스에 공유할 속성을 가진 Trip 클래스입니다 Trips 앱에 하위 클래스를 2개 새로 추가해 보겠습니다 하나는 출장용이고 하나는 개인 여행용입니다 iOS 26 이상에서는 @available을 붙여서 SwiftData의 상속 지원 조건에 맞춥니다 이제 하위 클래스에 하위 도메인 속성을 추가해 보겠습니다 출장에 perdiem를 추가하고 초기값을 설정합니다 PersonalTrip에는 Reason 열거형을 추가해 여행을 간 이유를 표시하려고 합니다 잠깐, 마지막으로 새 서브 클래스에 넣을 스키마를 업데이트합니다 출장과 개인 여행을 modelContainer 수정자에 추가하면 끝입니다 특별한 코드 없이도 SampleTrips 앱에 개인 여행은 블루로 출장은 그린으로 표시됩니다 클래스 상속은 강력한 도구이지만 모든 문제의 해결책은 아닙니다 언제 상속을 활용할 수 있는지 알아봅시다

    상속이 유용한 몇 가지 시나리오가 있습니다 모델이 자연스럽게 계층적 관계를 표현하고 상속하고자 하는 공통 특성이 있고 “~은 ~이다” 형태에서 상속이 유용할 수 있습니다

    상속받은 모델을 사용할 때 개인 여행은 여행이다 즉 여기 여행 Query에서 Trip 유형 관련 작업을 할 때 개인 여행과 출장을 포함해 부모 클래스 Trip의 모든 여행이 있다는 걸 알고 있죠 여기 보시면 UI와 같은 색깔의 비행기로 여행이 표시되어 있죠 모델 컨테이너에서 컨텍스트까지 쿼리로 이어지고 있습니다 하지만 모델 간 공통 속성을 공유하려고 상속을 사용하면 안 됩니다 예를 들어, name이라는 속성이 있는 모든 모델의 하위 클래스를 만든다면 공통 속성이 하나뿐이고 다른 특성은 하위 도메인에 격리된 수많은 하위 도메인이 있는 계층 구조가 되겠죠 자연스러운 계층 구조가 아닌 이런 하위 도메인은 프로토콜 준수로 표현하는 편이 좋습니다 프로토콜 준수는 서로 다른 도메인이 행동을 공유하되 관련 없는 다른 특성은 공유하지 않습니다 모델을 쿼리하거나 가져오는 방식에 따라 상속을 하기도 하죠

    데이터를 가져오는 방법에는 여러 가지가 있지만 Query 매크로로 모델 컨테이너에서 여행을 가져와 보기를 형성했습니다 이는 심층 검색의 예입니다

    심층 검색만 활용한다면 항상 모든 여행을 가져오고 Trip 유형만 활용하면 개인 여행인지 출장인지를 하위 클래스가 아닌 Trip 속성에서 알아보게 되죠 쿼리나 가져오기가 하위 클래스만 불러온다면 이를 얕은 검색이라고 합니다 이때는 Trip이 유형을 가져오거나 활용하지 않기 때문에 모델을 평탄화하는 것이 좋을 수 있습니다 하지만 심층 검색과 얕은 검색을 활용한다면 상속이 도움이 됩니다 모든 여행과 PersonalTrip 같은 특정한 하위 유형을 검색해서 해당 유형에 맞는 맞춤형 보기를 자주 구성한다면요 Trips 앱을 업데이트해서 개인 여행 또는 출장만 표시하는 방법을 잠시 살펴보겠습니다

    구분 제어기를 활용해서 모든 여행을 표시한 다음 특정 하위 클래스만 표시해 보죠

    선택한 구분만 사용해서 클래스가 특정 유형인지 결정하는 술어를 ’is’키워드로 도출합니다 예를 들어, 여기 이게 PersonalTrip인지 확인해 봅시다 그런 다음 술어를 주고 여행 startDate로 정렬해서 Query를 초기화합니다 앱에서 확인해 보죠 첫 화면인 여행 보기에서 모든 여행이 보입니다 특정 하위 클래스로 보기를 좁혀 보겠습니다 정말 흥미롭죠! iOS 26에서 클래스 상속을 이렇게 활용할 수 있습니다 아직 끝이 아닙니다 방금 스키마에 몇 가지 중요한 변경 사항을 만들었습니다 기존 앱에 미치는 영향과 데이터 마이그레이션 방법을 생각해야겠죠 SampleTrips 앱은 몇 차례의 릴리스를 거치며 여러 번 진화했습니다 잠깐 동안 버전 스키마와 스키마 마이그레이션 계획으로 그 진화를 살펴보겠습니다 최신 SampleTrips 앱으로 업그레이드할 때 사용자 데이터가 보존되도록 말이죠

    첫 번째 영상은 iOS 17에서 SwiftData를 소개하고 여행을 사용해 도입을 설명했습니다 그 소개 영상에서 우리는 독특한 여행 이름을 짓고 속성의 원래 이름을 변경해서 데이터를 마이그레이션할 때 데이터를 보존하는 법을 배웠죠

    iOS 17의 경우 새 버전 식별자 2.0으로 버전 스키마를 구축하고 고유한 이름으로 바꾸고 시작일과 종료일의 이름을 바꾼 변경된 모델 Trip을 제시했습니다

    다음으로 맞춤형 마이그레이션 단계를 추가해서 기존 여행을 복제했습니다 이때 ModelContext의 fetch 함수를 활용해 모든 여행을 가져오고 복제했습니다

    iOS 18에서는 index와 unique 매크로를 활용하고 삭제 시에 보존할 속성도 결정할 수 있었습니다

    이렇게 하면 데이터 저장소에서 삭제한 다음에도 모델을 식별할 수 있었죠

    버전 3으로 표시하는 iOS 18의 버전 스키마는 Trip 모델의 변경 사항을 저장합니다 새로운 unique와 index 매크로 사용법으로 데이터 중복을 제거하고 가져오기와 쿼리를 향상했습니다 또, 삭제할 때 보존 값과 동일한 속성에 적용해 삭제된 여행을 영구 기록에서 식별할 수 있게 했습니다

    또 다른 맞춤형 마이그레이션 단계를 추가해서 버전 2에서 버전 3으로 마이그레이션할 때 중복 여행을 제거했습니다 이제, iOS 26에서 버전 4를 추가할 예정입니다 하위 클래스가 있고 마이그레이션 단계가 가벼워졌죠 현재 버전 스키마는 버전 4로 표시하며 스키마에 있는 모든 모델과 새로운 하위 클래스를 나타냅니다 하위 클래스는 iOS 26 이상이 적용되었고 버전 스키마도 마찬가지입니다 이전과 동일한 필요 가용성으로 버전 3에서 버전 4로 가벼운 마이그레이션 단계를 추가해야 하죠 최종 버전 스키마와 마이그레이션 단계가 구축되면 스키마 마이그레이션 계획에 이 모든 것을 캡슐화해 버전 스키마의 순서와 실행할 마이그레이션 단계를 제공할 수 있게 됩니다 스키마 마이그레이션 계획은 릴리즈된 순서대로 배열된 스키마로 구성됩니다 iOS 26이 공개되면 하위 클래스가 있는 최신 스키마를 추가하고 마이그레이션 단계 배열을 추가해 한 릴리스에서 다음 릴리스로 넘어갈 수 있게 합니다 이것이 우리의 스키마 마이그레이션 계획 방식입니다 이제 버전 스키마와 상응하는 스키마 마이그레이션 계획을 구축했으니 다음은 SampleTrips 모델 컨테이너를 생성할 때 이를 활용하는 것입니다 modelContainer 수정자로 돌아가서 이를 스키마 마이그레이션 계획 모델 컨테이너로 업데이트합니다 먼저 응용 프로그램에 새 컨테이너 속성을 추가합니다 버전 4 스키마를 구성하고 스키마 마이그레이션 계획을 ModelContainer 이니셜라이저에 적용합니다 이제 modelContainer 수정자를 업데이트해서 컨테이너를 마이그레이션할 수 있게 만듭니다 여기까지 하면 상속을 위한 SampleTrips 업데이트로 이전에 출시했던 여러 버전을 마이그레이션하며 클라이언트의 데이터는 보존됩니다 이제 마이그레이션을 마쳤으니 보기와 마이그레이션 단계에서 활용했던 쿼리와 가져오기를 개선할 방법을 생각해 봅시다 Predicate로 선택한 세그먼트로 쿼리를 업데이트했지만 지난 비디오에서는 검색 창이 있었습니다 다시 추가한 다음 클라이언트가 입력한 검색 텍스트 처리로 바로 넘어가겠습니다

    제공된 searchText로 술어를 구축해 봅시다 먼저, 텍스트가 비어 있는지 확인합니다 비어 있지 않다면 복합 술어로 여행 이름이나 목적지에 주어진 텍스트가 있는지 확인해 보겠습니다 검색 술어와 클래스 술어로 복합 술어를 작성해 보겠습니다 마지막으로 Query 이니셜라이저를 업데이트해 새로운 복합 술어를 전달합니다 이렇게 업데이트하면 검색 창을 탭해서 입력 텍스트로 여행을 필터링하고 구분 제어기로 범위를 더욱 좁힐 수도 있습니다 필터링과 정렬은 쿼리와 가져오기 맞춤화 방법 중 하나일 뿐입니다 SwiftData 가져오기를 맞춤화하는 다른 방법도 몇 가지 살펴봅시다 버전 1에서 버전 2로의 맞춤형 마이그레이션 단계입니다 willMigrate 블록으로 모든 Trip을 가져옵니다 제 중복 제거 로직에서는 name 속성 하나에만 접근합니다 name이 버전 2의 고유한 속성이며 중복 확인에 사용하기 때문이죠 name이 접근 가능한 유일한 속성이기 때문에 propertiesToFetch 사용을 name으로 하도록 fetchDescriptor를 업데이트하면 마이그레이션에 필요한 데이터만 패킹합니다 또, 특정한 관계를 이동할 수 있는 경우 숙박 시설을 재할당해야 한다면 중복을 찾았을 때 relationshipsToPrefetch를 이용해 똑같이 해 볼 수 있겠죠 여기 living Accommodation 관계를 추가해 봅시다

    프리페치 속성을 도입했으니 SimpleTrips의 기존 위젯 코드를 업데이트해서 조금 더 개선해 봅시다 SampleTrips 위젯에는 가장 최근 여행의 쿼리가 있습니다 하지만 단일 값만 가져오도록 개선해 볼 수 있습니다 현재 위젯 코드는 첫 번째 가져오기 결과만 활용합니다 가져오기 한도를 설정해서 더 효율적으로 바꿀 수 있습니다

    가져오기 한도를 설정하면 위젯은 술어를 만족하는 첫 번째 여행을 가져오며 향후 계획된 여행이 많은 시나리오도 걱정할 필요가 없습니다 쿼리와 가져오기 개선으로 모델이 어떻게 바뀌었는지 한번 알아봅시다 영구적 모델은 모두 Observable이죠 withObservationTracking을 사용하면 모델의 관심 속성 변경 사항에 반응합니다 여행 시작일과 종료일의 변화를 알아보고 싶다면 datesChangedAlert 기능을 추가해서 사용자가 날짜를 변경하면 알림을 띄울 수 있습니다

    이 방식으로 PersistentModels에 생긴 많은 로컬 변경 사항을 관찰할 수 있어서 매우 유용합니다 Observable에 대한 최신 정보는 'Swift의 새로운 기능'에서 확인해 보세요 모든 변경이 Observable은 아니며 프로세스에서 모델에 발생한 변경 사항만 해당하죠. 위젯이나 확장 프로그램, 심지어 앱 내 다른 모델 컨테이너처럼 다른 프로세스 에서 발생한 데이터 스토어 에서 해당되지 않죠 같은 모델 컨테이너를 사용한 여러 모델 컨텍스트가 있을 때 앱에 대한 로컬 또는 내부 변경 사항입니다 다른 모델 컨텍스트 간에는 서로 변경 사항을 볼 수 있고 Query에는 변경 사항이 자동으로 적용됩니다 하지만 모델 컨텍스트의 fetch API를 사용한다면 리페치를 트리거할 때까지 다른 모델 컨텍스트의 변경 사항은 보이지 않습니다

    외부 작업으로 데이터가 변경될 수도 있습니다 위젯 저장이나 공유 App Group 컨테이너에 또 다른 앱이 덮어쓰는 것처럼요 이러한 변경 사항은 Query 기반 보기를 자동 업데이트합니다 하지만 fetch 사용은 다시 리페치해야 합니다 리페치는 비용이 많이 들며 관심 있는 모델 처리 변경 사항이 없다면 더욱 그렇죠

    다행히 SwiftData에는 영구 기록이 있습니다 언제 어떤 모델이 변경되었는지 누가 모델을 변경했는지 어떤 속성이 업데이트되었는지도 알 수 있습니다 preservedValueOnDeletion을 Trip의 여러 속성에 적용해 여행이 삭제되면 기록에 삭제 마커가 생기며 이 마커를 파싱해서 삭제된 여행을 식별할 수 있습니다 더 많은 기록은 WWDC24의 'SwiftData 기록으로 모델 변경 사항 추적하기'를 참고하시기 바랍니다

    리페치가 필요한지 여부를 영구 기록으로 알아보겠습니다 먼저 컨테이너에서 최신 기록 토큰을 가져옵니다 마지막으로 읽은 위치를 나타내는 데이터베이스 마커로 사용합니다 좋아하는 책에 읽다 만 부분을 표시해 두는 책갈피와 비슷하죠 기본 기록 트랜잭션에 대한 기록 설명자를 사용해 기록 가져오기를 설정합니다 영구 기록이 많은 경우에는 마지막 기록이 필요한데도 토큰으로 많은 데이터를 가져오게 될 수도 있죠 다행히 iOS 26의 새 기능은 sortBy로 기록을 가져올 수 있습니다 거의 모든 트랜잭션 속성을 키 패스로 지정할 수 있죠 예를 들어 author나 transactionIdentifier를 키 패스로 사용해 기록 결과를 정렬할 수 있습니다 새로운 정렬 기준을 설정해 보겠습니다 transactionIdentifier에 따라서, 역순으로요 그러면 최신 트랜잭션이 먼저 나옵니다 가장 최신의 첫 번째 트랜잭션만 필요하므로 결과를 1로 제한하겠습니다 이렇게만 하면 효과적으로 최신 기록 토큰을 가져오고 저장할 수 있죠 이 토큰을 저장하고 향후 기록을 가져올 때 사용할 수 있습니다 이제 위젯에서 여행을 업데이트하는 것처럼 새로운 변경 사항이 생기면 기록에 새 엔트리가 추가되고 앱에서 기록을 가져와 마지막 토큰 이후 변경 사항을 확인할 수 있죠 저장된 기록 토큰이 있으니 술어를 작성해서 해당 토큰 이후의 기록만 가져올 수 있습니다 관심 있는 변경 사항만 찾으려면 변경 사항을 알고자 하는 엔터티만 구축합니다 여기서는 여행이 변경되었는지 아니면 위젯이 머물 장소를 컨펌한 경우 숙박 시설이 변경되었는지 알고자 합니다 그리고 엔터티 이름을 변경 사항 술어로 사용해 원하는 변경 사항 유형으로 기록을 필터링합니다 마지막으로는 토큰 술어와 변경 술어로 복합 술어를 구축합니다 이 변경 사항을 모두 적용하면 기록을 가져올 때 토큰 이후 기록과 현재 처리 중인 필요한 엔터티만 가져올 수 있습니다 개선된 기록 가져오기에서는 관심 사항이 없으면 리페칭을 피할 수 있습니다 SwiftData 기록 덕분에 작업이 쉬워지죠 이제 우리는 모델과 데이터의 변경 사항을 보는 법을 배웠습니다 이 비디오가 유익했기를 바라며 영구성을 위해 SwiftData를 활용해 보시기를 바랍니다 모델 그래프를 구축할 때에는 상속이 적합한지와 그래프 변화하면서 마이그레이션이 미칠 영향을 고려해 보세요 데이터를 가져올 때에도 더 풍부하고 뛰어나게 가져오기와 쿼리를 구축해 보시기 바랍니다 데이터가 언제 변경되었는지 아는 것은 매우 중요합니다 Observation과 영구 기록이 여러분을 도와 드릴 것입니다 이상으로 마치겠습니다 즐거운 여행 되시길 바랍니다

    • 1:07 - Import SwiftData and add @Model

      // Trip Models decorated with @Model
      import Foundation
      import SwiftData
      
      @Model
      class Trip {
        var name: String
        var destination: String
        var startDate: Date
        var endDate: Date
        
        var bucketList: [BucketListItem] = [BucketListItem]()
        var livingAccommodation: LivingAccommodation?
      }
      
      @Model
      class BucketListItem { ... }
      
      @Model
      class LivingAccommodation { ... }
    • 1:18 - Add modelContainer modifier

      // SampleTrip App using modelContainer Scene modifier
      
      import SwiftUI
      import SwiftData
      
      @main
      struct TripsApp: App {
        var body: some Scene {
          WindowGroup {
            ContentView()
          }
          .modelContainer(for: Trip.self)
        }
      }
    • 1:30 - Adopt @Query

      // Trip App using @Query
      import SwiftUI
      import SwiftData
      
      struct ContentView: View {
        @Query
        var trips: [Trip]
      
        var body: some View {
          NavigationSplitView {
            List(selection: $selection) {
              ForEach(trips) { trip in
                TripListItem(trip: trip)
              }
            }
          }
        }
      }
    • 3:28 - Add subclasses to Trip

      // Trip Model extended with two new subclasses
      
      @Model
      class Trip { 
        var name: String
        var destination: String
        var startDate: Date
        var endDate: Date
        
        var bucketList: [BucketListItem] = [BucketListItem]()
        var livingAccommodation: LivingAccommodation?
      }
      
      @available(iOS 26, *)
      @Model
      class BusinessTrip: Trip {
        var perdiem: Double = 0.0
      }
      
      @available(iOS 26, *)
      @Model
      class PersonalTrip: Trip {
        enum Reason: String, CaseIterable, Codable {
          case family
          case reunion
          case wellness
        }
        
        var reason: Reason
      }
    • 4:03 - Update modelContainer modifier

      // SampleTrip App using modelContainer Scene modifier
      
      import SwiftUI
      import SwiftData
      
      @main
      struct TripsApp: App {
        var body: some Scene {
          WindowGroup {
            ContentView()
          }
          .modelContainer(for: [Trip.self, BusinessTrip.self, PersonalTrip.self])
        }
      }
    • 7:06 - Add segmented control to drive a predicate to filter by Type

      // Trip App add segmented control
      import SwiftUI
      import SwiftData
      
      struct ContentView: View {
        @Query
        var trips: [Trip]
        
        enum Segment: String, CaseIterable {
          case all = "All"
          case personal = "Personal"
          case business = "Business"
        }
        
        init() {
          let classPredicate: Predicate<Trip>? = {
            switch segment.wrappedValue {
            case .personal:
              return #Predicate { $0 is PersonalTrip }
            case .business:
              return #Predicate { $0 is BusinessTrip }
            default:
              return nil
            }
          }
          _trips = Query(filter: classPredicate, sort: \.startDate, order: .forward)
        }
        
        var body: some View { ... }
      }
    • 8:26 - SampleTrips Versioned Schema 2.0

      enum SampleTripsSchemaV2: VersionedSchema {
        static var versionIdentifier: Schema.Version { Schema.Version(2, 0, 0) }
        static var models: [any PersistentModel.Type] {
          [SampleTripsSchemaV2.Trip.self, BucketListItem.self, LivingAccommodation.self]
        }
      
        @Model
        class Trip {
          @Attribute(.unique) var name: String
          var destination: String
      
          @Attribute(originalName: "start_date") var startDate: Date
          @Attribute(originalName: "end_date") var endDate: Date
          
          var bucketList: [BucketListItem]? = []
          var livingAccommodation: LivingAccommodation?
          
          ...
        }
      }
    • 8:41 - SampleTrips Custom Migration Stage from Version 1.0 to 2.0

      static let migrateV1toV2 = MigrationStage.custom(
         fromVersion: SampleTripsSchemaV1.self,
         toVersion: SampleTripsSchemaV2.self,
         willMigrate: { context in
            let fetchDesc =  FetchDescriptor<SampleTripsSchemaV1.Trip>()
            let trips = try? context.fetch(fetchDesc)
        
            // De-duplicate Trip instances here...
      
            try? context.save()
          }, 
          didMigrate: nil
      )
    • 9:09 - SampleTrips Versioned Schema 3.0

      enum SampleTripsSchemaV3: VersionedSchema {
        static var versionIdentifier: Schema.Version { Schema.Version(3, 0, 0) }
        static var models: [any PersistentModel.Type] {
          [SampleTripsSchemaV3.Trip.self, BucketListItem.self, LivingAccommodation.self]
        }
      
        @Model
        class Trip {
          #Unique<Trip>([\.name, \.startDate, \.endDate])
          #Index<Trip>([\.name], [\.startDate], [\.endDate], [\.name, \.startDate, \.endDate])
      
          @Attribute(.preserveValueOnDeletion)
          var name: String
          
          @Attribute(hashModifier:@"v3")
          var destination: String
      
          @Attribute(.preserveValueOnDeletion, originalName: "start_date")
          var startDate: Date
      
          @Attribute(.preserveValueOnDeletion, originalName: "end_date")
          var endDate: Date
        }
      }
    • 9:33 - SampleTrips Custom Migration Stage from Version 2.0 to 3.0

      static let migrateV2toV3 = MigrationStage.custom(
        fromVersion: SampleTripsSchemaV2.self,
        toVersion: SampleTripsSchemaV3.self,
        willMigrate: { context in
          let trips = try? context.fetch(FetchDescriptor<SampleTripsSchemaV2.Trip>())
      
          // De-duplicate Trip instances here...
      
          try? context.save()
        }, 
        didMigrate: nil
      )
    • 9:50 - SampleTrips Versioned Schema 4.0

      @available(iOS 26, *)
      enum SampleTripsSchemaV4: VersionedSchema {
        static var versionIdentifier: Schema.Version { Schema.Version(4, 0, 0) }
      
        static var models: [any PersistentModel.Type] {
          [Trip.self, 
           BusinessTrip.self, 
           PersonalTrip.self, 
           BucketListItem.self,
           LivingAccommodation.self]
        }
      }
    • 10:03 - SampleTrips Lightweight Migration Stage from Version 3.0 to 4.0

      @available(iOS 26, *)
      static let migrateV3toV4 = MigrationStage.lightweight(
        fromVersion: SampleTripsSchemaV3.self,
        toVersion: SampleTripsSchemaV4.self
      )
    • 10:24 - SampleTrips Schema Migration Plan

      enum SampleTripsMigrationPlan: SchemaMigrationPlan {
        static var schemas: [any VersionedSchema.Type] {
          var currentSchemas: [any VersionedSchema.Type] =
            [SampleTripsSchemaV1.self, SampleTripsSchemaV2.self, SampleTripsSchemaV3.self]
          if #available(iOS 26, *) {
            currentSchemas.append(SampleTripsSchemaV4.self)
          }
          return currentSchemas
        }
      
        static var stages: [MigrationStage] {
          var currentStages = [migrateV1toV2, migrateV2toV3]
          if #available(iOS 26, *) {
            currentStages.append(migrateV3toV4)
          }
          return currentStages
        }
      }
    • 10:51 - Use Schema Migration Plan with ModelContainer

      // SampleTrip App update modelContainer Scene modifier for migrated container
      
      @main
      struct TripsApp: App {
      
        let container: ModelContainer = {
          do {
            let schema = Schema(versionedSchema: SampleTripsSchemaV4.self)
            container = try ModelContainer(
              for: schema, migrationPlan: SampleTripsMigrationPlan.self)
          } catch { ... }
          return container
        }()
        var body: some Scene {
          WindowGroup {
            ContentView()
          }
          .modelContainer(container)
        }
      }
    • 11:48 - Add search predicate to Query

      // Trip App add search text to predicate
      struct ContentView: View {
        @Query
        var trips: [Trip]
      
        init( ... ) {
          let classPredicate: Predicate<Trip>? = {
            switch segment.wrappedValue {
            case .personal:
              return #Predicate { $0 is PersonalTrip }
            case .business:
              return #Predicate { $0 is BusinessTrip }
            default:
              return nil
            }
          }
          
          let searchPredicate = #Predicate<Trip> {
            searchText.isEmpty ? true : 
              $0.name.localizedStandardContains(searchText) ||              
              $0.destination.localizedStandardContains(searchText)
          }
          
          let fullPredicate: Predicate<Trip>
          if let classPredicate {
            fullPredicate = #Predicate { classPredicate.evaluate($0) &&
                                         searchPredicate.evaluate($0)}
          } else { 
            fullPredicate = searchPredicate
          }
          _trips = Query(filter: fullPredicate, sort: \.startDate, order: .forward)
        }
        var body: some View { ... }
      }
    • 12:31 - Tailor SwiftData Fetch in Custom Migration Stage

      static let migrateV1toV2 = MigrationStage.custom(
         fromVersion: SampleTripsSchemaV1.self,
         toVersion: SampleTripsSchemaV2.self,
         willMigrate: { context in
            var fetchDesc =  FetchDescriptor<SampleTripsSchemaV1.Trip>()
            fetchDesc.propertiesToFetch = [\.name]
      
            let trips = try? context.fetch(fetchDesc)
        
            // De-duplicate Trip instances here...
      
            try? context.save()
          }, 
          didMigrate: nil
      )
    • 13:11 - Add relationshipsToPrefetch in Custom Migration Stage

      static let migrateV1toV2 = MigrationStage.custom(
         fromVersion: SampleTripsSchemaV1.self,
         toVersion: SampleTripsSchemaV2.self,
         willMigrate: { context in
            var fetchDesc =  FetchDescriptor<SampleTripsSchemaV1.Trip>()
            fetchDesc.propertiesToFetch = [\.name]
            fetchDesc.relationshipKeyPathsForPrefetching = [\.livingAccommodation]
      
            let trips = try? context.fetch(fetchDesc)
        
            // De-duplicate Trip instances here...
      
            try? context.save()
          }, 
          didMigrate: nil
      )
    • 13:28 - Update Widget to harness fetchLimit

      // Widget code to get new Timeline Entry
      
      func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
        let currentDate = Date.now
        var fetchDesc = FetchDescriptor(sortBy: [SortDescriptor(\Trip.startDate, order: .forward)])
        fetchDesc.predicate = #Predicate { $0.endDate >= currentDate }
      
        fetchDesc.fetchLimit = 1
        
        let modelContext = ModelContext(DataModel.shared.modelContainer)
        if let upcomingTrips = try? modelContext.fetch(fetchDesc) {
          if let trip = upcomingTrips.first { ... }
          
        }
      }
    • 16:24 - Fetch the last transaction efficiently

      // Fetch history with sortBy and fetchlimit to get the last token
      
      var historyDesc = HistoryDescriptor<DefaultHistoryTransaction>()
      historyDesc.sortBy = [.init(\.transactionIdentifier, order: .reverse)]
      historyDesc.fetchLimit = 1
      
      let transactions = try context.fetchHistory(historyDesc)
      if let transaction = transactions.last {
        historyToken = transaction.token
      }
    • 17:29 - Fetch History after the given token and only for the entities of concern

      // Changes AFTER the last known token
      let tokenPredicate = #Predicate<DefaultHistoryTransaction> { $0.token > historyToken }
      
      // Changes for ONLY entities of concern
      let entityNames = [LivingAccommodation.self, Trip.self]
      let changesPredicate = #Predicate<DefaultHistoryTransaction> {
                               $0.changes.contains { change in
                                 entityNames.contains(change.changedPersistentIdentifier.entityName)
                               }
                             }
      
      
      let fullPredicate = #Predicate<DefaultHistoryTransaction> {
                            tokenPredicate.evaluate($0)
                            &&
                            changesPredicate.evaluate($0)
                          }
      
      let historyDesc = HistoryDescriptor<DefaultHistoryTransaction>(predicate: fullPredicate)
      let transactions = try context.fetchHistory(historyDesc)
    • 0:00 - 서론
    • SwiftData를 사용하면 모든 Apple 플랫폼에서 앱 데이터를 모델링하고 유지할 수 있습니다. 이 프레임워크는 데이터 지속성, 스키마 모델링 및 마이그레이션, 그래프 관리, CloudKit 동기화를 간소화합니다. iOS 26부터 사용 가능한 새로운 기능인 클래스 상속을 사용하면 상속을 통해 모델 그래프를 구축할 수 있습니다.

    • 2:11 - 클래스 상속 사용하기
    • 클래스 상속은 강력한 툴로, 특히 모델이 자연스러운 계층 구조를 형성하고 공통적인 특성을 공유할 때 유용합니다. 상속을 통해 부모 클래스의 속성과 동작을 상속받는 하위 클래스를 만들 수 있어 코드 재사용이 촉진되고 구조화된 조직을 유지할 수 있습니다. SampleTrips 앱은 상속을 적용하여 개인 여행이나 출장 등 다양한 유형의 여행을 모델링합니다. 각 하위 클래스는 Trip 모델로부터 필수 속성을 상속받고 하위 도메인과 관련된 특정 속성을 추가합니다. 이러한 접근 방식을 사용하면 데이터를 보다 맞춤화하고 효율적으로 표현할 수 있습니다. 상속을 현명하게 사용하세요. 모델이 ‘is-a’ 관계를 설정하고 쿼리에 부모 클래스와 하위 클래스가 모두 포함되는 경우 상속이 적합합니다. 모델이 자연스러운 계층 구조 없이 공통 속성만 공유하는 경우 프로토콜을 준수하는 것이 더 적합한 접근 방식입니다. 상속과 프로토콜 적합성 간의 선택은 데이터에 대해 수행된 검색의 깊이에 따라 달라집니다.

    • 7:39 - 마이그레이션으로 데이터 개선하기
    • 앱의 iOS 릴리즈 전반에 걸친 SampleTrips의 데이터 마이그레이션 프로세스는 업그레이드하는 동안 사용자 데이터 보존을 보장하는 예를 잘 보여 줍니다. 앱의 스키마는 여러 릴리즈를 거치며 발전했습니다. iOS 17에서는 SwiftData 및 스키마 버전 2.0이 도입되어 여행 이름을 고유하게 만들고 속성 이름을 변경했습니다. iOS 18에선 버전 3.0이 추가되어 인덱스와 고유 매크로를 활용하고 삭제 시 속성을 보존합니다. 중복 제거 시, 사용자 정의 ‘MigrationStages’가 사용되었습니다. iOS 26에서는 하위 클래스를 포함하는 버전 4.0이 도입됩니다. 버전 3.0에서 4.0으로 가벼워진 ‘MigrationStage’가 필요합니다. ‘SchemaMigrationPlan’은 ‘VersionedSchemas’ 및 ‘MigrationStages’를 올바른 순서로 캡슐화하여 구성됩니다. ‘SchemaMigrationPlan’은 SampleTrips의 ‘ModelContainer’를 생성할 때 적용되어 사용자 데이터를 보존하면서 이전의 모든 반복 작업을 원활하게 마이그레이션할 수 있습니다.

    • 11:27 - 가져온 데이터 맞춤화하기
    • 쿼리 및 가져오기를 최적화하는 방법을 알아보기 위해 SampleTrips 앱에서 검색 바 기능을 다시 도입합니다. 앱은 클라이언트의 검색을 기반으로 술어를 구성한 다음 클래스 술어와 결합하여 여행을 필터링합니다. 검색 외에도 다음 기술은 가져오기 성능을 향상시킵니다. 마이그레이션하는 동안 ‘propertiesToFetch’를 사용하여 필요한 속성만 가져옵니다. ‘relationshipsToPrefetch’는 관계 탐색을 최적화하는 데 활용됩니다. 위젯 코드에 ‘fetchLimit’이 설정되어 가장 최근 여행 하나만 검색하여 효율성을 높였습니다.

    • 13:54 - 데이터 변경 관찰하기
    • SwiftData의 Observable 기능은 ‘PersistentModels’에 대한 로컬 변경 사항에 대응하는 데 도움이 됩니다. 하지만 모든 변화가 관찰 가능한 것은 아닙니다. 앱 내의 다른 프로세스, 외부 작업 또는 다른 모델 맥락으로 인한 변경 사항은 다시 가져와야 하는데, 비용이 많이 들 수 있습니다. 다시 가져오기를 최적화하려면 SwiftData의 영구적 기록 기능을 사용하면 됩니다. 최신 기록 토큰을 가져와 마커로 사용하면 마지막 토큰 이후에 발생한 기록 항목과 관심 있는 특정 엔티티만 가져오는 술어를 작성할 수 있습니다. 이러한 접근 방식을 사용하면 앱이 다시 가져오기가 필요한지 판단하여 불필요한 데이터 검색을 방지하고 성능을 향상시킬 수 있습니다.

    • 18:28 - 다음 단계
    • 모델 그래프를 작성할 때 상속과 마이그레이션에 대한 영향을 고려하세요. 성능을 위해 데이터 가져오기 및 쿼리를 향상시킵니다. 관찰 및 지속적인 기록을 활용하여 데이터 변경 사항을 추적합니다.

Developer Footer

  • 비디오
  • WWDC25
  • SwiftData: 상속 및 스키마 마이그레이션 자세히 알아보기
  • 메뉴 열기 메뉴 닫기
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    메뉴 열기 메뉴 닫기
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    메뉴 열기 메뉴 닫기
    • 손쉬운 사용
    • 액세서리
    • 앱 확장 프로그램
    • App Store
    • 오디오 및 비디오(영문)
    • 증강 현실
    • 디자인
    • 배포
    • 교육
    • 서체(영문)
    • 게임
    • 건강 및 피트니스
    • 앱 내 구입
    • 현지화
    • 지도 및 위치
    • 머신 러닝 및 AI
    • 오픈 소스(영문)
    • 보안
    • Safari 및 웹(영문)
    메뉴 열기 메뉴 닫기
    • 문서(영문)
    • 튜토리얼
    • 다운로드(영문)
    • 포럼(영문)
    • 비디오
    메뉴 열기 메뉴 닫기
    • 지원 문서
    • 문의하기
    • 버그 보고
    • 시스템 상태(영문)
    메뉴 열기 메뉴 닫기
    • Apple Developer
    • App Store Connect
    • 인증서, 식별자 및 프로파일(영문)
    • 피드백 지원
    메뉴 열기 메뉴 닫기
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program(영문)
    • News Partner Program(영문)
    • Video Partner Program(영문)
    • Security Bounty Program(영문)
    • Security Research Device Program(영문)
    메뉴 열기 메뉴 닫기
    • Apple과의 만남
    • Apple Developer Center
    • App Store 어워드(영문)
    • Apple 디자인 어워드
    • Apple Developer Academy(영문)
    • WWDC
    Apple Developer 앱 받기
    Copyright © 2025 Apple Inc. 모든 권리 보유.
    약관 개인정보 처리방침 계약 및 지침