모바일 앱

뜨루루루·2023년 12월 3일

sirenorder

목록 보기
4/9

시작

그간에 참고했던 벨로그들을 보면 파트별로 잘 나누어서 정리 해두던데 그렇게 세부적으로 나누기에는 머릿속에 정리된 내용을 글로 녹여내는 능력이 부족하단 거겠죠?

아무튼 이번에도 글하나에 진행내용을 정리해보겠습니다.

(내용이 별로없는 걸지도?)

환경

프레임워크 - Flutter
언어 - Dart
패턴 - Bloc, MVVM
IDE - Visual Studio Code

패턴에 왜 블록이 들어가있냐 하실수도 있을텐데 해당 상태관리 라이브러리를 공부하면서 정리된 글이나 혹은 영상등 그곳에서도 단순 라이브러리 보단 패턴이라고 소개하는곳이 종종 있었습니다.

실제로 라이브러리 없이 패턴화해서 사용하는 모습을 보여주기도 하였고 저또한 공부하고 사용해 나가면서 단순 라이브러리 보다는 블록이라는 패턴을 사용하게끔 정리해둔 라이브러리 같았습니다.

실제로 프로바이더와 매우 흡사하며 다르다면 이벤트를 사용하여 블록의 상태를 변경하고 위젯에 변경된 정보를 반영한다는 정도... 동시성 제어같은 부분에서도 지원하는 라이브러리가 존재해 매우 편리하게 관리했습니다.

MVVM 패턴같은 경우 Model, View, View Model로 나누어 코드를 작성하는 걸로 알고있는데 해당 부분마다 어떤 역할을 차지하는지에 대한 의견들이 정리된 글마다 살짝다른 부분이 있어 정확하게 뭔지는 모르겠지만 일단 정리를 해보자면 Model은 State가 가지게될 정보이고 View Model은 현재의 State를 정의해놓은 클래스라고 생각합니다.

View는 View Model을 구독하고 현재 State를 읽어와 정보를 읽고 유저에게 정보에 맞는 UI를 보여주는 거라고 생각합니다.

사실 패턴관련 질문을 받을때 마다 어떻게 대답해야할지 잘 모르겠는데 어떤 대답을 원할지, 해야할지 몰라서 인거 같기도 하고 어떤 대답을 하던 틀린말은 아니라 어떤게 더욱 장황하게 들릴지 고민되서 일수도 아니면 패턴의 사용이유를 정확하게 몰라서 일지도 모르겠습니다.

(IDE를 적어둔 이유는 Android Studio로 작업하시는 분들도 많이 계셔서 저는 VSCode를 사용하기에 적어두었습니다.)

코드 관리

Dart는 주로 사용하는 Typescript와 많이 다르고 결정적으로 타입을 지원하지 않아 클래스와 열거형을 이용해 그 기능을 대체하려 많이 노력했습니다.

클래스로 어느 객체의 성질을 작성할때 freezed를 많이 사용하고 그를 이용한 객체관리, factory 함수인 .copywith를 이용한 복제, 수정을 많이 이용하는것 같이 보였습니다.

이 프로젝트에선 equatable를 사용했고, 객체의 변환또한 내부 함수를 작성하고 호출해서 사용했습니다.

데이터가 3개의 플랫폼을 옮겨 다니다 보니 어느곳에선 정상작동하고 어느곳에선 오류를 뱉어내기도 합니다.

freezed를 사용하기엔 다트에 맞는 데이터 타입으로 변환하기가 매우 까다로웠고, 수정을 할 수 있다곤 하지만 분할된 파일을 일일이 찾아가 수정하는 작업이 매우 귀찮았습니다.

하나의 객체의 성질을 위한 의존성 객체들을 작성한다면 한파일에 몰아넣고 그곳에서 수정하는게 편리하다고 생각했고 무엇보다 직접타이핑 하는게 좋기도 해서 freezed를 사용하지 않았습니다.

enum 같은 경우 Record<T, E>와 함께 사용했는데 이를 사용할때는 해당 데이터가 정의된 타입이외에 데이터를 가질 필요가 없는 데이터일 경우에 지정해두었습니다.
(가변성을 가진 데이터이긴 합니다.)
아래는 사용했던 방식에 일부 입니다.

enum RequestRoute {
  tokenlogin,
  login,
  order,
  coupon,
  regist,
  publishcode,
  code,
  menu,
  orderstate,
}

const Map<RequestRoute, String> routes = {
  RequestRoute.tokenlogin: "/user/login/token",
  RequestRoute.login: "/user/login",
  RequestRoute.coupon: "/user/coupon",
  RequestRoute.regist: "/user/regist",
  RequestRoute.publishcode: "/user/regist/publish",
  RequestRoute.code: "/user/regist/verify",
  RequestRoute.order: "/store/order/send",
  RequestRoute.menu: "/menu",
  RequestRoute.orderstate: "/store/order/state",
};

Future<Response> fetchGet(
  RequestRoute route, {
  Map<String, dynamic>? queryParams,
  Map<String, dynamic>? headers,
}) async {
  final url = "$base${routes[route]}";
  final BaseOptions options = BaseOptions(
    baseUrl: url,
    connectTimeout: const Duration(seconds: 60),
    sendTimeout: const Duration(seconds: 120),
    receiveTimeout: const Duration(seconds: 120),
    maxRedirects: 0,
    headers: {
      "Content-Type": "application/json;charset=UTF=8",
      ...?headers,
    },
    queryParameters: queryParams,
  );

  final Dio dio = Dio(options);
  return await dio.get(url);
}

route를 그냥 url로 받아도 괜찮지 않나 생각할 수도 있지만, 이런식으로 정리를 해둔다면 오기입으로 인한 오류 발생이 적어지고 외부에서 해당 함수호출시에 그 코드에 양또한 줄어들고 타입유추가 가능하니 필요한 열거타입을 가져와 넣기만 해주면 사용하는 입장에서 편하기도 할거라 생각했습니다.
아래는 위 함수를 호출하는곳에 일부 입니다.

Future<Response> _sendLoginRequest(UserLoginEvent event) async {
    LoginType type = _getLoginType(event);
    RequestRoute route = _getRoute(type);

    switch (type) {
      case LoginType.tokenlogin:
        return await fetchPost(route,
            headers: {"authorization": event.tokenLogin});
      case LoginType.typinglogin:
        return await fetchPost(route, queryParams: {
          "email": event.typingLogin!.email,
          "pass": event.typingLogin!.pass,
        });
      default:
        throw BlocException(
          "로그인 요청중 오류가 발생했습니다.",
          ExceptionType.APIException,
        );
    }
  }

호출이 여러곳에서 일어나는 구문, 코드를 직접보기보단 이름으로 기능을 유추하기 쉬운 구문들은 모두 _getLoginType과 같은 형태로 작성했고 나누지 않았다면 100줄이 되었을 수도 있는 코드가 20줄로 작성되었고 그로인해 난독현상 또한 방지되었습니다.

주문 현황

이 부분을 작성하는데 생각보다 시간이 많이 걸렸습니다.

비동기 방식으로 일정주기마다 요청하고 데이터를 전달받는 과정에서 동시성이 문제가 될거같아 미리 순차적 처리를 해두었는데 문제는 거기서 터진게 아니라 비동기 형식에서 터졌습니다.

코드가 순차적으로 동작하지 않아 비동기를 사용한 방식자체가 틀렸던건 아니였고 문제는 상태변경에서 일어났습니다.

Stream 형식으로 받고있던 데이터는 일정주기마다 요청을 보냈고, 데이터를 전달받아 상태를 변경해주었는데 이 과정에서 앞전에 전달받은 데이터로 상태를 변경하는 와중에 다른 데이터가 넘어와 또 상태를 변경하려 했고 그로인해 병목현상이 발생했다고 판단했습니다.

단순히 데이터 요청의 주기를 늘린다? 그렇게 해결 할 수도 있겠지만 사용자 입장에서는 최대한 최신의 정보를 얻어야 하는게 맞다고 생각했습니다.

이전 요청이 끝날때 까지 다음 요청 처리를 보류한다? 또 다른 병목현상을 불러올거라 생각했습니다. 결국엔 받아온 데이터는 쌓이기 마련이니까요.

그래서 나누어 보았습니다.
데이터를 구독하는곳과 받은 데이터로 상태를 변경하는곳을
데이터를 구독한 부분에서 블록으로 이벤트를 날릴 수 있나? 에서 시작했던 의문을 실험해보았고 결과는 성공했습니다.

일정주기로 데이터를 요청하는건 일치하지만 주기를 줄이거나 늘리지 않고, 요청이 끝나길 대기하지 않고, 받아온 데이터를 블록에 이벤트로 새로 보내는 루틴을 작성했습니다.

해당 이벤트에서는 상태를 변경하지 않는 형식으로 작성했는데 아래는 그 코드에 일부분 입니다.

Future<void> _bindOrderState(
    UserBloc bloc,
    UserBlocState state,
    String orderId,
  ) async {
    final response = await fetchGet(
      RequestRoute.orderstate,
      queryParams: {
        "order_uid": orderId,
      },
    );
    if (response.data['message'] != null) {
      final failed = FailedResponse.fromJson(response.data);
      throw BlocException(failed.message, ExceptionType.APIException);
    }

    final success = SuccessResponse<String>.fromJson(response.data);
    if (success.status != 200) {
      throw BlocException(
        "서버에서 해당 요청을 수행하지 못했습니다.",
        ExceptionType.ServiceUnavailableException,
      );
    }
    final orderState = NotifyMethods.convertNotifyState(success.data);
    if (orderState == OrderState.finish || orderState == OrderState.refuse) {
      state.removeListener();
      bloc.add(AlertNotifyEvent(orderState));
      return;
    }
    bloc.add(AlertNotifyEvent(orderState));
  }

위 부분은 구독하는 부분이고

handleEvent(
    emit,
    UserEvent event,
    UserBlocState state, {
    UserRepository? repository,
  }) {
    if (event is! AlertNotifyEvent) {
      throw BlocException(
        "올바른 요청이 아닙니다.",
        ExceptionType.APIException,
      );
    }

    if (state is UserBlocNotifyState) {
      emit(UserBlocLoadedState(state.user, event.orderState));
    }

    emit(UserBlocNotifyState(state.user, event.orderState));
  }

위 부분은 단순하게 상태만 변경하는 부분입니다.
모든 코드는 이곳에서 확인이 가능합니다

그 외

피그마

일부파일들은 업로드 되어있지 않아 아직 그대로 사용해보실수는 없습니다.

모바일 앱 피그마는 스타벅스, 카카오 선물하기 이 두가지를 거의 그대로 참고하며 만들었기에 더 이상 피그마가 의미가 없다고 판단하여 그리지 않고 만들어진 터라 피그마에 그려진 UI와 현재 모바일 앱의 UI가 매우 상이해 피그마를 닫게 되었습니다.

대신 깃허브에 앱 사진을 첨부 해두었으니 필요하시다면 참고하시면 됩니다!

마치며

정리를 하고보니 생각보다 분량이 꽤나 나와서 시간 가는줄 모르고 작성했습니다.

사실 정리보단 일기에 가깝긴한데...

다음엔 서버를 정리해보도록 하겠습니다!

profile
개발 블로그보단 개발 일기 입...껄요?

0개의 댓글