[Error] Generic struct 'ForEach' requires that 'Infos' conform to 'Hashable' 오류 해결

알파관·2022년 11월 24일
1

날씨정보에 관한 API를 통해 날씨정보를 화면에 출력하고자 하던 중 위와 같은 오류를 접했는데 1시간이나 시간을 들인 끝에 해결할 수 있었다.

평소에도 자주 접했던 오류였던지라 이번 기회에 확실히 오류의 원인과 정확한 해결법을 정리해보고자 trouble shooting 게시글을 작성하게되었다.


- 코드 살펴보기

struct WeatherView: View {
    var weatherWebService: WeatherWebService = WeatherWebService()
    var url: String =
    "https://api.openweathermap.org/data/2.5/forecast?q=seoul&appid=6976ce3140a4a0d4cce84e76d77271c7"
    
    @State private var weathers: [Infos] = []

    var body: some View {
        List {
        //문제의 구문
            ForEach(weathers, id: \.self) { item in
                HStack {
                    Text("\(item.main.temp)")
                }
            }
        }
        .onAppear {
            Task {
                 do {
                     weathers = try await weatherWebService.fetchData(url: url)
                } catch {
                    print("\(error)")
                    return
                }
            }
        }
    }
}

다음은 에러가 발생했던 코드이다. 맥락의 이해를 위해 그냥 에러가 발생한 구조체 전체를 가져왔다.

코드에 대해 간략히 설명하자면

  1. url 변수의 api주소를 Task 구문 안에서 decode하여 weathers 상태 프로퍼티에 [Infos] 타입의 데이터를 넣어주고
  1. 해당 weathers 변수 (Infos 객체의 배열)를 ForEach 문을 통해 순회하며 item.main.temp 값 (기온정보)을 차례대로 List에 출력하는 형태였다.

(구조체의 자세한 정보는 뒤에 나오는 코드를 참고)


- 원인 탐색과정

처음에는 평상시에 분명 자주 봤던 오류였기에 코딩을 하다보면 자연스럽게 해결될 것이라고 막연하게 생각했다.

그래서, 오류문구를 그대로 해석하여 기존에 Codable 프로토콜만 채택하던 Infos 구조체가 Hashable 프로토콜도 채택하게끔 처리해주었다.

struct Infos: Codable, Hashable {
    let dt: Int
    let main: MainClass
    let weather: [Weather]
    let clouds: Clouds
    let wind: Wind
    let visibility: Int
    let pop: Double
    let sys: Sys
    let dtTxt: String
    let rain: Rain?

    enum CodingKeys: String, CodingKey, Hashable {
        case dt, main, weather, clouds, wind, visibility, pop, sys
        case dtTxt = "dt_txt"
        case rain
    }
}

이렇게 코드를 수정하면 문제가 해결될 줄 알았다...분명히..

하지만, 여기서 또다른 오류가 발생하였다.

Hashable 프로토콜을 채택했더니,, 이번에는 Infos 구조체가 Equatable 프로토콜에 순응하지 않는다는 오류가 떴다.

순간 머리가 하얘졌지만 침착하고 내가 여기서 지금 무엇을 해야 할지 곰곰히 생각해보았다.

예전에도 똑같은 오류를 겪었다가 해결했던거 같았는데 그 당시엔 어떻게 해결했는지 기억이나지 않아서 너무 아쉬웠다.

또다른 오류 문구가 떴기에 일단은 무작정 해석을 해보고 해석한대로 Equatable 프로토콜을 채택해보기도 했지만, 해결이 되지 않아서 결국 구글링을 하게 되었다.

Equatable을 왜 사용하는지 알아봤더니 다음과 같은 정보를 얻을 수 있었다.

Equatable은 타입끼리 비교연산을 하기 위해 필수적으로 구현되어야 함

위 정보를 통해 나는 어느 정도 정답을 얻을 수 있었다.

Infos 타입의 값들끼리 비교를 할 수 있어야하는데 현재 그것이 불가능하다는 상태인 것이다.

그러면 이를 해결하기 위해서는 어떻게 해야할까?

해답은 Hashable 프로토콜 채택에 있었다.

모든 데이터 타입들은 자신과 같은 타입의 값들과 비교를 하기 위해서 값 자신이 Hashable한 값이어야한다.

우리가 아는 String, Int와 같은 원시타입들은 애초에 Hashable한 타입으로 설정되어있기에 Hashable 프로토콜을 채택하지 않아도 상관없었지만, 본 코드에서는 타입의 대상이 구조체이기 새로이 Hashable선언을 해주어야 Equatable이 만족된다는 것이다.

근데, Info 구조체에 Hashable 프로토콜 채택하지 않았나..?

맞다, Hashable 프로토콜을 채택한 것은 사실이다.

하지만, 채택했음에도 오류가 떴던 이유는 Infos 구조체 내부를 살펴보면 바로 알 수 있다.

struct Infos: Codable, Hashable {
    let dt: Int
    let main: MainClass
    let weather: [Weather]
    let clouds: Clouds
    let wind: Wind
    let visibility: Int
    let pop: Double
    let sys: Sys
    let dtTxt: String
    let rain: Rain?

    enum CodingKeys: String, CodingKey, Hashable {
        case dt, main, weather, clouds, wind, visibility, pop, sys
        case dtTxt = "dt_txt"
        case rain
    }
}

위에 코드가 있지만, 보기 편하도록 다시 한번 코드를 가져왔다.

Infos 구조체 내부를 자세히 들여다보면 main, clouds, wind, sys, rain 등의 프로퍼티들이 있고, 이들은 각각 MainClass, Clouds, Wind, Sys, Rain과 같은 만들어진 타입(구조체 형태)을 타입으로 가진다.

그리고 이들의 타입 값들을 비교가능하게하기 위해서는 MainClass, Clouds, Wind, Sys, Rain 구조체들도 모두 Hashable해야 함을 의미한다.

결론 : MainClass, Clouds, Wind, Sys, Rain 구조체에도 Hashable 프로토콜을 채택해주자"


- 문제해결

struct MainClass: Codable, Hashable {
    let temp, feelsLike, tempMin, tempMax: Double
    let pressure, seaLevel, grndLevel, humidity: Int
    let tempKf: Double

    enum CodingKeys: String, CodingKey, Hashable {
        case temp
        case feelsLike = "feels_like"
        case tempMin = "temp_min"
        case tempMax = "temp_max"
        case pressure
        case seaLevel = "sea_level"
        case grndLevel = "grnd_level"
        case humidity
        case tempKf = "temp_kf"
    }
}

// MARK: - Rain
struct Rain: Codable, Hashable {
    let the3H: Double

    enum CodingKeys: String, CodingKey, Hashable {
        case the3H = "3h"
    }
}

// MARK: - Sys
struct Sys: Codable, Hashable {
    let pod: Pod
}

enum Pod: String, Codable, Hashable {
    case d = "d"
    case n = "n"
}

// MARK: - Weather
struct Weather: Codable, Hashable {
    let id: Int
    let main: MainEnum
    let weatherDescription, icon: String

    enum CodingKeys: String, CodingKey {
        case id, main
        case weatherDescription = "description"
        case icon
    }
}

enum MainEnum: String, Codable, Hashable {
    case clear = "Clear"
    case clouds = "Clouds"
    case rain = "Rain"
}

// MARK: - Wind
struct Wind: Codable, Hashable {
    let speed: Double
    let deg: Int
    let gust: Double
}

Hashable 프로토콜 채택이 필요한 모든 구조체에 Hashable 프로토콜을 채택해주었다.

그리고 그 결과 ForEach 문에서 오류가 사라졌음을 확인할 수 있었다.


참고링크

https://zeddios.tistory.com/498
https://babbab2.tistory.com/148

profile
iOS🍎

1개의 댓글

comment-user-thumbnail
2023년 4월 10일

감사합니다

답글 달기