Ver3.5 스위프트 메타프로그래밍과 Property Wrapper 커스텀 속성 완전 정복 가이드

스위프트 메타프로그래밍과 Property Wrapper 커스텀 속성 완전 정복 가이드
코드가 코드를 만드는 마법 🪄 — Property Wrapper로 Swift 개발 레벨업!
야 솔직히 말해서 Swift 공부하다가 @Published, @State, @Binding 이런 거 처음 봤을 때 "이게 뭔데?? 골뱅이가 왜 이렇게 많아??" 했던 사람 손 🙋 ㅋㅋㅋ
근데 이게 다 Property Wrapper라는 Swift의 핵심 기능이고, 더 나아가면 메타프로그래밍이라는 개념이랑 연결돼.
오늘은 이 두 개를 제대로 파헤쳐볼 건데, 진짜 실무에서 쓸 수 있는 수준으로 설명해줄게. 준비됐지? 고고 🚀
메타프로그래밍의 개념 → Swift에서의 메타프로그래밍 → Property Wrapper 기초 → 커스텀 Property Wrapper 만들기 → 실전 활용 패턴 → 고급 테크닉
메타프로그래밍(Metaprogramming)은 한마디로 "코드가 코드를 다루는 것"이야.
일반 프로그래밍이 데이터를 처리하는 거라면, 메타프로그래밍은 프로그램 자체를 데이터처럼 다루거나, 코드를 생성·변환·분석하는 기법이야.
쉽게 말하면 이런 거야 ㅋㅋ:
"야 나 매번 이 코드 반복 쓰기 귀찮은데, 자동으로 만들어주는 코드 없냐?"
→ 그게 바로 메타프로그래밍임 😎
메타프로그래밍의 핵심 목표는 크게 세 가지야:
예: 모든 프로퍼티에 로깅 기능 추가할 때 일일이 안 써도 됨
예: Swift Macro가 컴파일 시 코드를 자동 생성
예: SwiftUI의 @ViewBuilder, @resultBuilder
Property Wrapper (Swift 5.1+) · Result Builder (Swift 5.4+) · Mirror/Reflection · Swift Macro (Swift 5.9+) · Generics & Protocol
오늘 우리가 집중할 건 이 중에서 Property Wrapper야. 실무에서 제일 많이 쓰이고, 이해하면 SwiftUI 코드가 갑자기 다 읽히기 시작하거든 ㅋㅋㅋ
Swift 5.1부터 도입된 Property Wrapper는 프로퍼티에 반복적으로 적용되는 로직을 캡슐화하는 기능이야.
쉽게 말하면 "프로퍼티에 씌우는 래퍼(포장지)"인데, 이 포장지 안에 공통 로직을 넣어두고 여러 프로퍼티에 재사용하는 거야.
// Property Wrapper 없이 UserDefaults 저장하는 코드
// 매번 이렇게 써야 했음... 진짜 노가다 ㅠㅠ
class UserSettings {
var username: String {
get {
return UserDefaults.standard.string(forKey: "username") ?? ""
}
set {
UserDefaults.standard.set(newValue, forKey: "username")
}
}
var isDarkMode: Bool {
get {
return UserDefaults.standard.bool(forKey: "isDarkMode")
}
set {
UserDefaults.standard.set(newValue, forKey: "isDarkMode")
}
}
var fontSize: Double {
get {
return UserDefaults.standard.double(forKey: "fontSize")
}
set {
UserDefaults.standard.set(newValue, forKey: "fontSize")
}
}
// ... 이게 10개 20개면?? 😱
}
보이지? 같은 패턴이 계속 반복되잖아. 이게 5개, 10개, 20개 되면 진짜 손목 나가는 거야 ㅋㅋㅋ
이걸 Property Wrapper로 해결하면 이렇게 돼:
// Property Wrapper 사용 후 — 완전 깔끔 ✨
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
class UserSettings {
@UserDefault(key: "username", defaultValue: "")
var username: String
@UserDefault(key: "isDarkMode", defaultValue: false)
var isDarkMode: Bool
@UserDefault(key: "fontSize", defaultValue: 16.0)
var fontSize: Double
}
코드 줄 수가 확 줄고, 실수할 여지도 줄어들고, 유지보수도 쉬워짐 💪
Property Wrapper를 만들려면 @propertyWrapper 어트리뷰트를 struct, class, enum에 붙이고, 반드시 wrappedValue 프로퍼티를 정의해야 해.
추가로 projectedValue도 선택적으로 정의할 수 있어. 이게 $ 접두사로 접근하는 그거야!
@propertyWrapper
struct MyWrapper<T> {
// 1️⃣ 필수: wrappedValue — 실제 값을 저장/반환하는 핵심
private var value: T
var wrappedValue: T {
get { value }
set { value = newValue }
}
// 2️⃣ 선택: projectedValue — $변수명으로 접근
var projectedValue: String {
return "현재 값: \(value)"
}
// 3️⃣ 이니셜라이저
init(wrappedValue: T) {
self.value = wrappedValue
}
}
// 사용법
struct Example {
@MyWrapper var name: String = "Swift"
func show() {
print(name) // wrappedValue 접근
print($name) // projectedValue 접근 → "현재 값: Swift"
}
}
우리가 @MyWrapper var name: String = "Swift" 라고 쓰면, 컴파일러는 내부적으로 이렇게 변환해:
// 우리가 쓴 코드
@MyWrapper var name: String = "Swift"
// 컴파일러가 실제로 생성하는 코드 (개념적 표현)
private var _name: MyWrapper<String> = MyWrapper(wrappedValue: "Swift")
var name: String {
get { _name.wrappedValue }
set { _name.wrappedValue = newValue }
}
var $name: String { // projectedValue
get { _name.projectedValue }
}
@MyWrapper 한 줄 쓰면, 컴파일러가 자동으로 저 코드를 생성해주는 거거든. 코드가 코드를 만드는 것 😎
| 구분 | 접근 방법 | 용도 | 예시 |
|---|---|---|---|
| wrappedValue | 변수명 그대로 | 실제 값 접근 | name |
| projectedValue | $변수명 | 추가 정보/기능 제공 | $name |
| _변수명 | 언더스코어 접두사 | Wrapper 인스턴스 자체 | _name |
이제 진짜 실무에서 쓸 수 있는 커스텀 Property Wrapper들을 만들어볼 거야.
각각 다른 상황에서 쓰이는 패턴들이니까 잘 봐둬 👀
슬라이더 값이나 퍼센트 값처럼 특정 범위 안에 있어야 하는 값들 있잖아.
매번 min(max(value, 0), 100) 이런 거 쓰기 귀찮으면 이걸 써:
@propertyWrapper
struct Clamped<T: Comparable> {
private var value: T
private let range: ClosedRange<T>
var wrappedValue: T {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
init(wrappedValue: T, _ range: ClosedRange<T>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
}
// 사용 예시
struct GameCharacter {
@Clamped(0...100) var health: Int = 100
@Clamped(0.0...1.0) var opacity: Double = 1.0
@Clamped(1...5) var starRating: Int = 3
}
var hero = GameCharacter()
hero.health = 150 // 자동으로 100으로 클램핑됨 ✅
hero.health = -50 // 자동으로 0으로 클램핑됨 ✅
print(hero.health) // 0
hero.opacity = 2.5 // 자동으로 1.0으로 클램핑됨 ✅
사용자 입력 받을 때 앞뒤 공백 제거하는 거 맨날 까먹잖아 ㅋㅋㅋ 이걸로 해결:
@propertyWrapper
struct Trimmed {
private var value: String = ""
var wrappedValue: String {
get { value }
set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
}
init(wrappedValue: String) {
self.value = wrappedValue.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
struct UserProfile {
@Trimmed var name: String = ""
@Trimmed var email: String = ""
@Trimmed var bio: String = ""
}
var profile = UserProfile()
profile.name = " 김스위프트 "
print(profile.name) // "김스위프트" — 공백 자동 제거! ✨
profile.email = " swift@example.com "
print(profile.email) // "swift@example.com"
디버깅할 때 프로퍼티 값이 언제 바뀌는지 추적하고 싶을 때 진짜 유용해:
@propertyWrapper
struct Logged<T: CustomStringConvertible> {
private var value: T
private let label: String
var wrappedValue: T {
get { value }
set {
print("[\(label)] 변경: \(value) → \(newValue)")
value = newValue
}
}
// projectedValue로 현재 값의 문자열 표현 제공
var projectedValue: String {
return "\(label): \(value)"
}
init(wrappedValue: T, label: String) {
self.value = wrappedValue
self.label = label
}
}
struct AppState {
@Logged(label: "사용자 점수") var score: Int = 0
@Logged(label: "레벨") var level: Int = 1
}
var state = AppState()
state.score = 100 // 콘솔: [사용자 점수] 변경: 0 → 100
state.score = 250 // 콘솔: [사용자 점수] 변경: 100 → 250
state.level = 2 // 콘솔: [레벨] 변경: 1 → 2
print($state.score) // projectedValue: "사용자 점수: 250"
이메일, 전화번호 같은 거 유효성 검사할 때 쓰는 패턴이야.
projectedValue로 유효성 상태를 같이 제공하는 게 포인트!
@propertyWrapper
struct Validated {
private var value: String
private let validator: (String) -> Bool
var wrappedValue: String {
get { value }
set { value = newValue }
}
// $변수명으로 유효성 여부 확인
var projectedValue: Bool {
return validator(value)
}
init(wrappedValue: String, validator: @escaping (String) -> Bool) {
self.value = wrappedValue
self.validator = validator
}
}
struct SignUpForm {
@Validated(validator: { email in
let pattern = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
return email.range(of: pattern, options: .regularExpression) != nil
})
var email: String = ""
@Validated(validator: { $0.count >= 8 })
var password: String = ""
var isValid: Bool {
return $email && $password // projectedValue 사용!
}
}
var form = SignUpForm()
form.email = "swift@example.com"
form.password = "mypassword123"
print($form.email) // true — 유효한 이메일
print(form.isValid) // true — 폼 전체 유효성 ✅
즉,
protocol P { @MyWrapper var value: Int } 이런 건 안 됨!프로토콜에서는 일반 프로퍼티로 선언하고, 구현체에서 Wrapper를 적용해야 해.
SwiftUI 쓰면서 이 골뱅이들 맨날 보잖아. 이제 이게 다 Property Wrapper라는 걸 알았으니까, 각각이 어떻게 구현되어 있는지 개념적으로 이해해보자!
| Wrapper | 용도 | projectedValue ($) | 특징 |
|---|---|---|---|
@State |
뷰 내부 상태 관리 | Binding<T> | 뷰가 소유, 변경 시 뷰 재렌더링 |
@Binding |
부모 뷰 상태 참조 | Binding<T> | 소유권 없음, 참조만 |
@ObservedObject |
외부 객체 관찰 | ObservedObject<T>.Wrapper | ObservableObject 프로토콜 필요 |
@StateObject |
외부 객체 소유 및 관찰 | ObservedObject<T>.Wrapper | 뷰가 생명주기 관리 |
@EnvironmentObject |
환경 객체 주입 | ObservedObject<T>.Wrapper | 뷰 계층 전체에서 공유 |
@Environment |
시스템 환경값 접근 | - | colorScheme, locale 등 |
@Published |
Combine 퍼블리셔 생성 | Publisher<T, Never> | ObservableObject 내부에서 사용 |
@AppStorage |
UserDefaults 연동 | Binding<T> | 앱 재시작 후에도 유지 |
// @State의 개념적 구현 (실제 구현은 더 복잡함)
@propertyWrapper
struct State<T> {
private var value: T
var wrappedValue: T {
get { value }
nonmutating set {
// 실제로는 SwiftUI 렌더링 엔진에 변경 알림
value = newValue
// → 뷰 재렌더링 트리거!
}
}
// $변수명으로 Binding 반환
var projectedValue: Binding<T> {
Binding(
get: { self.wrappedValue },
set: { self.wrappedValue = $0 }
)
}
init(wrappedValue: T) {
self.value = wrappedValue
}
}
// 실제 사용
struct CounterView: View {
@State private var count: Int = 0 // wrappedValue
var body: some View {
VStack {
Text("\(count)") // count 직접 접근
Stepper("카운트", value: $count) // $count = Binding<Int>
}
}
}
$count는 State<Int>의 projectedValue인 Binding<Int>를 반환하는 거임.이제 SwiftUI 코드 읽을 때 "아 이게 projectedValue구나!" 하고 이해되지? ㅋㅋㅋ
Property Wrapper는 여러 개를 중첩해서 쓸 수 있어. 단, 순서가 중요해!
// 여러 Wrapper 중첩 — 바깥쪽부터 안쪽으로 적용됨
struct UserInput {
// Trimmed가 먼저 적용되고, 그 결과에 Validated 적용
// 실제로 Swift는 단일 Wrapper만 직접 중첩 지원
// 아래처럼 하나의 Wrapper에서 여러 기능을 조합하는 게 일반적
@Trimmed var username: String = ""
@Validated(validator: { !$0.isEmpty && $0.count >= 2 })
var displayName: String = ""
}
// 더 나은 방법: 기능을 합친 새 Wrapper 만들기
@propertyWrapper
struct TrimmedAndValidated {
private var value: String
private let minLength: Int
var wrappedValue: String {
get { value }
set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
}
var projectedValue: Bool {
return value.count >= minLength
}
init(wrappedValue: String, minLength: Int = 1) {
self.value = wrappedValue.trimmingCharacters(in: .whitespacesAndNewlines)
self.minLength = minLength
}
}
struct Form {
@TrimmedAndValidated(minLength: 2) var name: String = ""
@TrimmedAndValidated(minLength: 8) var password: String = ""
}
var form = Form()
form.name = " 김개발 "
print(form.name) // "김개발"
print($form.name) // true (2글자 이상)
struct 대신 class로 만들면 참조 타입이라 여러 곳에서 같은 인스턴스를 공유할 수 있어:
// 클래스 기반 Wrapper — 공유 상태 관리에 유용
@propertyWrapper
final class SharedStorage<T> {
private var value: T
private var observers: [(T) -> Void] = []
var wrappedValue: T {
get { value }
set {
value = newValue
observers.forEach { $0(newValue) } // 모든 옵저버에 알림
}
}
var projectedValue: SharedStorage<T> {
return self
}
init(wrappedValue: T) {
self.value = wrappedValue
}
func observe(_ handler: @escaping (T) -> Void) {
observers.append(handler)
}
}
class ViewModel {
@SharedStorage var theme: String = "라이트"
}
let vm = ViewModel()
$vm.theme.observe { newTheme in
print("테마 변경됨: \(newTheme)")
}
vm.theme = "다크" // 콘솔: 테마 변경됨: 다크
Property Wrapper의 이니셜라이저를 다양하게 오버로딩하면 더 유연하게 쓸 수 있어:
@propertyWrapper
struct SmartDefault<T> {
var wrappedValue: T
// 기본값으로 초기화
init(wrappedValue: T) {
self.wrappedValue = wrappedValue
}
// 클로저로 지연 초기화
init(defaultProvider: () -> T) {
self.wrappedValue = defaultProvider()
}
}
struct Config {
// 직접 값 지정
@SmartDefault var maxRetries: Int = 3
// 클로저로 복잡한 기본값 계산
@SmartDefault(defaultProvider: {
ProcessInfo.processInfo.processorCount * 2
})
var threadCount: Int
}
let config = Config()
print(config.maxRetries) // 3
print(config.threadCount) // CPU 코어 수 × 2
이게 실무에서 진짜 많이 쓰이는 패턴이야. JSON 파싱할 때 기본값 처리나 변환 로직을 Wrapper로 처리하는 거:
// Codable 친화적인 Property Wrapper
@propertyWrapper
struct DefaultEmpty: Codable {
var wrappedValue: String
init(wrappedValue: String = "") {
self.wrappedValue = wrappedValue
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
// null이거나 디코딩 실패 시 빈 문자열로 대체
wrappedValue = (try? container.decode(String.self)) ?? ""
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(wrappedValue)
}
}
@propertyWrapper
struct DefaultFalse: Codable {
var wrappedValue: Bool
init(wrappedValue: Bool = false) {
self.wrappedValue = wrappedValue
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
wrappedValue = (try? container.decode(Bool.self)) ?? false
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(wrappedValue)
}
}
// 서버 응답에서 null 값이 올 수 있는 필드들
struct UserResponse: Codable {
let id: Int
@DefaultEmpty var nickname: String // null이면 ""
@DefaultEmpty var bio: String // null이면 ""
@DefaultFalse var isPremium: Bool // null이면 false
@DefaultFalse var isVerified: Bool // null이면 false
}
// JSON: {"id": 1, "nickname": null, "isPremium": null}
// 파싱 결과: id=1, nickname="", bio="", isPremium=false, isVerified=false
// 에러 없이 안전하게 파싱됨! 🎉
서버에서 null이 올 수도 있는 필드들을 안전하게 처리할 수 있거든.
매번
if let 처리하는 것보다 훨씬 깔끔하지? 😎
메타프로그래밍의 또 다른 축인 리플렉션(Reflection)도 잠깐 짚고 가자.
Swift에서는 Mirror API를 통해 런타임에 타입 정보를 조회할 수 있어.
⚠️ Swift의 리플렉션은 다른 언어(Java, Python)에 비해 읽기 전용이야.
런타임에 프로퍼티 값을 읽을 수는 있지만, 수정은 불가능해.
그래서 주로 디버깅, 로깅, 직렬화 목적으로 사용돼.
struct Person {
var name: String = "김스위프트"
var age: Int = 25
var email: String = "swift@example.com"
}
let person = Person()
let mirror = Mirror(reflecting: person)
// 타입 정보 확인
print(mirror.displayStyle) // Optional(struct)
print(mirror.subjectType) // Person
// 모든 프로퍼티 순회
for child in mirror.children {
if let label = child.label {
print("\(label): \(child.value)")
}
}
// 출력:
// name: 김스위프트
// age: 25
// email: swift@example.com
// 실용적인 활용: 자동 로깅 함수
func autoLog<T>(_ object: T) {
let mirror = Mirror(reflecting: object)
let typeName = String(describing: type(of: object))
print("=== \(typeName) 상태 ===")
for child in mirror.children {
print(" \(child.label ?? "??"): \(child.value)")
}
}
autoLog(person)
// === Person 상태 ===
// name: 김스위프트
// age: 25
// email: swift@example.com
// Property Wrapper가 적용된 프로퍼티를 Mirror로 조회할 때
@propertyWrapper
struct Trackable<T> {
var wrappedValue: T
var projectedValue: Self { self }
var isModified: Bool = false
init(wrappedValue: T) {
self.wrappedValue = wrappedValue
}
mutating func update(_ newValue: T) {
wrappedValue = newValue
isModified = true
}
}
struct FormData {
@Trackable var name: String = ""
@Trackable var email: String = ""
}
var form = FormData()
form.name = "김개발"
// Mirror로 Wrapper 인스턴스 자체에 접근
let mirror = Mirror(reflecting: form)
for child in mirror.children {
print("\(child.label ?? ""): \(child.value)")
}
// _name: Trackable<String>(wrappedValue: "김개발", isModified: false)
// _email: Trackable<String>(wrappedValue: "", isModified: false)
// 언더스코어(_)가 붙은 게 Wrapper 인스턴스야!
Swift 5.9에서 Swift Macro가 나오면서 "Property Wrapper 이제 구식 아냐?" 하는 말도 나왔는데,
사실 둘은 용도가 달라. 비교해보자:
| 구분 | Property Wrapper | Swift Macro |
|---|---|---|
| 도입 버전 | Swift 5.1 | Swift 5.9 |
| 동작 시점 | 런타임 (일부 컴파일 타임) | 순수 컴파일 타임 |
| 적용 대상 | 프로퍼티만 | 선언, 표현식, 타입 등 다양 |
| 코드 생성 | 제한적 (wrappedValue 패턴) | 임의의 코드 생성 가능 |
| 복잡도 | 상대적으로 간단 | 복잡 (SwiftSyntax 필요) |
| 디버깅 | 쉬움 | 상대적으로 어려움 |
| 주요 용도 | 프로퍼티 동작 커스터마이징 | 보일러플레이트 코드 자동 생성 |
프로퍼티 레벨의 동작 커스터마이징은 Property Wrapper가 더 직관적이고 간단해.
Swift Macro는 더 복잡한 코드 생성이 필요할 때 쓰는 거야.
둘 다 알아두면 최강 콤보 💪
// Swift 5.9+ @Observable 매크로
// ObservableObject + @Published 조합을 대체
import Observation
@Observable // 이게 매크로!
class UserViewModel {
var name: String = ""
var age: Int = 0
var isLoggedIn: Bool = false
// @Published 없어도 자동으로 관찰 가능!
}
// 컴파일러가 자동 생성하는 코드 (개념적):
// - _$observationRegistrar 추가
// - 각 프로퍼티에 willSet/didSet 관찰 코드 추가
// - ObservationIgnored 등 지원
// SwiftUI에서 사용
struct ContentView: View {
@State private var vm = UserViewModel() // @StateObject 대신 @State!
var body: some View {
Text(vm.name)
}
}
enum LoadingState<T> {
case idle
case loading
case success(T)
case failure(Error)
}
@propertyWrapper
struct AsyncState<T> {
private var state: LoadingState<T> = .idle
var wrappedValue: LoadingState<T> {
get { state }
set { state = newValue }
}
// projectedValue로 편의 프로퍼티 제공
var projectedValue: AsyncStateHelper<T> {
AsyncStateHelper(state: state)
}
}
struct AsyncStateHelper<T> {
let state: LoadingState<T>
var isLoading: Bool {
if case .loading = state { return true }
return false
}
var value: T? {
if case .success(let v) = state { return v }
return nil
}
var error: Error? {
if case .failure(let e) = state { return e }
return nil
}
}
class ProductViewModel: ObservableObject {
@Published @AsyncState var products: LoadingState<[String]>
// 실제로는 @Published와 커스텀 Wrapper 조합이 복잡할 수 있음
// 아래처럼 분리하는 게 더 현실적
@Published var productState: LoadingState<[String]> = .idle
func fetchProducts() async {
productState = .loading
do {
// 네트워크 요청...
let data = ["상품A", "상품B", "상품C"]
productState = .success(data)
} catch {
productState = .failure(error)
}
}
}
// 멀티스레드 환경에서 안전한 프로퍼티 접근
@propertyWrapper
final class ThreadSafe<T> {
private var value: T
private let lock = NSLock()
var wrappedValue: T {
get {
lock.lock()
defer { lock.unlock() }
return value
}
set {
lock.lock()
defer { lock.unlock() }
value = newValue
}
}
init(wrappedValue: T) {
self.value = wrappedValue
}
}
class DataManager {
@ThreadSafe var cache: [String: Any] = [:]
@ThreadSafe var requestCount: Int = 0
func processRequest() {
requestCount += 1 // 스레드 안전하게 증가
// 여러 스레드에서 동시 접근해도 안전!
}
}
// 간단한 의존성 컨테이너
class DependencyContainer {
static let shared = DependencyContainer()
private var dependencies: [String: Any] = [:]
func register<T>(_ type: T.Type, factory: @escaping () -> T) {
let key = String(describing: type)
dependencies[key] = factory
}
func resolve<T>(_ type: T.Type) -> T? {
let key = String(describing: type)
if let factory = dependencies[key] as? () -> T {
return factory()
}
return nil
}
}
@propertyWrapper
struct Injected<T> {
private var value: T?
var wrappedValue: T {
get {
if let value = value { return value }
guard let resolved = DependencyContainer.shared.resolve(T.self) else {
fatalError("의존성 \(T.self)가 등록되지 않았습니다!")
}
return resolved
}
}
init() {}
}
// 프로토콜 정의
protocol NetworkService {
func fetch(url: String) async throws -> Data
}
// 실제 구현체
class RealNetworkService: NetworkService {
func fetch(url: String) async throws -> Data {
// 실제 네트워크 요청
return Data()
}
}
// 등록
DependencyContainer.shared.register(NetworkService.self) {
RealNetworkService()
}
// 사용
class ViewModel {
@Injected var networkService: NetworkService
// 자동으로 DependencyContainer에서 주입됨!
}
Swift 5.7부터 로컬 변수에도 Property Wrapper 사용 가능해졌어.
그 이전 버전에서는 struct/class 프로퍼티에만 사용 가능했음.
@MyWrapper lazy var value: Int = 0 — 이건 컴파일 에러남!lazy와 Property Wrapper는 같이 못 써.
프로토콜에서
@MyWrapper var value: Int 형태로 선언 불가.프로토콜에서는 일반 프로퍼티로 선언하고 구현체에서 Wrapper 적용해야 해.
클래스 기반 Wrapper는 힙 할당이 발생해서 성능에 영향 줄 수 있어.
성능이 중요한 코드에서는 struct 기반 Wrapper를 선호해.
하나의 Property Wrapper는 하나의 역할만 담당하게 해.
너무 많은 기능을 하나에 넣으면 재사용성이 떨어져.
@Clamped, @Trimmed, @Logged처럼 동작을 명확히 표현하는 이름을 써.@MyWrapper 같은 모호한 이름은 피해.
wrappedValue 외에 추가 정보가 필요하면 projectedValue를 활용해.
유효성 상태, 원본 값, 메타데이터 등을 $변수명으로 제공하면 사용성이 높아져.
Wrapper 내부 로직은 별도 함수로 분리해서 단위 테스트가 가능하게 해.
특히 유효성 검사 로직은 반드시 테스트 코드 작성해.
커스텀 Wrapper는 반드시 주석으로 동작 방식, 제약사항, 사용 예시를 문서화해.
팀 프로젝트에서 다른 사람이 이해하기 쉽게!
Property Wrapper를 잘 활용하면 코드 품질이 확 올라가.
반복되는 패턴을 발견했을 때 "이거 Property Wrapper로 만들 수 있지 않을까?" 하고 생각하는 습관을 들이면 돼.
처음엔 어색하지만 한 번 익숙해지면 없이는 못 살게 될 거야 ㅋㅋㅋ
Swift 개발 관련 재능을 공유하거나 배우고 싶다면 재능넷(jaenung.net)에서 Swift 전문가들과 연결해보는 것도 좋은 방법이야. 실무 경험 있는 개발자한테 직접 배우면 이런 고급 패턴들을 훨씬 빠르게 익힐 수 있거든 😎
관련 키워드
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.