Websocket과 stompjs를 활용해 채팅 기능을 구현하던 중, client가 null이라는 오류가 발생했다.
채팅을 구현하려면 먼저 연결(connect)을 해야 되고, 구독(subscribe), 보내기(send) 순으로 진행이 되어야 한다.
처음 구현하는 거라서, 구글링으로 채팅 구현을 찾아봤다. 그러나, 찾아본 모든 구글링에서 하나의 함수에 모든 기능을 구현했었다. 나는 채팅을 시작하는 페이지에서 구독과 연결을 하고, 채팅 페이지에서 보내기 기능을 사용해야 해서, 여러 custom hook에 기능을 나눠서 구현하려고 했다. 그러나, 여기서 stompjs를 사용한 client는 recoil에 담아 전역상태로 관리를 하지 못한다는 문제가 발생했다.
하나의 페이지에서 connectHandler를 작동하고 또다른 페이지에서 sendHandler를 작동하려고 하는데 이렇게 해서는 useChat()이 사용하는 곳마다 새로운 인스턴스를 사용하면 client 값이 초기화가 됐었다. 값을 유지하고 싶었고 recoil에 client를 담는 건 불가능이라고 떠서... connectHandler와 sendHandler를 다른 hooks로 분리하는 방법도 생각해 봤는데 그러면 또 client값이 connect 한 값이 아니었다. (한 함수에서 connectHandler, sendHandler, disconnectHandler 실행하면 잘 작동했었다)
(참고로, stompjs 버전은 7.0.0이다)
"@stomp/stompjs": "^7.0.0",
import { CompatClient, Stomp } from "@stomp/stompjs";
import { useRef } from "react";
import { useRecoilState } from "recoil";
import { inputMessageState, messageState } from "../../states/chatting";
export function useChat() {
const [messages, setMessages] = useRecoilState(messageState);
const [inputMessage, setInputMessage] = useRecoilState(inputMessageState);
const token = localStorage.getItem("accessToken");
// 채팅 연결 구독
const client = useRef<CompatClient>();
const connectHandler = () => {
client.current = Stomp.over(() => {
const sock = new WebSocket("wss://m-ssaem.com:8080/stomp/chat");
return sock;
});
client.current.connect(
{
token: token,
},
() => {
client.current &&
client.current.subscribe(`/sub/chat/room/1`, onMessageReceived, {
token: token!,
});
},
);
};
const onMessageReceived = (message: any) => {
setMessages((prevMessage) => [...prevMessage, JSON.parse(message.body)]);
};
// 채팅 나가기
const disconnectHandler = () => {
if (client.current) {
client.current.disconnect(() => {
window.location.reload();
});
}
};
// 채팅 보내기
const sendHandler = () => {
if (client.current && inputMessage.trim() !== "") {
client.current.send(
`/pub/chat/message`,
{
token: token,
},
JSON.stringify({
roomId: 1,
message: inputMessage,
type: "TALK",
}),
);
setInputMessage("");
}
};
return {
connectHandler,
disconnectHandler,
sendHandler,
};
}
아무리 생각해봐도 답이 나오지 않아.. 모든 커뮤니티에 글을 올려봤다.
지금 구조에선 clientRef는 useChat이 사용되는 곳마다 초기화될 수밖에 없다는 답변을 받았다. 사용하는 곳마다 새로운 인스턴스를 사용하는 상황이며 해결 방법 중 하나론 Context API로 전역에서 사용하거나 원하는 범위에서 사용하는 방법을 추천받았다.
그래서 Context API를 사용해 전역관리를 하기로 결정했다.
또한, 토큰은 connect 한테 인자로 받고, 리코일 상태는 같이 안 쓰고, sendHandler도 인풋메시지를 인자로 받는 편이 좋을 것 같다는 조언을 받아 추가로 코드를 개선했다.
먼저, index.js에 ChatProvider를 감싸줬다.
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { RecoilRoot } from "recoil";
import { QueryClient, QueryClientProvider } from "react-query";
import { ReactQueryDevtools } from "react-query/devtools";
import { ChatProvider } from "./hooks/chatting/ChatProvider";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement,
);
const queryClient = new QueryClient();
root.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={true} />
<RecoilRoot>
<ChatProvider>
<App />
</ChatProvider>
</RecoilRoot>
</QueryClientProvider>
</React.StrictMode>,
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
그리고, ChatProvider를 다음과 같이 작성했다.
import { CompatClient, Stomp } from "@stomp/stompjs";
import { createContext, useContext, useMemo, useRef } from "react";
import { useSetRecoilState } from "recoil";
import { messageState } from "../../states/chatting";
const ChatContext = createContext(
{} as {
connect: (roomId: number) => void;
disconnect: () => void;
send: (roomId: number, message: string) => void;
},
);
export const useChatContext = () => useContext(ChatContext);
export function ChatProvider({ children }: any) {
const setMessages = useSetRecoilState(messageState);
const token = localStorage.getItem("accessToken");
// 채팅 연결 구독
const client = useRef<CompatClient>();
const connect = (roomId: number) => {
client.current = Stomp.over(() => {
const sock = new WebSocket("wss://m-ssaem.com:8080/stomp/chat");
return sock;
});
client.current.connect(
{
token: token,
},
() => {
client.current &&
client.current.subscribe(
`/sub/chat/room/${roomId}`,
onMessageReceived,
{
token: token!,
},
);
},
);
return client;
};
const onMessageReceived = (message: any) => {
setMessages((prevMessage) => [...prevMessage, JSON.parse(message.body)]);
};
// 채팅 나가기
const disconnect = () => {
if (client.current) {
client.current.disconnect(() => {
window.location.reload();
});
}
};
// 채팅 보내기
const send = (roomId: number, message: string) => {
if (client.current) {
client.current.send(
`/pub/chat/message`,
{
token: token,
},
JSON.stringify({
roomId: roomId,
message: message,
type: "TALK",
}),
);
}
};
const handlers = useMemo(() => ({ connect, disconnect, send }), []);
return (
<ChatContext.Provider value={handlers}>{children}</ChatContext.Provider>
);
}
이렇게 ContextAPI를 사용함으로써 채팅 오류는 해결이 됐다!!!!!!!!
그러나, 이 코드에서도 문제가 있다. 새로고침 하지 않는 이상, 채팅이 잘 되지만, 새로고침을 하면 subscribe와 connect한 것들이 사라지는 문제점이다. 페이지를 새로고침할 때, 현재 페이지의 모든 상태와 메모리에 있는 데이터는 초기화되며, JavaScript의 실행 환경도 다시 시작된다. 따라서 WebSocket과 같은 연결도 닫히게 되고, 다시 연결해야 한다. 기본적으로 웹 브라우저는 새로고침을 할 때 현재 페이지의 모든 리소스와 상태를 초기화하고, 페이지를 처음부터 다시 로드하기 때문이다. useEffect에 연결을 시도하거나 reconnect 하는 로직을 추가해야 된다.
그래서 다음 포스팅에서는 새로고침을 해결하는 문제를 다룰 것이다.
'🍞 Front-End > React' 카테고리의 다른 글
[React] File과 JSON 동시에 주고 받기 (Multipart/form-data) (0) | 2023.02.12 |
---|---|
[React] useMutation를 활용한 데이터 생성, 수정, 삭제 (0) | 2023.02.06 |
[React] 리페칭(Re-fetching)에 대해서 (0) | 2023.02.06 |
[React] Recoil-persist 사용해보기 (0) | 2023.02.03 |
[React] CORS 에러 해결하기 (0) | 2023.01.30 |