일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
- swfitui
- MainActor
- assosiated type
- rest api
- StateObject
- actor
- async/await
- github
- restful api
- NavigationLink
- 동시성 프로그래밍
- unowned
- SwiftUI
- 스레드 점유권
- weak
- 순환참조
- git 명령어
- IOS
- REDRAW
- RESTful
- 앱실행
- MVVM
- ObservedObject
- Swift
- Git
- 격리 시스템
- environment object
- environment value
- navigationview
- Swift Concurrency
- Today
- Total
Develup
[SwiftUI] EnvironmentValues vs EnvironmentObject: 완벽한 데이터 공유 가이드 본문
[SwiftUI] EnvironmentValues vs EnvironmentObject: 완벽한 데이터 공유 가이드
Develup 2025. 3. 7. 14:21SwiftUI 앱을 개발하다 보면 여러 뷰 간에 데이터를 공유하고 전달해야 하는 상황이 자주 발생합니다. 특히 뷰 계층 구조가 복잡해질수록 단순히 프로퍼티를 통해 데이터를 전달하는 방식은 번거롭고 유지보수가 어려워집니다. 이러한 문제를 해결하기 위해 SwiftUI는 환경 변수(EnvironmentValues)와 환경 객체(EnvironmentObject)라는 강력한 메커니즘을 제공합니다.
이 글에서는 SwiftUI의 환경 변수와 환경 객체의 개념, 차이점, 그리고 실제 활용 방법을 자세히 살펴보겠습니다. 이를 통해 복잡한 뷰 계층 구조에서도 효율적으로 데이터를 관리하고 상태를 공유하는 방법을 알아보겠습니다.
환경 변수(EnvironmentValues)란 무엇인가?
환경 변수는 SwiftUI에서 뷰 계층 구조 전체에 걸쳐 데이터를 암시적으로 전달하는 시스템입니다. 색상 구성표, 텍스트 크기, 로케일 등과
같은 앱 전체 설정이나 뷰 계층 구조의 특정 부분에 적용되는 설정을 관리하는 데 이상적입니다.
환경 변수의 핵심 특징
- 시스템에서 제공하는 기본 환경 값들이 있음 (예: colorScheme, locale)
- 사용자가 직접 커스텀 환경 값을 정의할 수 있음
- 부모 뷰에서 설정한 값이 모든 자식 뷰에 자동으로 전파됨
- 읽기 전용으로 사용하거나 수정할 수 있음
기본 환경 값 사용하기
SwiftUI는 다양한 기본 환경 값을 제공합니다. 이 값들은 @Environment 프로퍼티 래퍼를 통해 접근할 수 있습니다.
struct ContentView: View {
@Environment(\.colorScheme) private var colorScheme
@Environment(\.locale) private var locale
var body: some View {
Text("Current color scheme: \(colorScheme == .dark ? "Dark" : "Light")")
Text("Current locale: \(locale.identifier)")
}
}
위 예제에서는 현재 색상 구성표와 로케일 정보를 환경 값에서 가져와 표시합니다.
환경 값 수정하기
특정 뷰 계층에 대한 환경 값을 수정하려면 .environment() 수정자를 사용합니다.
struct ContentView: View {
var body: some View {
VStack {
StandardText()
// 이 자식 뷰와 그 하위 뷰들에만 다른 텍스트 크기 적용
StandardText()
.environment(\.font, .title)
}
}
}
struct StandardText: View {
@Environment(\.font) private var font
var body: some View {
Text("Hello, World!")
.font(font)
}
}
커스텀 환경 값 생성하기
시스템에서 제공하는 환경 값 외에도 직접 커스텀 환경 값을 만들 수 있습니다. 이를 통해 앱에 특화된 설정을 뷰 계층 구조 전체에 전파할 수 있습니다.
커스텀 환경 값을 만들기 위한 3단계 과정을 살펴보겠습니다:
1. 환경 키 정의하기
먼저, EnvironmentKey 프로토콜을 준수하는 구조체를 만들어 환경 키를 정의합니다. 이 프로토콜은 기본값을 제공하는 defaultValue가 필요합니다.
struct ThemeKey: EnvironmentKey {
static let defaultValue: Theme = .light
}
// 테마 정의
enum Theme {
case light, dark, system
}
2. EnvironmentValues 확장하기
다음으로, EnvironmentValues를 확장하여 새로운 환경 값에 대한 접근자를 제공합니다.
extension EnvironmentValues {
var theme: Theme {
get { self[ThemeKey.self] }
set { self[ThemeKey.self] = newValue }
}
}
3. 환경 값 사용하기
이제 정의한 커스텀 환경 값을 사용할 수 있습니다.
struct ThemedView: View {
@Environment(\.theme) private var theme
var body: some View {
Text("Current theme: \(themeString)")
.foregroundColor(themeTextColor)
.padding()
.background(themeBackgroundColor)
}
private var themeString: String {
switch theme {
case .light: return "Light"
case .dark: return "Dark"
case .system: return "System"
}
}
private var themeTextColor: Color {
switch theme {
case .light: return .black
case .dark: return .white
case .system: return .primary
}
}
private var themeBackgroundColor: Color {
switch theme {
case .light: return .white
case .dark: return .black
case .system: return .background
}
}
}
// 사용 예시
struct ContentView: View {
var body: some View {
VStack {
ThemedView()
.environment(\.theme, .light)
ThemedView()
.environment(\.theme, .dark)
ThemedView()
.environment(\.theme, .system)
}
}
}
환경 객체(EnvironmentObject)란 무엇인가?
환경 객체는 관찰 가능한 객체(ObservableObject)를 뷰 계층 구조 전체에 걸쳐 공유하는 방법입니다. 복잡한 애플리케이션 상태나 비즈니스 로직을 여러 뷰에서 접근하고 수정해야 할 때 특히 유용합니다.
환경 객체의 핵심 특징
- ObservableObject 프로토콜을 준수하는 클래스로 정의됨
- 변경 사항이 발생하면 의존하는 뷰를 자동으로 업데이트함
- 뷰 계층 구조 내에서 객체 인스턴스를 명시적으로 전달할 필요가 없음
- 주로 앱의 전역 상태나 공유 데이터 모델에 사용됨
환경 객체 정의하기
환경 객체는 ObservableObject 프로토콜을 준수하는 클래스로 정의합니다. 이 프로토콜은 변경 사항을 발행하는 objectWillChange 퍼블리셔를 제공합니다.
class UserSettings: ObservableObject {
@Published var username: String = ""
@Published var isLoggedIn: Bool = false
@Published var prefersDarkMode: Bool = false
func login(username: String) {
self.username = username
self.isLoggedIn = true
}
func logout() {
self.username = ""
self.isLoggedIn = false
}
}
여기서 @Published 프로퍼티 래퍼는 프로퍼티가 변경될 때마다 알림을 발행하도록 합니다.
환경 객체 주입하기
환경 객체를 뷰 계층 구조에 주입하려면 .environmentObject() 수정자를 사용합니다.
@main
struct MyApp: App {
let userSettings = UserSettings()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(userSettings)
}
}
}
환경 객체 사용하기
주입된 환경 객체는 @EnvironmentObject 프로퍼티 래퍼를 통해 액세스할 수 있습니다.
struct ContentView: View {
@EnvironmentObject var userSettings: UserSettings
var body: some View {
VStack {
if userSettings.isLoggedIn {
Text("Welcome, \(userSettings.username)!")
Button("Logout") {
userSettings.logout()
}
} else {
LoginView()
}
Toggle("Dark Mode", isOn: $userSettings.prefersDarkMode)
.padding()
}
.padding()
}
}
struct LoginView: View {
@EnvironmentObject var userSettings: UserSettings
@State private var usernameInput: String = ""
var body: some View {
VStack {
TextField("Username", text: $usernameInput)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Button("Login") {
userSettings.login(username: usernameInput)
}
.disabled(usernameInput.isEmpty)
}
}
}
위 예제에서 ContentView와 LoginView는 모두 동일한 UserSettings 인스턴스에 접근하여 상태를 공유합니다. LoginView에서 로그인을 수행하면 ContentView가 자동으로 업데이트됩니다.
환경 변수 vs. 환경 객체: 언제 무엇을 사용해야 할까?
환경 변수와 환경 객체는 모두 데이터를 뷰 계층 구조 전체에 전파하는 메커니즘을 제공하지만 사용 사례가 다릅니다.
환경 변수를 사용해야 할 때
- 정적이거나 자주 변경되지 않는 설정이나 환경설정을 전달할 때
- 테마나 색상 구성표와 같은 UI 관련 정보를 제공할 때
- 뷰 계층 구조의 특정 부분에만 특정 설정을 적용하고 싶을 때
- 뷰의 동작을 구성하거나 사용자 정의하기 위한 간단한 값이 필요할 때
환경 객체를 사용해야 할 때
- 복잡한 상태나 비즈니스 로직을 여러 뷰에서 공유해야 할 때
- 자주 변경되는 데이터를 처리할 때
- 앱의 전역 상태나 사용자 세션 정보를 관리할 때
- 여러 뷰에 영향을 미치는 변경 가능한 데이터 모델이 필요할 때
실제 활용 예제: 장바구니 기능 구현하기
이제 환경 객체를 사용하여 간단한 쇼핑 앱의 장바구니 기능을 구현하는 예제를 살펴보겠습니다.
1. 데이터 모델 정의하기
struct Product: Identifiable {
let id = UUID()
let name: String
let price: Double
}
class CartManager: ObservableObject {
@Published var items: [Product] = []
var totalPrice: Double {
items.reduce(0) { $0 + $1.price }
}
func addToCart(product: Product) {
items.append(product)
}
func removeFromCart(productId: UUID) {
items.removeAll { $0.id == productId }
}
func clearCart() {
items.removeAll()
}
}
2. 환경 객체 주입하기
@main
struct ShoppingApp: App {
let cartManager = CartManager()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(cartManager)
}
}
}
3. 상품 목록 뷰 구현하기
struct ProductListView: View {
@EnvironmentObject var cart: CartManager
let products = [
Product(name: "Laptop", price: 1299.99),
Product(name: "Smartphone", price: 799.99),
Product(name: "Headphones", price: 199.99),
Product(name: "Tablet", price: 499.99)
]
var body: some View {
List(products) { product in
HStack {
VStack(alignment: .leading) {
Text(product.name)
.font(.headline)
Text("$\(product.price, specifier: "%.2f")")
.foregroundColor(.secondary)
}
Spacer()
Button("Add to Cart") {
cart.addToCart(product: product)
}
.buttonStyle(.bordered)
}
.padding(.vertical, 4)
}
.navigationTitle("Products")
}
}
4. 장바구니 뷰 구현하기
struct CartView: View {
@EnvironmentObject var cart: CartManager
var body: some View {
VStack {
if cart.items.isEmpty {
Spacer()
Text("Your cart is empty")
.font(.headline)
.foregroundColor(.secondary)
Spacer()
} else {
List {
ForEach(cart.items) { item in
HStack {
Text(item.name)
Spacer()
Text("$\(item.price, specifier: "%.2f")")
Button(action: {
cart.removeFromCart(productId: item.id)
}) {
Image(systemName: "trash")
.foregroundColor(.red)
}
}
}
}
VStack {
HStack {
Text("Total:")
.font(.headline)
Spacer()
Text("$\(cart.totalPrice, specifier: "%.2f")")
.font(.headline)
}
Button("Checkout") {
// 결제 처리 로직
cart.clearCart()
}
.buttonStyle(.borderedProminent)
.padding(.top)
}
.padding()
}
}
.navigationTitle("Shopping Cart")
.toolbar {
Button("Clear Cart") {
cart.clearCart()
}
.disabled(cart.items.isEmpty)
}
}
}
5. 메인 뷰 구현하기
struct ContentView: View {
@EnvironmentObject var cart: CartManager
var body: some View {
NavigationView {
ProductListView()
.toolbar {
NavigationLink(destination: CartView()) {
HStack {
Image(systemName: "cart")
Text("\(cart.items.count)")
}
}
}
}
}
}
이 예제에서는 CartManager 환경 객체를 통해 상품 목록 뷰와 장바구니 뷰 간에 장바구니 상태를 공유합니다. 사용자가 상품을 장바구니에 추가하면 장바구니 아이콘의 카운터와 장바구니 뷰가 자동으로 업데이트됩니다.
환경 변수와 환경 객체 사용 시 주의 사항
1. 성능 고려 사항
- 환경 변수나 객체를 너무 많이 사용하면 앱의 성능에 영향을 줄 수 있습니다.
- 특히 환경 객체를 사용할 때는 객체가 너무 많은 책임을 가지지 않도록 주의해야 합니다.
- 모든 것을 환경을 통해 전달하기보다는 필요한 경우에만 사용하는 것이 좋습니다.
2. 디버깅 어려움
- 환경을 통한 값 전달은 종종 "마법 같은" 느낌이 들 수 있으며, 디버깅이 어려울 수 있습니다.
- 오류가 발생했을 때 어디서 데이터가 변경되었는지 추적하기 어려울 수 있습니다.
3. 의존성 주입 관리
- 환경 객체를 사용할 때 항상 객체가 주입되었는지 확인해야 합니다. 그렇지 않으면 런타임 오류가 발생합니다.
- 테스트 시 환경 객체의 모의(mock) 버전을 제공하는 것을 고려해야 합니다.
// 환경 객체가 주입되지 않은 경우 런타임 오류가 발생합니다.
struct ExampleView: View {
@EnvironmentObject var settings: UserSettings // 주입 필요!
var body: some View {
Text("Username: \(settings.username)")
}
}
4. 적절한 도구 선택하기
- 항상 상황에 맞는 적절한 도구를 선택하세요.
- 단순히 부모에서 자식으로 데이터를 전달하는 경우, 직접 프로퍼티를 전달하는 것이 더 명확할 수 있습니다.
- 복잡한 상태 관리가 필요한 경우, SwiftUI의 내장 솔루션 외에도 Redux, Composable Architecture와 같은 상태 관리 라이브러리를 고려할 수 있습니다.
결론
SwiftUI의 환경 변수와 환경 객체는 뷰 간에 데이터를 효율적으로 공유하는 강력한 메커니즘을 제공합니다. 환경 변수는 정적이거나 UI 관련 구성에 적합하며, 환경 객체는 복잡하고 변경 가능한 상태 관리에 적합합니다. 두 도구 모두 적절하게 사용하면 코드의 가독성과 유지보수성을 크게 향상시킬 수 있습니다.
이 글에서 살펴본 예제와 패턴을 통해 SwiftUI 앱에서 복잡한 데이터 흐름을 효과적으로 관리하는 방법을 이해하셨기를 바랍니다. 상황에 맞는 적절한 도구를 선택하고, 앱의 아키텍처를 설계할 때 데이터 흐름을 신중하게 고려하면 더 나은 사용자 경험과 개발자 경험을 제공할 수 있습니다.
자주 묻는 질문 (FAQ)
환경 객체가 주입되지 않았을 때 "No observable object of type X was found" 오류를 방지하는 방법은?
개발 중에는 @EnvironmentObject 프로퍼티를 사용하는 모든 뷰에 해당 객체가 주입되도록 해야 합니다. 프리뷰에서 테스트할 때는 다음과 같이 .environmentObject() 수정자를 사용하세요:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(UserSettings()) // 미리보기에 환경 객체 주입
}
}
환경 변수와 @State 또는 @Binding의 차이점은 무엇인가요?
- @State는 단일 뷰 내에서 로컬 상태를 관리하는 데 사용됩니다.
- @Binding은 상위 뷰에서 관리하는 상태를 하위 뷰와 공유하는 데 사용됩니다.
- 환경 변수는 뷰 계층 구조 전체에 걸쳐 값을 공유하는 데 사용되며, 명시적인 프로퍼티 전달 없이도 깊이 중첩된 뷰에서 접근할 수 있습니다.
환경 객체는 싱글톤 패턴과 어떻게 다른가요?
환경 객체와 싱글톤 패턴은 모두 공유 상태를 제공하지만 중요한 차이가 있습니다:
- 싱글톤은 전역 접근이 가능하고 앱 전체에 하나의 인스턴스만 존재합니다.
- 환경 객체는 뷰 계층 구조 내에서만 공유되고, 여러 인스턴스를 가질 수 있으며, 테스트하기 더 쉽습니다.
- 환경 객체는 SwiftUI의 선언적 패러다임에 더 잘 맞으며, 변경 사항에 따라 UI를 자동으로 업데이트합니다.
'Swift > SwiftUI' 카테고리의 다른 글
[SwiftUI] NavigationView와 NavigationLink 상세 분석 (0) | 2025.03.08 |
---|---|
[SwiftUI] @StateObject vs @ObservedObject 완벽 가이드: 올바른 선택 방법 (0) | 2025.03.07 |
[SwiftUI] @State와 @Binding 완벽 이해하기: 상태 관리의 핵심 (1) | 2025.03.07 |
[SwiftUI] LazyVGrid와 LazyHGrid: 완벽 가이드 (0) | 2025.03.07 |
[SwiftUI] SwiftUI의 Redraw 프로세스: 효율적인 UI 업데이트 완벽 가이드 (1) | 2025.03.06 |