컴포넌트 생성

src/components/Modal/ImageModal.jsx 생성

src/components/Modal/ImageModal.jsx

이미지 추가 모달을 구현하는 컴포넌트

import {
  Button,
  Dialog,
  DialogActions,
  DialogContent,
  DialogTitle,
  Input,
} from '@mui/material'
import React, { useCallback, useState } from 'react'
import { v4 as uuidv4 } from 'uuid'
import '../../firebase'
import {
  getDownloadURL,
  getStorage,
  ref as ref_storage,
  uploadBytesResumable,
} from 'firebase/storage'
import { getDatabase, push, ref, serverTimestamp, set } from 'firebase/database'
import { useSelector } from 'react-redux'

export default function ImageModal({
  open,
  handleClose,
  setPercent,
  setUploading,
}) {
  // channer, user 가져오기
  const { channel, user } = useSelector(state => state)

  // 가져온 파일 set
  const [file, setFile] = useState(null)

  // onChange로 추가된 파일 set
  const onChangeAddFile = useCallback(e => {
    const addedFile = e.target.files[0]
    if (addedFile) setFile(addedFile)
  }, [])

  // 이미지를 추가한 메시지 생성
  const createImageMessage = useCallback(
    fileUrl => ({
      timestamp: serverTimestamp(),
      user: {
        id: user.currentUser.uid,
        name: user.currentUser.displayName,
        avatar: user.currentUser.photoURL,
      },
      image: fileUrl,
    }),
    [
      user.currentUser.uid,
      user.currentUser.displayName,
      user.currentUser.photoURL,
    ]
  )

  // 파일 업로드
  const uploadFile = useCallback(() => {
    setUploading(true) // 업로딩 중으로
    const filePath = `chat/${uuidv4()}.${file.name.split('.').pop()}` // 경로 지정 (split으로 확장자와 분리 후 pop으로 확장자 제거)

    // firebase에 업로드할 메소드
    const uploadTask = uploadBytesResumable(
      ref_storage(getStorage(), filePath),
      file
    )

    // firebase에 업로드
    // 3개의 observers를 등록해야함.
    // 1. 'state_changed observer 
    const unsubscribe = uploadTask.on(
      'state_changed',

      // progress 구현
      snap => {
        const percentUploaded = Math.round(
          (snap.bytesTransferred / snap.totalBytes) * 100
        )
        setPercent(percentUploaded)
      },
      // 2. error observer
      error => {
        console.error(error)
        setUploading(false)
      },

      // 3. completion observer
      async () => {
        try {
          const downloadUrl = await getDownloadURL(uploadTask.snapshot.ref)
          await set(
            push(ref(getDatabase(), 'messages/' + channel.currentChannel?.id)),
            createImageMessage(downloadUrl)
          )
          setUploading(false)
          unsubscribe()
        } catch (error) {
          console.error(error)
          setUploading(false)
          unsubscribe()
        }
      }
    )
  }, [
    channel.currentChannel?.id,
    createImageMessage,
    file,
    setPercent,
    setUploading,
  ])

  // onClick으로 메시지 보내기
  const handleSendFile = useCallback(() => {
    uploadFile() //파일 업로드
    handleClose() // 창 닫기
    setFile(null) // 파일 초기화
  }, [handleClose, uploadFile])

  return (
    <Dialog open={open} onClose={handleClose}>
      <DialogTitle>이미지 보내기</DialogTitle>
      <DialogContent>
        <Input
          margin="dense"
          inputProps={{ accept: 'image/jpeg, image/jpg, image/png, image/gif' }}
          type="file"
          fullWidth
          variant="standard"
          onChange={onChangeAddFile}
        />
      </DialogContent>
      <DialogActions>
        <Button onClick={handleClose}>취소</Button>
        <Button onClick={handleSendFile}>전송</Button>
      </DialogActions>
    </Dialog>
  )
}

src/components/Chat/ChatMessage.jsx

hasOwnProperty() 메소드를 사용한다.

Object.prototype.hasOwnProperty() - JavaScript | MDN

해당 객체가 property를 가지고 있는지 확인해주는 메소드로 message내부에 image 프로퍼티가 있는지 확인해준다.

// message의 프로퍼티 중 image가 있는지 확인해주는 메소드
const IsImage = message => message.hasOwnProperty('image')

위에서 message에 image가 있는지 확인하여 3항연산자로 img태그를 출력하도록 한다.

{/* 이미지가 메시지에 있으면 img태그 출력 *
          {IsImage(message) ? (
            <img
              alt="message"
              src={message.image}
              style={{ maxWidth: '100%' }}
            />
          ) : (
            <ListItemText
              align="left"
              xs={{ wordBreak: 'break-all' }}
              primary={message.content}
            />
          )}

채팅 입력 컴포넌트에 ImageModal을 추가하고 관련 state들을 전달해준다. src/components/Chat/ChatInput.jsx

import {
  Grid,
  IconButton,
  InputAdornment,
  LinearProgress,
  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'
import data from '@emoji-mart/data'
import Picker from '@emoji-mart/react'
import ImageModal from '../Modal/ImageModal'

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

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

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

  //Emoji toggle
  const [showEmoji, setShowEmoji] = useState(false)

  //이미지 추가 모달 토글
  const [imageModalOpen, SetImageModalOpen] = useState(false)

  // 이미지 업로드
  const [uploading, setUploading] = useState(false)

  // 이미지 업로드 퍼센트
  const [percent, setPercent] = useState(0)

  // 이미지 모달 열고 닫기
  const handleClickOpen = () => SetImageModalOpen(true)
  const handleClickClose = () => SetImageModalOpen(false)

  //이모지
  const handleTogglePicker = useCallback(() => {
    setShowEmoji(show => !show)
  }, [])

  //message onChange로 저장
  const handleChange = useCallback(e => {
    setMessage(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)
      setMessage('')
    } catch (error) {
      console.error(error)
      setLoading(false)
    }
  }, [message, channel.currentChannel?.id, createMessage])

  // 이모지를 파싱하는 메소드
  const handleSelectEmoji = useCallback(e => {
    const sym = e.unified.split('-')
    const codesArray = []
    sym.forEach(el => codesArray.push('0x' + el))
    const emoji = String.fromCodePoint(...codesArray)
    setMessage(messageValue => messageValue + emoji)
  }, [])

  return (
    <Grid container sx={{ p: '20px' }}>
      <Grid item xs={12} sx={{ position: 'relative' }}>
        {showEmoji && (
          <Picker
            data={data}
            onEmojiSelect={handleSelectEmoji}
            className="emojipicker"
            title="이모지를 선택하세요."
            emoji="point_up"
            style={{ position: 'absolute', bottom: '20px', right: '20px' }}
            theme="light"
          />
        )}
        <TextField
          InputProps={{
            startAdornment: (
              <InputAdornment position="start">
                <IconButton onClick={handleTogglePicker}>
                  <InsertEmoticonIcon />
                </IconButton>
                <IconButton onClick={handleClickOpen}>
                  <ImageIcon />
                </IconButton>
              </InputAdornment>
            ),
            endAdornment: (
              <InputAdornment position="start">
                <IconButton disabled={loading} onClick={clickSendMessage}>
                  <SendIcon />
                </IconButton>
              </InputAdornment>
            ),
          }}
          autoComplete="off"
          label="메세지 입력"
          fullWidth
          value={message}
          onChange={handleChange}
        />
        {uploading ? (
          <Grid item xs={12} sx={{ m: '10px' }}>
            <LinearProgress variant="determinate" value={percent} />
          </Grid>
        ) : null}
        <ImageModal
          open={imageModalOpen}
          handleClose={handleClickClose}
          setPercent={setPercent}
          setUploading={setUploading}
        />
      </Grid>
    </Grid>
  )
}