iOS/Swift

Swift) static member cannot be used on instance of type 오류 (feat. 중첩한 타입)

Dev.Andy 2023. 11. 9. 00:00

요약

  내용
상황 구조체와 클래스의 중첩된 타입(Nested Types) 상태에서, 인스턴스 멤버로 구조체/클래스 타입을 접근 시 static member 오류 접근 발생
원인 정적인 멤버(static member)는 인스턴스 멤버(instance member)로는 접근이 불가능하다
해결 중첩한 타입을 접근하려면 타입의 이름을 통해 접근해야 한다.
알게 된 내용
  • 구조체와 클래스는 모두 "(커스텀) 타입"이다
  • 타입은 정적 멤버(static member)이다.
  • 중첩된 타입(nested types)을 접근하기 위해서, 바깥에 위치한 타입은 이름으로 접근해야 한다.
           

머리말

프로젝트의 MVVM 패턴 적용

프로젝트에서 ViewModel Input, Output 구조체를 ViewController에서 적용할 때 의문이 들었다. 

클래스와 구조체가 중첩된 ViewModel

아래처럼 Input과 Output을 적용하기 위해서 기존의 ViewModel 클래스 안에 두 구조체(Input, Output)를 선언하였다.

따라서 하나의 클래스에 두 개의 구조체가 있는 구조가 되었다.

final class BoxOfficeViewModel: ViewModelType {
    
    struct Input {
        //...
    }
    
    struct Output {
        //...
    }
    
    //...
    
    func transform(input: Input) -> Output {
        //...
    }
}

ViewController에서 Input, Output의 사용

이제 실제 input과 output을 이용해 뷰에서 발생한 이벤트를 ViewModel로 입력 또는 출력하기 위해 ViewController 클래스 안에 `bind()` 메서드로 연결하였다.

final class BoxOfficeViewController: UIViewController {

    private let viewModel = BoxOfficeViewModel()
    
    //...
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //...
        bind()
    }
    
    private func bind() {
        let input = BoxOfficeViewModel.Input(searchBarText: searchBar.rx.text.orEmpty, searchButtonTap: searchBar.rx.searchButtonClicked, cellTap: tableView.rx.itemSelected)
        let output = viewModel.transform(input: input)
        
        //...
    }

본문

의문점

`bind()` 메서드를 작성하면서 아래와 같은 의문이 들었다.

Q: 왜 input일 때는 `BoxOfficeViewModel.Input`이고, output 일때는 `viewModel.transform(input: input)`인 걸까?

의도적인 수정과 오류 발생

그래서 output의 형태로 강제로 접근을 시도했다.

let input2 = viewModel.Input(searchBarText: searchBar.rx.text.orEmpty, searchButtonTap: searchBar.rx.searchButtonClicked, cellTap: tableView.rx.itemSelected)

실제로 input에 ViewModel이 아니라 그것의 인스턴스인 viewModel로 강제 접근을 하려 하면 아래와 같은 에러와 함께 안내 메시지가 뜬다.

인스턴스 멤버로 강제로 접근할 때 발생하는 'static member' 오류

Static member 'Input' cannot be used on instance of type 'BoxOfficeViewModel'
정적 멤버인 'Input'을 'BoxOfficeViewModel'의 타입의 인스턴스로 사용할 수 없습니다

그리고 친절하게 바로 아래 안내 메시지와 함께 Fix 버튼을 누르면 수정도 해준다.

Replace 'viewModel' with 'BoxOfficeViewModel'
'viewModel'을 'BoxOfficeViewModel로' 바꾸세요

Fix 버튼을 누르면 아래와 같이 수정이 된다.

//Fix 버튼 이전
let input2 = viewModel.Input(searchBarText: searchBar.rx.text.orEmpty, searchButtonTap: searchBar.rx.searchButtonClicked, cellTap: tableView.rx.itemSelected)

//Fix 버튼 이후
let input2 = BoxOfficeViewModel.Input(searchBarText: searchBar.rx.text.orEmpty, searchButtonTap: searchBar.rx.searchButtonClicked, cellTap: tableView.rx.itemSelected)

 

Input 구조체가 static member라고?

내가 알고 있는 static은 `static` 키워드를 활용한 타입 프로퍼티(`static let`)와 타입 메서드(`static func`)이다. 근데 Input 자체도 static member라니 헷갈렸다.

구조체 안의 클래스라면?

위는 `BoxOfficeViewModel` 클래스 안에 `Input` 구조체라면, 그 반대로 구조체 안의 클래스는 어떨까 해서 반대로도 만들어 보았다.

struct OuterStruct {
    
    class InnerClass {
        var name: String
        
        init(name: String) {
            self.name = name
        }
    }
}

클래스 역시 static member이다.

아래처럼 똑같은 오류가 발생한다.

let outer = OuterStruct()
let outerSelf = OuterStruct.self

//ERROR: Static member 'InnerClass' cannot be used on instance of type 'OuterStruct'
outer.InnerClass

다시 돌이켜 생각해 보면 클래스나 구조체의 타입이 정적인 멤버가 아니라면, 그 안에 있는 프로퍼티나 메서드, 또는 그중첩한 타입을 어떻게 접근할 수 있을까 하는 의문이 들었다.

구조체와 클래스는 모두 "타입"이다.

Swift 공식 문서에서 "구조체와 클래스(Structures and Classes)" 항목에 들어가면 해당 페이지의 맨 위에 아래처럼 적혀 있다.

Model custom types that encapsulate data.
데이터를 캡슐화하는 커스텀 타입을 모델화하세요.

Structures and Classes | Swift Documentation

 

구조체와 클래스의 정의는 곧 "타입"의 정의이다.

공식 문서의 해당 항목의 내용 중에서 "정의 구문(Deifinition Syntax)"을 살펴 보면 아래와 같이 적혀 있다.

Whenever you define a new structure or class, you define a new Swift type.
새로운 구조체나 클래스를 정의할 때마다, 새로운 Swift의 "타입"을 정의하는 것이다.

Definition Syntax - Structures and Classes | Swift Documentation

 

타입은 정적 멤버(static member)이다.

여태 배우고 사용한 static member는 "타입" 프로퍼티와 "타입" 메서드이다. 정의한 구조체와 클래스는 곧 "타입"이기에 이 역시 static member라 할 수 있다. 정적 멤버는 컴파일 시 데이터 영역에 올라가며, 프로세스의 종료 전까지 메모리에서 해제되지 않는 특징을 갖고 있다.

 

중첩한 타입의 참조(Referring Nested Types)

Swift 공식 문서에서 중첩된 타입(Nested Types) 항목을 찾아 보면 아래와 같은 내용이 있다.

중첩한 타입의 접근은 "이름"을 통해 접근한다.

To use a nested type outside of its definition context, prefix its name with the name of the type it’s nested within:
정의 컨텍스트의 외부에서 중첩한 (안쪽의) 타입을 사용하려면, 해당하는 타입의 이름 앞에 중첩한 (바깥의) 타입의 이름을 앞에 놓아야 한다.

Referring to Nested Type - Nested Types | Swift Documentation

 

따라서 `BoxOfficeViewModel` 클래스 안의 `Input` 구조체를 접근하기 위해서는 Input 타입의 이름 앞에 BoxOfficeViewModel 타입의 이름으로 접근해야 한다.

let input = BoxOfficeViewModel.Input(...)

꼬리말

값 타입/참조 타입인 것은 "인스턴스"인 경우이다.

구조체는 값 타입이고, 클래스는 참조 타입이다.

위의 내용만 이해하고 외우기만 했지, 그 전에 둘 다 "타입"이다.

구조체와 클래스는 "(커스텀) 타입"이다.