OVERVIEW

Untitled

채팅 입력 기능 추가

src/components/Chat/ChatInput.jsx

import { Grid, IconButton, InputAdornment, TextField } from '@mui/material'
import InsertEmoticonIcon from '@mui/icons-material/InsertEmoticon'
import ImageIcon from '@mui/icons-material/Image'
import SendIcon from '@mui/icons-material/Send'
import React, { useCallback, useState } from 'react'
import '../../firebase'
import { getDatabase, push, ref, serverTimestamp, set } from 'firebase/database'
import { useSelector } from 'react-redux'

export default function ChatInput() {
  // 채널 정보 가져오기
  const { channel, user } = useSelector(state => state)

  //message 상태
  const [message, setMesssage] = useState('')

  //loading 상태
  const [loading, setLoading] = useState(false)

  //message onChange로 저장
  const handleChange = useCallback(e => {
    setMesssage(e.target.value)
  }, [])

  // 메세지 생성
  const createMessage = useCallback(
    () => ({
      timestamp: serverTimestamp(),
      user: {
        id: user.currentUser.uid,
        name: user.currentUser.displayName,
        avatar: user.currentUser.photoURL,
      },
      content: message,
    }),
    [
      message,
      user.currentUser.uid,
      user.currentUser.displayName,
      user.currentUser.photoURL,
    ]
  )

  // 메시지 전송
  const clickSendMessage = useCallback(async () => {
    // 메세지가 없을경우 방어
    if (!message) return
    setLoading(true)

    // firebass realtimedatabase에 메세지 저장
    try {
      await set(
        push(ref(getDatabase(), 'messages/' + channel.currentChannel.id)),
        createMessage()
      )
      setLoading(false)
      setMesssage('')
    } catch (error) {
      console.error(error)
      setLoading(false)
    }
  }, [message, channel.currentChannel?.id, createMessage])

  return (
    <Grid container sx={{ p: '20px' }}>
      <Grid item xs={12} sx={{ position: 'relative' }}>
        <TextField
          InputProps={{
            startAdornment: (
              <InputAdornment position="start">
                <IconButton>
                  <InsertEmoticonIcon />
                </IconButton>
                <IconButton>
                  <ImageIcon />
                </IconButton>
              </InputAdornment>
            ),
            endAdornment: (
              <InputAdornment position="start">
                <IconButton disabled={loading} onClick={clickSendMessage}>
                  <SendIcon />
                </IconButton>
              </InputAdornment>
            ),
          }}
          autoComplete="off"
          label="메세지 입력"
          fullWidth
          value={message}
          onChange={handleChange}
        />
      </Grid>
    </Grid>
  )
}

firebase에 채널id와 같은 id로 메시지가 등록되도록 구현했다.

Untitled

채팅 메시지 표시 기능 구현

src/components/Chat.jsx

import { Divider, Grid, List, Paper, Toolbar } from '@mui/material'
import React, { useEffect, useState } from 'react'
import ChatHeader from './ChatHeader'
import { useSelector } from 'react-redux'
import ChatInput from './ChatInput'
import ChatMessage from './ChatMessage'
import '../../firebase'
import {
  child,
  get,
  getDatabase,
  onChildAdded,
  orderByChild,
  query,
  ref,
  startAt,
} from 'firebase/database'

export default function Chat() {
  // redux에서 채널명 가져오기
  const { channel, user } = useSelector(state => state)

  // 메세지
  const [messages, setMessages] = useState([])

  // firebase에 저장된 메시지 가져오기
  useEffect(() => {
    // 채널, 유저정보 없을 경우 방어
    if (!channel.currentChannel) return

    // 메시지 가져오기
    async function getMessages() {
      const snapShot = await get(
        child(ref(getDatabase()), 'messages/' + channel.currentChannel.id)
      )
      setMessages(snapShot.val() ? Object.values(snapShot.val()) : [])
    }
    getMessages()
    return () => {
      setMessages([])
    }
  }, [channel.currentChannel])

  // 메시지 정렬 기능 / 최적화
  useEffect(() => {
    if (!channel.currentChannel) return

    // timestamp를 기준으로 정렬
    const sorted = query(
      ref(getDatabase(), 'messages/' + channel.currentChannel.id),
      orderByChild('timestamp')
    )

    // 현재시점부터 추가된 메세지를 콜백으로 추가.
    const unsubscribe = onChildAdded(
      // onChildAdded는 순차적으로 데이터를 가져온다. (동기)
      query(sorted, startAt(Date.now())),
      snapshot => setMessages(oldMessages => [...oldMessages, snapshot.val()])
    )

    return () => {
      unsubscribe?.()
    }
  }, [channel.currentChannel])

  return (
    <>
      <Toolbar />
      <ChatHeader channelInfo={channel.currentChannel} />
      <Grid
        container
        component={Paper}
        variant="outlined"
        sx={{ mt: 3, position: 'relative' }}
      >
        <List
          sx={{
            height: 'calc(100vh - 350px)',
            overflow: 'scroll',
            width: '100%',
            position: 'relative',
          }}
        >
          {messages.map(message => (
            <ChatMessage
              key={message.timestamp}
              message={message}
              user={user}
            />
          ))}
        </List>
        <Divider />
        <ChatInput />
      </Grid>
    </>
  )
}

src/components/Chat/ChatMessage.jsx

Chat.jsx에서 가져온 message를 ChatMessage.jsx에 전달해준다.

import {
  Avatar,
  Grid,
  ListItem,
  ListItemAvatar,
  ListItemText,
} from '@mui/material'
import dayjs from 'dayjs'
import React from 'react'

const relativeTime = require('dayjs/plugin/relativeTime')
dayjs.extend(relativeTime)

export default function ChatMessage({ message, user }) {
  return (
    <ListItem>
      <ListItemAvatar sx={{ alignSelf: 'stretch' }}>
        <Avatar
          variant="rounded"
          sx={{ width: 50, height: 50 }}
          alt="profileImage"
          src={message.user.avatar}
        />
      </ListItemAvatar>
      <Grid container sx={{ ml: 2 }}>
        <Grid item xs={12} sx={{ display: 'flex', justifyContent: 'left' }}>
          <ListItemText
            sx={{ display: 'flex' }}
            primary={message.user.name}
            primaryTypographyProps={{
              fontWeight: 'bold',
              color:
                message.user.id === user.currentUser?.uid ? 'orange' : 'black',
            }}
            secondary={dayjs(message.timestamp).fromNow()}
            secondaryTypographyProps={{ color: 'gray', ml: 1 }}
          />
        </Grid>
        <Grid item xs={12}>
          <ListItemText
            align="left"
            xs={{ wordBreak: 'break-all' }}
            primary={message.content}
          />
          {/* TODO 이미지 추가 */}
          {/* <img alt="이미지" src="" style={{ maxWidth: '100%' }} /> */}
        </Grid>
      </Grid>
    </ListItem>
  )
}