[이타인클럽앱] 도움요청 구현 using React Native Firebase

etainclub(65)
Published in
#kr
Words
749
Reading
4 min
Listen
Play
7y

메시지 송신, 수신에 이어서 이타인클럽앱에서 도움요청하는 부분을 구현합니다.

도움 요청하기는 도움 요청 메시지를 작성하는 부분과 도움 요청을 대기하는 부분으로 구성됩니다.
보다 상세한 작업 흐름은 다음과 같습니다.

도움 요청

  • 메시지 작성
  • 도움 요청 버튼 클릭
  • 도움 요청 대기 화면
    • 도움 요청 경과 시간 표시
  • 도움 요청 취소
  • 도움 요청 취소시에 DB에서 해당 메시지 삭제 (미구현)
  • (도움주는 사람) 도움 수락 확인
  • 수락과 동시에 도움 수락자로부터 메시지를 수신한다.

도움 요청 화면

image.png

도움 요청 화면 구성은 이렇습니다. 도움 메지지를 적는 부분과 도움 요청하기 버튼으로 구성됩니다.

도움 대기화면

image.png

도움 요청 대기화면은 대기 중이라는 스피너가 가운데 표시되고, 도움 요청 경과 시간이 아래 표시됩니다. 그리고 중간에 도움 요청을 취소할 수 있는 버튼이 있습니다. 이 때, 취소 버튼을 누르면 확인창이 떠서 확인을 누르면 도움 요청이 취소됩니다.

도움 요청 수락 확인 화면도움 요청 수락 확인 화면

image.png

도움을 주고자 하는 사람이 도움 요청을 수락하면 도움 요청자는 도움 요청이 수락되었다는 화면으로 이동하고 수락자로부터 메시지를 받게 됩니다.

이러한 흐름으로 동작하는 코드를 구현해 보겠습니다. 지난 번에 메시지를 작성하고, DB에 저장하고 발신, 수신까지 구혀하였습니다. 이번에는 도움 요청 메시지를 발신하고 수락될 때까지 대기하는 부분을 구현합니다.

이전 코드에서 메시지를 보내는 부분입니다. 메시지를 DB에 쓰고, 대기 화면으로 이동하도록 "sendSuccess" 함수를 호출합니다.
ask actions과 관련된 내용을 actions/AskActions.js 파일에 구현합니다.

// action creator to send message to all users
export const sendMessage = ({ message }) => {
  return (dispatch) => {
    console.log('message', message);
    // get the current user
    const { currentUser } = firebase.auth();

    // get message count of the current user from db
    let count = 0;
    const messages = firebase.firestore().collection('asks')
      .doc(currentUser.uid).collection('messages');
    messages.get()
      .then(snapshot => {
        snapshot.forEach(() => {
          count++;
        });
        console.log('number of messages', count);
        // dispatch action
        dispatch({ type: SEND_MESSAGE, payload: { message, count } });
        // store the message in the firestore
        firebase.firestore()
          .collection('asks')
            .doc(currentUser.uid)
              .collection('messages')
                .doc(`${count}`)
                  .set({ message, accepted: false, timestamp: Date.now() });
        // dispatch success action
        sendSuccess(dispatch, message, currentUser.uid, count);
      })
      .catch(err => {
        console.log('Error getting the messages', err);
      });
  };
};

// action creator for send success. redirect to wait form
const sendSuccess = (dispatch, message, senderId, msgId) => {
  dispatch({
    type: SEND_SUCCESS,
    payload: message
  });

  // navigate
  Actions.wait_form({ senderId, msgId });
};

위 sendSuccess함수에서 action을 만들고, 마지막에 대기 화면으로 화면 전환시킵니다. 이 때 인자로 메시지의 내용과 message를 보낸 사람의 user id인 senderId, 그리고 메시지의 id인 msgId를 전달합니다.

위 actions들에 대한 reducer 부분은 생략하도록 합니다. 단순히 각 action에 대해 state를 업데이트 하는 부분으로 간단합니다.

Wait Form

도움 요청 메시지가 발송되었기 때문에 도움 요청자는 도움 요청이 수락되기를 기다려야 합니다. 제가 생각한 로직은 다음과 같습니다.

  1. 도움 메시지가 앱 사용자 다수에게 전송

  2. 그 중 한명이 수락 버튼 클릭

  3. 그러면, DB에서 해당 메시지를 찾아서 메시지 document의 accepted 필드값을 false에서 true로 변경시킵니다. 아래 Firebase의 예처럼 말이죠. 지금은 false로 되어 있습니다.
    image.png

  4. 도움 요청자는 자신이 보낸 메시지의 document의 "accepted" 필드값을 주기적으로 검사하여 true가 되는지 확인합니다.

  5. true가 되면 메시지가 수락된 것이므로, 도움 요청자에게 수락되었다고 알립니다.

  6. 도움 요청자는 중간에 도움 요청을 취소할 수 있습니다.

그럼 화면대기폼인 WaitForm.js 파일을 아래처럼 구현합니다.

// import 생략
class WaitForm extends Component {
  // state
  state = {
    showModal: false,
    startTime: null,
    timeElapsed: null,
  };

  componentDidMount() {
    // set start time
    this.setState({ startTime: Date.now() });
    // set interval -> 1000 ms
    this.interval = setInterval(() => {
      this.setState({
        timeElapsed: new Date() - this.state.startTime,
      });
    }, 1000);

    // set callback function of db triggering
    this.props.monitorAcceptance(this.props.senderId, this.props.msgId);
  }
 (생략)

참고로, 위 코드를 보면 컴포넌트에 state를 몇 개 포함시켰습니다. 다른 컴포넌트에서 접근이 필요없는 state를 굳이 redux store에서 관리하지 않고, 그냥 컴포넌트에서 구현하도록 합니다.

"componentDidMount"함수에서 컴포넌트가 마운트되면, 도움 요청 시작 시간을 기록합니다. 그리고 일정 주기(여기서는 1초)마다 경과 시간을 업데이트합니다. 화면에서 경과시간이 매초마다 업데이트 되어 표시됩니다.

그리고 함수 마지막에 DB에 수락 내용이 업데이트 되는지 검사하는 함수를 추가합니다. 이 함수의 본체는 편의상 HelpActions.js 파일에 구현하도록 합니다.

// action creator to monitor acceptance
export const monitorAcceptance = (senderId, msgId) => {
  return async (dispatch) => {
    console.log('senderId', senderId);
    console.log('msgId', msgId);
    const ref = firebase.firestore().collection('asks').doc(`${senderId}/messages/${msgId}`);
    console.log('message ref', ref);
    console.log('message data', ref.get());
    const unsubscribe = await ref
      .onSnapshot(docSnapshot => {
        console.log('doc snapshot', docSnapshot);
        console.log('doc snapshot data', docSnapshot.data());
        if (docSnapshot.data().accepted) {
          // update state
          askAccepted(dispatch);
          // unsubscribe
          unsubscribe();
        }
      }, err => {
        console.log(`Encountered error: ${err}`);
      });
  };
};

여기서 중요한 부분은 먼저 다음 코드입니다. Firestore DB에 있는 메시지 정보에 접근하기 위한 reference값을 얻는 코드입니다.

 const ref = firebase.firestore().collection('asks').doc(`${senderId}/messages/${msgId}`);

Firestore의 DB를 보면 asks(collection) -> uid(document) -> messages(collection) -> message id(document)의 경로를 갖고 있습니다.
image.png

상식적으로 특정 메시지에 접근하기 위해서는 아래와 같이 해야 될 거 같습니다.

const ref = firebase.firestore().collection('asks').doc(`${senderId}).collection('messages').doc(`${msgId}`);

그러나 위와 같이 하면 DB의 document에 접근할 수가 없습니다! 이 부분이 제대로 설명안되어 있어서 고생을 좀 많이 했습니다.
어쨌든 찾아보니 위쪽에 기술한 코드대로 최상위 collection밑에 document에 하위 collection 및 document를 한번에 넣어야 제대로 해당 message의 document에 접근할 수가 있습니다. 주의하세요.
참고. https://stackoverflow.com/questions/55476263/unable-to-fetch-data-from-firebase-firestore-in-react-native

Firebase 문서대로 했는데 React Native에서 안되는 경우가 많습니다. 아직 Firebase에서 React Native를 공식적으로 지원하지 않기 때문입니다. 당분간은 구현에 좀 어려움이 있을 거 같습니다.

이제 해당 메시지에 대한 reference를 얻었으니 아래와 같이 message 상태 변화를 감지할 수 있습니다.

const unsubscribe = await ref
      .onSnapshot(docSnapshot => {
        console.log('doc snapshot', docSnapshot);
        console.log('doc snapshot data', docSnapshot.data());
        if (docSnapshot.data().accepted) {
          // update state
          askAccepted(dispatch);
          // unsubscribe
          unsubscribe();
        }
      }, err => {
        console.log(`Encountered error: ${err}`);
      });
  };

"ref.onSnapshot" 처럼 onSnapshot 메소드를 통해서 해당 DB의 변화를 수신할 수 있습니다. 이 때 리턴값받는데, 메시지 감시를 더이상 할 필요가 없을 때 unsubscribe하기 위함입니다. Firebase의 데이터는 "docSnapshop.data()"처럼 data() 메소드를 통해서 얻을 수 있습니다.

명령 확인을 위한 확인 모달 구현
도움 요청 후에 도움 요청 취소 버튼을 눌렀을 때, 바로 취소하기 보다는 취소를 확인하는 모달(modal)을 띄우는 부분을 구현합니다.

코드는 간단합니다. react-native의 modal 라이브러리를 사용합니다. 단 디자인은 간단히 직접 구현합니다. 다음과 같이 Confirm.js파일을 만들고 내용을 입력합니다. 참고로 아래 코드에는 CardSection과 같이 몇 가지 설명되지 않은 파일들이 사용되고 있습니다. 해당 내용은 간단한 카드 섹션 구현 부분으로 여기서는 생략하도록 하겠습니다.

import React from 'react';
import { View, Text, Modal } from 'react-native';
import { CardSection } from './CardSection';
import { Button } from './Button';

const Confirm = (props) => {
  return (
    <Modal
      visible={props.visible}
      transparent
      animationType="slide"
      onRequestClose={() => {}}
    >
      <View style={styles.container}>
        <CardSection style={styles.cardSection}>
          <Text style={styles.text}>
            {props.children}
          </Text>
        </CardSection>

        <CardSection>
          <Button onPress={props.onAccept}>{props.yesText}</Button>
          <Button onPress={props.onDecline}>{props.noText}</Button>
        </CardSection>
      </View>
    </Modal>
  );
};

const styles = {
  cardSection: {
    justifyContent: 'center'
  },
  text: {
    flex: 1,
    fontSize: 18,
    textAlign: 'center',
    lineHeight: 40
  },
  container: {
    backgroundColor: 'rgba(0, 0, 0, 0.75)',
    position: 'relative',
    flex: 1,
    justifyContent: 'center'
  }
};

export { Confirm };

위 코드에서 중요부분은 Modal 부분입니다. 아래와 같이 설정합니다.

<Modal
      visible={props.visible}
      transparent
      animationType="slide"
      onRequestClose={() => {}}
    >

그리고, Modal에 표시할 문구와 버튼이 눌렸을 때 호출될 콜백함수들을 props으로 연결한 점을 주의해주세요. 먼저 모달에 표시할 메시지 부분입니다.

<Text style={styles.text}>
{props.children}
 </Text>

다음으로, 버튼 콜백함수 설정입니다.

<CardSection>
          <Button onPress={props.onAccept}>{props.yesText}</Button>
          <Button onPress={props.onDecline}>{props.noText}</Button>
</CardSection>

위 콜백함수는 WaitForm.js에 아래와 같이 구현합니다.

// accept modal -> cancel the request
  onAccept = () => {
    // make the modal invisible
    this.setState({ showModal: false });
    // cancel the request
    this.props.cancelRequest();
  }

  // declise modal -> keep waiting
  onDecline = () => {
    // make the modal invisible
    this.setState({ showModal: false });
  }

  // when a user presses the cancel button
  onCancelPress = () => {
    // show modal
    this.setState({ showModal: true });
  }

render() {
return (
(생략)
          {this.waitAcceptance()}
          <Confirm
            visible={this.state.showModal}
            yesText='YES'
            noText='NO'
            onAccept={this.onAccept.bind(this)}
            onDecline={this.onDecline.bind(this)}
          >
            도움 요청을 취소하시겠습니까?
          </Confirm>
(생략)
);
}

위 코드를 보면 Modal의 props을 설정하고 있습니다. 취소 버튼을 누르면 onCancelPress함수가 호출되어 확인 모달이 뜹니다. 모달에서 Yes를 누르면 도움 요청이 취소됩니다. No를 누르면 계속 요청 대기 합니다.

어려운 부분은 없습니다. 위 return 부분 안에 다음 함수를 주목합니다.
this.waitAcceptance()

도움 요청 경과 시간 표시

도움 요청 대기를 하면서 경과 시간 출력합니다. 이를 위해 moment라는 라이브러리를 사용합니다.
moment 사용법 참고: https://medium.com/quick-code/using-moment-js-in-react-native-d1b6ebe226d4

waitAcceptance() {
    if (!this.props.askAccepted) {
      console.log('not askAccepted', this.props.askAccepted);
      return (
        <View>
          <CardSection style={styles.messageContainer}>
            <Spinner size='large' />
          </CardSection>
          <View style={styles.subTextView}>
            <Text style={styles.subText}>
              요청 경과 시간: {moment(this.state.timeElapsed).format('mm[분] ss [초]')}
            </Text>
          </View>
          <CardSection>
            <Button style={styles.button} onPress={this.onCancelPress.bind(this)}>
              도움요청 취소하기
            </Button>
          </CardSection>
        </View>
      );
    }
(생략)
}

경과 시간 표시를 위해 아래와 같이 경과 시간 포맷을 적절히 표시해 주면 됩니다.

<Text style={styles.subText}>
              요청 경과 시간: {moment(this.state.timeElapsed).format('mm[분] ss [초]')}
</Text>

위외 같이 하면 경과 시간이 00분 00초 형태로 표시됩니다.

정리

이것으로 도움을 요청하고 대기하는 부분까지 코드를 구현해봤습니다. 주의할 것은 코드 전문이 포함되지 않고, 주요 부분만 설명된 점입니다.

그럼, 이어서 도움 요청을 수락하는 부분을 구현해 보겠습니다. 이건 더 간단합니다!

[이타인클럽앱] 도움요청 구현 using React Native Firebase | Ecency