Swift 초기화와 @main: 저장 프로퍼티·App 진입점·onAppear 구분

반응형

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 문서bodyScene을 구성하고, 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를 표시하고 SwiftUI App.body는 scene을 설명한다.
  • initializer, onAppear, .task는 owner·lifetime·cancellation 기준으로 나눈다.
  • 한 번만 해야 하는 작업은 callback 호출 횟수보다 idempotency를 code로 보장한다.

참고 자료

반응형
KEEP READING
카테고리 전체 보기 →

댓글