[Flutter] Retrofit-Restaurant Repository 구현하기

겨레·2024년 7월 18일
0
post-thumbnail

📍 Retrofit

REST API를 위한 HTTP 클라이언트를 생성하는 라이브러리이다.
Retrofit은 간편하게 사용 가능하고 다양한 http 요청, 옵션, 기능을 제공한다.


① Retrofit 패키지 다운로드 및 설치

dependencies:
  retrofit: '>=4.0.0 <5.0.0'
  logger: any  #for logging purpose
  json_annotation: ^4.8.1

dev_dependencies:
  retrofit_generator: '>=7.0.0 <8.0.0'   // required dart >=2.19
  build_runner: '>=2.3.0 <4.0.0'
  json_serializable: ^6.6.2

Retrofit을 어떤 위치에 사용하게 될지 일단 알아보고 본격적으로 시작해보자!

restaurant_screen.dart , restaurant_detail_screen.dart 코드를 보면
Future를 반환해주는 함수를 입력해 주고 있음.

(1) RestaurantScreen
RestaurantScreen의 경우에는 paginateRestaurant이라는 함수 안에 API 요청을 하고 있음.
그리고 이 API 요청을 return resp.data['data']; 이렇게 반환해 주면,
(아래로 쭉 내려가서) 그 값을 final pItem = RestaurantModel.fromJson(item); 이렇게 모델로 변환하고 있음!

(2) RestaurantDetailScreen
RestaurantDetailScreen도 역시 getRestaurantDetail 함수 안에 API 요청을 하고 있음.
API 요청을 return resp.data; 반환하고, final item = RestaurantDetailModel.fromJson(snapshot.data!);
이렇게 모델로 변경을 해 주고 있는 걸 알 수 있음!


이런 과정... 반복적임!

그래서 모델만 fromJson, toJson으로 만들어서 자동화하지 말고,
아예 API 요청부터 모델로 만드는 과정까지 전부 다 자동화시켜 버리자!

그래서 Retrofit을 사용하는 것!


  • RestaurantScreen의 URL
    final resp = await dio.get(
      'http://$ip/restaurant',
      options: Options(
        headers: {
          'authorization': 'Bearer $accessToken',
        },
      ),
    );

  • RestaurantDetailScreen의 URL
restaurant_detail_screen.dart

  final resp = await dio.get(
      'http://$ip/restaurant/$id',
      options: Options(
        headers: {
          'authorization': 'Bearer $accessToken',
        },
      ),
    );

비교해보면 사실상 패턴은 똑같음!
요청 → 응답 → 모델로 변경하고 무조건 해당 모델의 fromJson이라는 constructor를 사용해서 변경

차이점이라고 한다면 반환 값과 요청 URL, 그리고 아래 쪽에 전환되는 모델이 다름.
하지만 과정의 순서 그러니까 어떤 식으로 변환되는지는 똑같음!

이렇게 다른 값들만 추가로 정의해주면 자동으로 모델까지 변경되는 과정을
Retrofit을 통해 구현
할 수 있음!


① restaurant/repository/restaurant_repository.dart 폴더 및 파일 생성

retrofit 대신 repository라고 만들기.
정확히 말하면 아키텍처에서 말하는 repository와는 조금 부족한 느낌이 있는데,
일단은 repository 폴더로 만들고 시작하자...!

② 클래스를 만들고 @RestApi( ) 어노테이션 달기
code generation할거니까 그냥 class를 정의해주면 됨!

import 'package:retrofit/http.dart';

()
class RestaurantRepository {}

③ part 임포트 작성

code generation할거니까!!!
그러면 code generation 준비 완료~

그리고 터미널에 flutter pub run build_runner build watch 해주기.

그러면 restaurant_repository.g.dart 파일이 생긴 걸 볼 수 있음!


④ RestaurantRepository 클래스 abstract로 선언하기

무조건 abstract로 선언해줘야 한다.
왜? 👉 인스턴스화가 안되게!

import 'package:retrofit/http.dart';

part 'restaurant_repository.g.dart';

()
abstract class RestaurantRepository {}




⑤ factory constructor 생성하기

JsonSerializable에서 했던 거랑 똑같은거임!
이렇게 구조를 잡는 건 Retrofit 스탠다드에 맞추기 위한 거임!
그냥 외우면 되는 것...

( )에는 Dio를 받음.
Dio는 지금까지 사용했던 HTTP 요청 패키지!
그리고 이걸 외부에서 생성해서 넣어주자!
여러 개의 Repository에서 같은 Dio 인스턴스를 공유해야 하는 이유는?


그리고 ( ) 안에 {String baseUrl}도 넣어서 URL도 받아옴.

baseUrl의 역할은?
현재 Repository 안에서 공통되는 부분을 모두 baseUrl에 넣을 거임.

그리고 = 하고 _RestaurantRepository; 넣어주기 _



⑥ 이어서 2가지 API 요청 구현하기

지금까지 구현한 레스토랑 관련 API 요청은 아래 두 가지!

  • (1) RestaurantScreen
    /restaurant 해서 pagination 하는 거
  Future<List> paginateRestaurant() async {
    final dio = Dio();

    final accessToken = await storage.read(key: ACCESS_TOKEN_KEY);
    final resp = await dio.get(
      'http://$ip/restaurant',
      options: Options(
        headers: {
          'authorization': 'Bearer $accessToken',
        },
      ),
    );

  • (2) RestaurantDetailModel
    /restaurant/$id 특정 레스토랑의 상세 정보로 가져오는 거
Future<Map<String, dynamic>> getRestaurantDetail() async {
    final dio = Dio();
    final accessToken = await storage.read(key: ACCESS_TOKEN_KEY);

    final resp = await dio.get(
      'http://$ip/restaurant/$id',
      options: Options(
        headers: {
          'authorization': 'Bearer $accessToken',
        },
      ),
    );

    return resp.data;
  }

이렇게 인데, 이 두 가지를 abstract class RestaurantRepository에 구현할 거임!


⑥ 이어서 2가지 API 요청 구현하기

(1) RestaurantScreen에서 pagination하는 거는
그냥 paginate라고 함수 이름을 부르겠음.

근데 이건 좀 특별한 경우임... 왜냐면 pagination을 일반화하기 위해
일부러 paginate라고 부르는 거임!

이어서...

(2) RestaurantDetailModel 같은 경우에는
나머지 요청들(요청 메소드: get/post/delete)은 앞에 붙여주고,
실제 어떤 요청인지 붙여서 정의해주면 됨!


하지만 이렇게 함수를 정했다고 해서 바로 API 요청이 간단히 되진 않겠쥬?

자.. 그럼 어떻게 이걸 바꿔야할까?
어노테이션을 넣어주면 된다. 👉 @ + 요청 타입 (단, 대문자로!!!)

pagination 요청은 get 요청이었으니까 @GET 해주면...
@GET(path) 이렇게 자동 완성이 된다.
path는 어디에 요청할래? 라는 의미이다.

baseUrl + path 가 되면 된다.

나는 http://$ip/restaurant 까지만 일반화 할 거임!
즉, baseUrl = http://$ip/restaurant 까지인 거...

그 뒤에 나머지 공통되지 않은 부분만 따로 입력하면 된다.



⑦ 각 함수의 { } 바디 삭제하기

이렇게 만들어둔 2개의 함수를 어떻게 자동으로 API 요청으로 만들 수 있을까?

먼저 { } 이 바디를 삭제하자!

원래 함수라면 { } 바디를 넣는 게 맞다. 그런데 왜 삭제하는 걸까...?
👉 abstract 함수로 선언을 했기 때문에!
그러면 어떤 함수들이 있어야 되는지만 정의하면 됨!!!

그렇다면 이제 입력해줘야 할 것들은???

  • 각 함수에 어떤 값이 들어가야 하는지
  • 어떤 값이 반환되는지



⑧ getRestaurantDetail 만들기
paginate( ) 쪽은 잠시 주석 처리하고, getRestaurantDetail( )부터 만들어보자!

getRestaurantDetail은 어떤 값을 반환하고 있을까?
RestaurantDetailScreen으로 가서 확인해보면,
snapshot.data에 해당되는 데이터를 가지고 있는데
실제로 어떤 모델로 매팽되어야 하는지만 보면 된다!

final item = RestaurantDetailModel.fromJson(snapshot.data!);
확인해보면 RestaurantDetailModel로 매핑이 되면 된다.

그리고 postman에서 확인해 보면, 응답이 오는 구조
그러니까 RestaurantDetailModel의 구조랑 완전히 똑같은 걸 볼 수 있음.
속성도 차이 없이 똑같음.

그렇기 때문에 RestaurantDetailScreen에 가서
snapshot.data로 return resp.data; 데이터를 받아서
RestaurantDetailModel의 .fromJson 안에 넣어주면
한 번에 RestaurantDetailModel로 전환됨

Retrofit에서는 이게 중요함!!!
실제로 응답받는 형태와 똑같은 형태의 class를 반환 값으로 넣어줘야 함!
그러면 자동으로 데이터의 json 값이 자동으로 매핑되어
class의 인스턴스가 된다.



restaurant_repository.dart으로 가서 한 번 확인해보자....
getRestaurantDetail( ) 앞에 RestaurantDetailModel을 넣어보자.
다만, Future꼭 달아줘야 함.
왜냐면 API 요청이라 외부에서 오니깐!

그런데 getRestaurantDetail을 요청 넣을 때, 이 {id} id 변수를 요청할 때마다 넣어줘야 함.

getRestaurantDetail에 네임드 파라미터를 넣어주자!
이렇게 해 주면, String id라는 Path 안에 있는 변수('/{id}')를 자동으로
@Path() required String id 이 안에다 파라미터로 넣은 값('/{id})으로 대체
해 줄 수 있음.

그리고 어떤 이름의 변수를 대체할 건지는 실제 파라미터에 입력한

getRestaurantDetail({
    () required String id, 
  });'

변수의 이름(String id라는 id)과 똑같은 변수로 '/{id}'가 대체된다.

그리고 저장하면 아래 사진처럼 restaurant_repository.dart 파일이
잘 생겨난 것을 확인할 수 있다.

그리고 restaurant_repository.dart 파일을 확인해 보면
자동으로 getRestaurantDetail 함수가 만들어진 것을 볼 수 있다.

restaurant_detail_screen.dart으로 가서 드래그 부분 다 지우기.
(그럼 Dio만 남음, 엑세스 토큰도 넣지 않은 상태가 됨 -> 이건 나중에 진행!)

지운 부분에 restaurant_repository의 인스턴스 만들어주기

 final repository =
        RestaurantRepository(dio, baseUrl: 'http://$ip/restaurant');

    return repository;


그럼 이제 Future하고서 <Map<String, dynamic>>이 들어가는 게 아니라
restaurant_repository 안에서 Future< RestaurantDetailModel >의 RestaurantDetailModel 값이
반환해주는 RestaurantDetailModel이 된다.

그러면 또 그 아래 child: FutureBuilder<Map<String, dynamic>>에도
RestaurantDetailModel
이 들어가야 함!

그러면 또 아래 AsyncSnapshot 타입도 RestaurantDetailModel로 바꿔줘야 함!

이렇게 되는 거겠지~ ㅎㅎㅎ

그러면 더 이상 final item = RestaurantDetailModel.fromJson(snapshot.data!);
이 아이템은 필요없게
되니까 코드 삭제해버린다.

왜? 👉 snapshot.data에서 바로 매핑된 모델이 나오기 때문에!

그럼 오류 나는 model: item과 products: item.products를
model: snapshot.data!
products: snapshot.data!.products,
이렇게 수정해주면 되겠지?

그러면 또 return repository;도 오류가 난다.

이제 이게 아니라 return repository.getRestaurantDetail(id: id);가 맞게 된다.

그럼 repository 안에 getRestaurantDetail을 실행하게 되면??

  • 현재 상태
    RestaurantDetailModel이라는 모델로 매핑되어서 결과값이 응답올 거라고 정의해 둔 상태!
    그리고 실제로 이렇게 구현되는 과정은 이미 생성된 restaurant_repository.g.dart 코드 안에서 로직이 구현되어 있음.


    앱을 실행했더니 아래 사진처럼 무한 로딩이 된다;;

왜일까??
RestaurantDetailScreen을 보면 이렇게 데이터가 없다고 해뒀기 때문이다.
그래서 data가 없다고 걸려서 무한 로딩이되는 것!

어떤 에러인지 찍어서 확인해보자...

401 에러가 났다고 나오는데 이건 토큰이 잘못됐을 때 나는 에러임!!!

임시방편으로 실제 작용되는 토큰을 발급받아서
한번 RestaurantRepository에 넣어보자.

postman에서 로그인 send 했을 때 나온 엑세스 토큰을 복사해와서
강제로 헤더에 추가하기

강제로 헤더 추가하는 방법

({
    'authorization': 'Bearer 토큰값'
   })

그런데 @Headers라는 class가 Dio, Retrofit에 중복으로 선언되어서 빨간 줄....
둘 중 한 곳에서 가려주기


아니 근데 왜 아직 나는 오류나지? ㅠㅠ

profile
호떡 신문지에서 개발자로 환생

0개의 댓글