본문 바로가기
플러터(flutter)

플러터에서 Rest API로 Firebase Realtime Database 사용하기

by 기계공학 주인장 2023. 12. 23.
반응형

이번에는 플러터에서 Rest API로 Firebase의 Realtime Database를 사용하는 방법에 대해 알아보겠습니다.

 

굳이 Firebase의 Realtime Database을 사용하는 이유는 Rest API를 지원하기 때문입니다.

 

아래의 공식 문서를 참조하시길 바랍니다.

 

 

Firebase 데이터베이스 REST API

 

firebase.google.com

 

Firebase에서 프로젝트 세팅은 이미 끝난다는 전제에 시작하겠습니다.


플러터의 http 패키지 설치하기

플러터에서 Rest API를 사용하기 위해서는 http 패키지가 필요합니다.

 

 

http | Dart Package

A composable, multi-platform, Future-based API for HTTP requests.

pub.dev

 

위 사이트를 참조하여 다음과 같은 방법으로 업데이트가 필요합니다.

 

flutter pub add http

 

또는 pubspec.yaml 파일 안에 직접 적어 넣습니다.

 

dependencies:
  http: ^1.1.2

플러터에서 POST를 사용하여 데이터 보내기

먼저 다음과 같이 TextButton을 사용하여 간단한 UI를 만듭니다.

 

 

"send data" 버튼을 누를 경우 http의 POST를 사용해서 데이터를 Firebase Realtime Database에 보낼 예정입니다.

 

UI 파일 안에 다음과 같이 http 패키지를 넣습니다.

 

주의할 점은 as를 사용해서 패키지의 이름을 따로 지정해줬다는 것입니다.

 

(이것은 http 패키지의 공식 문서에도 있는 사용 권장 사항임)

 

import 'package:http/http.dart' as http;

 

그리고 다음과 같이 POST로 데이터를 보내는 함수를 작성합니다.

 

final _name = "testName";
final _quantity = "2";
final _category = "testCategory";

// 간단한 데이터를 json으로 변환하여 POST로 데이터를 보낸다.
void _saveItem() {
    // 참조 - https://firebase.google.com/docs/reference/rest/database?hl=ko
    final url = Uri.https(
      '자신의 Firebase Realtime Database 주소',
      'flutter-architecture-test.json',
    );
    http.post(
      url,
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode(
        {
          'name': _name,
          'quantity': _quantity,
          'category': _category,
        },
      ),
    );
}

 

Uri.https에서 사용되는 Firebase Realtime Database의 주소는 여기서 확인할 수 있습니다.

 

자신의 Firebase 프로젝트의 Realtime Database에 들어가면 아래의 부분을 복사해서 붙여 넣으면 됩니다.

 

단, 그대로 붙여 넣는 것이 아니라 앞부분의 https::// 부분과 제일 끝에 있는 /를 빼야 합니다.

 

예를 들면 다음과 같습니다.

 

flutter-test-123456-default-rtdb.asia-southeast1.firebasedatabase.app

 

 

그리고 버튼을 클릭했을 때 다음과 같이 _saveItem 함수를 호출하도록 하면

 

// 간단한 TextButton
Widget _simpleTextButton(Function buttonClicked) {
    return  Padding(
      padding: const EdgeInsets.all(12),
      child: TextButton(
        onPressed: () {
          buttonClicked();
        },
        child: const Text(
          "send data",
          style: TextStyle(fontSize: 24),
        ),
      ),
    );
}

@override
Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Firebase Realtime Database"),
      ),
      body: Center(
        child: Column(
          children: [
            _simpleTextButton(_saveItem)
          ],
        ),
      ),
    );
}

 

Realtime Database에 다음과 같이 기록되는 것을 확인할 수 있습니다.

 

 

참고로 데이터를 POST로 보내면 위 사진과 같이 고유 ID 밑에 저장한 데이터가 나오는데

 

해당 고유 ID는 특정 데이터를 직접 찾을 때 사용됩니다.

 

혹시 데이터가 송신되지 않는 경우

특별히 에러 로그가 나오지 않는데 데이터 송신이 안되면

 

애뮬레이터가 아니라 실제 기기에 설치해서 테스트해 보시길 바랍니다.


POST로 보낸 데이터가 올바르게 처리되었는지 확인하기

데이터가 제대로 Firebase Realtime Database에 전송되었는지 확인하는 작업도 중요합니다.

 

Flutter에서는 asyncawait를 사용하여 처리가 완료됐을 경우 해당 결과를 받아올 수 있습니다.

 

// 간단한 데이터를 json으로 변환하여 POST로 데이터를 보낸다.
void _saveItem() async {
    // 참조 - https://firebase.google.com/docs/reference/rest/database?hl=ko
    final url = Uri.https(
      dotenv.env['firebase.realtime.database.url']!,
      'flutter-architecture-test.json',
    );
    await http
        .post(
      url,
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode(
        {
          'name': _name,
          'quantity': _quantity,
          'category': _category,
        },
      ),
    )
    // then을 사용하여 await로 결과가 돌아오면 이후 처리를 하도록 한다.
    .then((response) {
      if (response.statusCode == 200) {
        if (!context.mounted) {
          // 이렇게 await 안에 context를 사용하기 위해선 context가 mounted 상태인지 확인해야한다
          // mount되지 않았다면 return을 상요해서 진행되지 않도록 하기
          return;
        }
        print(response.body);
      }
    });
}

 

송신 처리된 결과의 ResponseCode가 StatusCode로 돌아오기 때문에 이를 사용하여

 

정상적으로 처리됐는지 확인할 수 있다.

 

참고로 위 코드에선 firebase 주소를 env를 사용하여 참조했습니다.

자세한 사항은 이 포스팅을 봐주세요.

 

 

flutter_dotenv을 사용해서 local에 API Key 보관하기

안드로이드에서 API Key 같은 것을 Project 내에 보관해서 사용할 때는 local.properties에 값을 저장해서 사용하곤 했다. 하지만, 해당 방법을 flutter에서 사용하기 쉽지 않기 때문에 플러터의 flutter_dotenv

android-developer.tistory.com


플러터의 http의 GET을 사용하여 모든 데이터 가져오기

먼저 위에서 만든 UI에 하나의 버튼을 더 추가합니다.

 

 

 

클릭 이벤트를 정의하기 전에 가져온 데이터를 Wrapping 해야 하기 때문에 다음과 같이 클래스를 정의합니다.

 

class TestFirebaseRealTimeModel {
  const TestFirebaseRealTimeModel({
    required this.id,
    required this.name,
    required this.quantity,
    required this.category,
  });
  
  // firebase realtime database에 있는 ID를 여기에 넣어준다.
  final String id;
  final String name;
  final int quantity;
  final String category;
}

 

그리고 다음과 같이 변수와 Get Data 버튼을 클릭 시 Firebase Realtime Database에서 데이터를 가져오도록 합니다.

 

var getDataResult = "";
  List<TestFirebaseRealTimeModel> resultData = [];

  void _getData() async {
    final url = Uri.https(
      dotenv.env['firebase.realtime.database.url']!,
      'flutter-architecture-test.json',
    );
    http.get(url).then((response) {
      final responseData = jsonDecode(response.body);
      final List<TestFirebaseRealTimeModel> loadedItems = [];
      setState(() {
      // 가져온 데이터를 다른 변수에 담는다.
        for (final item in responseData.entries) {
          loadedItems.add(
            TestFirebaseRealTimeModel(
              id: item.key,
              name: item.value['name'],
              quantity: item.value['quantity'],
              category: item.value['category'],
            ),
          );
        }
        // 가져온 데이터를 새로운 변수에 옮겨서 데이터를 업데이트 한다.
        resultData = loadedItems;
        print("resultData: $resultData");
      });
    });
  }

 

저는 Get Data로 받은 데이터(위에서는 resultData)를 사용해서 ListView를 만들도록 했기 때문에

 

Get Data를 클릭한 결과는 다음과 같이 나옵니다.

 


Delete를 사용하여 데이터 삭제하기

이제 반대로 Delete를 사용하여 생성한 데이터를 삭제해보겠습니다.

 

간단하게 ListView에 있는 Item을 클릭하면 해당 데이터가 삭제되도록 해보겠습니다.

 

다음과 같은 _deleteItem 함수를 생성합니다.

 

void _deleteItem(String targetId) async {
    // 참조 - https://firebase.google.com/docs/reference/rest/database?hl=ko
    final url = Uri.https(
      dotenv.env['firebase.realtime.database.url']!,
      // http의 url 파라미터로 대상이 되는 id를 넣어줘야한다.
      'flutter-architecture-test/$targetId.json',
    );
    await http.delete(url).then(
      (response) {
        if (response.statusCode == 200) {
          showToast("targetId: $targetId 삭제됨");
          _getData();
        }
      },
    );
}

 

위 코드를 보면 이전과는 다르게 url에 대상이되는 targetId를 지정하는 것을 볼 수 있습니다.

 

그리고 다음과 같이 클릭 리스너를 가진 Widget을 설정하면됩니다.

 

_simpleVerticalTextList(
  clickListener: (index) {
    _deleteItem(resultData[index].id);
  },
  dataList: resultData,
)

플러터 프로젝트의 전체 코드

위 프로젝트의 전체 코드는 다음과 같습니다.

 

(조금 다른 부분이 있을지도 모르겠지만 전반적으로 과정 자체는 동일합니다.)

 

class FirebaseRealtimeDatabaseFunction extends StatefulWidget {
  const FirebaseRealtimeDatabaseFunction({super.key});

  @override
  State<FirebaseRealtimeDatabaseFunction> createState() => _FirebaseRealtimeDatabaseFunctionState();
}

class _FirebaseRealtimeDatabaseFunctionState extends State<FirebaseRealtimeDatabaseFunction> {
  final _name = "testName4";
  final _quantity = 4;
  final _category = "testCategory4";

  var getDataResult = "";
  List<TestFirebaseRealTimeModel> resultData = [];

  void _getData() async {
    final url = Uri.https(
      dotenv.env['firebase.realtime.database.url']!,
      'flutter-architecture-test.json',
    );
    http.get(url).then((response) {
      final responseData = jsonDecode(response.body);
      final List<TestFirebaseRealTimeModel> loadedItems = [];
      setState(() {
        for (final item in responseData.entries) {
          loadedItems.add(
            TestFirebaseRealTimeModel(
              id: item.key,
              name: item.value['name'],
              quantity: item.value['quantity'],
              category: item.value['category'],
            ),
          );
        }
        resultData = loadedItems;
        print("resultData: $resultData");
      });
    });
  }

  // 간단한 데이터를 json으로 변환하여 POST로 데이터를 보낸다.
  void _saveItem() async {
    // 참조 - https://firebase.google.com/docs/reference/rest/database?hl=ko
    final url = Uri.https(
      dotenv.env['firebase.realtime.database.url']!,
      'flutter-architecture-test.json',
    );
    await http
        .post(
      url,
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode(
        {
          'name': _name,
          'quantity': _quantity,
          'category': _category,
        },
      ),
    )
        .then((response) {
      if (response.statusCode == 200) {
        if (!context.mounted) {
          // 이렇게 await 안에 context를 사용하기 위해선 context가 mounted 상태인지 확인해야한다
          // mount되지 않았다면 return을 상요해서 진행되지 않도록 하기
          return;
        }
        showToast("전송 성공: ${response.body}");
        _getData();
      }
    });
  }

  void _deleteItem(String targetId) async {
    // 참조 - https://firebase.google.com/docs/reference/rest/database?hl=ko
    final url = Uri.https(
      dotenv.env['firebase.realtime.database.url']!,
      'flutter-architecture-test/$targetId.json',
    );
    await http.delete(url).then(
      (response) {
        if (response.statusCode == 200) {
          showToast("targetId: $targetId 삭제됨");
          _getData();
        }
      },
    );
  }

  Widget _simpleTextButton(Function buttonClicked, String buttonText) {
    return Padding(
      padding: const EdgeInsets.all(12),
      child: TextButton(
        onPressed: () {
          buttonClicked();
        },
        child: Text(
          buttonText,
          style: const TextStyle(fontSize: 24),
        ),
      ),
    );
  }

  ListView _simpleVerticalTextList({Function(int)? clickListener, required List<TestFirebaseRealTimeModel> dataList}) {
    return ListView.separated(
      shrinkWrap: true,
      scrollDirection: Axis.vertical,
      itemBuilder: (context, index) => GestureDetector(
        onTap: () {
          clickListener!(index) ?? () {};
        },
        child: Row(
          children: [
            Container(
              decoration: const BoxDecoration(color: Colors.transparent),
              child: Padding(
                padding: dataList[index].name.isEmpty ? const EdgeInsets.all(0) : const EdgeInsets.all(20),
                child: Text(
                  dataList[index].name,
                  style: Theme.of(context).textTheme.titleMedium,
                ),
              ),
            ),
            Container(
              decoration: const BoxDecoration(color: Colors.transparent),
              child: Padding(
                padding: const EdgeInsets.all(20),
                child: Text(
                  dataList[index].quantity.toString(),
                  style: Theme.of(context).textTheme.titleMedium,
                ),
              ),
            ),
            Container(
              decoration: const BoxDecoration(color: Colors.transparent),
              child: Padding(
                padding: dataList[index].category.isEmpty ? const EdgeInsets.all(0) : const EdgeInsets.all(20),
                child: Text(
                  dataList[index].category,
                  style: Theme.of(context).textTheme.titleMedium,
                ),
              ),
            ),
          ],
        ),
      ),
      separatorBuilder: (context, index) => Container(
        decoration: BoxDecoration(color: Colors.grey.withOpacity(0.5)),
        child: const SizedBox(height: 2),
      ),
      itemCount: dataList.length,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Firebase Realtime Database"),
      ),
      body: Center(
        child: Column(
          children: [
            _simpleTextButton(_saveItem, "Save Data"),
            _simpleTextButton(_getData, "Get Data"),
            _simpleVerticalTextList(
              clickListener: (index) {
                _deleteItem(resultData[index].id);
              },
              dataList: resultData,
            )
          ],
        ),
      ),
    );
  }
}

참고로 위 방법은 FutureBuilder를 사용해서 만들수도 있습니다.

 

아래 포스팅을 참고해보세요.

 

 

플러터 기초 - FutureBuilder를 사용한 비동기 작업 수행

플러터에서는 StatelessWidget을 사용하더라도 비동기 작업을 할 수 있는 방법이 존재한다. 바로 FutureBuilder를 사용하는 것이다. StatefulWidget 대신 FutureBuilder를 사용하면 다음과 같은 이점이 존재한다.

android-developer.tistory.com

 

반응형


"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."


댓글