서버-클라이언트 실시간 상호작용이 있는 RPS Brawl 어플리케이션을 구현하기 위해서
웹소켓을 사용할 일이 생겼는데,
웹소켓의 적절한 위치가 어디인지에 대한 고민을 하게 되었다.
컨텍스트, 훅, 또는 컴포넌트에 직접 넣는 방식 등 여러가지가 있을 것 같은데, 본 글에서는 웹소켓을 위치시킬 수 있는 다양한 장소와 각각의 장단점을 분석해보려고 한다.
첫번째로, 웹소켓을 컴포넌트에 직접 위치시키는 방법이다 .
가장 간단하게 작동하며 일반적으로 사용되는 방법이다.
함수형 컴포넌트에서는 클래스 컴포넌트처럼 인스턴스 변수를 사용할 수 없으므로 useRef 훅을 사용하여 동일한 기능을 구현해야 한다.
export const WsFunc = () => {
const [val, setVal] = useState(null);
const ws = useRef(null); // 소켓을 저장할 인스턴스 변수
useEffect(() => {
const socket = new WebSocket("wss://echo.websocket.events/");
socket.onopen = () => { // 소켓 연결시 이벤트
console.log("opened");
};
socket.onclose = () => { // 소켓 해제시 이벤트
console.log("closed");
};
socket.onmessage = (event) => {
console.log("got message", event.data);
setVal(event.data);
};
ws.current = socket;
return () => {
socket.close();
};
}, []);
return <div>Value: {val}</div>;
};
📌 왜 웹소켓에 useRef를 사용해야할까?
사실 useRef는 참조용이 아니다. 클래스의 인스턴스 변수와 동일하게 활용할 수 있다.
useState는 리렌더링을 트리거하고 useRef는 그렇지 않기 때문에 useRef를 사용해야 한다.
웹소켓 연결이 생성될 때 컴포넌트를 리렌더링할 필요가 없으므로(새로운 메시지를 받을 때에만 useState를 사용), useRef를 사용하도록 할 것이다.
⭕ 장점
❌ 단점
다음과 같이 웹소켓 훅을 구현할 수도 있다. 하지만 경우에 따라 적합하지 않을 수도 있다.
export const useWs = ({ url }) => {
const [isReady, setIsReady] = useState(false);
const [val, setVal] = useState(null);
const ws = useRef(null);
useEffect(() => {
const socket = new WebSocket(url);
scss
Copy code
socket.onopen = () => setIsReady(true);
socket.onclose = () => setIsReady(false);
socket.onmessage = (event) => setVal(event.data);
ws.current = socket;
return () => {
socket.close();
};
}, []);
return [isReady, val, ws.current?.send.bind(ws.current)];
};
그리고 아래와 같이 사용할 수 있다.
export const WsHook = () => {
const [ready, val, send] = useWs("wss://echo.websocket.events/");
useEffect(() => {
if (ready) {
send("test message");
}
}, [ready, send]); // send를 의존성 배열에 포함해야 합니다.
return (
<div>
Ready: {JSON.stringify(ready)}, Value: {val}
</div>
);
};
이렇게 하면 잘 동작하고 재사용하기 쉽다! 그러나 모든 상황에 적합한 것은 아닌 것 같다.
웹소켓을 훅에 넣는 것은 여러 부분이 각각 다른 서버에 연결해야 할 경우 가장 적합하다. 동일한 서버에 연결해야 하는 것이 여러 부분인 경우에는 훅이 적합하지 않을 수 있다.
⭕ 장점:
❌ 단점:
하지만 앱의 여러 부분이 동일한 서버에 연결해야 하는 경우에는 어떻게 해야 할까? 부모 컴포넌트에 연결을 전달하는 것이 최선일까?
그렇지 않다. 이럴 경우에 Context 를 이용해볼 수 있다.
Context 안에 Websocket을 넣을수가 있다.
export const WebsocketContext = createContext(false, null, () => {});
// ready, value, send
// WebsocketProvider를 컴포넌트 트리의 가장 상위에 위치시키는 것을 잊지 마세요.
export const WebsocketProvider = ({ children }) => {
const [isReady, setIsReady] = useState(false);
const [val, setVal] = useState(null);
const ws = useRef(null);
useEffect(() => {
const socket = new WebSocket("wss://echo.websocket.events/");
scss
Copy code
socket.onopen = () => setIsReady(true);
socket.onclose = () => setIsReady(false);
socket.onmessage = (event) => setVal(event.data);
ws.current = socket;
return () => {
socket.close();
};
}, []);
const ret = [isReady, val, ws.current?.send.bind(ws.current)];
return (
<WebsocketContext.Provider value={ret}>
{children}
</WebsocketContext.Provider>
);
};
그리고 이제 우리의 컨텍스트를 사용하기 위해 컨슈머를 생성해야 합니다.
// 위의 WsHook 컴포넌트와 매우 유사합니다.
export const WsConsumer = () => {
const [ready, val, send] = useContext(WebsocketContext); // 훅처럼 사용하세요
useEffect(() => {
if (ready) {
send("테스트 메시지");
}
}, [ready, send]); // send를 의존성 배열에 포함시키는 것을 잊지 마세요.
return (
<div>
준비됨: {JSON.stringify(ready)}, 값: {val}
</div>
);
};
⭕ 장점
❌ 단점
부모 컴포넌트에 웹소켓을 위치시키면 모든 하위 컴포넌트가 동일한 웹소켓 연결을 사용할 수 있게된다.
export const Parent = () => {
const [isReady, setIsReady] = useState(false);
const [val, setVal] = useState(null);
const ws = useRef(null);
useEffect(() => {
const socket = new WebSocket("wss://echo.websocket.events/");
socket.onopen = () => setIsReady(true);
socket.onclose = () => setIsReady(false);
socket.onmessage = (event) => setVal(event.data);
ws.current = socket;
return () => {
socket.close();
};
}, []);
return (
<div>
<Child ws={ws.current} />
<AnotherChild ws={ws.current} />
</div>
);
};
⭕ 장점:
❌ 단점:
지금까지 웹소켓을 위치시킬 수 있는 여러가지 방법에 대해 알아보았다.
내 경우에는
Websocket을 배치할 수 있는 다양한 방법이 있는데, 각각에는 장단점이 있다. 각자 자신의 상황에 맞게 장단점을 잘 고려해서 적합한 방식을 선택하는 것이 좋을 것 같다.
웹소켓은 파라미터로 넘길 수 없다.
🚫 페이지 이동시에 웹소켓을 전달하려는 시도를 해보았지만, 웹소켓은 연결된 상태를 유지해야 할 뿐더러 전달이 가능한 직렬화된 데이터가 아니었다.
you can only send serializable data in route state
https://stackoverflow.com/questions/71755580/cant-send-socket-with-usenavigate-hook
https://www.kianmusser.com/articles/react-where-put-websocket/
https://stackoverflow.com/questions/59698850/maintaining-a-websocket-connection-with-react-router