본문 바로가기

공부일지/Project

React Project ) Slack Clone, 채널, 채팅

728x90
반응형
728x90
반응형

안녕하세요. 프론트를 공부하고 있는 비전공자 에디터 M입니다.

저번시간에 이어서 Slcak Clone 코딩 세번째 이야기!! Slack Clone 코딩은 다음이야기가 마지막이 될거 같네요.

아마 해당 프로젝트 리뷰가 끝나면 다음시간부터는 프로젝트가 아닌 Three.js 공부에 관한 블로그 글을 작성 할 것 같아요.

왜냐면 요즘 Three.js쪽에 너무 관심이 많이가네요. 꽤 재밌는거 같아요.

이전편을 못보신 분들은 아래의 링크를 남겨 놓을께요.

 

Slack Clone 1편

2023.06.28 - [공부일지/project] - React Project ) Slack Clone, 회원가입 페이지, 로그인 페이지 만들기, redux

 

 

React Project ) Slack Clone, 회원가입 페이지, 로그인 페이지 만들기, redux

안녕하세요. 프론트 공부중인 비전공자 에디터 M입니다. 회사 업무와 여려가지 이유로 잠시 블로그에 글을 작성하지 못했는데 포기하지 않고 다시 블로그 글을 작성하러왔습니다. 그럼 제가 블

min-webstory.tistory.com

Slack Clone 2편

2023.06.29 - [공부일지/project] - React Project ) Slack Clone, 메인페이지, Header, 프로필 변경, 로그아웃 작업

 

React Project ) Slack Clone, 메인페이지, Header, 프로필 변경, 로그아웃 작업

안녕하세요. 저번시간에 이어서 Slack Clone 코딩 두번째 이야기!! 프론트 공부하고 있는 비전공자 에디터 M입니다. 저는 원래 퍼블리싱을 하였는데 개발쪽 공부를 하려고 하니 예전에 jsp와 php 배웠

min-webstory.tistory.com

 

그럼 이제 코드 리뷰 시작해 볼께요.

 

Slack Clone - 채널 작업


저번 시간에 만들었던 Main 파일에 있던 ChannelMenu 컴포넌트의 완성 코드는 깃허브를 보시면 됩니다.

Slack Clone 채널화면

 

아래의 코드는 채널 리스트를 출력하는 코드와 다이아로그 (팝업창)을 띄우는 기능을 작업한 코드입니다.

const [open, setOpen] = useState(false);
const [channels, setChannels] = useState([]);
const [activeChannelId, setActiveChannelId] = useState('');

const handleClose = () =>{
    setOpen(false)
    setChannelName('')
    setChannelDetail('')
}

const handleOpen = () =>{
    setOpen(true)
}

<List sx={{overflow:'auto', width:240, backgroundColor:theme.mainTheme}}>
    <ListItem secondaryAction={
        <IconButton sx={{color:'#9a939b'}} onClick={handleOpen}>
            <AddIcon />
        </IconButton>
    }>
        <ListItemIcon sx={{color:'#9a939b'}} >
            <ArrowDropDownIcon />
        </ListItemIcon>
        <ListItemText primary='채널' sx={{wordBreak:'break-all', color:'#9a939b'}} />
    </ListItem>
    <List component='div' disablePadding sx={{pl:3}}>
        {
            channels.map(channel=>(
                <ListItemButton 
                    key={channel.id} 
                    onClick={()=>changeChannel(channel)}
                    selected={channel.id === activeChannelId}
                >
                    <ListItemText primary={`# ${channel.name}`} sx={{wordBreak:'break-all', color:'#918890'}} />
                </ListItemButton>
            ))
        }
    </List>
</List>

* 참고로 채널 리스트 출력부분은 실질적인 채널 추가 기능이 없을때는 아무것도 할 수 없는 그런 코드입니다.

 

채널 추가 작업을 데이터를 할때 넣을 채널들의 정보를 담을 channels라는 state를 하나 만듭니다.
해당 state를 map으로 하나씩 출력시킵니다. ListItemButton으로 채널을 클릭하면 해당 채널의 채팅으로 전환되며 해당 아이템을 select했는지 여부도 체크 하는 기능을 가집니다.

 

이제 채널 추가 버튼을 눌렀을때 나오는 팝업화면과 채널 추가 기능 코드를 볼께요.

const {theme} = useSelector(state=>state)
const [channelName, setChannelName] = useState('');
const [channelDetail, setChannelDetail] = useState('');
const [activeChannelId, setActiveChannelId] = useState('');
const [firstLoaded, setFirstLoaded] = useState(true)
useEffect(()=>{
    const unsubscribe = onChildAdded(ref(getDatabase(), 'channels'), (snapshot)=>{
        setChannels((channelArr)=>[...channelArr, snapshot.val()])
    })
    return () => {
        setChannels([])
        unsubscribe()
    }
},[]);

const changeChannel = (channel) => {
    setActiveChannelId(channel.id);
    dispatch(setCurrentChannel(channel))
}

const handleSubmit = useCallback(async () => {
    const db = getDatabase();
    const key = push(child(ref(db), 'channels')).key;
    const newChannel = {
        id:key,
        name:channelName,
        details:channelDetail
    };

    const updates = {};
    updates['/channels/'+key] = newChannel;

    try {
        await update(ref(db), updates)
        handleClose()
    } catch (error) {
        console.error(error)
    }
}, [channelDetail, channelName])

useEffect(()=>{
    if(channels.length > 0 && firstLoaded){
        setActiveChannelId(channels[0].id);
        dispatch(setCurrentChannel(channels[0]))
        setFirstLoaded(false)
    }
},[channels, dispatch, firstLoaded])


<Dialog open={open} onClose={handleClose}>
    <DialogTitle>채널 추가</DialogTitle>
    <DialogContent>
        <DialogContentText sx={{marginBottom:'10px'}}>
            생성할 채널명과 설명을 입력해주세요.
        </DialogContentText>
        <TextField autoFocus margin='dense' label='채널명' type='text' fullWidth variant='standard' onChange={(e)=>setChannelName(e.target.value)} />
        <TextField autoFocus margin='dense' label='설명' type='text' fullWidth variant='standard' onChange={(e)=>setChannelDetail(e.target.value)} />
    </DialogContent>
    <DialogActions>
        <Button onClick={handleClose}>취소</Button>
        <Button onClick={handleSubmit}>생성</Button>
    </DialogActions>
</Dialog>

handleSubmit을 눌렀을때 firebase에서 db를 불러오고 데이터를 push()해서 key값을 받습니다. 참고로 push는 array에서 사용하는 push가 아니라 firebase에서 사용하는 기능입니다. 데이터를 추가하는 기능이라고 합니다.

받을 키값과 채널, 채널 설명의 데이터를 가지고 newChannel의 변수를 만들어 firebase에 데이터를 넣을 객체를 만듭니다.

try catch를 사용해 문제가 없으면 db에 데이터를 넣고 에러가 나면 에러코드를 출력시킵니다.

 

 

useEffect(()=>{
    const unsubscribe = onChildAdded(ref(getDatabase(), 'channels'), (snapshot)=>{
        setChannels((channelArr)=>[...channelArr, snapshot.val()])
    })
    return () => {
        setChannels([])
        unsubscribe()
    }
},[]);

채널이 추가되면 채널리스트에도 추가되야 되니  먼저 db에 저장되어있는 채널목록을 가져고 오는데 onChildAdded를 사용해 검색하거나 아이템 리스트에 변화가 있을때 사용합니다.

새로운 채널이 추가 되었으므로 기존 채널에 새로운 채널을 추가하는 합니다. 추가할떄는 push가 아니가 스프레드문법을 사용해서 추가하여야 합니다.

그리고 리턴할때 기존 채널 리스트를 한번 초기화한 후 위에 선언한 unsubscribe를 불러와 리스트의 정보를 갱신 합니다.

useEffect(()=>{
    if(channels.length > 0 && firstLoaded){
        setActiveChannelId(channels[0].id);
        dispatch(setCurrentChannel(channels[0]))
        setFirstLoaded(false)
    }
},[channels, dispatch, firstLoaded])

다시 로그인 해서 접속시 채널의 목록이 있으면 0번째 채널을 선택합니다.

그리고 리렌더링시 다시 채널이 firstLoaded이 계속 true면 0번째 채널이 계속 선택되는걸 방지하기 위해 loaded는 false로 변경합니다.

const changeChannel = (channel) => {
    setActiveChannelId(channel.id);
    dispatch(setCurrentChannel(channel))
}

채널 변경을 할때 store에 해당 채널의 id를 전달해줍니다.

 

Channel reducer의 코드도 보여드릴께요.

const SET_CURRENT_CHANNEL = 'SET_CURRENT_CHANNEL';

export const setCurrentChannel = (channel) => ({type:SET_CURRENT_CHANNEL, currentChannel:channel})

const initialState = {currentChannel:null}

const channelReducer = (state=initialState, action) => {
    switch (action.type){
        case SET_CURRENT_CHANNEL :
            return{
                currentChannel:action.currentChannel
            }
        default:
            return state;
    }
}

export default channelReducer

이렇게 채널 목록 리스트 변경작업을 완료 하였습니다.

 

이제 채팅 화면 코드를 리뷰해 볼께요.

 

Slack Clone - 채널 작업


해당 페이지는 기능과 UI를 한번에 다 보여드리기엔 코드 길이가 길거 같아서 보기 불편하실거 같아 UI코드 먼저 보여드립니다.

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

            </div>
        </List>
        <Divider />
        <ChatInput />
    </Grid>
</>

채팅 화면 작업에서 제일 중요한건 store에서 채널과 유저의 정보를 가져오는것과 db에서 내용 들고오는 부분입니다. 그런데 중요한 부분은 처리하면 채팅화면은 완료 되는거 같습니다.

 

채팅화면은 입장 후 입력하면 DB와 화면에 출력되는 부분과 접속시 기존에 작성되었던 메세지들을 불러오는 부분 2가지로 작업했어요.

먼저 실시간으로 채팅을추가하는 부분입니다.

채널과 유저정보, db에서 내용을 들고오는 부분 부터 볼께요.

const {channel, user} = useSelector(state => state);
const [messages, setMessages] = useState([]);
const messageEndRef = useRef()

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])

useSelector으로 store에서 유저와 채널 정보를 가져오고 메세지 정보를 담을 배열도 하나 만들어준다.

 

채널클릭해서 전달한 채널정보의 데이터를 기반으로 해당 정보가 없으면 바로 return이 되도록 한다.

데이터가 있을시 async로 getMessages의 함수를 만들고 해당 정보의 id값을 db의 messages의 테이블에서 채널 아이디와 같은 값의 데이터들을 가져온다.

그리고 가져온 데이터가 있을 시 객체의 값만 가져와 messages에 넣고 데이터가 없으면 빈 배열을 선언한다.

위에서 만든 getMessages를 불러오는것으로 메세지 출력 부분은 완료가 된다.

firebase의 Method들은 나도 정확하게는 알지 못해서 말씀을 못드려서 죄송합니다. 계속 공부중이라 그분도 제가 공부하고 알게 되면 다음 블로그에서는 조금 더 상세하게 적어 드릴게요.

 

기존에 있던 채팅들을 가져오는 코드입니다.

useEffect(() => {
    if(!channel.currentChannel) return;
    const sorted = query(ref(getDatabase(), 'messages/'+channel.currentChannel.id), orderByChild('timestamp'))
    const unsubscribe = onChildAdded(
        query(sorted, startAt(Date.now())),
        (snapShot) => 
            setMessages((oldMessages)=>[...oldMessages, snapShot.val()])
    );
    return ()=>{
        unsubscribe?.();
    }
}, [channel.currentChannel]);

실시간 메세지 방식과 비슷하죠

이번에는 update하는게 아니라 read하는 거라 query를 사용했습니다. query는 db에서 데이터를 읽는데 사용한다고 합니다. 그리고 조건문을 같이 작성 할 수 있다고 해요.

 

채팅 메세지를 db에서 가져오는데 orderByChild에서 timestamp를 넣었는데 시간순으로 정렬되게 작업했습니다.

그리고 그 시간을 현재 시간을 기준으로 오래된 데이터 부터 차례대로 setMessages에 넣습니다.

그러면 이제 채팅 화면 부부은 작업이 완료되었습니다.

 

후..... 주니어라 코드 리뷰도 엉성하고 힘드네요..... 그래도 점점 발전하는 모습을 보여드릴께요.

왜냐면 저는 할 수 있으니깐요. 혹시 저처럼 주니어 분들도 하나씩 같이 해봐요. 할 수 있을꺼요.

다음시간에는 채팅 메세지입력과, 테마 를 작업해 볼께요.

그럼 빠잉~~

 

728x90
반응형