리액트-파이어베이스 (채팅 어플리케이션 만들기) #3. 채팅 페이지 기본 구조

김지원·2020년 12월 13일
0

React

목록 보기
25/31

채팅 페이지 기본 구조

채팅 페이지 UI 만들기

리액트에서 bootstrap 사용하기

설치
$ npm install react-bootstrap bootstrap

index.js에 import 추가

import "bootstrap/dist/css/bootstrap.min.css";

사용하고자하는 파일에 import 해주고 사용하면 된다!!

import Dropdown from "react-bootstrap/Dropdown";
import Image from "react-bootstrap/Image";

...

 <Image
          src="holder.js/171x180"
          style={{ width: "30px", height: "30px", marginTop: "3px" }}
          roundedCircle
        />
<Dropdown>
          <Dropdown.Toggle id="dropdown-basic">user name</Dropdown.Toggle>

          <Dropdown.Menu>
            <Dropdown.Item href="#/action-1">프로필 사진 변경</Dropdown.Item>
            <Dropdown.Item href="#/action-2">로그아웃</Dropdown.Item>
          </Dropdown.Menu>
        </Dropdown>

...

react-icons에서 icon 사용하기

import { IoIosChatboxes } from "react-icons/io";

...

 <h3 style={{ color: "white", display: "flex", alignItems: "center" }}>
        <IoIosChatboxes />
        <div style={{ marginLeft: "5px" }}> Chat App</div>
      </h3>

리덕스에서 user 정보가져와서 프로필 만들기

useSelector로 리덕스 store에 저장된 currentUser를 가져와서
user가 있다면 user에 저장된 photoURL로 이미지 src를 설정해준다.
user에 있는 displayName으로 프로필 이름도 설정해준다.

import { useSelector } from "react-redux";

const UserPanel = () => {
  const user = useSelector((state) => state.user.currentUser);
  return (
    ...
    <Image
          src={user && user.photoURL}
          ...
    />
	
   <Dropdown>
         ...
          
  {user && user.displayName}
        ...

실행 화면

로그아웃 & 리덕스 스토에서 유저 정보 지워주기

1. firebase에서 로그아웃하기

firebase에서 로그아웃 하려면
.auth에 접근해서 signOut 메소드를 사용해서 로그아웃 해주면 된다!

import firebase from "../../../firebase";

const UserPanel = () => {
 
const handleLogout = () => {
   firebase.auth().signOut();
 };
 
 ...
 
<Dropdown.Item onClick={handleLogout}>로그아웃</Dropdown.Item>
 

2. 리덕스 스토어에서 user 지워주기

App.js에서 dispatch(clearUser())

import { setUser, clearUser } from "./redux/actions/user_action";
...
function App() {
  
useEffect(() => {
    firebase.auth().onAuthStateChanged((user) => {
      //user가 있으면 로그인이 된 상태
      if (user) {
        history.push("/");
        dispatch(setUser(user));
      } else {
        history.push("/login");
>>>>    dispatch(clearUser());
      }
    });
  }, []);
  ...

redux actions user_action.js

import { SET_USER, CLEAR_USER } from "./types";
...

export function clearUser() {
  return {
    type: CLEAR_USER,
  };
}

types.js에 추가
export const CLEAR_USER = "clear_user";

redux reducers user_reducer.js에 CLEAR_USER 추가

currentUser를 null로 바꾸준다!

import { SET_USER, CLEAR_USER } from "../actions/types";
...
  case CLEAR_USER:
      return {
        ...state,
        currentUser: null,
        isLoading: false,
      };
...

프로필 이미지 수정하기

Firebase Storage 서비스에 파일을 넣어준다.
파일에 대한 정보는 Firebase DB에 넣어준다.

프로필 사진 변경을 누르면 파일 업로드 창 뜨게 하기

type이 file인 input 창을 display: 'none'으로 없애주고 ref를 이용해서 '프로필 사진 변경'을 눌렀을 때 눌리게 해주자!

<Dropdown.Item onClick={handleOpenImageRef}>
              프로필 사진 변경
            </Dropdown.Item>
...
  <input
        onChange={handleUploadImage}
        accept="image/jpeg, image/png"
        style={{ display: "none" }}
        ref={inputOpenImageRef}
        type="file"
      /

useRef를 사용해서 원하는 dom에 접근한다.

 const inputOpenImageRef = useRef();

 const handleOpenImageRef = () => {
    inputOpenImageRef.current.click();
  };

이미지 클릭 후 먼저 Firebase 스토리지에 저장하기

uploadTaskSnapshot = await firebase.storage().ref()
    .child(`user_image/${user.uid}`)
    .put(file, metadata)

업로드할 file의 데이터를 첫번째 인자에 넣어주고, 두번째 인자에는 metadata를 넣어줘야한다.
metadata는 {contentType: 'image/png'}
file을 살펴보면 type에 image/png가 적혀있음 이 type을 metadata에 넣어주어도 되고, mime-types를 이용해서 metadata에 넣어 줘도 된다.

설치
$ npm install mime-types --save

handleUploadImage 함수 작성하기

 const handleUploadImage = async (event) => {
    const file = event.target.files[0];
    const metadata = { contentType: mime.lookup(file.name) };

    //스토리지에 파일 저장하기
    //user_image 폴더 안에다가 저장이 됨
    try {
      let uploadTaskSnapshot = await firebase
        .storage()
        .ref()
        .child(`user_image/${user.uid}`)
        .put(file, metadata);
    } catch (error) {}
  };

스토리지에 이미지가 저장된 화면

리덕스 스토어 데이터 변경 및 데이터베이스에 데이터 저장하기

Firebase 스토리지에 저장시킨 사진 url을 받아와서
1. currentUser.updateProfile의 photoURL 업데이트시켜준다.

  1. 그리고 dispatch를 사용해서 리덕스 스토어에 저장된 photoURL도 변경시켜줘서 화면에 출력되는 이미지를 변경시킨다.

  2. 마지막으로 데이터베이스에 저장되어있는 image를 변경시켜 준다.

import { setPhotoURL } from "../../../redux/actions/user_action";

const UserPanel = () => {
  ...
  const dispatch = useDispatch();
   ...

  const handleUploadImage = async (event) => {
    ...
     let downloadURL = await uploadTaskSnapshot.ref.getDownloadURL();

      //프로필 이미지 수정
      await firebase.auth().currentUser.updateProfile({
        photoURL: downloadURL,
      });

      dispatch(setPhotoURL(downloadURL));

      //데이터베이스 유저 이미지 수정
      await firebase
        .database()
        .ref("users")
        .child(user.uid)
        .update({ image: downloadURL });
    } catch (error) {
      alert(error);
    }
  };
    

redux actions user_actions.js

import { SET_USER, CLEAR_USER, SET_PHOTO_URL } from "./types";

export function setPhotoURL(photoURL) {
  return {
    type: SET_PHOTO_URL,
    payload: photoURL,
  };
}

redux actions types.js 코드
SET_PHOTO_URL 추가

//USER TYPES
export const SET_USER = "set_user";
export const CLEAR_USER = "clear_user";
export const SET_PHOTO_URL = "set_photo_url";

redux reducers user_reducer.js

currentUser에서 모든 state는 그대로 두고 photoURL만 변경!

 case SET_PHOTO_URL:
      return {
        ...state,
        currentUser: { ...state.currentUser, photoURL: action.payload },
        isLoading: false,
      };

데이터베이스에서 image가 gravatar에서 firebasestorage..로 변경된것을 확인 할 수 있다.

바뀐 프로필 사진

채팅 룸 리스트 UI 만들기

(chatRoom.js는 파이어베이스에서 데이터 실시간으로 받아올때 함수형 컴포넌트로 할때 오류가 나서 class 컴포넌트로 했다고 함)

부트스트랩을 사용해서 create chat room 모달 창 만들기

import { FaRegSmileWink } from "react-icons/fa";
import { FaPlus } from "react-icons/fa";
import Modal from "react-bootstrap/Modal";
import Button from "react-bootstrap/Button";
import Form from "react-bootstrap/Form";

class ChatRoom extends Component {
  ...
 <div>
        <div
          style={{
            position: "relative",
            width: "100%",
            display: "flex",
            alignItems: "center",
          }}
        >
          <FaRegSmileWink style={{ marginRight: 3 }} />
          CHAT ROOMS (1)
          <FaPlus
            style={{ position: "absolute", right: 0, cursor: "pointer" }}
            onClick={this.handleShow}
          />
        </div>

        {/* ADD CHAT ROOM MODAL */}
        <Modal show={this.state.show} onHide={this.handleClose}>
          <Modal.Header closeButton>
            <Modal.Title>Create a chat room</Modal.Title>
          </Modal.Header>
          <Modal.Body>
            <Form onSubmit={this.handleSubmit}>
              <Form.Group controlId="formBasicEmail">
                <Form.Label>방 이름</Form.Label>
                <Form.Control
                  onChange={(e) => this.setState({ name: e.target.value })}
                  type="text"
                  placeholder="Enter a chat room name"
                />
              </Form.Group>

              <Form.Group controlId="formBasicPassword">
                <Form.Label>방 설명</Form.Label>
                <Form.Control
                  onChange={(e) =>
                    this.setState({ description: e.target.value })
                  }
                  type="text"
                  placeholder="Enter a chat room description"
                />
              </Form.Group>
            </Form>
          </Modal.Body>
          <Modal.Footer>
            <Button variant="secondary" onClick={this.handleClose}>
              Close
            </Button>
            <Button variant="primary" onClick={this.handleSubmit}>
              Create
            </Button>
          </Modal.Footer>
        </Modal>
      </div>
    );
  }

파이어베이스 데어터베이스에 chatRooms 테이블 만들고 거기에 추가한 chatRoom들 저장해주기

  • chatRoomsRef를 이용해 firebase database chatRooms 테이블에 접근한다.

  • isFormValid로 이름과 설명이 있는지만 유효성 검사를 해준다.

  • addChatRoom 함수에서는 chatRooms 테이블에 자동으로 생성시킨 키를 만들어주고,

  • newChatRoom에 현재 상태의 name과 description을 넣어주고,

  • 리덕스에서 user를 받아와 user 정보도 넣어준다.

  • Firebase에 chatRooms 테이블에 key를 넣어주고
    거기에 채팅방 정보를 넣어준다.
    chatRoomsRef.child(key).update(newChatRoom);

class ChatRoom extends Component {
  state = {
    show: false,
    name: "",
    description: "",
    //firebase database chatRooms table에 접근
    chatRoomsRef: firebase.database().ref("chatRooms"),
  };
  handleClose = () => this.setState({ show: false });
  handleShow = () => this.setState({ show: true });

  handleSubmit = (e) => {
    e.preventDefault();
    const { name, description } = this.state;

    if (this.isFormValid(name, description)) {
      this.addChatRoom();
    }
  };

  isFormValid = (name, description) => {
    return name && description;
  };

  addChatRoom = async () => {
    //자동으로 생성된 키 넣어주기
    const key = this.state.chatRoomsRef.push().key;
    const { name, description } = this.state;
    const { user } = this.props;
    const newChatRoom = {
      id: key,
      name: name,
      description: description,
      createdBy: {
        name: user.displayName,
        image: user.photoURL,
      },
    };
    try {
      await this.state.chatRoomsRef.child(key).update(newChatRoom);
      this.setState({
        name: "",
        description: "",
        show: false,
      });
    } catch (error) {
      alert(error);
    }
  };
  • 클래스형 컴포넌트 이므로 리덕스에서 user를 받아올때 hook을 사용하지 못하고, connect를 사용한다.

  • mapStateToProps를 사용해 state에 들어있는 정보를 여기서 props로 바꿔서 사용해준다.

import { connect } from "react-redux";
...
//state에 들어있는거를 여기서 props로 바꿔서 사용하겠다
const mapStateToProps = (state) => {
  return { user: state.user.currentUser };
};
export default connect(mapStateToProps)(ChatRoom);

파이어베이스에 추가된것을 볼 수 있다.

Firebase에서 데이터 실시간으로 받기

Firebase에서 데이터가 추가가 되면 DataSnapshot에 저장이 된다.

추가된 채팅 룸 목록들 보여주기

화면에 보여주기 위해서 statechatRooms 배열을 만들어준다.

  • AddChatRoomsListeners에서 chatRoomsRef에 child가 추가될때마다 DataSnapshot에 들어가게 된다.
  • DataSnapshot의 value를 배열로 받은 후 state의 chatRooms를 업데이트 시켜준다.
  • renderChatRooms에서 chatRooms안에 채팅 룸이 하나 이상일 때 룸의 이름을 반환해준다.
class ChatRoom extends Component {
  state = {
    show: false,
    name: "",
    description: "",
    //firebase database chatRooms table에 접근
    chatRoomsRef: firebase.database().ref("chatRooms"),
>>> chatRooms: [],
  };
componentDidMount() {
    this.AddChatRoomsListeners();
  }

  AddChatRoomsListeners = () => {
    let chatRoomsArray = [];

    //listener를 통해서 생성된 데이터가 바로 array에 들어감
    this.state.chatRoomsRef.on("child_added", (DataSnapshot) => {
      chatRoomsArray.push(DataSnapshot.val())
      this.setState({ chatRooms: chatRoomsArray });
    });
  };

renderChatRooms = (chatRooms) =>
    chatRooms.length > 0 &&
    chatRooms.map((room) => <li key={room.id}># {room.name}</li>);

...
render() {
    return (
      <div>
      ...
      <ul style={{ listStyleType: "none", padding: 0 }}>
          {this.renderChatRooms(this.state.chatRooms)}
        </ul>
...

누른 채팅 룸 리덕스에 저장해주기


  changeChatRoom = (room) => {
    //리덕스에 지금 선택한 채팅룸 전달
 >>> this.props.dispatch(setCurrentChatRoom(room));
    //active 된 채팅룸 id  배경색 변경하기 위해 설정
    this.setState({ activeChatRoomId: room.id });
  };
...
 renderChatRooms = (chatRooms) =>
    chatRooms.length > 0 &&
    chatRooms.map((room) => (
      <li
        key={room.id}
        style={{
          cursor: "pointer",
          backgroundColor:
            room.id === this.state.activeChatRoomId && "#ffffff45",
        }}
>>>     onClick={() => this.changeChatRoom(room)}
      >
        # {room.name}
      </li>
    ));

redux actions types

//CHAT ROOM TYPES
export const SET_CURRENT_CHAT_ROOM = "set_current_chat_room";

redux actions chatRoom_action.js

import { SET_CURRENT_CHAT_ROOM } from "./types";

export function setCurrentChatRoom(currentChatRoom) {
  return {
    type: SET_CURRENT_CHAT_ROOM,
    payload: currentChatRoom,
  };
}

redux reducers chatRoom_reducer

import { SET_CURRENT_CHAT_ROOM } from "../actions/types";

const initialChatRoomState = {
  currentChatRoom: null,
};

export default function (state = initialChatRoomState, action) {
  switch (action.type) {
    case SET_CURRENT_CHAT_ROOM:
      return {
        ...state,
        currentChatRoom: action.payload,
      };
    default:
      return state;
  }
}

clean up event listener

componentWillUnmount() {
    //child_added listener off 해주는 것
    this.state.chatRooms.off();
  }

0개의 댓글

관련 채용 정보