Javascript/React

[React] Spring Boot with React 채팅 서버 : 5. components

noahkim_ 2022. 3. 13. 19:38

1. navigation

  • 채팅이 내가 있는 방에 도착하게 될 경우 네비게이션 오른쪽 상단의 messenger icon에 빨간색 원을 그려줌
  • 처음 componenet가 렌더링 될 때, dispatch() 함수를 호출하여 client값을 store에 save함
  • 로그인 성공 이후 localstorage에 저장된 email을 가지고 사용자 정보를 서버에 요청하며 로그아웃 시 email 값이 remove되므로
    userdata가 undefined일 경우 login 창으로 redirect 시켜 로그인하도록 함
const Navigation = () => {

    const email = localStorage.getItem("email");
    const {
        data: userData,
        error,
        revalidate,
        mutate
    } = useSWR<IChatUser | undefined>(email, fetchByEmail);

    const dispatch = useDispatch()
    const history = useHistory();
    const [messengerAlarmed, setMessengerAlarmed] = useState(false)

    const onLogout = useCallback(() => {
        axios.post(`/api/logout`, null, {
            withCredentials: true
        })
            .then(() => {
                mutate(undefined, false);
                localStorage.removeItem("email")
                console.log("logout success");
                console.log("cached user : " + JSON.stringify(userData));
            })
    }, [])

    const onMessengerClick = useCallback(() => {
        setMessengerAlarmed(false)
        console.log("redirect to /messenger");
        history.push('/messenger')
    }, [])

    const subscribeCallback = useCallback(({body}) => {
        console.log("messageAlarm ! " + JSON.stringify(body))
        console.log("current path : " + window.location.pathname)

        if (!window.location.pathname.startsWith("/messenger")) {
            setMessengerAlarmed(true)
        } else {
            console.log("your path is not allowd for messenger notification!")
        }
    }, [])

    const client = useRef<Client>();
    const [connect, disconnect] = useStomp(client, '/sub/messenger/icon/' + userData?.userId, subscribeCallback)

    useEffect(() => {
        connect();
        dispatch(saveStompData(client))
        return () => disconnect();
    }, []);

    if (userData === undefined) {
        console.log("redirect to login");
        return <Redirect to="/login"/>;
    }

    return (
        <Nav>
            <div className="nav-container">
                <div className="nav-1">
                    <FaSatellite/>
                </div>
                <Input className="input-search" id="searchInput"
                       placeholder="검색"
                       type="search"/>
                <div className="nav-2">
                    <span onClick={onLogout}>
                        <BiLogOut>로그아웃</BiLogOut>
                    </span>
                    <span><FaHome/></span>
                    {messengerAlarmed ?
                        <span onClick={onMessengerClick}
                              className="fa-stack fa-sm">
                            <FaRegCircle className="fa-stack-2x" color="red"/>
                            <FaFacebookMessenger className="fa-stack-1x"/>
                        </span>
                        :
                        <span onClick={onMessengerClick}>
                            <FaFacebookMessenger/>
                        </span>
                    }
                    <span><FaRegHeart/></span>
                    <span><FaRegUser/></span>
                </div>
            </div>
        </Nav>
    );
};

export default Navigation;

 

2. chatMenu

  • messenger page에 왼쪽 사이드바 컴포넌트이며, 사용자의 채팅방 목록을 띄워줌
  • 채팅방 목록을 서버에 요청한 후, 응답받은 list의 element 각각에 navlink 태그를 달아 채팅방의 채팅내역을 보도록 컴포넌트를 달아줌
  • 각 element는 conversation 컴포넌트로 만들어 props를 전달해줌
const ChatMenu: FC = () => {
    const email = localStorage.getItem("email");
    const {
        data: userData,
        error,
        revalidate,
        mutate
    } = useSWR<IChatUser>(email, fetchByEmail, {
        dedupingInterval: 20000, // 20초
    });

    const {data: chatRoomsList} = useSWR<IChatRoom[]>(
        `/api/chat/room/list?userId=${userData?.userId}`, fetchChatRooms, {
            dedupingInterval: 5000, // 5초
        });

    return (
        <Div>
            <div className="chatMenuWrapper">
                {chatRoomsList?.map(room => {
                    return (
                        <NavLink key={room.chatRoomId}
                                 activeClassName="selected"
                                 to={{
                                     pathname: `/messenger/dm/${room.chatRoomId}`,
                                     state: {chatRoom: room}
                                 }}>
                            <Conversation chatRoom={room}
                                          currentUserId={userData?.userId}/>
                        </NavLink>
                    )
                })}
            </div>
        </Div>
    )
}

export default ChatMenu

 

3. conversation

  • 채팅방은 총 3개의 구성요소를 가지고 있음
    • 맨 왼쪽에 나와 채팅하는 상대의 프로필 이미지를 띄워줌
    • 그리고 오른쪽에 상대의 닉네임을 띄워주고
    • 가장 마지막에 채팅했던 메시지를 띄워줌
interface Props {
    chatRoom: IChatRoom;
    currentUserId?: number;
}

const Conversation: FC<Props> = ({chatRoom, currentUserId}) => {

    const [user, setUser] = useState<IChatUser>()
    const PF: string | undefined = process.env.REACT_APP_PUBLIC_FOLDER
    const friend: IChatUser | undefined = chatRoom.chatRoomMembers.find((m: IChatUser) => m.userId !== currentUserId);
    const friendId = friend?.userId

    useEffect(() => {
        const getUser = async () => {
            try {
                const friend = await axios("/api/user/?userId=" + friendId);
                setUser(friend.data.HCS.item.profile);
            } catch (err) {
                console.log(err)
            }
        }
        getUser();
    }, [currentUserId, chatRoom])

    return (
        <Div className="conversation">
            <Img
                className="conversationImg"
                src={user?.profileImage ? PF + "person/noAvatar.png" : user?.profileImage}
            />
            <div className="conversationContent">
                <div className="conversationName">{user?.nickname}</div>
                <div
                    className="lastMessage">{chatRoom.lastChatMesg.message}</div>
            </div>
        </Div>
    )
}

export default Conversation

 

4. chatBox

  • 채팅방에 들어갈 때 채팅 내역과 채팅입력이 가능한 컴포넌트로 구성된 컴포넌트이다
  • 최근 15개의 채팅내역이 무엇인지 서버에 요청하여 응답값을 띄워주고
  • dispatch()로 store에 저장된 stomp client를 fetching한다
    • 리턴된 stomp client로 subscribe하여 새로운 메시지가 해당 방에 들어올 때마다 chatList에 메시지를 추가시킨다
  • 또한 채팅메시지를 보낼 때 publish() 호출하여 서버에 메시지 전송
const ChatBox = () => {
    const stompReducer = useSelector((state: RootReducerType) => state.StompReducer)
    const dispatch = useDispatch()
    const {roomId} = useParams<{ roomId?: string }>();
    const chatRoomData = useLocation<any>().state.chatRoom
    const email = localStorage.getItem("email");
    const {
        data: userData,
        error,
        revalidate,
        mutate
    } = useSWR<IChatUser>(email, fetchByEmail, {
        dedupingInterval: 20000, // 20초
    });

    const friend: IChatUser = chatRoomData?.chatRoomMembers.find((m: IChatUser) => m.userId !== userData?.userId)!;
    const [chatList, setChatList] = useState<IChatMessage[]>([])
    const [chat, onChangeChat, setChat] = useInput('');

    const fetchChatMesgs = useCallback(() => {
            const chatListData = axios.get(
                `/api/chat/room/?roomId=${roomId}`
            )
            chatListData.then((response) => setChatList(response.data.HCS.item.latestChatMessages))
        }, []
    )

    const client = useRef<Client>();

    useEffect(() => {
        dispatch(fetchStompData())
        if (stompReducer.success) {
            console.log("dispatch for getting client is success! ")
            client.current = stompReducer.stomp
            client.current?.subscribe('/sub/chat/room/' + chatRoomData.chatRoomId, ({body}) => {
                setChatList((_chatList: IChatMessage[]) => [..._chatList, JSON.parse(body)])
            })
        }
        fetchChatMesgs()
    }, []);
    
    const onSubmitForm = useCallback(
        (e) => {
            e.preventDefault();
            console.log(chat);
            if (chat?.trim()) {
                client.current?.publish({
                    destination: '/pub/chat/message',
                    body: JSON.stringify({
                        roomId: roomId,
                        authorId: userData?.userId,
                        message: chat
                    })
                })
                setChat('');
            }
        },
        [chat, chatList, userData, friend, roomId]
    );

    return (
        <Div>
            <div className="chatBoxWrapper">
                <div className="chatBoxTop">
                    {chatList && chatList.map((chat: IChatMessage, index) => {
                        if (chat.authorId === userData?.userId) {
                            return <Message key={index} isOwn
                                            chatMessage={chat}
                                            me={userData}/>;
                        } else {
                            return <Message key={index}
                                            chatMessage={chat} me={friend}/>;
                        }
                    })}
                </div>
                <div className="chatBoxBottom">
                    <ChatInput chat={chat} friend={friend}
                               onChangeChat={onChangeChat}
                               onSubmitForm={onSubmitForm}/>
                </div>
            </div>
        </Div>
    );
};

export default ChatBox

 

5. message

  • 채팅방의 메시지 각각을 message 컴포넌트로 렌더링하여 뿌림
  • 채팅을 작성한 사람의 프로필 이미지, 이름, 메시지 내용을 가지고 태그로 씌워 컴포넌트를 완성한다
  • 해당 메시지의 작가가 지금 로그인한 사람인지 아닌지에 따라 message 컴포넌트가 왼쪽에 배치될지 결정하고,
    isOwn 속성 여부에 따라 style 을 다르게 하여 스타일을 준다
interface Props {
    isOwn?: boolean;
    chatMessage?: IChatMessage
    me: IChatUser
}

const Message: FC<Props> = ({isOwn = false, chatMessage, me}) => {

    const PF: string | undefined = process.env.REACT_APP_PUBLIC_FOLDER

    return (
        <Div className="message" isOwn={isOwn}>
            <div className="messageTop">
                <Img
                    className="conversationImg"
                    src={me?.profileImage ? PF + "person/noAvatar.png" : me?.profileImage}
                />
                <p className="messageText">{chatMessage?.message}</p>
            </div>
        </Div>
    )
}

export default Message