안녕하세요! 메로나입니다.
오늘은 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 상태 관리에 집중
- 도메인 규칙 적용 및 사용자 액션 처리
- Repository
- 아키텍처 원칙 준수
- 단일 책임 원칙을 지켜 데이터 변환 로직이 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 |