본문 바로가기

iOS/SwfitUI

SwiftUI) ContentView 이해하기 (1/2) - some View가 뭔가요?

 

 

 

안녕하세요~ 소들입니다 :>

아침형 인간이 되기 위한 주니어 개발자의 발악 1일차

출근(재택)시간보다 2시간 전에 일어나 운동도 하고 공부도 하는 중이다 이말입니다 하하

 

1일차 느낀점

일찍 일어나는 새가 일찍 졸리다

아우 졸려 zZ..

 

쨌든 오늘은 SwiftUI 프로젝트를 생성하면 가장 먼저 보이는

 

 

struct ContentViewView {
    var bodysome View {
        Text("Hello, world!")
            .padding()
    }
}

 

 

이 코드에 대해서 살펴보려고 해요!!

저번 포스팅에서는 대충 화면을 그리는 코드라고만 설명하고 넘어갔기에,

이번엔 이 코드가 어떻게 구성되어 있고

저 some이 뭐하는 놈인지, body가 무엇인지에 대해 알아보겠읍니다

모든 포스팅은 편의 말투로 합니다~!!

 

이번 포스팅은 뭔가 내가 쓰면서도 좀 이해가 잘 안가는 부분이 있어서리..

절대 이 포스팅을 맹신하지 마시고,,, 피드백은 꼭 바랍니다,,

 

 

 

 

1.  some은 뭐 하는 애야?

 

 

자 처음에 SwiftUI를 공부해보자 하고 야심차게 프로젝트를 생성하면

가장 먼저 막히는 부분이 바로 이 some이라는 키워드일 것임

 

 

struct ContentViewView {
    var bodysome View {
        Text("Hello, world!")
            .padding()
    }
}

 

 

some의 정의에 대해서 말해보면,

 

some이라는 키워드는 Swift 5.1에서 등장한 새로운 기능으로,

해당 키워드가 반환 타입 앞에 붙을 경우,

해당 반환 타입이 불투명한 타입(Opaque Type)이라는것을 나타낸다

 

히히 정의는 더 어려워졌네

자, 불투명한 타입(Opaque Type)이란 게 도대체 뭔데? 싶겠지만,

불투명한 타입이란 것을 보통 "역 제네릭 타입(reverse generic types)"이라고 표현한다고 함

 

역 제네릭이요..?

내용이 살짝 어려울 수 있으니 차근차근 제네릭에 대해서부터 간단하게 다시 보자 :)

 

 

 

1-1. 제네릭 타입(Generic Types)

 

 

func swap<T>(a: inout T, b: inout T) { }

 

 

자, 위와 같은 함수가 바로 제네릭 함수잖음!?

타입에 의존하지 않는 범용 코드를 만들 떄 사용하는!

 

다만, 제네릭을 사용할 경우 

함수를 작성할 당시엔 실제 어떤 타입이 들어올지에 대해서는 당연히 알 수 없음

당연히 함수 본문에서 매개변수로 많은 작업을 할 수 없음

(매개변수 타입이 어떤 게 들어올지 모르니까)

 

따라서 만약 제네릭 타입에 대한 제약이 필요할 경우, 

프로토콜 정도로 다음과 같이 제한을 할 수 있었음

 

 

func swap<T>(a: inout T, b: inout T) where T: Equatable { }

 

 

요롷게!

함수 내부에선 a, b 매개변수가 어떤 타입일진 모르지만,

Equatable을 채택하고 있으니 a,b에 한해서 == 연산 정도는 사용 가능하겠거니만 알고 작업할 수 있음

 

따라서 제네릭의 경우 

함수 구현 내에서는 값의 실제 타입에 대해서 알 수 없음(숨겨짐)

 

그렇다면 제네릭 함수의 실제 타입에 대해서는 어디서 알 수 있을까!?

바로 제네릭 함수를 호출하는 "외부"에서임

 

 

var a: String = "so"
var b: String = "deul"
swap(a: &a, b: &b)

 

 

이렇게! 실제로 호출하는 곳인 외부에서

나는 제네릭 타입의 함수를 String으로 호출하는구나!란 것을 알 수 있음

 

 

 

1-2. 불투명한 타입(Opaque Types)

 

아까 불투명한 타입이 역 제네릭 타입이라고 불린다고 했잖음!?

위에서 제네릭이 함수 "외부"에서 해당 타입에 대해 알 수 있는 반면,

불투명한 타입의 경우, 외부에서 함수의 반환 값 유형을 정확하게 알 수 없음

다만 함수 내부에서는 어떤 타입을 다루는지 정확히 알고 있음

 

왜 역 제네릭 타입이란지 알겠음?

아직 모를 거 알기에 예제를 보며 이해를 해보자 :)

 

 

자, 우리가 만약 다음과 같은 선물 상자를 하나 만든다고 가정 해보셈

 

 

 

근데 이 상자 안엔 

 

사과 

체리

 

두 가지 중 하나가 들어 있음!!!

근데 겉모습으로 봤을 땐 똑같이 노란 상자기 때문에 

겉으로는 해당 상자가 어떤 과일(타입)을 갖고 있는지 알 수 없음

 

자, 이를 프로토콜로 구현해보면,

 

 

struct Apple {}
struct Cherry {}
 
protocol GiftBox {
    associatedtype giftType
    
    var giftgiftType { get }
}

 

 

이런 식으로 associatedtype을 이용해서 구현할 수 있음

(참고로 associatedtype은 Protocol 내의 Generic Type이라고 생각하면 됨)

 

실제 선물인 gift의 타입이 사과인지 체리인지는 알 수 없으니,

associatedtype(giftType)으로 만들고,

이제 선물을 포장하는 곳에서 다음과 같이 구현 할 것임

 

 

struct AppleGiftBoxGiftBox {
    var giftApple
}
 
struct CherryGiftBoxGiftBox {
    var giftCherry
}

 

 

뭐 자주 보던 Protocol 패턴이잖음?_?

실제 GiftBox를 채택하는 사과/체리 선물 상자들은 위처럼 만들어서 쓰겠징

 

자, 내가 만약 선물을 포장하는 사람이라고 생각을 해보셈

GiftBox를 만들기 위한 작업을 makeGiftBox라는 함수로 구현할 것임

 

 

func makeGiftBox() -> GiftBox {
    return AppleGiftBox.init(gift: .init())
}

 

 

근데 나는 오늘은 체리를 선물하고 싶고, 내일은 사과를 선물하고 싶은데

리턴 타입을 AppleGiftBox / CherryGiftBox라고 지정해버리면 범용적으로 쓸 수 없으니

위와 같이 그냥 GiftBox를 리턴하는 함수를 만들어 버렸음

 

오늘은 사과로 선물을 만들테지만, 내일은 만약 체리를 선물 하고 싶다면,

외부에서 보여질 함수 모양은 건들 필요 없고,

함수 내부의 return 구문만 AppleGiftBo를 CherryGiftBox로 수정해주면 끝이잖음?

 

근데 위처럼 짜주면 다음과 같은 에러가 발생함

 

 

Protocol 'GiftBox' can only be used as a generic constraint because it has Self or associated type requirements

 

 

위 에러가 발생하는 이유는,

반환 타입이 GiftBox인데, 받는 사람은 GiftBox 안에 사과가 있을지 체리가 있을지 알 수 없어!

하고 에러를 뱉는 거임

 

ㅋㅋ...;;  근데 생각해보면 당연한 거셈 ㅋㅋ;;

나는 선물 만들 때 분명 사과를 담았지만

선물을 받는 입장에서는 안에 들은 게 어떤 것인지 알 수 없는 것임

말 그대로 GiftBox라는 프로토콜을 냅다 받은 거라고 생각할 수 있음

 

왜 이런 문제가 생겼냐?

gift라는 프로퍼티의 타입이 GiftBox란 프로토콜 내에 지정된 타입(String / Int 등)이 아닌,

associatedtype(or Self) 이기 때문

(실제 타입이 해당 프로토콜을 준수하는 객체에서 지정될테니!)

 

 

자,

나는 분명 함수 "내부"에서 "사과"로 만들었는데!?!?(억울) 할 수 있지만

근데 여기서 문제는 이 사실을 바로 "함수 내부"만 알고 있단 것임!!

 

외부에선 GiftBox로 나오는데,

GiftBox의 gift 프로퍼티가 associatedtype으로 선언되어 있어서,

"외부"에선 해당 GiftBox 안에 어떤 타입의 gift가 들어있는지를 알 수 없음

 

아까 불투명 타입이 왜 "역 제네릭 타입"이란지 좀 알겠음?

 

실제 타입을 함수 "내부"에서 알 수 없고, "외부"에서 결정 짓는 게 제네릭 타입이었다면,

실제 타입을 함수 "외부"에선 알 수 없고, "내부"에서 결정 짓는 게 불투명 타입인 것임

 

여기서 중요한 건,

함수 내부(선물 주는 사람)의 단순 변심에 의해

함수 내부의 내용을 바꿔버리면(AppleGiftBox->CherryGiftBox) 리턴 타입이 바뀐다는 것임!!

 

따라서, 이때

내 함수는 반환타입이 내 변심에 의해 바뀔 수는 있지만,

항상 특정 타입만 반환할 거야!!

라고 컴파일러(받는 사람)에게 알릴 때 사용하는 것이 바로 some이라는 키워드임

 

 

func makeGiftBox() -> some GiftBox {
    return AppleGiftBox.init(gift: .init())
}

 

 

위처럼 some이란 키워드를 추가하면 에러가 사라지면서

반환 타입인 GiftBox가 "불투명 타입"이 된다는 것임..!

 

 

하하하 갑자기 위에 한 5줄 정도가 이해 안 갈 것 알아요

흐음 어려운 내용이라 생각해서.. 다시 천천히 봐봅시다 :)

 

 

위에서 웬 갑자기 특정 타입만 반환?? 이라고 생각하잖음?

 

자, 함수를 짜는 입장(선물하는 사람 입장)에서 나는 "사과"를 포장하는 코드를 짰음

이 경우, 받은 사람의 GiftBox가 "체리"로 바뀔 일은 절대 없고,

특정 타입(사과)가 든 GiftBox만 항상 반환한다는 것을 알리는 것

 

언제까지?

내가 직접 함수 내부에서 체리로 포장하는 코드로 고치기 전!!!

 

만약 내가 내일 "체리로 포장하는 코드로 함수 내부를 수정 했다면,

특정 타입(체리)가 든 GiftBox만 반환한다는 것을 알리는 것이 바로

 

some, 즉 불투명 리턴 타입 인 것임!

정리하자면,

 

some이란 것은

명확하지 않은 타입(associatedType or Self)이 프로토콜 내에 정의되어 있고,

이 프로토콜을 함수(및 연산 프로퍼티)의 반환 타입으로 가질 때

반환 타입을 "불투명 타입"으로 만들어주기 위해 사용하고,

 

some이라는 것을 통해 반환 타입을 "불투명 타입"으로 만든단 것은,

반환 타입이 어떤 타입인지 컴파일러(및 함수 외부)는 1도 모르겠지만

함수 내부에선 어떤 타입을 반환하는지 명확히 알고 있고,

따라서 내 함수는 정해진 "특정 타입만 반환"된다고 컴파일러에게 알려주는 것임

 

위 예제에서 "특정 타입"은 함수 내부에서 어떻게 구현하냐에 따라 달라지겠지만,

GiftBox 프로토콜을 준수하고 있는 AppleGiftBox / CherryGiftBix 중 하나가 될 것임

 

조금 이해가 갔으려나?ㅠㅠ..

이해한대로 정리해봔쓴데 만얃 틀린 설명이면 말해줘요..ㅠㅠㅠ

 

그럼 이 불투명 타입을 썼을 때의 이점..!?은 다음 some View를 보면서 확인 해보겠음 :)

 

 

 

 

2. 그래서 some View 구만..

 

돌고 돌아 온 것 같은데, 위 내용을 정확히 이해 했다면

금방 이해할 수 있음~!!

 

 

struct ContentViewView {
    var bodysome View {
        Text("Hello, world!")
            .padding()
    }
}

 

 

여기서 body라는 것은 View라는 프로토콜에 정의된 프로퍼티인데,

이 프로퍼티는 연산 프로퍼티(Computed Property)로 

위 코드에선 Text라는 것을 return 하고 있다고 보면 됨

 

자, 근데 위에서 배운 것을 토대로 한다면

some은 언제쓴다??

 

명확하지 않은 타입(associatedType or Self)이 프로토콜 내에 정의되어 있고,

이 프로토콜을 반환 타입으로 가지고 싶을 때 쓴다!!!

 

오홍 그럼 View프로토콜일 것이고!!! 속성 중 하나는 associatedtype(or Self)로 선언되어 있겠네!?

 

 

 

 

호옹이.. 진짜잖어?

그럼 body라는 연산 프로퍼티는 View라는 프로토콜 타입을 반환하지만

View라는 프로토콜 내에 명확하지 않은 타입이 정의되어 있으니

some을 통해 불투명 타입이라고 밝혀준 것이구나!

 

따라서 body 내부에 내가 작성하는 코드에 따라 리턴 타입이 달라지겠지만,

 View 프로토콜을 준수하는 타입만 리턴이 가능하겠군!

 

라고 이해했는데^^;;

 

 

struct ContentViewView {
    var bodysome View {
        Text("Hello, world!")
            .padding()
    }
}

 

 

자, 위 코드는

컴파일러(및 함수 외부)에서는 View라는 프로토콜을 준수하는 객체가 나올 것은 알지만,

정확히 어떤 타입이 나올지는 모름 

다만 위에서 body는 특정 타입(현재는 내가 Text를 리턴 했으니 Text)만

항상 반환된단 것을 알려주는 것이 some(불투명 타입)임

 

엥 갑자기 기획자가 내부 디자인을 Text가 아닌 버튼으로 바꿔달라네?

 

 

struct ContentViewView {
    var bodysome View {
        Button {
        } label: {
            Text
("Hello, world!")
    }
}

 

 

이렇게 Button으로 바꾸면 컴파일러 및 함수 외부에선 

여전히 정확히 어떤 타입이 나올지는 모름 

다만 위에서 body는 특정 타입(현재는 내가 Button를 리턴 했으니 Button)만

항상 반환된단 것을 알려주는 것이 some(불투명 타입)임

 

 

 

2-1. some을 사용하는 이유? 이점?

 

이렇게 불투명한 타입을 사용할 경우,

함수 내부에서 내가 짜는 코드에 따라 시시각각 리턴 타입이 변경 되었지만 (Text -> Button)

ㅇㅣ거에 대해 따로 내가 리턴 타입을 바꿔줄 필요가 없음!!!

 

 

좀 더 극단적으로 보자면, 다음과 같은 코드가 있을 때

 

 

struct ContentViewView {
    var bodyVStack<TupleView<Text,Image>> {
       VStack {
          Text
("Hello, world!")
          Image(systemName: "sodeul!")
        }
    }
}

 

 

만약 some을 통해 불투명 타입으로 선언하지 않았다면 우린 

이런 식으로 body의 타입을 저렇게 다 끔찍하게 명명해줬어야 했을 거임

한마디로 불필요한 타입에 대해 우리가 알 필요가 없음! 컴파일러가 알아서 함

 

 

 

 

 

 

.

.

.

내용이 별로 맘에 안 들어서...;

만약 더 쉬운 설명이 생각나면 언제든 뜯어 고칠 생각 10000%

혹은 잘못된 내용이 있으면 꼭!! 피드백 주세요ㅠㅠㅠㅠ

최ㅐ대한 이해하려고 노력하고 쉽게 풀어 쓰려 노력했는데

생각보다 쉽지 않네요,,,,,,;;;;;

 

https://medium.com/@PhiJay/whats-this-some-in-swiftui-34e2c126d4c4

 

What’s this “some” in SwiftUI?

An Intuitive Explanation of The New Opaque Return Type

medium.com

 

 

이 포스팅을 참고 했으니 이해 안가는 점이 있다면 위 포스팅을 보시길 바랍니다..!

 

 



Calendar
«   2024/05   »
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
최근 댓글
Visits
Today
Yesterday