[React] 웹소켓의 적절한 위치

사요·2022년 12월 30일
1

UNIJAM

목록 보기
2/3

서버-클라이언트 실시간 상호작용이 있는 RPS Brawl 어플리케이션을 구현하기 위해서
웹소켓을 사용할 일이 생겼는데,
웹소켓의 적절한 위치가 어디인지에 대한 고민을 하게 되었다.

컨텍스트, 훅, 또는 컴포넌트에 직접 넣는 방식 등 여러가지가 있을 것 같은데, 본 글에서는 웹소켓을 위치시킬 수 있는 다양한 장소와 각각의 장단점을 분석해보려고 한다.

  • 컴포넌트 내부
  • 컨텍스트 ( Context API )
  • 훅 ( Hook )

컴포넌트 내부

첫번째로, 웹소켓을 컴포넌트에 직접 위치시키는 방법이다 .
가장 간단하게 작동하며 일반적으로 사용되는 방법이다.

구현

함수형 컴포넌트에서는 클래스 컴포넌트처럼 인스턴스 변수를 사용할 수 없으므로 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를 사용하도록 할 것이다.

장단점

⭕ 장점

  • 컴포넌트에 웹소켓을 넣는 것은 가장 쉽고 간단한 방법이며 자체적으로 독립적이다.
  • 모든 웹소켓 코드가 이 컴포넌트에 남게되므로 추적하기가 더 쉽다.

❌ 단점

  • 다른 컴포넌트에서 쉽게 접근할 수 없다(자식 컴포넌트에만 props를 통해 전달된 경우는 제외). - 앱의 한 부분만 웹소켓이 필요한 경우에는 괜찮지만, 여러 부분으로 확장되면 불편해질 수 있다.

웹소켓 훅 사용

다음과 같이 웹소켓 훅을 구현할 수도 있다. 하지만 경우에 따라 적합하지 않을 수도 있다.

구현

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>
);
};

이렇게 하면 잘 동작하고 재사용하기 쉽다! 그러나 모든 상황에 적합한 것은 아닌 것 같다.

장단점

웹소켓을 훅에 넣는 것은 여러 부분이 각각 다른 서버에 연결해야 할 경우 가장 적합하다. 동일한 서버에 연결해야 하는 것이 여러 부분인 경우에는 훅이 적합하지 않을 수 있다.

⭕ 장점:

  • 다른 서버에 대해 여러 웹소켓 연결을 처리하는 데 용이하다. 동일한 훅을 사용하여 서로 다른 서버에 쉽게 연결할 수 있다.
  • 컴포넌트와 함께 사용하기 쉽다. 모든 부분에서 useWs와 useEffect 훅만 사용하면 된다.

❌ 단점:

  • 단일 연결에는 적합하지 않다. 앱의 여러 부분이 동일한 웹소켓에 접근해야 하는 경우 이 훅은 작동하지 않는다.
  • useWs를 사용할 때마다 새로운 연결이 생성되므로 동일한 서버로 여러 연결이 생성될 수 있다.

하지만 앱의 여러 부분이 동일한 서버에 연결해야 하는 경우에는 어떻게 해야 할까? 부모 컴포넌트에 연결을 전달하는 것이 최선일까?

그렇지 않다. 이럴 경우에 Context 를 이용해볼 수 있다.

Context API

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>
);
};

장단점

⭕ 장점

  • 하나의 Websocket만 사용된다. WebsocketContext를 사용하는 앱의 모든 부분이 동일한 Websocket을 사용한다.
  • 컴포넌트와 함께 사용하기 쉽다. 위의 훅 방법과 동일하게 간단하다.

❌ 단점

  • 웹소켓을 선언한 모든 부분이 동일한 메시지를 수신한다. 특정 부분만 특정 메시지를 수신하도록 다중화를 구현해야 할 수 있다.
  • 여러 서버에 연결하기 어렵다. 이 경우 코드를 복제하거나 여러 개의 컨텍스트를 생성하는 함수를 만들어야 한다.
    대부분의 경우 (단일 Websocket 연결 및 여러 부분이 필요한 경우)

부모 컴포넌트

부모 컴포넌트에 웹소켓을 위치시키면 모든 하위 컴포넌트가 동일한 웹소켓 연결을 사용할 수 있게된다.

구현

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을 배치할 수 있는 다양한 방법이 있는데, 각각에는 장단점이 있다. 각자 자신의 상황에 맞게 장단점을 잘 고려해서 적합한 방식을 선택하는 것이 좋을 것 같다.

P.S

웹소켓은 파라미터로 넘길 수 없다.

🚫 페이지 이동시에 웹소켓을 전달하려는 시도를 해보았지만, 웹소켓은 연결된 상태를 유지해야 할 뿐더러 전달이 가능한 직렬화된 데이터가 아니었다.

you can only send serializable data in route state

Ref

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

profile
하루하루 나아가는 새싹 개발자 🌱

0개의 댓글