Swift: 스위프트에서 데이터를 다루는 방법(1)
1. TabView
앱에서 여러 화면을 탭으로 전환할 때 쓰는 컨테이너 뷰. 각 Tab은 탭 바에 표시될 이름과 아이콘, 그리고 해당 탭을 눌렀을 때 보여줄 뷰로 구성된다. 탭 개수만큼 하단 탭 바가 자동으로 만들어진다.
TabView {
Tab("Friends", systemImage: "person.and.person") {
FriendList()
}
Tab("Movies", systemImage: "film.stack") {
MovieList()
}
}
systemImage는 Apple이 기본 제공하는 SF Symbols 아이콘 이름이다. 수천 개의 아이콘이 있으며 SF Symbols 앱에서 이름을 확인할 수 있다.
탭이 많아지면 사용성이 떨어지기 때문에 보통 2~5개 사이로 유지하는 것이 좋다.
2. @Model
SwiftData에서 저장 가능한 데이터 클래스를 선언할 때 쓰는 매크로. 클래스 앞에 @Model을 붙이면 SwiftData가 자동으로 해당 클래스의 저장/조회/삭제를 처리해준다.
@Model
class Friend {
var name: String
init(name: String) {
self.name = name
}
}
struct가 아닌 class를 써야 하는 이유는 SwiftData가 데이터의 변경을 추적하기 위해 참조 타입(class)을 필요로 하기 때문이다. struct는 값 타입이라 변경 추적이 불가능하다.
@Model을 붙이는 순간 내부적으로 여러 프로토콜이 자동으로 추가되는데, 그 중 Identifiable도 포함되어 있어서 ForEach에서 별도로 id를 지정하지 않아도 된다.
3. static 프로퍼티
static을 붙이면 해당 프로퍼티는 인스턴스가 아닌 타입 자체에 소속된다. 즉 객체를 생성하지 않아도 타입이름.프로퍼티로 바로 접근할 수 있다.
// static 없을 때 → 반드시 인스턴스 생성 후 접근
let friend = Friend(name: "Elena")
friend.sampleData // 인스턴스를 통해서만 접근 가능
// static 있을 때 → 인스턴스 없이 바로 접근
Friend.sampleData // 타입 이름으로 바로 접근 가능
모델 클래스 안에 샘플 데이터를 static으로 선언해두면 어디서든 Friend.sampleData로 접근할 수 있어서 데이터를 한 곳에서 관리할 수 있다.
@Model
class Friend {
var name: String
init(name: String) { self.name = name }
static let sampleData = [
Friend(name: "Elena"),
Friend(name: "Graham"),
Friend(name: "Mayuri"),
Friend(name: "Rich"),
Friend(name: "Rody"),
]
}
4. Schema / ModelConfiguration / ModelContainer
SwiftData 저장소를 만들기 위한 3단계 구성 요소. 각각의 역할이 명확하게 분리되어 있다.
Schema
앱에서 저장할 모델 타입들을 SwiftData에 등록하는 단계. 어떤 클래스를 저장소에서 관리할지 알려주는 목록이라고 보면 된다.
let schema = Schema([Friend.self, Movie.self])
Friend.self처럼 .self를 붙이는 건 인스턴스가 아닌 타입 자체를 넘기는 것이다. "Elena"라는 실제 친구 데이터가 아니라 "Friend라는 클래스가 어떻게 생겼는지 설계도"를 넘기는 거라고 이해하면 된다.
ModelConfiguration
저장소의 동작 방식을 설정하는 단계.
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
isStoredInMemoryOnly: true로 설정하면 데이터가 기기에 저장되지 않고 메모리에만 존재한다. 앱을 재실행하거나 프리뷰를 새로고침하면 데이터가 초기화된다. 프리뷰에서는 항상 깨끗한 상태로 시작해야 하기 때문에 이 옵션을 쓴다. 반대로 실제 앱에서는 false(기본값)로 설정해 기기에 영구 저장한다.
ModelContainer
Schema와 ModelConfiguration을 바탕으로 만들어지는 실제 저장소. @Query와 @Environment(\.modelContext)가 동작하려면 반드시 이 컨테이너가 연결되어 있어야 한다.
let container = try ModelContainer(for: schema, configurations: [config])
try가 필요한 이유는 저장소 생성이 실패할 수 있기 때문이다. 예를 들어 모델 구조가 기존 저장 데이터와 호환되지 않을 때 실패한다.
5. try / do-catch / fatalError
Swift에서 실패 가능한 코드를 안전하게 다루는 오류 처리 구조.
try
"이 함수는 실패할 수 있다"는 표시. throws가 붙은 함수를 호출할 때 반드시 앞에 붙여야 한다. 붙이지 않으면 컴파일 에러가 난다.
// throws가 붙은 함수
func riskyFunction() throws { ... }
// try 없이 호출 → 컴파일 에러
riskyFunction()
// try 붙여서 호출 → OK
try riskyFunction()
do-catch
try 코드의 성공/실패를 처리하는 구조. do 블록 안의 코드가 성공하면 계속 진행하고, 실패하면 catch 블록이 실행된다. catch 블록에는 error라는 변수가 자동으로 만들어져서 어떤 오류인지 확인할 수 있다.
do {
modelContainer = try ModelContainer(for: schema, configurations: [config])
insertSampleData()
try context.save()
} catch {
// error 변수에 오류 정보가 자동으로 들어옴
print(error)
}
fatalError
복구가 불가능한 치명적인 오류일 때 앱을 즉시 종료시킨다. 개발 중에 절대 일어나면 안 되는 상황을 명시적으로 표시할 때 주로 사용한다. 저장소 생성 실패처럼 앱이 아예 동작할 수 없는 상황에서만 써야 하고, 일반적인 오류(네트워크 실패, 입력값 오류 등)에는 절대 쓰면 안 된다.
do {
modelContainer = try ModelContainer(for: schema, configurations: [config])
} catch {
fatalError("저장소 생성 실패: \(error)")
}
6. 싱글톤 패턴 (Singleton Pattern)
앱 전체에서 특정 객체의 인스턴스를 딱 하나만 만들고 공유하는 디자인 패턴. static let shared로 클래스 내부에서 단 하나의 인스턴스를 생성하고, private init()으로 외부에서 직접 생성하는 것을 막는다.
class SampleData {
static let shared = SampleData() // 클래스 자체에 속하는 단 하나의 인스턴스
private init() {
// 초기화 로직
}
}
// 외부에서 사용할 때
SampleData.shared.modelContainer // ✅ 항상 같은 인스턴스
SampleData() // ❌ 컴파일 에러 (private)
같은 저장소를 모든 프리뷰에서 공유하기 때문에 뷰마다 따로 샘플 데이터를 만들 필요가 없어진다. UIApplication.shared, UserDefaults.standard처럼 Apple 프레임워크에서도 이 패턴이 자주 쓰인다.
7. @MainActor
Swift의 동시성(Concurrency) 시스템에서 특정 코드를 메인 스레드에서만 실행하도록 보장하는 어노테이션.
앱에서는 여러 작업이 동시에 실행될 수 있는데, 같은 데이터를 여러 스레드에서 동시에 읽고 쓰면 충돌이 생긴다. Swift는 이를 막기 위해 Actor라는 개념을 제공하고, 메인 Actor는 UI 업데이트를 담당하는 메인 스레드에서만 코드가 실행되도록 보장한다.
modelContainer.mainContext는 메인 Actor에서만 접근할 수 있도록 설계되어 있어서, @MainActor가 없으면 에러가 난다.
// @MainActor 없으면
class SampleData {
var context: ModelContext {
modelContainer.mainContext // ❌ 에러: 메인 Actor에서만 접근 가능
}
}
// @MainActor 붙이면
@MainActor
class SampleData {
var context: ModelContext {
modelContainer.mainContext // ✅ 클래스 전체가 메인 스레드에서 실행 보장
}
}
SwiftUI 코드도 기본적으로 메인 스레드에서 실행되기 때문에 SampleData를 SwiftUI에서 사용할 때 자연스럽게 조건이 맞아떨어진다.
8. @Query
SwiftData 저장소에서 데이터를 읽어오는 프로퍼티 래퍼. 단순히 한 번 읽어오는 게 아니라 저장소의 데이터가 변경될 때마다 자동으로 뷰를 업데이트해준다. @State처럼 값이 바뀌면 뷰가 다시 그려지는 방식이다.
// 이름 순으로 정렬해서 Friend 배열 가져오기
@Query(sort: \Friend.name) private var friends: [Friend]
// 정렬 없이 가져오기
@Query private var friends: [Friend]
sort 파라미터에 키패스(\Friend.name)를 넘기면 해당 프로퍼티 기준으로 자동 정렬된다. 정렬 외에도 필터 조건(#Predicate)을 추가해서 원하는 데이터만 가져올 수도 있다.
@Query는 반드시 ModelContainer가 연결된 환경에서만 동작한다. 연결되지 않은 상태에서 쓰면 런타임 크래시가 난다.
9. @Environment(.modelContext)
저장소에 데이터를 삽입하거나 삭제할 때 필요한 ModelContext를 환경에서 가져오는 방식. @Query가 읽기 전용이라면 modelContext는 쓰기/삭제 전담이다.
@Environment(\.modelContext) private var context
// 데이터 삽입
context.insert(Friend(name: "Elena"))
// 데이터 삭제
context.delete(friend)
// 저장소에 반영 (insert/delete는 즉시 반영되지 않음, save 필요)
try context.save()
@Environment는 부모 뷰에서 자식 뷰로 값이 자동으로 전달되는 SwiftUI의 환경 시스템을 이용한다. App.swift에서 .modelContainer를 한 번만 붙이면 그 안의 모든 자식 뷰에서 자동으로 같은 컨텍스트를 받아올 수 있다.
10. 프리뷰 vs 실제 앱의 저장소
프리뷰는 App.swift나 ContentView를 거치지 않고 해당 뷰만 독립적으로 실행하기 때문에, 각 뷰 파일마다 .modelContainer를 직접 붙여줘야 한다.
// 프리뷰: 메모리 저장, 샘플 데이터
#Preview {
FriendList()
.modelContainer(SampleData.shared.modelContainer)
}
// 실제 앱: App.swift에 한 번만, 영구 저장
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.modelContainer(for: [Friend.self, Movie.self])
}
}
}
| 프리뷰 | 실제 앱 | |
|---|---|---|
| 저장 방식 | 메모리 (임시) | 기기에 영구 저장 |
| 새로고침 시 | 데이터 초기화 | 데이터 유지 |
| 데이터 출처 | SampleData 샘플 | 사용자가 입력한 데이터 |
| 연결 위치 | 각 뷰 파일마다 직접 | App.swift에 한 번만 |
11. NavigationSplitView
목록과 상세 화면을 연결하는 내비게이션 컨테이너. iPhone에서는 목록에서 항목을 탭하면 상세 화면으로 슬라이드 전환되고, iPad/Mac에서는 목록과 상세 화면이 좌우로 나란히 표시된다. 코드 한 벌로 두 가지 레이아웃을 자동으로 처리해주는 것이 핵심 장점이다.
NavigationSplitView {
// sidebar: 목록 화면
List { ... }
.navigationTitle("Friends") // 루트 타이틀 → 크게 왼쪽 표시
} detail: {
// 아무것도 선택 안 했을 때 표시 (iPad/Mac 전용)
Text("Select a friend")
.navigationTitle("Friends")
}
detail 클로저는 iPad/Mac처럼 두 화면을 동시에 보여줄 수 있는 기기에서 아무것도 선택되지 않았을 때 오른쪽 영역에 표시된다. iPhone에서는 무시된다.
12. NavigationLink
목록의 각 행을 탭했을 때 상세 화면으로 이동하게 해주는 뷰. 첫 번째 인자로 목록에 표시할 텍스트를 받고, 클로저 안에 이동할 상세 화면 뷰를 넣는다. NavigationSplitView 또는 NavigationStack 안에서만 동작한다.
NavigationLink(friend.name) {
Text(friend.name)
.navigationTitle(friend.name)
.navigationBarTitleDisplayMode(.inline)
}
.navigationBarTitleDisplayMode로 타이틀 표시 방식을 결정한다.
| 모드 | 크기 | 위치 | 사용 상황 |
|---|---|---|---|
.large (기본값) |
크게 | 왼쪽 아래 | 루트(목록) 화면 |
.inline |
작게 | 가운데 위 | 상세 화면 |
목록에서 항목을 탭하면 내비게이션 바의 타이틀이 위로 올라가면서 뒤로 가기 버튼으로 변하는 애니메이션이 자동으로 적용된다.
전체 흐름
① Friend/Movie 클래스에 @Model 선언
→ SwiftData가 저장 가능한 타입으로 인식
② 각 모델에 static let sampleData 선언
→ 프리뷰용 샘플 데이터를 한 곳에서 관리
③ SampleData.shared 초기화 (싱글톤)
→ Schema → ModelConfiguration → ModelContainer 생성
→ insertSampleData()로 sampleData 배열을 context.insert()
→ context.save()로 저장소에 반영
④ 각 뷰 파일의 #Preview에
.modelContainer(SampleData.shared.modelContainer) 연결
→ @Query가 이 저장소에서 데이터를 읽어와 배열 자동으로 채움
⑤ List + ForEach로 목록 표시
→ @Query 배열이 바뀔 때마다 뷰 자동 업데이트
⑥ NavigationLink 탭 → NavigationSplitView 상세 화면으로 이동
→ .navigationBarTitleDisplayMode(.inline)으로 타이틀 축소
2026년 5월 28일 AM 7:39
로그인 필요