기어가더라도 제대로

[Swift-기초] 제네릭을 실전처럼 이용해보기 - generics 본문

Swift - 기초

[Swift-기초] 제네릭을 실전처럼 이용해보기 - generics

Damagucci-juice 2022. 11. 12. 13:26
사실 기초가 아니지만,
어려운 예제지만 꼭 이해해야 더 풍성한 앱을 만드는데 도움이 되는 내용이기에 짚고 넘어갑니다. 

Bundle에서 파일을 올리는 예제

  • 번들에 하나의 타입을 담고 있는 JSON 파일이 각각 여러개라고 가정
  • 제네릭을 쓰지 않으면, 파일 하나하나당 타입을 명시해줘야해서 재사용성이 떨어짐
func decode(_ file: String) -> [String: Astronaut] { }
func decode(_ file: String) -> [Mission] { }
  • 각각은 missions.json, astronauts.json 두개의 파일을 올리기 위해서 거의 같은 내용의 메서드 두개를 만들어야할 것이다. 
  • 천천히 하나부터 기능이 동작하게 만들고 그다음에 이것을 제네릭으로 바꿔보자. 

astronauts.json
0.02MB
missions.json
0.01MB

 

decoding Astronauts - 특정 타입 하나만 디코딩하기

struct Astronaut: Codable, Identifiable {
    let id: String
    let name: String
    let description: String
}

extension Bundle {
    func decode(_ file: String) -> [String: Astronaut] {
	    // 번들에서 해당 이름을 가진 파일의 URL을 찾음
        guard let url = self.url(forResource: file, withExtension: nil) else {
            fatalError("Failed to locate \(file) in bundle.")
        }

		//	URL로 Data 를 만들어냄
        guard let data = try? Data(contentsOf: url) else {
            fatalError("Failed to load \(file) from bundle.")
        }

        let decoder = JSONDecoder()
		// 데이터를 특정타입으로 변환
        guard let loaded = try? decoder.decode([String: Astronaut].self, from: data) else {
            fatalError("Failed to decode \(file) from bundle.")
        }

        return loaded
    }
}
  • 이렇듯 특정 유형의 파일만을 디코딩할 수 있는 메서드가 만들어졌다. 
  • 하지만 이 코드를 활용해서 Mission까지 디코딩할 수 있도록 제네릭을 이용해 만들어보자. 

 

generics 로 다수의 파일 decoding 하기

struct Mission: Codable, Identifiable {
    struct CrewRole: Codable {
        let name: String
        let role: String
    }

    let id: Int
    let launchDate: String?
    let crew: [CrewRole]
    let description: String
}
  • 추가하려는 타입은 이렇다. 
  • 위의 decode 함수를 제네릭으로 바꾸기 위해서는 꺾은 선을 활용해야한다. 
func decode<T>(_ file: String) -> [String: Astronaut] {
}
  • 여기서 T는 어떠한 타입이든 올 수 있는 메타몽 같은 존재다.
  • 하지만 반환타입이 이것을 받을 준비가 안되어있다. 
func decode<T>(_ file: String) -> T {
}
  • T 라는 타입을 반환하려 한다. 
  • 근데 JSON 파일이 여러 인스턴스를 가질것이라고 [T] 타입을 반환하게 되면 절대 안된다. 
  • 좀 어렵지만, JSON 자체를 읽고 어떤 타입을 반환할지 결정을 한다. 
    • missions.json -> [Mission]
    • astronauts.json -> [String: Astronaut]
  • [Mission], [String: Astronaut] 요것들 각각이 T 라고 인식을 하기 때문에 굳이 [T] 라고 할 필요가 없는 것이다. 
    • 만약 한다면 이런 모습일 것이다. [T] -> [[Mission]]
  • 그치만 여기까지 해도 오류가 난다. 왜와이?
  • T 는 어떤 타입이든 올 수 있다고 했는데 우리가 저 T 를 가지고 할 것은 decoding 이다
    • 그렇다
    • T는 Codable을 만족해야 디코딩될 수 있다.
func decode<T: Codable>(_ file: String) -> T {
}
  • 아주 깔끔하다. 
  • decode 의 최종 코드는 다음과 같다. 
extension Bundle {
    func decode<T: Codable>(_ file: String) -> T {
	    // 번들에서 해당 이름을 가진 파일의 URL을 찾음
        guard let url = self.url(forResource: file, withExtension: nil) else {
            fatalError("Failed to locate \(file) in bundle.")
        }

		//	URL로 Data 를 만들어냄
        guard let data = try? Data(contentsOf: url) else {
            fatalError("Failed to load \(file) from bundle.")
        }

        let decoder = JSONDecoder()
		// 데이터를 특정타입으로 변환
        guard let loaded = try? decoder.decode(T.self, from: data) else {
            fatalError("Failed to decode \(file) from bundle.")
        }

        return loaded
    }
}
  • guard let loaded 가 있는 구문 주의
  • T.self로 반환하려는 타입의 T로 변경
여기서 잠깐... T는 뭔지 알고 Swift가 변환을 하는가...? 
여기서 type annotation이 등장한다.
let astronauts: [String: Astronaut] = Bundle.main.decode("astronauts.json")
let missions: [Mission] = Bundle.main.decode("missions.json")
  • 반환타입이 무엇인지 type annotation 에서 밝혀준다. 
  • 어떤 타입인지 확실히 밝히는 주석

 

요약

  • 제네릭을 잘쓰면 코드가 많이 줄 수 있다.
  • 이 포스팅은 번들 파일을 다뤘지만, 네트워크 파일 또한 마찬가지로 적용할 수 있다. 
Comments