Swift 초기화 규칙과 SwiftUI app lifecycle은 서로 연결되지만 같은 규칙은 아니다. 모든 stored property를 instance 사용 전에 초기화해야 한다는 것은 Swift type 전체에 적용된다. @main App 안에 print를 바로 적을 수 없는 이유도 “App만 더 엄격해서”가 아니라 type declaration 안에 executable statement를 둘 수 없기 때문이다.
stored property는 instance가 준비되기 전에 값이 있어야 한다
Swift initialization 문서는 class와 struct가 생성될 때 모든 stored property에 적절한 initial value가 있어야 한다고 정의한다.
struct UserProfile {
let name: String
var loginCount: Int
init(name: String) {
self.name = name
self.loginCount = 0
}
}
값을 준비하는 방법은 하나가 아니다.
- declaration에서 default value를 준다.
- initializer에서 모든 required property를 설정한다.
- optional stored property는 별도 값을 주지 않으면
nil로 시작할 수 있다. lazy var는 처음 접근할 때 initialization을 미룬다.
lazy는 initialization 의무를 없애는 keyword가 아니다. 접근 시 계산할 initializer expression을 저장하는 방식이며, 동시 접근에서 정확히 한 번만 만들어진다는 synchronization guarantee도 아니다.
type body에는 declaration을 쓰고 statement는 code block에 둔다
다음 code는 compile되지 않는다.
struct Configuration {
let greeting = "Hello"
print(greeting) // declaration이 아닌 executable statement
}
이는 SwiftUI나 @main만의 제한이 아니다. type body에는 property, method, initializer 같은 member declaration을 둔다. 실행할 statement는 initializer나 method, computed property의 code block 안에 있어야 한다.
struct Configuration {
let greeting: String
init() {
greeting = "Hello"
print(greeting)
}
}
Swift declaration 문서는 executable top-level code와 declaration을 구분한다. file의 program entry point에 놓는 top-level statement와 type declaration 안에 섞어 쓰는 statement는 다른 문법 위치다.
@main은 program entry point를 표시한다
@main은 해당 type이 program의 top-level entry point를 제공한다고 표시한다. Swift main attribute 문서에 따르면 type은 argument가 없고 Void를 반환하는 main type function을 제공하는 조건을 만족해야 한다.
SwiftUI에서는 App protocol이 그 entry machinery를 제공한다. Apple App protocol 문서는 body가 Scene을 구성하고, framework가 app을 launch하는 default main()을 제공한다고 설명한다.
import SwiftUI
struct AppEnvironment {
let apiBaseURL: URL
}
@main
struct SampleApp: App {
private let environment: AppEnvironment
init() {
environment = AppEnvironment(
apiBaseURL: URL(string: "https://example.com")!
)
}
var body: some Scene {
WindowGroup {
ContentView(environment: environment)
}
}
}
initializer는 synchronous dependency를 구성하고 required property를 채우는 데 쓸 수 있다. 하지만 blocking network request나 큰 database migration을 넣으면 launch가 지연될 수 있으므로 “초기화할 수 있다”와 “여기서 해야 한다”를 구분한다.
init, onAppear, task는 실행 시점이 다르다
| 위치 | 적합한 일 | 주의점 |
|---|---|---|
| type/property initializer | instance가 유효하려면 반드시 필요한 synchronous value | view init은 여러 번 호출될 수 있음 |
App.init |
app-level dependency 구성 | 무거운 I/O로 launch를 막지 않음 |
onAppear |
view가 나타날 때 필요한 synchronous action | view가 다시 나타나면 또 호출될 수 있음 |
.task |
view lifetime에 연결된 asynchronous work | view identity가 바뀌거나 사라지면 restart·cancel 가능 |
Apple onAppear 문서는 action이 해당 view가 나타나기 전에 실행된다고 설명한다. 이것을 application 전체에서 정확히 한 번 실행되는 hook으로 보면 안 된다.
struct ContentView: View {
let environment: AppEnvironment
@State private var hasLoaded = false
var body: some View {
Text(hasLoaded ? "Loaded" : "Loading")
.task {
guard !hasLoaded else { return }
// 실제 code에서는 service의 async method 호출
hasLoaded = true
}
}
}
정말 process lifetime 동안 한 번이어야 하는 작업은 view callback 횟수에 기대지 말고 별도 owner나 service에 idempotent guard를 둔다. 화면이 다시 나타났을 때 갱신해야 하는 작업이라면 반대로 반복 실행이 올바를 수 있다.
View와 model의 owner를 나누는 기준은 SwiftUI View 구조, property wrapper의 underscore 의미는 Swift 언더스코어 사용법과 연결된다.
정리
- definite initialization은 모든 Swift class와 struct의 stored property에 적용된다.
- type body의 standalone
print오류는 App 전용 초기화 규칙이 아니라 declaration 위치의 문법 문제다. @main은 entry point를 표시하고 SwiftUIApp.body는 scene을 설명한다.- initializer,
onAppear,.task는 owner·lifetime·cancellation 기준으로 나눈다. - 한 번만 해야 하는 작업은 callback 호출 횟수보다 idempotency를 code로 보장한다.
참고 자료
'배움과 성장 > 소프트웨어 개발' 카테고리의 다른 글
| Swift ARC의 strong·weak·unowned: 수명 관계로 참조 고르기 (2) | 2024.11.10 |
|---|---|
| Swift closure와 callback: capture·escaping·호출 시점 구분하기 (4) | 2024.11.09 |
| SwiftUI some View 이해하기: Opaque Return Type과 #Preview (8) | 2024.10.28 |
| Swift 언더스코어(_): wildcard pattern·인자 레이블·property wrapper (4) | 2024.10.25 |
| Swift extension: 계산 프로퍼티·프로토콜 준수·접근 제어 경계 (1) | 2024.10.24 |
댓글