[Flutter]플러터와 파이어베이스로 룰베이스 챗봇 구현

nan_nui·2023년 8월 10일
0

웹/앱

목록 보기
1/1
post-thumbnail

방학 동안 짧게 생성형 인공지능을 사용한 관광 분야 해커톤에 참가하게 되었다.

원래는 인공지능 파트도 참여하고 싶었는데, 어쩌다 보니 플러터로 앱 프론트엔드 개발만 맡게 되었다 ;_;

그래도 이렇게 단기간에 하나의 서비스를 구축하는 경험은 나름 재밌었던 것 같다.

또 평소 구현해보고 싶던 채팅 시스템을 개발하면서 새로운 시도를 몇 개 이룬 것 같아서 포스트를 작성하기로 하였다.

1. 파이어베이스 연동

파이어베이스란 무엇일까? 예전에 프론트엔드 개발자도 파이어베이스를 사용하여 직접 api를 테스트해볼 수 있다는 식의 강의를 들은 적이 있었다. 그 때는 이걸 대체 왜 사용하는건지 잘 와닿지 않았는데, 이번에 플러터와 파이어베이스로 채팅 구현을 하면서 누구나 어렵지 않게 사용할 수 있으면서 필요한 기능들을 제공하는 플랫폼이라는 것을 느낄 수 있었다.

파이어베이스는 모바일 앱 개발을 위한 플랫폼으로, 개발을 위한 다양한 툴을 모아놓은 툴킷으로 생각할 수 있었다. 그 중에서도 백엔드 요소를 다루는 툴을 사용하기 쉬운 형태로 제공하여, 간단하게 테스팅을 하거나 가벼운 앱을 빠른 시간 안에 만들 수 있다는 장점이 있는 것 같다.

1. 파이어베이스 콘솔 생성

https://firebase.google.com/?hl=ko
위 url로 접속한 후 "콘솔로 이동하기" 버튼을 클릭하여 파이어베이스 프로젝트를 바로 생성할 수 있다. 클릭 몇 번으로 파이어베이스에서 제공하는 다양한 기능들을 위한 인프라를 바로 빌릴 수 있었다.

콘솔과 프로젝트를 생성하면, 그림의 왼쪽에 보이는 기능들을 자유롭게 사용할 수 있다. 물론 제공되는 서비스에 따라 요금이 부가될 수도 있지만, 나는 거대한 규모의 서비스를 출시하는 것은 아니어서 무료로 사용할 수 있었다.

로그인, 회원가입 관련의 Authentication, 실시간 데이터베이스인 Firestore/Realtime 데이터베이스, 대량의 콘텐츠를 저장할 수 있는 Storage, 머신러닝에 대한 이해가 없는 사람도 머신러닝을 사용할 수 있도록 알기 쉬운 인터페이스로 구현해놓은 Machine Learning 등의 기능이 있다.

나는 채팅 시스템에 필요한 Firestore DB만 사용하였다.

2. 애플리케이션 선택 및 환경설정

해당 페이지에서 파이어베이스를 연동하고자 하는 애플리케이션을 선택하고 연동을 위한 가이드라인을 쭉 따라가면서 앱에 Firestore DB를 쌓을 수 있었는데, 자세한 과정은 공식 문서와 다른 블로그에도 많이 다루고 있으니 생략하고 다른 글을 참고했음에도 불구하고 발생했던 에러에 대해 조금 정리해보려고 한다.
(firestore 시작하기: https://firebase.google.com/docs/firestore/quickstart?hl=ko#set_up_your_development_environment)

  1. GoogleService-Info.plist
    [출처: tlsgks48님 블로그]
    파이어베이스 가이드라인을 따라가다 보면 애플리케이션 패키지 등록 이후에 GoogleService-Info.plist 파일을 다운 받아서 ios 프로젝트 루트 경로 (Info.plist와 같은 디렉토리)에 해당 파일을 옮기라고 한다. 그러나 옮겼는데도 빌드 과정에서 AppDelegate.swift에서 정의한, 유효한 GoogleService-Info.plist를 찾지 못한다는 내용의 에러가 발생하였다.

    Exception NSException * "`[FIRApp configure];` 
    (`FirebaseApp.configure()` in Swift) could not find a valid 
    GoogleService-Info.plist in your project. Please download one from 
    https://console.firebase.google.com/." 0x00000002826939f0

    결론적으로 xcode에서 GoogleService-Info.plist가 추가되었다는 사실을 제대로 인식하지 못해서 발생하는 것 같았다. 따라서 xcode에서 프로젝트 디렉토리에 대해 우클릭->Add files to "project_name"을 통해 직접 GoogleService-Info.plist를 추가해주어야 한다.

    에러 해결에 도움이 되었던 링크:
    https://ychcom.tistory.com/entry/Exception-NSException-FIRApp-configure-FirebaseAppconfigure-in-Swift-could-not-find-a-valid-GoogleService-Infoplist-in-your-project

  2. SDK 설정
    GoogleService-Info.plist에 대한 설정이 완료되면 firebase IOS SDK를 설치하라는 내용이 나온다. 파이어베이스 가이드라인에 명시된 대로 SDK를 설치한 후 애플리케이션 진입 지점에 특정 코드를 추가하라는 스텝이 나오는데, 여기서 또 사소한(?) 문제가 발생한다.

  • 애플리케이션 진입점

    가이드라인에 따르면 애플리케이션 진입점에 특정 코드를 추가하라고 되어있는데, 플러터의 AppDelegate의 경우 프로젝트 기본 코드로 이미 해당하는 내용과 다른 내용으로 함수가 구현되어 있었다.
    처음에는 기존 코드에서 넣으라고 한 코드를 최대한 반영하는 식으로 수정했는데, 기존 코드에서 상속 받는 클래스와 새롭게 주입되는 코드에서 상속 받는 클래스 사이에 포함관계가 있는 것 같았다.
    그래서 두 코드에서 사용된 클래스를 모두 상속받으려고 하니 중복 상속 에러가 발생했고, 다음 링크를 참고하였다.
    https://stackoverflow.com/questions/52808717/confusion-connecting-ios-portion-of-flutter-app-to-firebase

    그러나 위 내용대로 빌드하였을 때 다음과 같은 에러가 발생하였다.

    [Firebase/Core][I-COR000005] No app has been configured yet.

    조금 더 찾아보니 다른 플러그인을 등록하기 전에 파이어베이스 구성이 먼저 이루어져야 하는 것 같았다. 따라서 위 내용대로 수정한 상태에서 아래의 링크 내용을 반영하여 FirebaseApp.configure()GeneratedPluginRegistrant.register(with: self) 앞에 두어 해결하였다.
    https://stackoverflow.com/questions/64014893/my-flutter-firebase-app-is-showing-no-app-has-been-configured-yet

  • SDK 재정의

    다시 빌드를 진행해보니 다음과 같은 에러가 나타났다.

    /Users/<MyUser>/Desktop/projects/app/ios/Pods/Firebase/CoreOnly/Source
    s/module.modulemap:1:8: error: redefinition of module 'Firebase'
    module Firebase {
           ^

    파이어베이스 모듈이 재정의되었다는 에러이다. 하라는대로 설치했는데 왜 이런 에러가 발생했나 해서 찾다보니 이번에도 스택 오버플로우에서 그 답을 구할 수 있었다.

    https://stackoverflow.com/questions/70760326/flutter-on-ios-redefinition-of-module-firebase

    이 단계가 특정 프레임워크에만 해당되는 스텝이라고 하는데 가이드라인이 조금 개선되어야 하는 것인지 ,,, 하는 생각이 들었다.
    링크대로 ios sdk를 삭제했더니 해당 에러는 해결되었다.

  • main.dart
    드디어 마지막 에러이다.

    No Firebase App '[DEFAULT]' has been created - call Firebase.initializeApp()

    firebase는 main.dart에서 초기 설정을 해주어야 하는 것 같았다. 따라서 다음 링크에 나온 대로 initialize를 진행하였다.
    https://yj95.tistory.com/217

이렇게 몇 개(?)의 에러를 거쳐서 최종적으로 파이어베이스가 연동된 플러터 애플리케이션을 빌드할 수 있었다!

플러터는 누구나 쉽게 배울 수 있는 프레임워크를 지향한다는 점에서 초보자들에게는 다소 난관일 수 있는 사소한 에러들이 많이 발생하는 점이 아쉬웠고, 이런 부분은 반드시 고쳐져야 할 점이라고 생각한다는 말이 많이 공감됐다.

2. 플러터로 채팅 시스템 구현

내가 구현해야 하는 챗봇이 포함해야 하는 기능은 다음과 같았다.

  • 일반적인 채팅 기능
    • 사용자/챗봇 말풍선 구별
    • 시간순 정렬
    • 등등...
  • 키워드를 기반으로 답변 자동 생성 또는 서버로 api 요청 전송

1. 파이어베이스 DB 콜렉션 생성 및 필드 정의

먼저 대화를 입력하면 생성되는 채팅 버블을 저장하는 컬렉션을 상위 폴더 경로 /chat으로 생성하였다.

사용자가 입력한 채팅과 챗봇이 생성한 채팅의 버블 색과 위치를 다르게 구성하였는데, 이를 구현하기 위해서는 채팅 데이터에 사용자인지 챗봇인지 식별하는 필드가 필요하다.

또한 시간 순으로 정렬이 되어 화면에 보여져야 하기 때문에, 채팅 데이터 필드에 채팅이 생성된 타임 스탬프를 추가하였다.

그렇게 구성한 채팅 버블 데이터 구조는 다음과 같다.

그림의 "문서 추가" 버튼을 눌러서 DB에 데이터를 직접 추가할 수 있다.

  • isUser: 사용자가 입력한 대화면 true, 챗봇이 생성한 대화인 경우 false
  • text: 대화 내용
  • time: Timestamp.now() 함수 호출로 생성한 타임스탬프(채팅 생성 시각)

2. 채팅 생성

1. 입력된 텍스트 인식

채팅 필드는 TextField 위젯과 TextEditingController를 사용한다.
그리고 채팅 필드가 속한 상위 클래스에 텍스트 필드에 입력된 내용을 저장하는 변수 _userEnteredMessage를 정의하였다.

내가 만들어야 하는 채팅 필드는 이렇게 생겼다. 이를 구현한 코드 중 핵심이 되는 부분은 다음과 같다.

Row(
  children: [
    Expanded(
      child: TextField(
        controller: _textController,
        decoration:
            InputDecoration.collapsed(hintText: "원하는 맛집에 대해 물어보세요!"),
        onChanged: (val) {
          _userEnteredMessage = val;
          // setState(() {
          //   _userEnteredMessage = val;
          // });
        },
      ),
    ),
    Container(
        margin: const EdgeInsets.symmetric(horizontal: 8),
        child: Transform.rotate(
          angle: 0.8,
          child: IconButton(
              icon: Image.network(
                imgURL,
                width: 25,
                height: 25,
                fit: BoxFit.fill,
              ),
              onPressed: _userEnteredMessage.trim().isEmpty
                  ? null
                  : _handleSubmitted),
        ))
  ],
),

UI적인 측면에서의 설명보다는 동작하는 기능 위주로 살펴보면, TextField 위젯은 속성으로 필드에 입력되는 텍스트를 제어하는 controller, 그리고 필드에 입력되는 데이터에 변경이 생길 때마다 호출되는 onChanged 함수를 갖는다.

controller는 이름에서부터 알 수 있듯이 TextEditingController 객체 변수인 _textController에 해당하는 속성이다. _textController를 통해서 메세지 전송 시 지금까지 필드에 입력된 내용을 초기화한다.

입력된 내용을 _userEnteredMessage에 저장하는 함수는 onChanged 함수가 담당한다. 텍스트 필드에 글자가 입력 또는 삭제될 때마다 onChanged 함수가 호출되는데, 그 내용을 자세히 살펴보면 다음과 같다.

onChanged: (val) {
              _userEnteredMessage = val;
              // setState(() {
              //   _userEnteredMessage = val;
              // });
        },

구조는 매우 간단하다.
매개 변수로 전달되는 변수 val을 _userEnteredMessage에 대입해주는 것이 전부이다. 웹앱 개발이 처음인 사람은 val에 어떤 값이 전달되는 것인지 혼란이 올 수 있다.

TextField 처럼 사용자로부터 값을 입력 받는 인풋(input) 관련 컴포넌트들은 자신에게 일어나는 변화를 알아서 감지하여 업데이트된 최신 상태 값을 매개변수로 전달한 후 onChanged로 정의된 함수를 호출하는 automatic change detection을 수행한다.

결론적으로 val이라는 이름으로 넘어오는 매개변수 값은, 사용자가 가장 최근에 TextField에 입력한 텍스트 값이 되는 것이다.

원래는 _userEnteredMessage의 값을 setState를 통해 변경하였는데, 현재 프로젝트는 사용자가 채팅을 생성하기 전에는 챗봇에서 채팅을 연속적으로 생성하거나 하는 일이 없으므로 _userEnteredMessage가 변경될 때마다 화면을 새로 렌더할 필요가 없었다.
따라서 setState로 값을 갱신하지 않고 직접 대입해 주었다.
(state 변수는 값이 변할 때마다 화면이 다시 렌더되기 때문)

2. 텍스트 전송, DB에 채팅 데이터 추가

이렇게 입력된 텍스트를 전송하려면, 전송 버튼이 클릭되었을 때 발생하는 이벤트를 처리해주어야 한다.
전송 버튼 구현 부분을 잘라서 보면 다음과 같다.

Container(
    margin: const EdgeInsets.symmetric(horizontal: 8),
    child: Transform.rotate(
      angle: 0.8,
      child: IconButton(
          icon: Image.network(
            imgURL,
            width: 25,
            height: 25,
            fit: BoxFit.fill,
          ),
          onPressed: _userEnteredMessage.trim().isEmpty
              ? null
              : _handleSubmitted),
    ))

마찬가지로 클릭 이벤트 관련 기능 위주로 살펴보자.
onPressed는 컴포넌트가 터치되었을 때 호출되는 함수이다.

onPressed에서 삼항 연산자 '?'를 사용하여 입력된 메세지가 없는 경우는 호출되는 함수 대신 null을 넣어서 전송이 이루어지지 않도록 하고, 입력된 메세지가 있는 경우는 전송 이벤트 핸들러인 _handleSubmitted가 호출되도록 하였다.

void _handleSubmitted() {
  FirebaseFirestore.instance.collection('chat').add(
      {'text': _userEnteredMessage, 
      'time': Timestamp.now(), 
      'isUser': true});
  debugPrint("get response");
  getResponse(_userEnteredMessage);
  _textController.clear();
}

_handleSubmitted 함수의 구현이다.
FirebaseFirestore.instance.collection('chat')을 통해서 플러터 프로젝트와 연동된 파이어베이스 DB의 chat 콜렉션을 가져왔다.

chat 콜렉션의 문서(데이터)가 갖는 필드는 위에서 언급한 것처럼 사용자 여부(isUser), text(대화 내용), time(대화 생성 시간)이므로, 그 구조에 맞게 객체를 생성하여 콜렉션에 add하는 것을 확인할 수 있다.

이 때 전송 버튼 클릭 이벤트는 모두 사용자가 발생시키는 것이므로, 이 때 전송되는 채팅은 모두 사용자가 입력한 내용으로 이루어질 것이다.
따라서 여기서 isUser 필드 값은 true로 고정이다.

또한 사용자가 입력한 내용은 모두 _userEnteredMessage에 최신 상태로 저장되어 있으므로 text 필드 값에 넣어주고, time은 Timestamp.now()를 호출하여 현재 시각을 넣어주었다.

이렇게 콜렉션에 객체를 add하는 것만으로도 DB에 데이터가 추가된다.

3. 채팅 목록 렌더

이렇게 파이어베이스 DB에 추가된 채팅 데이터들을 플러터로 가져와서 화면에 보여주려면 어떻게 해야할까?

처음에는 리스트 형식으로 가져와서 차례대로 순회하면서 렌더하는 방법을 생각했지만, StreamBuilder와 ListView를 같이 사용하면 매우(x100) 간편하게 렌더할 수 있다는 것을 알게 되었다. 그 방법은 다음과 같다.

@override
Widget build(BuildContext context) {
  return StreamBuilder(
      stream: FirebaseFirestore.instance
          .collection("chat")
          .orderBy('time', descending: true)
          .snapshots(),
      builder: (context,
          AsyncSnapshot<QuerySnapshot<Map<String, dynamic>>> snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return Center(child: CircularProgressIndicator());
        }
        final chatDocs = snapshot.data!.docs;
        return Padding(
          padding: const EdgeInsets.symmetric(horizontal: 18),
          child: ListView.builder(
              reverse: true,
              itemCount: chatDocs.length,
              itemBuilder: (context, index) {
                return chatDocs[index]['isUser']
                    ? MyChat(chat: chatDocs[index]['text'])
                    : BotChat(chat: chatDocs[index]['text']);
              }),
        );
      });
}

Stream은 데이터의 흐름이다.

StreamBuilder는 stream 속성으로 파이어베이스에서 지정된 콜렉션의 상태를 실시간으로 가져온다. 이는 snapshot 메소드 호출로 이루어진다.

builder는 스냅샷을 비동기 요청으로 가져오고, 만약 데이터가 방대하여 가져오는데 시간이 걸리는 경우는 로딩 중임을 알려주는 화면이 나타난다.

스냅샷의 데이터 중에서 콜렉션에 저장된 문서들을 chatDocs에 저장하고, 그 내용을 그대로 ListView 안에서 채팅 버블 모양으로 나타날 수 있도록 하였다.

여기까지 하면 사용자가 생성하는 채팅을 실시간으로 반영하여 화면에 보여줄 수 있다. 이제 남은건 챗봇의 답변을 생성하는 것이다.

3. 키워드 기반 룰 설계

룰베이스 챗봇의 전반적인 내용은 다음 링크를 참고하였다.
https://datasciencedojo.com/blog/rule-based-chatbot-in-python/

룰 설계 과정을 설명하면 다음과 같다.

1. 키워드 정의 및 의미 별로 그룹핑

[참고]
원문은 위 과정에서 워드넷을 사용하여 각 의미별로 지정한 대표 단어와 유사한 의미를 갖는 단어들을 뽑아내는 식으로 의미 별 단어들을 그룹핑 했다.
그래서 워드넷 한글 버전 데이터를 찾아봤는데 kolnpy에서 제공하는 kolaw는 법률 관련 데이터여서 관광을 목적으로 하는 우리 프로젝트에서는 적합하지 않을 것 같았다. 또 다른 데이터로는 kolex라고 있었는데, 아직 IDE에서 import해서 사용할 수는 없는 것 같았고 메일을 보내봤는데 답이 오지 않았다.
어차피 자동 응답을 생성해야 하는 경우의 가짓수가 한정적이었기 때문에 임의로 비슷한 단어들을 묶어서 진행하였다.

여기서 Key에 해당하는 부분이 의미, Value에 해당하는 단어 리스트가 키워드이다.
만약 사용자의 질문이 Value의 단어를 포함하고 있다면, 해당하는 의미인 Key로 매핑하는 과정을 거친다.

예를 들어 사용자가 '안녕하세요 반갑습니다'라고 했다면 키워드 중 "안녕하세요"를 포함하고 있으므로 의미 상 'greet'에 해당하는 것이고, 그에 따라 'greet'에 대해 정의된 응답 채팅을 생성할 것이다.

2. 의미에 따른 응답 정의

가게 정보에 대한 응답은 두 가지로 구분된다.

  1. 해당 가게의 정보 선택지 제공
  2. 가게 정보 제공

팀원들과의 회의를 거친 결과 처음에는 사용자가 궁금해하는 가게의 이름을 인식하고, 그 뒤에 그 가게의 어떤 정보를 인식하는지 우리가 선제적으로 선택지를 제공하면 키워드 인식 비용도 줄고 구현도 더 편하겠다는 결론이 나와서, ARS와 비슷한 시스템으로 챗봇을 구성하게 되었다.

사용자가 언제 다른 가게의 정보를 원할지 모르니, 가게 이름을 인식하는 것이 우선순위가 제일 높아야 한다.
코드 내부에서 가게 이름을 인식하는 코드가 가장 먼저 오게 하고, 인식되는 경우 그에 해당하는 응답을 생성하고 동작을 종료한다.
사용자가 다시 채팅을 생성하면, 그 때 가게 이름 인식 또는 선택지, 정보 제공 등의 동작을 포함하는 함수가 호출될 것이다.

1. 가게 이름 인식 및 해당 가게의 정보 선택지 제공

for (final r in restaurants) {
  if (inputString.contains(r)) {
    restName = r;
    FirebaseFirestore.instance.collection("chat").add({
      'text':
          "${restName}의 어떤 정보를 원하시나요?\n\n번호나 형식으로 입력해주세요!\n\n1. 주소\n2. 매장 번호\n3. 영업 시간\n4. 가격\n5. 추가 정보(기타)",
      'time': Timestamp.now(),
      'isUser': false
    });
    isRestaurant = true;
    break;
  }
}

가게 이름을 인식하는 코드이다. 사용자가 입력한 텍스트가 가게 이름을 포함하는지 순차적으로 조회하고, 포함한다면 가게의 어떤 정보를 원하는지 선택지를 제공하는 응답을 생성한다.

이런 식으로 해당 가게에서 제공할 수 있는 정보들을 보여준다.
번호 또는 옵션을 입력하여 원하는 정보를 요청할 수 있다.

위의 1~5번 선택지 중 사용자가 가격을 원한다면,
"price"에 해당하는 가격, 요금, 비용, 4를 입력하여 가격 정보를 요청할 수 있다.

사용자가 입력한 텍스트가 가격, 요금, 비용, 4와 같은 단어들을 포함하는지 확인하는 로직이다. 위에 사진의 keywordsDict의 키들을 가져오고, 키들에 해당하는 값인 string 배열의 원소들에 접근하여 사용자 입력 텍스트가 해당 문자열을 포함하는지 확인한다.

(사실 이렇게 키워드가 매치되는 순간 break를 넣어 구현하면 두 개 이상의 정보를 한 번에 제공하지 못한다는 단점이 있는데, ARS처럼 선택지를 제공하고 있으니 사용자가 한 번에 하나의 정보만 요청하기를 바라는 수밖에 없는데 이런 경우까지 고려하지 못한건 아쉬움으로 남는다.)

2. 가게 정보 제공

응답을 생성하는 부분이다.

사용자 입력 문자열이 특정 단어를 포함하면 그에 해당하는 keywordsDict의 키를 key라는 배열에 추가해두었다.
여기에 추가된 키들을 하나씩 접근하면서 가게 정보가 아닌 경우는 reponseText 변수에 저장된 내용을 그대로 응답으로 생성하고, 가게 정보인 경우는 가게 이름과 정보 유형을 서버에 요청하고, 서버로부터 받아온 응답을 정제하여 채팅으로 생성하였다.

정보 타입은 setType 함수에서 키워드에 따라 서버에 요청할 정보의 유형을 저장하는 type 변수에 값을 부여하거나, 가게 정보에 대한 키워드가 아닌 경우 responseText에 그 응답을 직접 넣어주었다.

void setType(String key) async {
  switch (key) {
    case "greet":
      responseText = "안녕하세요, 무엇을 도와드릴까요?\n\n알고 싶은 가게의 이름을 입력해주세요!";
      break;
    case "price":
      type = "가격";
      break;
    .
    .
    .
    case "fallback":
      responseText = "없는 정보입니다. 알고 싶은 가게의 이름을 입력해주세요!";
      break;
  }

이렇게 setType을 통해 영어에서 한글로 바꿔주는 이유는 서버에서 type 값을 한글로 인식하도록 명세가 작성되었기 때문이다.
가게 정보 외의 응답은 responseText를 통해서 바로 생성한다.

String makeResponse(String restaurantName, String type, dynamic info) {
  switch (type) {
    case "주소":
      return "${restaurantName}은(는) ${info}에 위치하고 있습니다.";
    case "전화번호":
      return "${restaurantName}의 매장 번호는 ${info}입니다.";
    case "영업시간":
      String times = "";
      for (var t in jsonDecode(info).entries) {
        times = "$times\n\n${t.key}:\n${t.value}";
      }
      return "${restaurantName}의 영업 시간은 아래와 같습니다.${times}";
    case "가격":
      dynamic entry = jsonDecode(info).entries.toList()[0];
      return "${restaurantName}의 대표 메뉴와 그 가격을 알려드릴게요.\n\n${restaurantName}의 대표 메뉴는 ${entry.key}이고, 그 가격은 ${entry.value}입니다.";
    case "기타":
      return "${restaurantName}의 추가 정보를 알려드릴게요.\n\n${info}";
    default:
      return "올바르지 않은 요청";
  }
}

마지막으로 서버에서 받아온 가게 정보를 채팅 응답처럼 보이게 정제하여 반환하는 makeResponse 함수이다. 위 함수로부터 받아온 문자열을 firebase에 채팅 데이터로 생성하면, 자동 응답 생성 구현은 끝이 난다.

4. 결과

이 외에도 주제에 맞는 기능을 몇가지 추가해서 앱을 빌드하였다.

profile
nannui의 개발로그

2개의 댓글

comment-user-thumbnail
2024년 2월 14일

새로운 채팅방의 첫 채팅이 화면 하단에 시작하는건 reverse를 사용하는 한 어쩔수없는걸까요?

1개의 답글