본문 바로가기

Flutter

[Flutter] 서버와 통신하는 방법

안녕하세요! 메로나입니다.

 

오늘은 Flutter 개발 중인데 서버와 통신이 필요해서 공부하려고 합니다.

개발 방법은 사람마다 다르지만, 제가 적용한 방법을 말씀드리려고 합니다.

 

클린 아키텍처
  • Presentation Layer
    • 사용자 인터페이스를 담당하며, 사용자와의 상호작용을 처리합니다.
  • Application Layer
    • UI와 도메인 계층 사이의 중재자로서, 사용자 입력을 처리하고, 도메인 계층의 로직을 호출합니다.
  • Domain Layer
    • 비즈니스 로직과 규칙을 포함하며, 애플리케이션의 핵심 기능을 정의합니다.
  • Data Layer
    • 외부 데이터 소스와의 상호작용을 처리합니다.
구성 요소
  • API: Dio 객체를 통해 서버와 통신하여 데이터를 요청하고 응답받았습니다.(Dio는 라이브러리로 서버와 통신할 수 있게 도와줍니다.)
  • Repository: 데이터 소스(API/DB)와의 상호작용을 추상화하여 원본 데이터를 도메인 모델로 변환하고 데이터 접근 방식을 캡슐화합니다.
  • ViewModel: Repository에서 제공한 모델을 기반으로 UI 상태 관리와 표현 계층의 비즈니스 로직 처리에 집중합니다. Repository가 네트워크/파싱 오류를 throw 하면 ViewModel이 이를 캐치하여 UI로 표시해 줍니다.
  • UI: ViewModel을 관찰하며 상태 변경을 감지하여 화면을 업데이트합니다.
UI -> ViewModel -> Repository -> API -> Repository -> ViewModel -> UI

 

Repository 계층의 필요성
  • 역할 분리
    • Repository
      • API 응답을 앱 전용 모델로 변환하는 데이터 변환 책임 전담
      • 데이터 소스에 대한 추상화 계층 역할
      • 여러 데이터 소스 통합 관리 및 캐싱 전략 구현
    • ViewModel
      • 변환된 모델을 기반으로 UI 상태 관리에 집중
      • 도메인 규칙 적용 및 사용자 액션 처리
  • 아키텍처 원칙 준수
    • 단일 책임 원칙을 지켜 데이터 변환 로직이 UI 로직과 혼재되는 문제를 방지합니다.
    • 의존성 역전 원치을 적용하여 데이터 소스 변경 시 ViewModel 수정 없이 Repository만 조정 가능하게 합니다.
    • 테스트 용이성을 확보하여 모델 변환 로직을 Repository에서 독립적으로 검증할 수 있습니다.
Dio와 Repository의 상태 관리를 위해 Riverpod 사용
  • 의존성 주입 자동화
    • ViewModel은 직접 API나 Dio를 몰라도 되고, 테스트, Mock주입, 구성 변경이 유연해집니다.
  • 앱 전체에서 하나의 Dio 인스턴스를 공유
    • Dio를 여러 번 생성하면 interceptor, header 등이 꼬이거나 연결이 끊길 수 있습니다. 그래서 싱글턴처럼 하나만 두고 쓰는 것이 일반적입니다.
// 1. Dio 설정 Provider (싱글톤)
final dioProvider = Provider<Dio>((ref) {
  final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
  dio.interceptors.add(LogInterceptor(responseBody: true)); // 예시용 인터셉터
  return dio;
});

// 2. API 객체 Provider (Dio 의존)
class UserApi {
  final Dio dio;
  UserApi(this.dio);

  Future<Map<String, dynamic>> fetchUserJson() async {
    final response = await dio.get('/user');
    return response.data as Map<String, dynamic>;
  }
}

final userApiProvider = Provider<UserApi>((ref) {
  return UserApi(ref.watch(dioProvider));
});

// 3. Repository Provider (API 객체 의존)
class UserRepository {
  final UserApi api;
  UserRepository(this.api);

  Future<UserModel> fetchUser() async {
    final json = await api.fetchUserJson();
    return UserModel.fromJson(json);
  }
}

final userRepositoryProvider = Provider<UserRepository>((ref) {
  return UserRepository(ref.watch(userApiProvider));
});

// 4. ViewModel Provider (Repository 의존)
class UserViewModel extends StateNotifier<UserState> {
  final UserRepository repository;
  UserViewModel(this.repository) : super(UserState.initial());

  Future<void> loadUser() async {
    try {
      final user = await repository.fetchUser();
      state = state.copyWith(user: user, error: null);
    } catch (e) {
      state = state.copyWith(error: e.toString());
    }
  }
}

 

어노테이션 기반 Provider 사용 여부(RiverPod 어노테이션)
  • 장점
    • Provider 선언, 변수, 타입 등을 일일이 적지 않아도 됩니다.
    • 코드 자동 생성으로 타입 안정성이 강화됩니다.
    • IDE에서 Provider 참조 경로 추적이 쉬워집니다.
  • 단점
    • build_runner를 실행해야 하며, 자동 생성된. g.dart 파일을 직접 디버깅하거나 분석하기 어렵습니다.
    • 처음 접한 팀원은 "코드가 없는데 어디서 호출되는 거지?"라고 생각하며 당황할 수 있습니다.

 

오늘은 서버와 통신하는 방법을 공부했습니다.

20000~

공감 누르지 마세요!

'Flutter' 카테고리의 다른 글

[Flutter] 라이센스 중요성  (0) 2025.02.13
[Flutter] Flutter Navigation  (0) 2025.01.28
[Flutter] pubspec.yaml  (0) 2025.01.27
[Flutter] Flutter의 Widget Lifecycle  (0) 2025.01.19
[Flutter] Stateful Widget vs Stateless Widget 차이  (0) 2025.01.19