제네릭

BS_Lee·2025년 7월 7일

swift

목록 보기
12/21

제네릭

Swift에서 프로토콜, 제네릭, 그리고 확장(extension)이 섞인 코드를 보다 보면 뭔가 어려운 듯하면서도, 익숙한 듯하면서도… 이상하게 머리에 잘 안 들어온다.
나도 그랬다. 특히 associatedtype이 처음 등장했을 때는 대체 얘는 왜 필요한 건지부터가 궁금했다.

그럼, 처음부터 차근차근 짚어보자.


제네릭이란?

제네릭(Generic)은 “타입을 고정하지 않고, 나중에 사용할 때 결정하도록 만드는 프로그래밍 기법”이다.
예를 들어 Array<T>는 어떤 타입이든 저장할 수 있다. Array<Int>, Array<String>처럼.

Swift에서 제네릭은 다음과 같은 곳에 쓰인다.

func swapValues<T>(_ a: inout T, _ b: inout T) {
    let temp = a
    a = b
    b = temp
}

같은 로직을 여러 타입에 대해 재사용할 수 있게 해주니, 유연하고 타입 안정적인 코드를 만들 수 있다.


다음 코드를 봐보자. 역시 아무것도 모르는게 당연하다. 이 코드가 예제가 되어줄거다.
(주석으로 달은건 무시해도 된다. 강의를 보면서 대충 이해한대로 적은 것이니....)

rotocol View {
    func addSubView(_ view: View)
}

extension View {
    func addSubView(_ view: View) {
    }
}

struct Button: View {}

protocol PresentableAsView {
    associatedtype ViewType: View // generic 역할
    func produceView() -> ViewType // ViewType을 지정해줄 애
    func configure(superView: View, thisView: ViewType)
    func present(view: ViewType, on superView: View)
}

extension PresentableAsView {
    func present(view: ViewType, on superView: View) {
        superView.addSubView(view) // 이건 잘 모르겠음
    }
}

struct MyButton: PresentableAsView { // MyButton 구조체를 만들어 줌
    func produceView() -> Button { Button() }   // 구현체로부터 받은 produceView를 Button()으로 초기화 여기서 ViewType 자동추론이 이루어진다.
    func configure(superView: View, thisView: Button) {} // 의미 없음
}

extension PresentableAsView where ViewType == Button { // Veiw타입이 Button하고 같으면 아래 함수를 실행
    func doSomethingWithButton() {
        "This is a button"
    }
}

let button = MyButton()
button.doSomethingWithButton()

그럼 여기서 궁금한 게 하나 생긴다.

Swift 프로토콜에서는 왜 직접 제네릭을 못 쓰고 associatedtype을 써야 할까?


associatedtype이란?

Swift에서는 프로토콜에 직접 <T> 같은 제네릭을 붙일 수 없다.
대신 associatedtype을 이용해서 추상적인 타입을 정의한다.

protocol PresentableAsView {
    associatedtype ViewType: View
}

이건 “이 프로토콜을 따르는 애는 ViewType이라는 타입을 나중에 정하게 될 건데, 그 타입은 최소한 View를 따라야 해”라는 뜻이다.

즉, 프로토콜은 타입을 알 수 없으니 대충 틀만 잡고,
실제 타입은 나중에 이 프로토콜을 채택하는 쪽에서 정해줘야 한다는 거다.


그럼 또 하나.

아래처럼 쓴 present(view: Button, ...)는 과연 프로토콜 요구사항을 만족할까?

extension PresentableAsView {
    func present(view: Button, on superView: View) {
        superView.addSubView(view)
    }
}

아니다 ❌
이건 프로토콜에 정의된 func present(view: ViewType, on:)와는 다른 함수다.
ViewTypeButton은 같은 타입이 아니기 때문이다. 이름만 같아 보여도, Swift는 타입까지 엄격하게 본다.

따라서 정확하게 구현하려면 이렇게 써야 한다:

extension PresentableAsView {
    func present(view: ViewType, on superView: View) {
        superView.addSubView(view)
    }
}

그럼 where ViewType == Button 조건은 언제 만족할까?

extension PresentableAsView where ViewType == Button {
    func doSomethingWithButton() {
        print("This is a button")
    }
}

이건 조건부 확장이다.
말 그대로, ViewType이 Button일 때만 저 확장이 붙는다.

그럼 ViewType은 언제 Button으로 정해질까?

struct MyButton: PresentableAsView {
    func produceView() -> Button { Button() } // 여기서 정해진다.
    func configure(superView: View, thisView: Button) {} // produceView()가 없었다면 여기서 정해졌을것이다.
}

여기서 Swift는 추론한다:

  • produceView()의 반환값이 Button이니까 → ViewType = Button
  • configure()thisView: Button이니까 → 역시 ViewType = Button

즉, 컴파일러는 "아 너 ViewType = Button이구나" 하고 알아서 추론해준다.
그래서 아래 같은 코드도 실행된다:

let my = MyButton()
my.doSomethingWithButton()

마지막 퍼즐 – produceView, configure, present는 왜 필요한가?

지금 당장 보기엔 아무 일도 안 하는 함수처럼 보일 수 있다.
하지만 이들은 설계상의 인터페이스 역할을 한다.

  • produceView() → 뷰를 만들고
  • configure() → 뷰를 설정하고
  • present() → 부모 뷰에 뷰를 추가한다

이렇게 만들면 어떤 뷰 타입이 오든 같은 흐름으로 처리할 수 있다.
→ 매우 확장성과 재사용성이 높은 구조다.


요약 정리

항목설명
associatedtype프로토콜 내부에서 타입을 일반화하는 키워드. 제네릭처럼 작동함
ViewType: ViewViewType은 최소한 View를 따라야 함
produceView() -> Button이걸 통해 ViewType이 Button으로 추론됨
extension where ViewType == Button조건부 확장. 오직 Button일 때만 실행
present(view: Button, ...)이건 ViewType이 Button일 때만 맞는 함수이고, 일반 요구사항과는 별개
인터페이스 메서드들구조화된 설계를 위해 사용됨. 재사용성과 유연성을 높임

마무리하며....

이 코드는 Swift 제네릭 프로그래밍, 프로토콜 중심 설계, 타입 추론, 조건부 확장의 정수가 다 들어 있는 예제다.
처음엔 헷갈릴 수밖에 없다.
하지만 흐름을 알고 나면, Swift가 왜 이렇게 설계되었는지 조금씩 이해가 간다.

그리고 이제 나도 이렇게 말할 수 있다:
associatedtype은 프로토콜 속 제네릭이다.

0개의 댓글