SwiftUI some View 이해하기: Opaque Return Type과 #Preview

반응형

SwiftUI의 some View는 “아무 View나 반환한다”는 뜻이 아니다. 호출자에게 구체 타입은 숨기되, 구현 안에서는 하나의 구체 타입으로 고정되는 opaque return type이다. 이 성질을 이해하면 some View, any View, @ViewBuilder를 혼동하지 않고 Preview 코드도 훨씬 단순하게 읽을 수 있다.

some View는 왜 필요한가

SwiftUI의 View 프로토콜은 body에 연관 타입을 요구한다. VStack, Text, modifier를 조합하면 실제 반환 타입은 중첩된 제네릭 타입이 되어 길고 구현 변화에도 민감하다.

struct GreetingView: View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("Hello")
                .font(.title)
            Text("SwiftUI")
                .foregroundStyle(.secondary)
        }
    }
}

여기서 body의 실제 타입은 존재한다. 컴파일러도 그 타입을 알고 있다. 다만 외부에는 “View를 따르는 어떤 하나의 타입”이라고만 공개한다. 덕분에 컴파일 타임 타입 정보와 최적화 가능성을 유지하면서 구현 세부사항은 감출 수 있다.

some Viewany View의 차이

두 표현은 비슷해 보여도 역할이 다르다.

표현 의미 구체 타입
some View opaque type 구현마다 하나로 고정되고 컴파일러가 알고 있음
any View existential type 런타임에 서로 다른 준수 타입을 담을 수 있음
AnyView SwiftUI의 type eraser 서로 다른 View 타입을 하나의 타입으로 지움

some View는 호출할 때마다 Text였다가 Image가 되는 반환 형식이 아니다. 같은 선언은 일관된 underlying type을 반환해야 한다.

조건문에서 서로 다른 View가 가능한 이유

SwiftUI의 body에는 result builder인 @ViewBuilder가 적용된다. 그래서 다음 조건문의 두 분기가 겉보기에는 서로 다른 타입이어도, 빌더가 전체를 하나의 구체적인 조건부 View 타입으로 구성할 수 있다.

struct StatusView: View {
    let isOnline: Bool

    var body: some View {
        if isOnline {
            Label("온라인", systemImage: "checkmark.circle.fill")
                .foregroundStyle(.green)
        } else {
            Label("오프라인", systemImage: "xmark.circle")
                .foregroundStyle(.secondary)
        }
    }
}

일반 함수에서 빌더 없이 서로 다른 구체 타입을 임의로 반환하는 것과는 다르다. 조건부 UI를 별도 프로퍼티나 함수로 추출한다면 필요에 따라 @ViewBuilder를 붙인다.

@ViewBuilder
private var statusBadge: some View {
    if isOnline {
        Text("온라인")
    } else {
        Image(systemName: "wifi.slash")
    }
}

Preview는 현재 #Preview 매크로가 기본

예전 예제에는 PreviewProvider가 많이 등장하지만, 현재 Xcode와 SwiftUI 문서에서는 #Preview 매크로를 중심으로 설명한다.

#Preview("기본") {
    GreetingView()
}

#Preview("다크 모드") {
    GreetingView()
        .preferredColorScheme(.dark)
}

Preview는 앱 전체를 매번 직접 실행하지 않고도 Canvas에서 View의 모양과 상호작용을 빠르게 확인하게 해 준다. 다만 실제 기기의 성능, 권한, 네트워크와 전체 앱 생명주기를 모두 대체하는 테스트는 아니다.

상태가 있는 View를 Preview하는 방법

Preview에는 실제 서버 대신 작은 샘플 데이터를 넣는 편이 좋다. 같은 View를 여러 상태로 나란히 만들면 빈 화면, 긴 문자열, 오류 상태 같은 경계를 빠르게 확인할 수 있다.

struct ProfileCard: View {
    let name: String
    let isOnline: Bool

    var body: some View {
        HStack {
            Text(name)
            Spacer()
            Image(systemName: isOnline ? "circle.fill" : "circle")
        }
        .padding()
    }
}

#Preview("온라인") {
    ProfileCard(name: "Junhee", isOnline: true)
}

#Preview("긴 이름 · 다크 모드") {
    ProfileCard(
        name: "아주 긴 사용자 이름이 들어오는 경우",
        isOnline: false
    )
    .preferredColorScheme(.dark)
}

Preview가 느리거나 실패한다면 View 안에서 네트워크·데이터베이스 같은 외부 의존성을 직접 만들고 있지 않은지 먼저 살펴본다. 의존성을 주입하고 샘플 구현을 제공하면 Preview와 테스트가 함께 쉬워진다.

자주 생기는 오해

  • some View는 동적 타입이 아니라 컴파일 타임에 정해지는 opaque type이다.
  • 구체 타입을 숨긴다고 해서 타입 안정성이 사라지지 않는다.
  • some View 자체가 여러 반환 타입을 허용하는 것은 아니다. @ViewBuilder가 조건부 View 구성을 돕는다.
  • AnyView는 편리하지만 무조건 필요한 기본 해법은 아니다. 우선 제네릭과 빌더로 타입 구조를 유지할 수 있는지 본다.
  • Preview는 UI 반복 속도를 높이지만 실제 기기 테스트를 대신하지 않는다.

SwiftUI View를 프로퍼티와 타입 중 어디에 둘지 헷갈린다면 SwiftUI에서 View를 var·struct·class로 선언하는 기준을, 작업 속도를 높이고 싶다면 Xcode 단축키 모음을 이어서 볼 수 있다.

참고 자료

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

댓글