[Flutter] Dio 간단 정리

leeeeeoy·2021년 10월 4일
12

이 글은 공식 문서와 블로그 문서를 보고 정리한 글입니다.

Dio

공식 문서에는 A powerful Http client for Dart 라고 소개하고 있다. http 처럼 서버와 통신을 하기 위해 필요한 패키지다. 다른점이 있다면 사용하기 쉽게 보다 많은 기능들을 제공하고 있고 여러가지 커스텀을 해서 사용하기가 편한 것 같다. 공식 문서와 몇가지 예제를 참고하여 주요 기능들에 대해 정리해보았다.

요즈음 Riverpod와 Dio를 공부하고 있는데, 최근 새로운 프로젝트를 시작하면서 Dio, Retrofit, Json_Serializable 조합을 사용해보려고 하고있다. 따로따로 익힐땐 어렵지 않았는데 실제 여러가지 케이스에 대해 작성해보려고 하니 아직 이해가 많이 부족한 것 같다... 천천히 다시 공부해보면서 익혀보려고 한다.

Package 설정

pubspec.yaml

dependencies:
  dio: ^4.0.0

dio 의존성만 추가해주면 된다. 현재 기준 4.0.0이 최신 버전이다. (Null Safety 적용)

주요 기능들

1. Request & Response

var dio = Dio();

// 첫번째 방법
final response = await dio.get('/test?id=12&name=wendu');

// 두번째 방법
final response = await dio.get('/test', queryParameters: {'id': 12, 'name': 'wendu'});

// 세번째 방법
final response = await dio.request(
  '/test',
  data: {'id':12,'name':'xx'},
  options: Options(method:'GET'),
);

// post
final response = await dio.post('/test', data: {'id': 12, 'name': 'wendu'});

request와 response는 다음과 같이 요청 메소드와 url을 같이 작성해주면 된다. 각 메서드 별로 함께 넘길 수 있는 여러가지 파라미터들이 있다. 요청 방법에는 여러가지가 있는데 위의 예시처럼 쿼리로 넘길수도 있고 body를 통해 넘길수도 있다.

2. Options

var dio = Dio(); 

dio.options.baseUrl = 'https://www.xx.com/api';
dio.options.connectTimeout = 5000; //5s
dio.options.receiveTimeout = 3000;

var options = BaseOptions(
  baseUrl: 'https://www.xx.com/api',
  connectTimeout: 5000,
  receiveTimeout: 3000,
);

Dio dio = Dio(options);


// BaseOptions 객체
BaseOptions({
    String? method,
    int? connectTimeout,
    int? receiveTimeout,
    int? sendTimeout,
    String baseUrl = '',
    Map<String, dynamic>? queryParameters,
    Map<String, dynamic>? extra,
    Map<String, dynamic>? headers,
    ResponseType? responseType = ResponseType.json,
    String? contentType,
    ValidateStatus? validateStatus,
    bool? receiveDataWhenStatusError,
    bool? followRedirects,
    int? maxRedirects,
    RequestEncoder? requestEncoder,
    ResponseDecoder? responseDecoder,
    ListFormat? listFormat,
    this.setRequestContentTypeWhenNoPayload = false,
  })

dio 객체를 생성하면서 공통적으로 사용하고 싶은 것들을 BaseOptions를 통해 지정할 수 있다. 주로 사용하는 옵션들은 다음과 같다

  • baseUrl: 요청할 기본 주소를 설정할 수 있다.
  • connectTimeout: 서버로부터 응답받는 시간을 설정할 수 있다.
  • receiveTimeout: 파일 다운로드 등과 같이 연결 지속 시간을 설정할 수 있다.
  • headers: 요청의 header 데이터를 설정할 수 있다.
    ex) 인증 토큰

옵션의 경우 각 요청마다 설정도 가능하고 처음 dio 객체를 생성할 때 설정도 가능하다. 공통적으로 사용되는 것은 dio 생성시에 설정하고, 그 외에 것들은 요청에 맞게 설정하는 식이 좋아보인다.

3. Interceptor

사실 최근 dio를 공부하면서 가장 필요했던 부분이 아닌가 싶다. Interceptor는 요청때마다 가로채는 역할을 하는데, Interceptor를 통해 요청때마다 반복적인 작업을 처리할 수 있다. 예를 들어 토큰의 유효성을 검사한다거나 로그를 처리한다거나 등이 될 수 있다.

class Interceptor {
  void onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) =>
      handler.next(options);

  void onResponse(
    Response response,
    ResponseInterceptorHandler handler,
  ) =>
      handler.next(response);

  void onError(
    DioError err,
    ErrorInterceptorHandler handler,
  ) =>
      handler.next(err);
}

interceptor를 보면 3가지 메서드가 있는데 각각 요청, 응답, 에러가 발생했을 때 동작을 처리할 수 있다. 공식 문서에 나와있는 몇가지 예제들을 정리해봤다.

3-1. Lock/unlock the interceptors

인터셉터를 통해 요청을 lock/unlock 할 수 있다. 공식 문서에 따르면 이렇게 될 경우, 대기열에 추가되어 인터셉터가 unlock이 될 때까지 대기한다고 한다.

dio.interceptors.add(InterceptorsWrapper(
  onRequest: (Options options, handler) async {
    print('send request:path:${options.path},baseURL:${options.baseUrl}');
    if (csrfToken == null) {
      print('no token,request token firstly...');
      //lock the dio.
      dio.lock();
      tokenDio.get('/token').then((d) {
        options.headers['csrfToken'] = csrfToken = d.data['data']['token'];
        print('request token succeed, value: ' + d.data['data']['token']);
        print( 'continue to perform request:path:${options.path},baseURL:${options.path}');
        handler.next(options);
      }).catchError((error, stackTrace) {
        handler.reject(error, true);
      }) .whenComplete(() => dio.unlock()); // unlock the dio
    } else {
      options.headers['csrfToken'] = csrfToken;
      handler.next(options);
    }
  }
));

예시 코드는 토큰의 유무를 검사하는 코드 예시이다. 인터셉터를 통해서 요청때마다 토큰의 유무를 검사하여 만약 토큰이 없다면 새로운 토큰을 요청해서 다시 이후에 요청을 진행한다. 이 때 토큰이 없다면 인터셉터를 lock해서 대기열에 넣은 후 토큰을 받고 나서 다시 unlock한다.

3-2. Resolve and reject the request

dio.interceptors.add(InterceptorsWrapper(
  onRequest:(options, handler) {
   return handler.resolve(Response(requestOptions:options,data:'fake data'));
  },
));
Response response = await dio.get('/test');

print(response.data);
//'fake data'

모든 요청에 대해 응답을 제어할 수 있는데, 예시 코드처럼 모든 요청에 대해 fake data를 반환하는 것이 그 예시이다. 실제 사용을 해보진 못했지만 에러가 났을 때 특정 경로로 재요청을 하는 식으로 사용이 가능할 것 같다.

3-3. Log

// 기본 Log
dio.interceptors.add(LogInterceptor());

// CustomLog
dio.interceptors.add(CustomLogInterceptor());

class CustomLogInterceptor extends Interceptor {
  
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    print('REQUEST[${options.method}] => PATH: ${options.path}');
    super.onRequest(options, handler);
  }

  
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    print(
      'RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path}',
    );
    super.onResponse(response, handler);
  }

  
  void onError(DioError err, ErrorInterceptorHandler handler) {
    print(
      'ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions.path}',
    );
    super.onError(err, handler);
  }
}

Log 역시 지정할 수 있는데 dio에서 기본적으로 제공하는 로그를 사용할 수 있다. 개인적으로는 기본 로그 형태가 한눈에 보기 쉬운건 아니어서 위 예시처럼 직접 커스텀을 해서 사용할 것 같다. 커스텀은 위의 Interceptor 객체를 상속받아 각 메서드를 구현해주면 된다.

사용 예시

코드 작성

간단하게 dio와 retrofit을 이용해서 요청을 구현해보았다. 사용한 api는 다음과 같다

// https://reqres.in/api/users/2

{
  "data": {
    "id": 2,
    "email": "janet.weaver@reqres.in",
    "first_name": "Janet",
    "last_name": "Weaver",
    "avatar": "https://reqres.in/img/faces/2-image.jpg"
  },
  "support": {
    "url": "https://reqres.in/#support-heading",
    "text": "To keep ReqRes free, contributions towards server costs are appreciated!"
  }

https://reqres.in/ 라는 곳의 API를 사용했다. 사실 이번에 정리하면서 처음 알게 되었는데 각 메서드 별로 작성되어 있어서 테스트를 할 때 종종 이용할 것 같다.

data.dart

part 'data.g.dart';

()
class User {
  User({
    required this.data,
  });

  Data data;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

()
class Data {
  Data({
    required this.id,
    required this.email,
    required this.firstName,
    required this.lastName,
    required this.avatar,
  });

  int id;
  String email;
  (name: 'first_name')
  String firstName;
  (name: 'last_name')
  String lastName;
  String avatar;

  factory Data.fromJson(Map<String, dynamic> json) => _$DataFromJson(json);
  Map<String, dynamic> toJson() => _$DataToJson(this);
}

먼저 사용할 모델을 정의해주었다. JsonSerializable을 이용해 json 메서드를 자동으로 생성해주도록 하였다.

rest_client.dart

import 'package:dio/dio.dart';
import 'package:flutter_study/pages/dio/data.dart';
import 'package:retrofit/retrofit.dart';

part 'rest_client.g.dart';

(baseUrl: 'https://reqres.in/api')
abstract class RestClient {
  factory RestClient(Dio dio, {String baseUrl}) = _RestClient;

  ('/users/{id}')
  Future<User> getUser({() required int id});
}

Retrofit을 이용해 사용할 RestClient를 생성해주었다. 예시에서 사용한 요청은 GET요청으로 id를 넘기면 User를 받도록 작성했다.

이렇게 두 파일을 작성하고 flutter pub run build_runner bulid를 실행해 코드를 생성해줬다.


dio_result_page.dart

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_study/pages/dio/custom_log_interceptor.dart';
import 'package:flutter_study/pages/dio/data.dart';
import 'package:flutter_study/pages/dio/rest_client.dart';

//  테스트 API
//  https://reqres.in/

class DioResultPage extends StatelessWidget {
  DioResultPage({Key? key}) : super(key: key);
  final dio = Dio()
    ..interceptors.add(
      CustomLogInterceptor(),
    );

  
  Widget build(BuildContext context) {
    final _client = RestClient(dio);

    return Scaffold(
      appBar: AppBar(
        title: Text('Dio'),
      ),
      body: Center(
        child: FutureBuilder<User?>(
          future: _client.getUser(id: 1),
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              User? userInfo = snapshot.data;
              if (userInfo != null) {
                Data userData = userInfo.data;
                return Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Image.network(userData.avatar),
                    SizedBox(height: 16.0),
                    Text(
                      '${userInfo.data.firstName} ${userInfo.data.lastName}',
                      style: TextStyle(fontSize: 24.0),
                    ),
                    Text(
                      userData.email,
                      style: TextStyle(fontSize: 24.0),
                    ),
                  ],
                );
              }
            }
            return CircularProgressIndicator();
          },
        ),
      ),
    );
  }
}

간단하게 FutureBuilder를 이용해 결과페이지를 작성해주었다. dio 생성시 interceptor를 추가했는데, 위에서 말한 CustomLog를 추가해주었다.

custom_log_interceptor.dart

import 'package:dio/dio.dart';

class CustomLogInterceptor extends Interceptor {
  
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    print('REQUEST[${options.method}] => PATH: ${options.path}');
    super.onRequest(options, handler);
  }

  
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    print(
      'RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path}',
    );
    super.onResponse(response, handler);
  }

  
  void onError(DioError err, ErrorInterceptorHandler handler) {
    print(
      'ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions.path}',
    );
    super.onError(err, handler);
  }
}

요청시에는 요청 메서드와 경로를, 응답시에는 응답 코드와 경로를, 에러가 발생했을 땐 에러와 경로를 출력하도록 작성했다.

결과


결과 화면과 설정한 로그가 출력된 화면이다.

정리

dio를 간단하게 정리해봤다. 사실 글에 적지 않았지만 파일 다운로드, FormData 요청, Cancellation, Transformer 등 여러가지 기능들이 있었는데 실제 자주 사용할 것 같은 기능들 위주로 간단하게 정리했다. 최근 프로젝트를 하면서 jwt 인증 토큰관련해서 코드를 작성하던 중 이해가 잘 되지 않아 작성이 조금 더뎠는데 참고해서 다시 차근차근 작성을 할 것 같다. 현재 retrofit을 같이 사용하고 있는데 retorifit으로 엔드포인트를 작성하고, dio에서 여러 옵션들을 설정해서 각 요청별 처리를 작성할 것 같다. 작성이 어느정도 진행되면 dio와 retrofit을 이용한 조금 더 복잡한 예제도 정리해서 올려야겠다.


소스코드 https://github.com/leeeeeoy/flutter_personal_study/tree/master/lib/pages/dio


참고자료

profile
100년 후엔 풀스택

0개의 댓글