본문 바로가기

공부일지/Project

React Project ) Slack Clone, 이모지, 첨부파일, 채팅입력

728x90
반응형
728x90
반응형

안니영~~~ 드디어 Slack Clone 마지막 시간으로 돌아온 에디터 M입니다.

다들 취미가 어떻게 되시나요?? 저는 시간날때 메이플을 하는거 같아요. 그냥 메일 코드만 보다가 아무 생각없이 뇌빼기 사냥을 하면서 머리를 비우는거 같아요.

 

다들 너무 공부만 하지말고 쉬엄 쉬엄 쉬면서 공부해보세요.

 

그럼 시작할게요~~

 

 

Slack Clone - 이모지, 첨부파일, 채팅입력


저번시간에 보셨는지 모르겠지만 Chat 컴포넌트에 메세지 출력 컴포넌트가 있었는데 

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


function ChatMessage({message, user}) {

    const IsImages = (message) => message.hasOwnProperty('image')

    return (
        <ListItem>
            <ListItemAvatar sx={{alignSelf:'stretch'}}>
                <Avatar 
                    variant='rounded' 
                    sx={{width:50, height:50}} 
                    alt='profile image'
                    src={message.user.avatar}
                />
            </ListItemAvatar>
            <Grid container sx={{ml:2}}>
                <Grid item xs={12} sx={{display:'flex', justifyContent:'left'}} >
                    <ListItemText 
                        sx={{display:'flex', mb:0}} 
                        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}}
                    >
                        
                    </ListItemText>
                </Grid>
                <Grid item xs={12}>
                    {
                        IsImages(message) 
                        ? <img alt="message" src={message.image} style={{maxWidth:'100%'}} />
                        : <ListItemText align='left' sx={{wordBreak:'break-all'}} primary={message.content} />
                    }
                </Grid>
            </Grid>
        </ListItem>
    )
}

간단히 설명드리자면 작성자면 이름에 주황색으로 나오고 다른 유저면 이름이 검은색으로 나옵니다. 그리고 

dayjs를 불러와 오늘 날짜 기준으로 몇일 차이나는지 나오는 시간 부분도 만들어줍니다.

 

채팅입력 영역 작업


Input 영역

const handleMessage = 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)
    try {
        await set(
            push(ref(getDatabase(), 'messages/' + channel.currentChannel.id)),
            createMessage()
        );
        setLoading(false);
        setShowEmoji(false)
        setMessage('')
    } catch (error) {
        console.error(error)
    }

}, [message, channel.currentChannel?.id, createMessage])

const handleKeyUp = (e) => {
    if (e.keyCode === 13) {
        clickSendMessage()
    }
}

<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>
        )
    }}
    onKeyUp={handleKeyUp}
    label='메세지 입력'
    fullWidth
    value={message}
    onChange={handleMessage}
/>

위에 코드는 input부분만 적용되는 코드입니다.

먼저 input에 작성한 내용과 그 내용을 기반으로 채팅 기본 데이터를 설정하는 코드입니다.

const handleMessage = 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])

채팅정보에 타임은 서버의 시간으로 했다. 유저는 user의 정보들을 각각 다 전달하고 컨텐츠에 메세지를 저장한다.

제일 중요한 채팅 데이터를 db로 보내는 작업이다.

const clickSendMessage = useCallback(async () => {
    if (!message) return;
    setLoading(true)
    try {
        await set(
            push(ref(getDatabase(), 'messages/' + channel.currentChannel.id)),
            createMessage()
        );
        setLoading(false);
        setShowEmoji(false)
        setMessage('')
    } catch (error) {
        console.error(error)
    }

}, [message, channel.currentChannel?.id, createMessage])

메세지는 db에서 messages 테이블에 channel.id에 저장한다.

db에 저장된 채널과 메세지들

엔터와 전송버튼을 클릭하면 이렇게 db에 저장이 된다.

그럼 어떤 코드로 데이터를 저장하는지 한번 확인해 보자.

const clickSendMessage = useCallback(async () => {
    if (!message) return;
    setLoading(true)
    try {
        await set(
            push(ref(getDatabase(), 'messages/' + channel.currentChannel.id)),
            createMessage()
        );
        setLoading(false);
        setShowEmoji(false)
        setMessage('')
    } catch (error) {
        console.error(error)
    }

}, [message, channel.currentChannel?.id, createMessage])

const handleKeyUp = (e) => {
    if (e.keyCode === 13) {
        clickSendMessage()
    }
}

이제 어느정도 firebase의 코드에 대해서 눈에 익는거 같아요. 데이터를 update를 하기 위해 push로 데이터를 넣는작업을 해줘요.

ref로 db에서 어디로 넣을지 정하고 위에서 만든 message를 넣어주면 끝이네요 밑에 emoji는 조금있다가 말씀드릴께요.

엔터키가 키번호가 13이라 해당 키를 눌렀다 뗐을때 클릭과 동일한 기능을 하도록 했어요. 

 

채팅에 사진을 넣는 거는 또 모달을 만들어서 작업했기 때문에 이모지 먼저 설명드릴께요.

 

이모지 작업


const addEmoji = 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);
}, [])


{showEmoji && (
    <div style={{ position: 'absolute', bottom: '80px' }}>
        <Picker 
            set='google' 
            className='emojipicker' 
            previewEmoji='point_up'
            title='이모티콘을 선택해 주세요.'
            onEmojiSelect={addEmoji}
        />
    </div>
)}

이모지 같은경우에는 emoji-mart 로 이모지를 선택했는데 해당 Method에 다양한 옵션들은 아래 깃헙에서 확인하시면 됩니다.

https://github.com/missive/emoji-mart

 

GitHub - missive/emoji-mart: 🏪 One component to pick them all

🏪 One component to pick them all. Contribute to missive/emoji-mart development by creating an account on GitHub.

github.com

이모지 기능은 따로 설명드리지느 못할 것같고 위에 코드랑 동일하게 작성하시면됩니다. 

그리고 기존 메세지 데이터에 이모지를 추가해서 다시 메세지를 저장하셔야 합니다. 안그럼 엑박이 뜨거나 이모지가 안나타 날꺼에요.

 

 

이미지 작업


function ImageModal({open, handleClose, setPercent, setUploading}) {
    const {channel, user} = useSelector(state=>state);

    const [file, setFile] = useState(null)

    const addFile = useCallback((e)=>{
        const fileItem = e.target.files[0]
        if(fileItem) setFile(fileItem);
    }, []);

    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()}`;
        const uploadTask = uploadBytesResumable(ref_storage(getStorage(),filePath), file)
        const unsubscribe = uploadTask.on('state_changed', (snap)=>{
            const percentUploaded = Math.round((snap.bytesTransferred / snap.totalBytes) * 100)
            setPercent(percentUploaded)
        }, (error)=>{
            setUploading(false)
            console.error(error)
        }, async () => {
            try {
                const downloadUrl = await getDownloadURL(uploadTask.snapshot.ref)
                await set(
                    push(ref(getDatabase(),'messages/'+channel.currentChannel?.id)),
                    createImageMessage(downloadUrl)
                )
                unsubscribe()
            } catch (error) {
                setUploading(false)
                console.error(error);
                unsubscribe();
            }
        }
        );
    }, [channel.currentChannel?.id, createImageMessage, file, setPercent, setUploading])

    const sendFile = 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={addFile}
                />
            </DialogContent>
            <DialogActions>
                <Button onClick={handleClose}>취소</Button>
                <Button onClick={sendFile}>전송</Button>
            </DialogActions>
        </Dialog>
    )
}

채팅기능과 비슷한게 많을꺼에요. 왜냐면 사진은 첨부파일로 올리면 바로 채팅으로 등록되기때문이에요.

채팅입력과 중복되는 부분은 빼고 코드를 볼께요.

const [file, setFile] = useState(null)

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 addFile = useCallback((e)=>{
    const fileItem = e.target.files[0]
    if(fileItem) setFile(fileItem);
}, []);

const uploadFile = useCallback(() => {
    setUploading(true)
    const filePath = `chat/${uuidv4()}.${file.name.split('.').pop()}`;
    const uploadTask = uploadBytesResumable(ref_storage(getStorage(),filePath), file)
    const unsubscribe = uploadTask.on('state_changed', (snap)=>{
        const percentUploaded = Math.round((snap.bytesTransferred / snap.totalBytes) * 100)
        setPercent(percentUploaded)
    }, (error)=>{
        setUploading(false)
        console.error(error)
    }, async () => {
        try {
            const downloadUrl = await getDownloadURL(uploadTask.snapshot.ref)
            await set(
                push(ref(getDatabase(),'messages/'+channel.currentChannel?.id)),
                createImageMessage(downloadUrl)
            )
            unsubscribe()
        } catch (error) {
            setUploading(false)
            console.error(error);
            unsubscribe();
        }
    }
    );
}, [channel.currentChannel?.id, createImageMessage, file, setPercent, setUploading])

input에 첨부파일을 가져와서 storage에 chat이라는 저장소를 만들고 그안에 uuid 첫번째 시간에 말했던 이름을 암호화 하는 기능 진짜 암호화는 아니지만 무튼 그렇게 파일명을 변경하고 확장자를 저장합니다. split해서 pop으로 제일 왼쪽에 있는데이터를 빼주고 확장자만 추가합니다.

 

그렇게 수정한 파일명을 스토리지에 추가하고 코드에 uploadBytesResumable 은 아래 링크에서 자세히 확인 가능합니다.
https://firebase.google.com/docs/storage/web/upload-files?hl=ko

 

웹에서 Cloud Storage로 파일 업로드  |  Cloud Storage for Firebase

Google I/O 2023에서 Firebase의 주요 소식을 확인하세요. 자세히 알아보기 의견 보내기 웹에서 Cloud Storage로 파일 업로드 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하

firebase.google.com

그리고 이미지 업로드시 몇퍼센트 완료되었는지 확인할 수 있도록 퍼센트 작업도 진행했습니다.

이렇게 하면 업로드는 끝입니다. 그런데  퍼센트 코드 밑에  async해서 downloadURL로 시작하는 코드들이 있죠??

이게 뭐냐면 이제 이미지는 업로드 했으니 채팅창에 출력을 시켜야 해서 다운로드 URL을 가지고 채팅창에 추가합니다.

그러면 이제 업로드하고 업로드 했던 이미지를 다시 가져오는 코드로 채팅작업이 완료되었습니다.

 

이제 그럼 채팅 부분의 코드들은 끝이 납니다.

slack clone 완성 페이지

비록 인강을 보며 따라 쳤던 코드였지만 지금까지 했던 코드들에 비해 생각보다 이렇다 할 오류들은 없었던 프로젝트였습니다.

 

좀더 자세한 코드는 깃허브를 참고해 주시면 됩니다.

 

그래도 길었던 slack clone코딩이 끝이 났네요. 많이 부족하지만 봐주신분들 감사드립니다.

 

그리고 제가 많이 모르기때문에 잘못된 내용이 있으면 댓글로 부탁드립니다.

 

 

728x90
반응형