Community

๐Ÿค–SwiftUI ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜์— SwiftData ์ ์šฉํ•˜๊ธฐ

## ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜ + SwiftUI์—์„œ SwiftData๋Š” ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„์ด ๋ผ์•ผ ํ• ๊นŒ? ์•ˆ๋…•ํ•˜์„ธ์š”! ์˜ค๋Š˜์€ SwiftUI์™€ SwiftData๋ฅผ ํ™œ์šฉํ•ด ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด ๋ณด๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. iOS ๊ฐœ๋ฐœ์ž๋กœ์„œ UI์™€ ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋–ป๊ฒŒ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ• ์ง€ ๊ณ ๋ฏผ์ด ๋งŽ์œผ์‹œ์ฃ ? ๊ทธ๋ ‡๋‹ค๋ฉด ์ด๋ฒˆ ํฌ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ๋‹ค์–‘ํ•œ Tips์„ ์–ป์–ด๋ณด์„ธ์š”. ### ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜๋ž€? ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜(Clean Architecture)๋Š” ์†Œํ”„ํŠธ์›จ์–ด ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด ์ค‘ ํ•˜๋‚˜๋กœ, ๊ฐ ๋ ˆ์ด์–ด๊ฐ€ ๋…๋ฆฝ์ ์œผ๋กœ ๋™์ž‘ํ•˜๋„๋ก ๋งŒ๋“ค์–ด ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์šฉ์ดํ•˜๊ณ  ํ…Œ์ŠคํŠธ๊ฐ€ ์‰ฌ์šด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๋ฐ ๋ชฉํ‘œ๋ฅผ ๋‘๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. - ๋ทฐ(View) : ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค(UI)๋ฅผ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค. - ํ”„๋ ˆ์  ํ…Œ์ด์…˜(Presentation) : UI ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๊ณ  ํ‘œ์‹œํ•  ๋ฐ์ดํ„ฐ๋ฅผ ์ค€๋น„ํ•ฉ๋‹ˆ๋‹ค. - ๋„๋ฉ”์ธ(Domain) : ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. - ๋ฐ์ดํ„ฐ(Data) : ๋ฐ์ดํ„ฐ ์†Œ์Šค์™€์˜ ํ†ต์‹ ์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค. ### SwiftData๋ž€? SwiftData๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด Apple์—์„œ ์ œ๊ณตํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค. Core Data๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๊ณ  ์žˆ์ง€๋งŒ, ๋” ํ˜„๋Œ€์ ์ด๊ณ  Swift์— ์ ํ•ฉํ•œ ๋ฌธ๋ฒ•์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์„ ์ •์˜ํ•˜๊ณ , ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ณ , ๊ฐ€์ ธ์˜ค๋Š” ์ž‘์—…์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ### SwiftData๋ฅผ ์‚ฌ์šฉํ•œ ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜ ๊ตฌํ˜„ SwiftData์™€ ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ iOS ์•ฑ์„ ์ž‘์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋‹จ๊ณ„๋ณ„๋กœ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. #### 1. ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ์ •์˜ ๋จผ์ € ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•  ๋ชจ๋ธ๋“ค์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. SwiftData๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด @Model ์–ด๋…ธํ…Œ์ด์…˜์„ ์ด์šฉํ•ด ๊ฐ„๋‹จํžˆ ๋ชจ๋ธ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ```swift import SwiftData @Model public final class Task { @Attribute var title: String @Attribute var isCompleted: Bool @Attribute var createdAt: Date init(title: String, isCompleted: Bool, createdAt: Date) { self.title = title self.isCompleted = isCompleted self.createdAt = createdAt } } ``` ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด title , isCompleted , createdAt ํ”„๋กœํผํ‹ฐ๋ฅผ ๊ฐ€์ง„ Task ๊ฐ์ฒด๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. SwiftData์˜ @Model ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜๋ฉด ์ž๋™์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€์˜ ์—ฐ๋™์ด ์ด๋ฃจ์–ด์ง‘๋‹ˆ๋‹ค. #### 2. ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ƒ์„ฑ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋Š” ๋ฐ์ดํ„ฐ ์†Œ์Šค์™€ ์ƒํ˜ธ ์ž‘์šฉํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€์˜ ์˜์กด์„ฑ์„ ์ตœ์†Œํ™”ํ•˜๊ณ , ๋ฐ์ดํ„ฐ ์•ก์„ธ์Šค๋ฅผ ์บก์Аํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ```swift import SwiftData import SwiftUI public protocol TaskRepository { func fetchTasks() -> [Task] func addTask(_ task: Task) func deleteTask(_ task: Task) } public final class TaskRepositoryImp: TaskRepository { @Container private var container: DataContainer public func fetchTasks() -> [Task] { return container.fetch() } public func addTask(_ task: Task) { container.save(task) } public func deleteTask(_ task: Task) { container.delete(task) } } ``` ์ด ์ฝ”๋“œ๋Š” TaskRepository ๋ผ๋Š” ํ”„๋กœํ† ์ฝœ์„ ์ •์˜ํ•˜๊ณ , ์ด๋ฅผ ๊ตฌํ˜„ํ•œ TaskRepositoryImp ํด๋ž˜์Šค๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€์˜ ๋ณต์žกํ•œ ๋กœ์ง์„ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. #### 3. ์œ ์Šค์ผ€์ด์Šค ์ •์˜ ์œ ์Šค์ผ€์ด์Šค๋Š” ๋„๋ฉ”์ธ ๋กœ์ง์„ ๋ถ„๋ฆฌํ•˜์—ฌ ์‚ฌ์šฉ์ž์˜ ์š”์ฒญ์— ๋”ฐ๋ผ ํŠน์ • ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ```swift import Combine public protocol FetchTasksUseCase { func execute() -> AnyPublisher } public final class FetchTasksUseCaseImp: FetchTasksUseCase { private let repository: TaskRepository public init(repository: TaskRepository) { self.repository = repository } public func execute() -> AnyPublisher { return Just(repository.fetchTasks()) .eraseToAnyPublisher() } } ``` ์œ„ ์ฝ”๋“œ๋Š” FetchTasksUseCase ํ”„๋กœํ† ์ฝœ๊ณผ ๊ทธ ๊ตฌํ˜„์ฒด๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. execute ๋ฉ”์„œ๋“œ๋Š” ์ €์žฅ๋œ Task ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. #### 4. ViewModel ์ƒ์„ฑ ViewModel์€ ์œ ์Šค์ผ€์ด์Šค๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ์™€ UI๋ฅผ ์—ฐ๊ฒฐํ•˜๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค. ```swift import Combine public final class TaskViewModel: ObservableObject { @Published var tasks: [Task] = [] private let fetchTasksUseCase: FetchTasksUseCase private var cancellables: Set = [] public init(fetchTasksUseCase: FetchTasksUseCase) { self.fetchTasksUseCase = fetchTasksUseCase fetchTasks() } public func fetchTasks() { fetchTasksUseCase.execute() .sink { [weak self] tasks in self?.tasks = tasks } .store(in: &cancellables) } } ``` ViewModel์€ @Published ๋ฅผ ์‚ฌ์šฉํ•ด UI์— ๋ฐ”์ธ๋”ฉํ•  ๋ฐ์ดํ„ฐ๋ฅผ ์„ ์–ธํ•˜๊ณ , ์œ ์Šค์ผ€์ด์Šค๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. #### 5. SwiftUI View ๋งŒ๋“ค๊ธฐ ์ตœ์ข…์ ์œผ๋กœ SwiftUI View๋ฅผ ์ƒ์„ฑํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ```swift import SwiftUI struct TaskListView: View { @ObservedObject var viewModel: TaskViewModel var body: some View { List(viewModel.tasks) { task in TaskRow(task: task) } .onAppear { viewModel.fetchTasks() } } } struct TaskRow: View { var task: Task var body: some View { HStack { Text(task.title) Spacer() if task.isCompleted { Image(systemName: "checkmark") } } } } ``` ViewModel์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€์„œ SwiftUI์˜ List๋ฅผ ํ†ตํ•ด ํ™”๋ฉด์— ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ### ์š”์•ฝ ์ด๋ฒˆ ํฌ์ŠคํŠธ์—์„œ๋Š” SwiftUI์™€ SwiftData๋ฅผ ์‚ฌ์šฉํ•ด ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์‚ดํŽด๋ณด์•˜์Šต๋‹ˆ๋‹ค. ๊ฐ ๋ ˆ์ด์–ด๋ฅผ ๋ถ„๋ฆฌํ•˜์—ฌ ๋…๋ฆฝ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ  ์œ ์ง€๋ณด์ˆ˜ํ•˜๋Š” ํšจ๊ณผ์ ์ธ ๋ฐฉ๋ฒ•์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ฐฉ๋ฒ•์„ ํ†ตํ•ด ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ๋†’์—ฌ๋ณด์„ธ์š”! ๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๊ฑฐ๋‚˜ ๋” ์ด์•ผ๊ธฐํ•˜๊ณ  ์‹ถ์€ ์ฃผ์ œ๊ฐ€ ์žˆ๋‹ค๋ฉด ๋Œ“๊ธ€๋กœ ๋‚จ๊ฒจ์ฃผ์„ธ์š”. Happy Coding! ๐Ÿš€

์•Œ๋ฆผ

์•Œ๋ฆผ์ด ์—†์Šต๋‹ˆ๋‹ค