본 글은 [Do it! 깡샘의 플러터&다트 프로그래밍] 의 내용을 발췌한 것입니다.
책의 모든 내용을 저자 직강으로 진행한 강의는 ssamz.com 에서 들으실 수 있습니다.
앞에서 서버와 HTTP 통신할 때 http 패키지를 이용하는 방법을 알아봤는데, 만약 더 많은 기능을 제공하는 패키지가 필요하다면 dio가 있습니다. dio를 사용하는 구체적인 방법은 pub.dev/packages/dio에서 확인할 수 있으며 여기서는 기본 사용법만 소개하겠습니다.
우선 dio 패키지를 이용하려면 pubspec.yaml 파일에 다음처럼 추가합니다.
dependencies:
dio: ^4.0.4
간단하게 GET 방식으로 서버에 요청하는 코드는 다음과 같습니다. Dio 객체의 get() 함수를 호출하여 서버에 요청하며 결과는 Response 타입의 객체입니다. Response의 statusCode로 상태 코드를 얻고 서버에서 전송한 데이터는 data 속성으로 얻습니다.
try {
var response = await Dio().get('https://reqres.in/api/users?page=2');
if (response.statusCode == 200) {
String result = response.data.toString();
print("result... $result");
}
} catch (e) {
print(e);
}
예에서는 GET 방식으로 요청을 보내면서 서버에 전송할 데이터를 URL 뒤에 ?로 추가했습니다. 이렇게 작성해도 되지만 서버에 전송할 데이터를 다음처럼 queryParameters 매개변수에 Map 객체로 지정해도 됩니다.
var response = await Dio().get('https://reqres.in/api/users', queryParameters: {'page':2});
GET 이외에 POST, PUT, DELETE 방식으로 요청하는 post(), put(), delete() 함수도 제공합니다. 다음은 POST 방식으로 서버에 요청하는 예입니다.
var response = await Dio().post(
'https://reqres.in/api/users',
data: {
"name": "kkang",
"job": "instructor"
}
);
request() 함수로 요청하기
서버에 요청할 때 get(), post(), put(), delete() 함수를 이용해도 되지만, request() 함수를 이용해 어떤 방식으로 요청할지 options 매개변수로 지정할 수도 있습니다.
var response = await Dio().request(
'https://reqres.in/api/users',
data: {
"name": "kkang",
"job": "instructor"
},
options: Options(method: 'POST')
);
BaseOptions로 Dio 속성 지정하기
Dio 객체를 생성할 때 생성자의 매개변수로 BaseOptions 객체를 지정하여 다양하게 설정할 수 있습니다. connectTimeout, receiveTimeout 등 타임 아웃을 설정할 수 있으며 baseUrl로 서버 URL의 공통 부분을 명시해 놓으면 이후 실제 서버에 요청할 때는 path 부분만 지정할 수 있습니다.
var dio = Dio(BaseOptions(
baseUrl: "https://reqres.in/api/",
connectTimeout: 5000,
receiveTimeout: 5000,
headers: {
HttpHeaders.contentTypeHeader: 'application/json',
HttpHeaders.acceptHeader: 'application/json'
},
));
var response = await dio.get('users?page=2');
동시 요청하기
dio에서는 여러 요청을 List 타입으로 지정하여 동시에 처리할 수 있습니다. 다음 코드에서는 Future.wait()를 이용해 모든 요청이 끝날 때까지 기다립니다. Future에 관해서는 이후에 자세히 살펴보겠습니다. 요청이 여러 개인 만큼 결괏값도 여러 개입니다. 따라서 결과는 List<Response> 타입으로 나옵니다.
List<Response<dynamic>> response =
await Future.wait([dio.get('https://reqres.in/api/users?page=1'),
dio.get('https://reqres.in/api/users?page=2')]);
response.forEach((element) {
if (element.statusCode == 200) {
String result = element.data.toString();
print("result... $result");
}
});
파일 전송하기 — MultifileUpload
dio를 이용해 파일을 전송하는 기능을 구현할 수 있습니다. 파일을 전송하려면 가장 먼저 파일을 MultipartFile 객체로 준비해야 합니다. MultipartFile 객체 하나가 전송할 파일 하나를 의미하며, MultipartFile 객체 여러 개를 List에 담아 여러 파일을 한꺼번에 전송할 수도 있습니다.
MultipartFile에는 전송할 파일 정보가 담기는데 파일 경로일 수도 있고 파일을 읽어 들인 바이트 데이터일 수도 있습니다. fromFile() 생성자로 전송할 파일을 지정해 MultipartFile을 생성합니다. 다음 코드에서는 ‘./test.txt’가 전송할 파일이며 filename 매개변수에 지정한 ‘upload.txt’는 서버에 전송할 파일 이름입니다.
MultipartFile.fromFile('./test.txt',filename: 'upload.txt')
만약 파일의 데이터를 지정해 MultipartFile을 생성하려면 다음처럼 fromBytes() 생성자를 이용합니다.
MultipartFile multipartFile = new MultipartFile.fromBytes(
imageData, // 파일 데이터
filename: 'load_image',
contentType: MediaType("image", "jpg"),
);
MultipartFile을 생성할 때 contentType 매개변수에 전송할 파일의 타입을 지정할 수 있습니다. 이렇게 준비한 MultipartFile 객체를 전송하려면 FormData 객체에 담아야 합니다. FormData는 MultipartFile뿐만 아니라 서버에 전송할 여러 가지 데이터를 표현하는 객체입니다.
FormData의 fromMap() 생성자 매개변수에 서버에 전송할 데이터를 Map 객체로 지정합니다. 다음 코드에서는 MultipartFile 이외에 ‘name’:’kkang’이라는 데이터를 함께 전송합니다. 파일을 전송하려면 POST 방식을 이용해야 하며 post() 함수의 data 매개변수에 준비한 FormData 객체를 지정합니다.
var formData = FormData.fromMap({
'name': 'kkang',
'file': await MultipartFile.fromFile('./test.txt',filename: 'upload.txt')
});
var response = await dio.post('/info', data: formData);
요청이나 응답 가로채기 — Interceptor
인터셉터interceptor는 dio에서 제공하는 유용한 기법의 하나입니다. 인터셉터는 데이터를 가로챈다는 의미입니다. Dio의 get(), post() 함수 등으로 요청하면 서버는 요청에 응답해 결과를 앱에 전달합니다. 그런데 인터셉터를 이용하면 요청이나 응답을 가로챌 수 있습니다.
이 기능을 이용하면 서버와 연동할 때마다 똑같이 실행할 코드를 반복하지 않고 인터셉터에 작성할 수 있습니다. 대표적인 예는 로그를 남기는 경우입니다. 서버에 요청할 때 로그를 남기거나 서버의 응답 상태를 로그로 남겨야 한다면 해당 코드를 인터셉터에 작성하고 get()이나 post() 함수로 서버와 연동할 때 실행합니다.
인터셉터를 이용하려면 Interceptor를 상속받는 클래스를 작성하거나 이미 만들어진 Interceptors Wrapper 클래스를 이용할 수 있습니다. 먼저 Interceptor를 상속받아 클래스를 작성하는 방법은 다음과 같습니다.
class MyInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
print('request... ${options.method} , ${options.path}');
print('request data : ${options.data}');
super.onRequest(options, handler); // 서버 요청
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
print('response... ${response.statusCode}, ${response.requestOptions.path}');
print('response data : ${response.data}');
super.onResponse(response, handler); // 결괏값 반환
}
@override
void onError(DioError err, ErrorInterceptorHandler handler) {
super.onError(err, handler);
print('error... ${err.response?.statusCode}, ${err.requestOptions.path}');
}
}
Interceptor를 상속받은 클래스에 onRequest(), onResponse(), onError() 함수를 재정의합니다. onRequest()는 서버 요청을 가로채는 함수이며, onResponse()는 서버 응답을 가로채는 함수입니다. 또한 onError()는 서버 연동에 오류가 발생할 때 실행되는 함수입니다. 이 함수를 모두 재정의할 필요는 없으며 필요한 함수만 작성하면 됩니다.
onRequest() 함수의 첫 번째 매개변수가 RequestOptions인데 이 객체로 요청 정보를 전달해 줍니다. RequestOptions의 method 속성으로 요청 방식을 확인할 수 있으며, path로 요청 서버 URL을 확인할 수 있습니다. 또한 data 속성으로 서버에 전송하는 데이터를 확인할 수 있습니다.
onResponse() 함수의 첫 번째 매개변수가 Response 객체인데 여기에 응답 정보가 있습니다. statusCode 속성으로 서버 응답 코드를 확인할 수 있으며, data 속성으로 서버에서 전달한 데이터를 얻을 수 있습니다.
onRequest() 함수에서 서버에 요청하려면 super.onRequest() 함수를 호출합니다. 만약 이 함수를 호출하지 않으면 요청은 발생하지 않습니다. 또한 onResponse() 함수에서도 super.onResponse() 함수를 호출해야 실제 요청한 곳에 서버 응답이 전달됩니다.
이렇게 작성한 Interceptor 클래스의 객체를 서버에 요청하기 전에 dio에 설정해 줍니다. dio 객체의 interceptors.add() 함수로 인터셉터 객체를 지정하며, 원한다면 여러 개의 인터셉터 객체를 지정할 수도 있습니다.
var dio = Dio();
dio.interceptors.add(MyInterceptor());
await dio.post(
'https://reqres.in/api/users',
data: {
"name": "kkang",
"job": "instructor"
});
Interceptor를 상속받은 클래스를 이용하는 것이 기본이지만, 편의를 고려해 Interceptors Wrapper 클래스를 이용할 수도 있습니다. InterceptorsWrapper를 이용한다면 개발자 클래스를 만들지 않아도 되며 생성자의 onRequest, onResponse 매개변수에 함수를 등록하면 됩니다.
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
print('request... ${options.method} , ${options.path}');
print('request data : ${options.data}');
handler.next(options); // 서버 요청
},
onResponse: (response, handler) {
print('response... ${response.statusCode}, ${response.requestOptions.path}');
print('response data : ${response.data}');
handler.next(response); // 결괏값 반환
}
));
앞 예에서는 onRequest() 함수에서 handler.next(options) 구문으로 서버에 요청하는데 때로는 서버에 요청하지 않고 onRequest() 함수에서 임의의 데이터를 구성해 서버에서 응답한 것처럼 처리할 수도 있습니다.
다음 코드에서 onRequest() 함수가 호출되었다는 것은 어디선가 서버 요청이 발생했다는 의미입니다. 그런데 onRequest() 함수에서 handler.next() 함수를 호출하지 않고 handler.resolve() 함수로 임의의 데이터를 만들어 서버에서 응답한 것처럼 처리할 수 있습니다. 이렇게 하면 resolve() 함수에 명시한 Response 객체가 get()이나 post() 등의 함수를 호출한 곳에 전달됩니다.
onRequest: (options, handler) {
print('request... ${options.method} , ${options.path}');
print('request data : ${options.data}');
// handler.next(options); // 서버 요청
handler.resolve(Response(requestOptions: options, data: {"hello":"world"}));
},
또한 onRequest() 함수에서 요청을 대기 상태로 만들 수 있습니다. 서버 요청을 취소한 것은 아니지만 먼저 처리해야 할 일이 있을때 대기 상태로 만들었다가 다시 요청할 수 있습니다.
다음 코드를 보면 onRequest()에 handler.next() 구문으로 서버에 요청하는데, handler.next() 이전에 dio.lock() 함수를 이용해 요청을 대기 상태로 만들었습니다. 따라서 실제 서버 요청은 발생하지 않고 대기합니다. 그러다가 dio.unlock() 함수를 호출하는 순간 대기 상태에 있던 요청이 실행됩니다. 예에서는 3초 후에 unlock() 함수를 호출한 예입니다. 결국 서버에 요청은 3초 후에 발생합니다.
onRequest: (options, handler) {
dio.lock();
handler.next(options);
Timer(Duration(seconds: 3),() {
dio.unlock();
});
},
책의 모든 내용을 저자 직강으로 진행한 강의는 ssamz.com 에서 들으실 수 있습니다.
'flutter' 카테고리의 다른 글
플러터 - Isolate (0) | 2023.03.13 |
---|---|
플러터 - Future, FutureBuilder (0) | 2023.03.13 |
플러터 - Navigation (0) | 2023.03.13 |
플러터 - Scaffold (0) | 2023.03.13 |
플러터 - ListView (0) | 2023.03.13 |