network

์šฉ๋„

์„œ๋ฒ„์™€ ํ†ต์‹ ํ•˜๊ธฐ ์œ„ํ•จ์ด๋‹ค. ํ†ต์‹ ์„ ํ•  ๋•Œ ๊ธฐ๋ณธ์ ์œผ๋กœ ํ—ค๋”์— ์‹ค์–ด์„œ ์ „๋‹ฌํ•ด์•ผํ•  ์ •๋ณด๋“ค๋„ ์žˆ๊ณ , ์ธํ„ฐ์…‰ํ„ฐ๋ฅผ ํ™œ์šฉํ•ด์„œ ์„œ๋ฒ„์˜ ์‘๋‹ต์— ๋”ฐ๋ผ ์ „์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ๋„ ์žˆ๋‹ค. ์ด ๋ชจ๋“  ์‚ฌํ•ญ์„ ๋Œ€์‘ํ•˜๊ธฐ ์œ„ํ•œ ๋ชจ๋“ˆ์ด๋‹ค.

๊ตฌํ˜„

dio ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

$ flutter pub add dio

data ๋ ˆ์ด์–ด ๋‚ด์— network ๋ง๊ณ  response, request, repository ๋ฅผ ๋‘˜ ์ˆ˜ ์žˆ๋‹ค. ๋‚˜์ค‘์— domain ์˜์—ญ์—์„œ๋„ ์ •๋ฆฌํ•˜๊ฒ ์ง€๋งŒ repository ์˜ ๊ฒฝ์šฐ domain ๋ ˆ์ด์–ด์— ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋‘๊ณ  data ๋ ˆ์ด์–ด์—์„œ ๊ตฌํ˜„์ฒด๋ฅผ ๋งŒ๋“ค์–ด์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์—„๊ฒฉํ•˜๊ฒŒ ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์ด๋‹ค. ํ•˜์ง€๋งŒ ํ”„๋กœ์ ํŠธ๋ฅผ ํ•ด๋ณด๋‹ˆ ๋ฐ์ดํ„ฐ ์†Œ์‹ฑ ์ „๋žต์ด ์ „์ฒด์ ์œผ๋กœ ๋ฐ”๋€๋‹ค๋˜๊ฐ€ ํ•˜์ง€ ์•Š๋Š” ์ด์ƒ ๋ถˆํ•„์š”ํ•˜๊ฒŒ ๊ณ„์ธต์„ ๋‚˜๋ˆ„๋Š” ํ–‰์œ„๋ผ๊ณ  ๋А๊ปด์กŒ๋‹ค. ๊ฒฐ๋ก ์ ์œผ๋กœ repository ๋Š” data ๋ ˆ์ด์–ด์—๋งŒ ๋ฐ”๋กœ ๊ตฌํ˜„์ฒด๋ฅผ ๋‘๋Š” ๊ฒƒ์ด ์ข‹์€ ๊ฒƒ ๊ฐ™๋‹ค.

import 'base_http_client.dart';

class ApiClient extends BaseHttpClient {
  ApiClient({
    required super.clientBaseUrl,
    required super.customInterceptors,
    super.customOptions,
  });
}
import 'package:dio/dio.dart';

import '../../../application/environments/environment.dart';

abstract class BaseHttpClient with DioMixin implements Dio {
  final String clientBaseUrl;
  final List<Interceptor> customInterceptors;
  BaseOptions? customOptions;

  BaseHttpClient({
    required this.clientBaseUrl,
    required this.customInterceptors,
    this.customOptions,
  }) : super() {
    if (customOptions != null) {
      options = customOptions!;
    }
    httpClientAdapter = HttpClientAdapter();
    options = BaseOptions(
      baseUrl: clientBaseUrl,
      connectTimeout: const Duration(seconds: 10),
      receiveTimeout: const Duration(seconds: 10),
    );

    if (Environment.isDevelopment) {
      interceptors.add(LogInterceptor(requestBody: true, responseBody: true));
    }
    interceptors.addAll(customInterceptors);
  }
}

์œ„์™€ ๊ฐ™์ด ์ž‘์—…ํ•ด์ฃผ๋ฉด ํด๋ผ์ด์ด์–ธํŠธ ์ •์˜๋Š” ๋์ด ๋‚œ๋‹ค. ์•„๋ž˜๋Š” ๋‚ด๊ฐ€ ์‚ฌ์šฉํ•œ ์ธํ„ฐ์…‰ํ„ฐ๋‹ค. ๊ธฐ๋ณธ์ ์ธ ๊ฒƒ๋“ค์ด๋‹ค.

BaseHeaderInterceptor ๋Š” ํ—ค๋”์— ๊ธฐ๋ณธ์ ์œผ๋กœ ๋‹ด์„ ๋‚ด์šฉ๋“ค์„ ์„ธํŒ…ํ•œ๋‹ค.

import 'dart:io';

import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
// import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:package_info_plus/package_info_plus.dart';

import '../../../barrel.dart';

class BaseHeaderInterceptor extends InterceptorsWrapper {
  @override
  void onRequest(
      RequestOptions options, RequestInterceptorHandler handler) async {
    await _setHeaderWithAccessToken(options.headers);
    await _setHeaderWithPackageInfo(options.headers);
    await _setHeaderWithDeviceInfo(options.headers);
    // await _setHeaderWithPushToken(options.headers);

    super.onRequest(options, handler);
  }

  Future<void> _setHeaderWithAccessToken(Map<String, dynamic> headers) async {
    final String? accessToken = await AccessManager.getAccessToken();
    if (accessToken == null) {
      return;
    }

    const String ACCESS_TOKEN_PREFIX = 'Bearer';
    const String SPACE = ' ';
    headers['access-token'] = ACCESS_TOKEN_PREFIX + SPACE + accessToken;
  }

  Future<void> _setHeaderWithPackageInfo(Map<String, dynamic> headers) async {
    final PackageInfo packageInfo = await PackageInfo.fromPlatform();
    headers['app-name'] = packageInfo.appName;
    headers['package-name'] = packageInfo.packageName;
    headers['version'] = packageInfo.version;
    headers['build-number'] = packageInfo.buildNumber;
  }

  Future<void> _setHeaderWithDeviceInfo(Map<String, dynamic> headers) async {
    final DeviceInfoPlugin deviceInfo = Injector.deviceInfoPlugin;
    if (Platform.isAndroid) {
      final AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
      headers['uuid'] = androidInfo.id;
      headers['operating-system'] = 'Android';
      headers['operating-system-version'] =
          androidInfo.version.sdkInt.toString();
      return;
    }

    if (Platform.isIOS) {
      final IosDeviceInfo iOSInfo = await deviceInfo.iosInfo;
      headers['uuid'] = iOSInfo.identifierForVendor;
      headers['operating-system'] = 'iOS';
      headers['operating-system-version'] = iOSInfo.systemVersion;
    }
  }

// Future<void> _setHeaderWithPushToken(Map<String, dynamic> headers) async {
//   String? pushToken = await FirebaseMessaging.instance.getToken();
//   headers['push-token'] = pushToken ?? '';
// }
}

TokenInterceptor ๋Š” ํ† ํฐ ๋งŒ๋ฃŒ์‹œ ๋ฐ”๋กœ ์—๋Ÿฌ๋ฅผ ๋‚ด๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์„ ์ด์šฉํ•ด์„œ ๋‹ค์‹œ ํ† ํฐ์„ ์žฌ๋ฐœ๊ธ‰๋ฐ›์•„์„œ ๊ธฐ์กด์˜ ์š”์ฒญ์„ ๊ทธ๋Œ€๋กœ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์ด๋‹ค. ์„œ๋ฒ„์—์„œ ํ† ํฐ์ด ๋งŒ๋ฃŒ์ผ๋•Œ ์•„๋ž˜์™€ ๊ฐ™์ด ์•ฝ์†๋œ ์ฝ”๋“œ์™€ status๋ฅผ ๋‚ด๋ ค์ฃผ๊ณ  ๋‘ ์กฐ๊ฑด์ด ๋ชจ๋‘ ๋งŒ์กฑํ•  ๋•Œ ์žฌ๋ฐœ๊ธ‰ ์ฒ˜๋ฆฌ๋ฅผ ํ–ˆ๋‹ค. ๋‚˜๋Š” ์„œ๋ฒ„์—์„œ๋„ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์„ ๋ณด๊ด€ํ•˜๋„๋ก ํ•ด์„œ ๋ณด์•ˆ์„ ๊ฐ•ํ™” ํ•˜๋Š” ์ชฝ์œผ๋กœ ๊ตฌํ˜„ํ–ˆ๋‹ค.

import 'package:dio/dio.dart';

import '../../../barrel.dart';

class TokenInterceptor extends InterceptorsWrapper {
  TokenInterceptor();

  @override
  Future<void> onError(
      DioException err, ErrorInterceptorHandler handler) async {
    if (err.response?.statusCode == 401 &&
        err.response?.data['code'] == 'AU01') {
      final String? refreshToken = await AccessManager.getRefreshToken();

      if (refreshToken != null) {
        try {
          Response response = await Injector.apiClient
              .put('/user/token/refresh?refreshToken=$refreshToken');

          final String newAccessToken = response.data['accessToken'];
          final String newRefreshToken = response.data['refreshToken'];
          await AccessManager.replaceTokens(
              accessToken: newAccessToken, refreshToken: newRefreshToken);

          // ์›๋ž˜์˜ ์š”์ฒญ์„ ์žฌ์‹คํ–‰
          RequestOptions requestOptions = err.requestOptions;
          requestOptions.headers['access-token'] = 'Bearer $newAccessToken';

          Response newResponse = await Injector.apiClient.request(
            requestOptions.path,
            queryParameters: requestOptions.queryParameters,
            options: Options(
              method: requestOptions.method,
              headers: requestOptions.headers,
              responseType: requestOptions.responseType,
              contentType: requestOptions.contentType,
              extra: requestOptions.extra,
            ),
            data: requestOptions.data,
          );

          handler.resolve(newResponse);
        } catch (exception) {
          // ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ์‹คํŒจ์‹œ ๋กœ์ง ์ฒ˜๋ฆฌ
          handler.next(err);
        }
      }
    } else {
      handler.next(err);
    }
  }
}

์•„๋ž˜๋Š” ์‹œ์Šคํ…œ ์ ๊ฒ€์ค‘์ผ๋•Œ ์‹œ์Šคํ…œ ์ ๊ฒ€์ค‘์ด๋ผ๋Š” ๋‹ค์ด์–ผ๋กœ๊ทธ๋ฅผ ๋„์šฐ๊ณ  ์„ ํƒ์ง€ ์—†์ด ์•ฑ์„ ์ข…๋ฃŒ์‹œํ‚ค๋„๋ก ํ•˜๋Š” ์ธํ„ฐ์…‰ํ„ฐ์ด๋‹ค. ํ† ํฐ ์ธํ„ฐ์…‰ํ„ฐ์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์„œ๋ฒ„์—์„œ ์ •์˜๋œ ์ฝ”๋“œ์™€ status ๋ฅผ ๋‚ด๋ ค์ฃผ๊ฒŒ ํ–ˆ๋‹ค.

import 'dart:io';

import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';

import '../../../barrel.dart';

class ServerMaintenanceInterceptor extends Interceptor {
  ServerMaintenanceInterceptor();

  @override
  Future<void> onError(
      DioException err, ErrorInterceptorHandler handler) async {
    if (err.response?.statusCode == 503 &&
        err.response?.data['code'] == 'SY01') {
      BasicDialog.show(
          context: RouterConfiguration.navigatorKey.currentContext!,
          title: TextManager.serviceMaintenanceTitle.tr(),
          contents: TextManager.serviceMaintenanceMessage.tr(),
          confirmAction: () {
            exit(0);
          });
    } else {
      handler.next(err);
    }
  }
}

์‚ฌ์šฉ

import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
import 'package:flutter_template/barrel.dart';
import 'package:get_it/get_it.dart';

class Injector {
  Injector._();

  static DeviceInfoPlugin get deviceInfoPlugin =>
      GetIt.instance.get<DeviceInfoPlugin>();

  static Dio get apiClient => GetIt.instance.get<Dio>();

  static Future registerDependencies() async {
    _registerUtils();
    _registerNetworks();
    _registerRepositories();
  }

  static _registerUtils() async {
    GetIt.instance
        .registerLazySingleton<DeviceInfoPlugin>(() => DeviceInfoPlugin());
  }

  static _registerNetworks() async {
    GetIt.instance.registerLazySingleton<Dio>(
      () => ApiClient(
        clientBaseUrl: Environment.baseUrl,
        customInterceptors: [
          BaseHeaderInterceptor(),
          // TokenInterceptor(),
        ],
      ),
    );
  }

  static _registerRepositories() async {}
}

์‚ฌ์šฉ์€ ์œ„์™€ ๊ฐ™์ด Injector ์— ์ •์˜ํ•ด๋‘๊ณ  ํ•„์š”ํ•  ๋•Œ(= Bloc ์— ์ฃผ์ž… ๋˜๋Š” Repository ์— ์ฃผ์ž…) ๊บผ๋‚ด์„œ ์‚ฌ์šฉํ•œ๋‹ค.

Last updated