PromleeBlog
sitemapaboutMe

posting thumbnail
Dio와 Retrofit을 사용하여 API 통신하기 (Flutter)
Communicate API using Dio and Retrofit on Flutter

📅

🚀

들어가기 전에🔗

이 포스팅은 Flutter 3.24 버전 기준으로 작성되었습니다.
앱, 또는 웹 프론트엔드를 개발하다 보면 백엔드, 혹은 오픈 API를 사용해야 할 때가 있습니다. 이때 API를 호출하기 위해서는 HTTP 통신을 해야 하죠. Flutter에서는 Dio와 Retrofit을 사용하여 API 통신을 할 수 있습니다.
물론 HTTP 패키지를 사용하여 API 통신을 할 수 있지만, Dio와 Retrofit을 사용하면 더 편리하게 API를 호출할 수 있습니다. 특히 Retrofit은 타입 정의를 통해 API 응답을 쉽게 처리할 수 있어, 개발 생산성을 높일 수 있습니다.
이번 글에서는 Flutter에서 Dio와 Retrofit을 사용하여 API 통신하는 방법과 타입 정의의 중요성에 대해 알아보겠습니다.

🚀

1. 패키지 설치🔗

Retrofit을 사용하기 위해서는 Dio와 Retrofit 패키지등 4개의 패키지를 설치해야 합니다. 또한 3가지 패키지는 개발 시에만 사용하므로 dev_dependencies에 추가합니다.
아래는 패키지를 설치하는 명령어입니다.
dart pub add json_annotation dio retrofit
dart pub add --dev build_runner retrofit_generator json_serializable
pubspec.yaml 파일에 아래와 같이 추가되어 있는지 확인합니다.(버전은 작성 시점 기준입니다.)
pubspec.yaml
dependencies:
	retrofit: ^4.4.2
	dio: ^5.7.0
	json_annotation: ^4.9.0
 
dev_dependencies:
	build_runner: ^2.4.13
	retrofit_generator: ^9.1.5
	json_serializable: ^6.9.0
각 패키지의 역할은 아래와 같습니다.

🚀

2. 요청 코드 작성🔗

예시 API 요청을 위한 모델 클래스와 인터페이스를 작성합니다. API는 더미 데이터를 제공해주는 JSONPlaceholder를 사용해 보겠습니다.
가장 기본적으로, 파라미터가 없는 GET 요청을 보내고 응답을 받아오는 예시를 작성해 보겠습니다. 위 사이트에서 확인했을 때 https://jsonplaceholder.typicode.com/users API를 호출하면 아래와 같은 응답을 받을 수 있습니다.
[
	{
		"id": 1,
		"name": "Leanne Graham",
		"username": "Bret",
		"email": "Sincere@april.biz",
		"address": {
			"street": "Kulas Light",
			"suite": "Apt. 556",
			"city": "Gwenborough",
			"zipcode": "92998-3874",
			"geo": {
				"lat": "-37.3159",
				"lng": "81.1496"
			}
		},
		"phone": "1-770-736-8031 x56442",
		"website": "hildegard.org",
		"company": {
			"name": "Romaguera-Crona",
			"catchPhrase": "Multi-layered client-server neural-net",
			"bs": "harness real-time e-markets"
		}
	},
	// ...
]
위 응답을 기반으로 코드를 작성해 보겠습니다. 필자는 /lib/model 폴더에 user_dto.dart, user.dart 파일을 생성했습니다.

2.1 API 요청을 위한 모델 클래스 작성🔗

API 요청을 위한 모델 클래스를 작성합니다. 아래는 예시 코드입니다.
lib/model/user_dto.dart
import 'package:json_annotation/json_annotation.dart';
part 'user_dto.g.dart';
 
@JsonSerializable()
class UserDto {
	final int id;
	final String name;
	final String username;
	final String email;
	final Address address;
	final String phone;
	final String website;
	final Company company;
 
	UserDto({
		required this.id,
		required this.name,
		required this.username,
		required this.email,
		required this.address,
		required this.phone,
		required this.website,
		required this.company,
	});
 
	factory UserDto.fromJson(Map<String, dynamic> json) => _$UserDtoFromJson(json);
	Map<String, dynamic> toJson() => _$UserDtoToJson(this);
} 
 
@JsonSerializable()
class Address {
	final String street;
	final String suite;
	final String city;
	final String zipcode;
	final Geo geo;
 
	Address({
		required this.street,
		required this.suite,
		required this.city,
		required this.zipcode,
		required this.geo,
	});
 
	factory Address.fromJson(Map<String, dynamic> json) => _$AddressFromJson(json);
	Map<String, dynamic> toJson() => _$AddressToJson(this);
}
 
@JsonSerializable()
class Geo {
	final String lat;
	final String lng;
 
	Geo({
		required this.lat,
		required this.lng,
	});
 
	factory Geo.fromJson(Map<String, dynamic> json) => _$GeoFromJson(json);
	Map<String, dynamic> toJson() => _$GeoToJson(this);
}
 
@JsonSerializable()
class Company {
	final String name;
	final String catchPhrase;
	final String bs;
 
	Company({
		required this.name,
		required this.catchPhrase,
		required this.bs,
	});
 
	factory Company.fromJson(Map<String, dynamic> json) => _$CompanyFromJson(json);
	Map<String, dynamic> toJson() => _$CompanyToJson(this);
}

2.2 API 요청을 위한 인터페이스 작성🔗

lib/model/user.dart
import 'package:dio/dio.dart';
import 'package:retrofit/retrofit.dart';
import 'user_dto.dart';
 
part 'user.g.dart'; // 생성될 파일
 
@RestApi(baseUrl: "https://jsonplaceholder.typicode.com") // base url
abstract class UserApi {
	factory UserApi(Dio dio, {String baseUrl}) = _UserApi;
 
	@GET("/users") // 요청할 API
	Future<List<UserDto>> getUsers(); // 요청할 API의 응답을 받을 모델 클래스
}

🚀

3. 코드 생성🔗

위 코드를 작성 했을 때 IDE에서 오류(빨간 줄)가 발생할 수 있습니다. 이는 코드 생성이 필요하다는 의미입니다.
코드 생성을 위해 루트 프로젝트 폴더에서 아래 명령어를 실행합니다.
dart run build_runner build
해당 폴더에 다음과 같은 파일이 생성되었는지 확인합니다.
lib/model/user_dto.g.dart
lib/model/user.g.dart
이제 오류가 사라졌다면, API를 호출할 준비가 되었습니다.

🚀

4. API 호출🔗

이제 API를 호출할 수 있습니다. 적당한 곳에 아래 함수를 작성 후 호출합니다.
import 'dart:developer';
 
import 'package:dio/dio.dart';
import 'package:retrofit/retrofit.dart';
import 'package:{project명}/model/user/user.dart'; // 작성한 프로젝트의 경로에 맞게 수정해주세요.
 
void runApiExample() {
  final dio = Dio();
  final client = GetUserApi(dio);
 
  client.getUsers().then((userList) => {
        for (final user in userList) {log(user.toJson().toString())}
      });
}
 
위와 같이 코드를 작성하면, API를 호출할 수 있습니다.
[log] {id: 1, name: Leanne Graham, username: Bret, email: Sincere@april.biz, address: Instance of 'Address', phone: 1-770-736-8031 x56442, website: hildegard.org, company: Instance of 'Company'}
[log] {id: 2, name: Ervin Howell, username: Antonette, email: Shanna@melissa.tv, address: Instance of 'Address', phone: 010-692-6593 x09125, website: anastasia.net, company: Instance of 'Company'}
[log] {id: 3, name: Clementine Bauch, username: Samantha, email: Nathan@yesenia.net, address: Instance of 'Address', phone: 1-463-123-4447, website: ramiro.info, company: Instance of 'Company'}
[log] {id: 4, name: Patricia Lebsack, username: Karianne, email: Julianne.OConner@kory.org, address: Instance of 'Address', phone: 493-170-9623 x156, website: kale.biz, company: Instance of 'Company'}
[log] {id: 5, name: Chelsey Dietrich, username: Kamren, email: Lucio_Hettinger@annie.ca, address: Instance of 'Address', phone: (254)954-1289, website: demarco.info, company: Instance of 'Company'}
[log] {id: 6, name: Mrs. Dennis Schulist, username: Leopoldo_Corkery, email: Karley_Dach@jasper.info, address: Instance of 'Address', phone: 1-477-935-8478 x6430, website: ola.org, company: Instance of 'Company'}
[log] {id: 7, name: Kurtis Weissnat, username: Elwyn.Skiles, email: Telly.Hoeger@billy.biz, address: Instance of 'Address', phone: 210.067.6132, website: elvis.io, company: Instance of 'Company'}
[log] {id: 8, name: Nicholas Runolfsdottir V, username: Maxime_Nienow, email: Sherwood@rosamond.me, address: Instance of 'Address', phone: 586.493.6943 x140, website: jacynthe.com, company: Instance of 'Company'}
[log] {id: 9, name: Glenna Reichert, username: Delphine, email: Chaim_McDermott@dana.io, address: Instance of 'Address', phone: (775)976-6794 x41206, website: conrad.com, company: Instance of 'Company'}
[log] {id: 10, name: Clementina DuBuque, username: Moriah.Stanton, email: Rey.Padberg@karina.biz, address: Instance of 'Address', phone: 024-648-3804, website: ambrose.net, company: Instance of 'Company'}

🚀

5. 기타 추가 설정 방법🔗

5.1 API 요청 시 파라미터 전달🔗

@GET('/users/{id}')
Future<UserDto> getUserByPathId(@Path() int id); // 파라미터 전달

5.2 API 요청 시 쿼리 파라미터 전달🔗

@GET('/users')
Future<List<UserDto>> getUserByQueryId(
	@Query('id') int id,
); // 쿼리 파라미터 전달

5.3 API 요청 시 헤더 전달🔗

고정 헤더를 전달할 때는 아래와 같이 작성합니다.
@GET('/users')
@Headers(<String, dynamic>{
	'Content-Type': 'application/json',
	'Authorization': 'Bearer {token}',
})
Future<List<UserDto>> getUsers(); // 헤더 전달
헤더를 동적으로 전달할 때는 아래와 같이 작성합니다.
@GET('/users')
Future<List<UserDto>> getUsers(
	@Header('Content-Type') String content_type, 
	@Header('Authorization') String authorization,
); // 헤더 전달

5.4 API 요청 시 바디 전달🔗

@POST('/users')
Future<UserDto> createUser(@Body() UserDto user); // 바디 전달

🚀

문제 발생 및 해결🔗

1. Class 명 변경 시🔗

요청 인터페이스의 Class 명을 변경했을 때, 코드 생성 후에도 오류가 발생할 수 있다
이 때는 생성된 d.dart 파일을 삭제 후 다시 생성하면 해결된다.
rm lib/model/user/user.g.dart
dart run build_runner build

🚀

결론🔗

이번 글에서는 Flutter에서 Dio와 Retrofit을 사용하여 API 통신하는 방법과 타입 정의의 중요성에 대해 알아보았습니다. 타입 정의를 통해 API 응답을 쉽게 처리할 수 있어, 개발 생산성을 높일 수 있습니다.

더 생각해 보기🔗

API 응답을 받을 모델 클래스의 필드가 많아질수록, 코드가 길어져 가독성이 떨어질 수 있다. 이 때는 json_to_dart와 같은 툴을 사용하여 모델 클래스를 생성하면 편리하다.

참고🔗