티스토리 뷰

하루종일 고민했는데...

 

라이브러리

RetrofitFreezed를 이용해서 REST 통신을 하는 상황에서 에러가 발생했습니다.

Retrofit은 Dio라이브러리를 이용해 네트워킹을 해주는 라이브러리이고,

Freezed는 json을 객체로 매핑하는 작업 (serialize)을 도와주는 라이브러리입니다. (json_serializable 라이브러리를 의존)

두 라이브러리 모두 어노테이션을 이용한 코드 제너레이터 기능이 있기 때문에 타이핑 코드양을 정말 많이 줄일 수 있습니다.

 

 

에러


DioError [DioErrorType.other]: type 'String' is not a subtype of type 'Map<String, dynamic>?' in type cast

 

타입 에러가 발생했습니다. Map<String, dynamic>으로 들어와야할 값이 String으로 들어왔다고 합니다.

 

해당 에러가 발생한 코드는 제너레이터로 자동생성된 코드인데 살펴보면 다음과 같습니다.

  // DefaultRetrofit.g.dart
  
  @override
  Future<IsSmartMeterDTO> checkIsSmartMeter(customerNumber) async {
    const _extra = <String, dynamic>{};
    final queryParameters = <String, dynamic>{r'custNum': customerNumber};
    final _headers = <String, dynamic>{};
    final _data = <String, dynamic>{};
    // 여기가 오류 지점
    final _result = await _dio 
        .fetch<Map<String, dynamic>>(_setStreamType<IsSmartMeterDTO>(Options( 
      method: 'GET',
      headers: _headers,
      extra: _extra,
    )
            .compose(
              _dio.options,
              'kepcoCustNumValidation',
              queryParameters: queryParameters,
              data: _data,
            )
            .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
    final value = IsSmartMeterDTO.fromJson(_result.data!);
    return value;
  }

Tstory 코드블럭이 Dart를 지원하지 않는게 매우 화나네요...그나마 java가 제일 비슷해서 예쁘게 나오는 것 같습니다.

 

dio로 fetch를 실행할때 결과값을 <Map<String, dynamic>>으로 받는데 여기에 String이 들어와서 발생한 오류입니다.

 

 

원인


서버에서 받은 요청 header가 어떻게 되어 있는지 포스트맨으로 확인해보았는데 Content-Type이 text/plain이네요.

dio 라이브러리를 뜯어보진 않았지만 Content-Type에 따라 Decode를 진행해주는 로직이 추상화되어있는 것 같습니다.

Content-Type이 json이었다면 Map<string, dynamic>으로 디코드해서 리턴해줬을 텐데,

text/plain이라 String 그대로 dio가 반환해준 것 같네요.

 

 

해결방법


Solution 1. 백엔드 서버를 고칠 수 있다면

서버에서 해당 API의 response 'Content-Type'을 'application/json'으로 바꿔주어야합니다.

사실, 미디어 데이터 이외엔 거의 json으로 통신하기 때문에 request에서 따로 명시하지 않으면 기본적으로 'application/json'이 되도록 세팅해놓습니다. 

따라서 json 통신을 하려고 백엔드와 합의했는데 이런 오류가 뜨는거라면 백엔드쪽 실수일 가능성이 높습니다.

 

Solution 2. 백엔드 서버를 고칠 수 없다면 (Open API 등 접근 불가능한 서버이용)

request 헤더에 'Accept' : 'application/json'을 추가해볼 수 있습니다.

이 헤더는 응답을 json 형태만 받겠다고 명시하는 것이므로 그 외 응답은 허용하지 않습니다.

만약 Accept 요청에 따라 응답형태를 바꿔주는 로직이 서버에 존재한다면 성공할 수도 있습니다.

 

Solution 3. jsonDecode 이용 (최후의 수단)

  // DefaultRetrofit.g.dart
  
  @override
  Future<IsSmartMeterDTO> checkIsSmartMeter(customerNumber) async {
    const _extra = <String, dynamic>{};
    final queryParameters = <String, dynamic>{r'custNum': customerNumber};
    final _headers = <String, dynamic>{};
    final _data = <String, dynamic>{};
    // 여기가 오류 지점
    final _result = await _dio 
        .fetch<String>(_setStreamType<IsSmartMeterDTO>(Options( // 일단 String으로 받기
      method: 'GET',
      headers: _headers,
      extra: _extra,
    )
            .compose(
              _dio.options,
              'kepcoCustNumValidation',
              queryParameters: queryParameters,
              data: _data,
            )
            .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
    var decodeData = jsonDecode(_result.data!); // String을 json으로 디코드
    final value = IsSmartMeterDTO.fromJson(decodeData); // 디코드 된것을 객체로 매핑
    return value;
  }

제너레이트된 코드를 변경하여 해결할 수 있습니다.

일단 응답은 String으로 받고 이것을 다시 json으로 Decode해주는 코드를 추가해주면 됩니다.

jsonDecode는 내부 라이브러리 함수로 dart:convert를 Import하면 사용할 수 있습니다.

 

그러나, Retrofit을 사용하는 이유는 해당 코드를 자동으로 생성하기 위함입니다.

만약 다시 제너레이터를 빌드하게 된다면 고쳤던 코드가 원상복구되며,

한두개가 아닌 api 호출에 모두 decode(파싱) 코드를 넣어주어야합니다.

파싱까지 자동으로 해주길 바래서 사용한 라이브러리인데,

이걸 일일이 변경할거면 코드제너레이터를 사용하지 않았겠죠...

그러므로 백엔드에서 Content-type 변경이 불가능하다면 Retrofit라이브러리 없이 Dio나 http만을 이용해서 통신/디코드 하시는 것을 추천드립니다.

 

 

혹시 저처럼 고생하실까봐 포스팅합니다. 화이팅!!

 

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   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
글 보관함