[React Native] 채팅 구현

What I want

  1. 두 사용자 간의 채팅화면 및 Component 구성
  2. 통신

라이브러리 :

  1. react-native-gifted-chat
    https://github.com/FaridSafi/react-native-gifted-chat
  2. react-native-firebase ( realtime-database )

What I did

먼저 채팅을 구현하기 위해 화면 구성이 필요했는데 리액트 네이티브 라이브러리에서 gifted-chat 이라는 좋은 라이브러리가 있어 사용해 보았다. 프로젝트에 간단히 import만 하여도 기본 채팅화면을 렌더링 할 수 있고 자신만의 화면으로 커스터마이징 하기 위해 다양한 Props 옵션들을 제공하고 있었다.

// 코드의 일부분
import {  GiftedChat , SystemMessage } from 'react-native-gifted-chat';

<GiftedChat
           messages={messages}
           onSend={messages => onSend(messages)}
           isTyping={true}
           renderSystemMessage={this.onRenderSystemMessage}
           renderUsernameOnMessage={true}
           renderSend = {RenderSend}
           // textInputProps= {{  autoFocus : true  }}
           textInputStyle = {{ alignSelf: 'center' }}
           onPressActionButton= { () => { }}
           alwaysShowSend={true}
           showUserAvatar={true}
           
           placeholder='메시지를 입력하세요.'
           user={{
           _id: 2 ,
           name: 'beanzinu' ,
           }}
       />

messages : 채팅 메시지들의 형식인데 이를 json 데이터들의 array로 저장하고 있다. 예를 들어

[
       {
           _id : 1 ,
           text : '내용1' , 
           createdAt : new Date() ,    
       },
       {
           _id : 2 ,
           text : '내용2' ,
           createdAt : new Date() , 
           system: true ,
       }]

messages의 형식이 현재 위와 같다면 messages[0] 는 '내용1'이라는 정보를 포함하고 messages[1]는 '내용2'라는 정보를 포함하고 있다.

onSend() : 메시지를 전송을 할때 실행하는 callBack 함수이다.

 const onSend = React.useCallback( (msg = []) => {
        setMessages(previousMessages => GiftedChat.append(previousMessages, msg)) ;
      
    }, []);

기존 messages의 state를 setMessages 함수를 통해 기존 데이터 array에 append 하는 형식이다. ( 위의 messages 형식이였다면 다음 메시지는 messages[2]가 될 것이다. )

[통신]
1. 채팅 메시지 간의 유저 구분 : gifted-chat 의 Props 의 user._id로 구분
2. 유저 간의 통신 : Google의 Firebase 이용하여 Realtime Database로 구현해 보려고 한다.

Realtime Database 란?

클라우드 호스팅 데이터베이스이다. 데이터는 JSON 형식으로 저장되며 연결된 모든 클라이언트에 실시간으로 동기화됩니다. iOS, Android, 자바스크립트 SDK로 크로스 플랫폼 앱을 빌드하면 모든 클라이언트가 하나의 실시간 데이터베이스 인스턴스를 공유하고 자동 업데이트로 최신 데이터를 수신합니다.

기본 원리 : Firebase 실시간 데이터베이스로 클라이언트측 코드에서 데이터베이스에 직접 안전하게 액세스하여 다기능 협업 애플리케이션을 개발할 수 있습니다. 데이터가 로컬에 유지되고 오프라인일 때도 실시간 이벤트가 계속 발생하므로 최종 사용자에게 원활한 환경이 제공됩니다. 기기가 다시 연결되면 클라이언트가 오프라인일 때 발생한 원격 업데이트와 로컬 데이터 변경이 동기화되고 모든 충돌이 자동으로 해결됩니다.
실시간 데이터베이스가 제공하는 유연한 표현식 기반 규칙 언어인 Firebase 실시간 데이터베이스 보안 규칙을 통해 데이터의 구조 및 데이터를 읽거나 쓸 수 있는 조건을 정의할 수 있습니다. 개발자는 Firebase 인증과 통합하여 사용자의 데이터 액세스 권한 및 액세스 방법을 정의할 수 있습니다.
실시간 데이터베이스는 NoSQL 데이터베이스로서 최적화 방식과 기능성이 관계형 데이터베이스와 다릅니다. Realtime Database API는 오로지 작업 실행 속도를 위주로 설계되었으므로 수백만 사용자가 실시간으로 쾌적하고 원활하게 이용할 수 있는 탁월한 실시간 환경을 구축할 수 있습니다. 따라서 사용자의 데이터 액세스 방법을 미리 계획하고 적절히 구조화하는 것이 중요합니다.

채팅에서 실시간으로 이벤트(메시지)가 공유되는 것도 중요하지만 어플리케이션이 꺼졌을 때 메시지가 수신이 되지 않는 문제를 해결해야 했는데 백그라운드 모드에서 메시지 수신을 기다리는 것은 비효율적일 수 밖에 없는데 연결이 끊어졌다가 다시 연결되면 변경사항이 동기화되고 모든 충돌이 자동으로 해결되는 부분이 매력적이었다. 또한 수백만 사용자가 실시간으로 쾌적하고 원할하게 접근 가능하기 때문에 문제가 없을 것이라고 판단하였다.

접근법
1. Realtime Database로부터 내가 읽지 못했던 이벤트들은 매 채팅시마다 동기화한다.
2. 동기화가 완료된 메시지들은 로컬에 cache 한다.
3. 실시간 -> 현재 채팅화면일 때는 실시간으로 동기화를 받는다.

고려해야할 부분
1. Realtime Database가 이벤트들을 제대로 동기화 하지 못했을 경우
2. 동기화 완료 후 로컬에 cache가 안됐을 경우
3. 인터넷 연결이 끊어져 Realtime Database에 동기화되지 못한 경우

사용법
Publish & Subscribe 구조와 유사하다고 느꼈다.
'/users/123'이라는 노드에 realtime changes를 받아보고 싶다면 아래와 같다.

database()
  .ref('/users/123')
  .on('value', snapshot => {
    console.log('User data: ', snapshot.val());
  });

어떤 특정한 이벤트에 대해 unsubscribe하고 싶으면 다음과 같다.

function User({ userId }) {
  useEffect(() => {
    const onValueChange = database()
      .ref(`/users/${userId}`)
      .on('value', snapshot => {
        console.log('User data: ', snapshot.val());
      });
    // Stop listening for updates when no longer required
    return () => database().ref(`/users/${userId}`).off('value', onValueChange);
  }, [userId]);
}

내가 subscribe 중인 node에 추가적인 이벤트가 발생했을 때
( 나의 경우 "어떤 사용자가 메시지를 보냈을 때"일 것이다. )

function User({ userId }) {
  useEffect(() => {
    const onChildAdd = database()
      .ref('/users')
      .on('child_added', snapshot => {
        console.log('A new node has been added', snapshot.val());
      });
    // Stop listening for updates when no longer required
    return () => database().ref('/users').off('child_added', onChildAdd);
  }, [userId]);

이 외 불러온 이벤트들에 대한 여러 함수들을 제공한다.

  • Ordering : 이벤트들의 json value에 따라 정렬
    ex)
const scores = database().ref('scores').orderByValue().once('value');
  • Limiting : 이벤트들의 결과 개수를 제한
const users = database().ref('users').limitToFirst(10).once('value');

문제점 :
어떤 특정 사용자가 어디까지 채팅을 읽었는지 정확히 알 수 없었다.
node에 추가적인 이벤트를 발생했을 때

1. 채팅화면에 그대로 있을 경우 : 추가적인 이벤트만 snapshot으로 로드된다.
2. 화면을 다시 로드했을 경우 : 처음~마지막 이벤트까지 모두 로드한다.
인터넷 연결 문제로 메시지가 local database에 저장 되었지만 realtime database로 push가 되지 않아 화면 상에 메시지가 렌더링 되지 않음.
1. 인터넷이 다시 연결 될 경우( 채팅 화면 내에서 ) : database.push()가 성공적으로 다시 수행이 되어 then() 이후의 코드들을 실행시킨다.
2. 다시 채팅방으로 들어올 경우 ( 인터넷을 다시 연결하고 ) :
자동으로 local 과 서버간의 충돌을 해결하고 이벤트들이 서버에 푸쉬 되어있음.

좋은 웹페이지 즐겨찾기