이 글은 전에 작성했던 게시물과 이어진다.
- 지난번 실시간으로 데이터를 가져오는 것에 대한 몇 가지 방법에 대해 작성했었다.
- 당시 작성했던 웹소켓 연결 코드는 정말 간단한 부분만 구현되어 있었기 때문에, 좀더 그럴싸하게(?) 보충할 수 있지 않을까 하고 몇 가지를 추가해보았다.
※ 아래의 코드는 핵심적인 부분만 발췌해 추가하였기에 실제로 사용하려면 개인의 환경에 맞게끔 보완하는 과정이 필요하리라 예상된다.
초기 코드
서론에서 이미 언급했듯 초기 코드는 상당히 기본적인 기능만 구현되어있었다. 아래가 대강의 초기 코드를 재현한 코드이다. 나중에 다른 페이지에서도 사용할 수 있게 만들기 위해 커스텀 훅의 형태로 구현한 모습이다.
import { useEffect } from 'react';
export const useWebSocket = (url: string, callback: () => void) => {
const websocketRef = useRef<WebSocket>(null);
useEffect(() => {
// WebSocket 연결 시작
websocketRef.current = new WebSocket(url);
websocketRef.current.onopen = (e: MessageEvent) => {
console.log('Websocket opened');
};
websocketRef.current.onmessage = (e: MessageEvent) => {
callback();
};
websocketRef.current.onerror = (error: Event) => {
// Handle Error...
console.log(error);
};
}, []);
return () => {
if (websocketRef.current) {
websocketRef.current.close();
}
};
};
- useEffect를 사용해서 페이지 마운트 시 웹소켓을 연결한다.
- useEffect의 클린업 함수를 통해 웹소켓 연결을 해제한다.
- 서버로부터 이벤트를 수신할 시 특정 로직을 수행한다.
...다만 이것만 쓰기엔 뭔가 부족해보여서, 더 추가할 수 있는게 없나 싶어 찾아보던 중 GPT에게 어떤 점을 보완하면 좋을지 의견을 구해보았다. GPT가 제시한 것은 아래와 같다.
- 연결이 끊겼을 때 재연결 로직을 넣어보기
→ EventSource를 사용해서 SSE를 구현하면 재연결이 자동적으로 이루어지지만, 웹소켓은 자동 재연결이 없으므로 직접 구현할 필요가 있다. - 에러 처리를 구현해보기 (위 코드는 아주 간략하게 에러 내용을 출력하는 코드가 추가되어있다)
- 기타 등등
이런 점들을 보다 보니 문득 웹소켓 연결을 쉽게 할 수 있도록 구현된 라이브러리가 있지 않을까? 해서 살펴보니 과연 있었다. reconnecting-websocket이라는 라이브러리인데 여기서 제공하는 기능 중 '웹소켓 연결이 끊겼을 시, 특정 시간 이후(reconnectInterval)에 최대 N(maxReconnectAttempts)번까지 재연결을 시도하는 기능'이 있었다.
이 정도의 기능은 구현할 수 있을 것 같아 기존 코드를 좀더 보충해보기로 했다. 크게 보충할 점은 아래와 같다.
- 기존 웹훅에 새로운 인자를 몇 개 더 추가한다. 추가할 인자는 아래와 같다.
- retry: 재연결을 몇 번까지 시도할지를 나타내는 인자이다.
- reconnectInterval: 재연결을 얼마나 뒤에 시도할 것인지 시간을 나타내는 인자이다.
+) 사실 reconnectInterval을 왜 추가하는 걸까? 싶어서 알아보길, 아래와 같은 이점 때문이라고 한다.
- 서버의 과부하를 방지할 수 있다. 여러 클라이언트가 동시에 재연결을 시도할 경우, 그만큼 서버에 가해지는 부담이 커지기 때문에 이를 예방한다.
- 재연결에 약간의 텀을 주는 것으로 서버가 대응할 시간을 줌으로써, 연결 회복 시간을 확보한다.
- 웹소켓이 끊기고 바로 연결 시도 → 다시 끊김의 과정을 반복하면 사용자에게 좋지 못한 경험을 줄 수 있다.
→ 이와 연관된 전략을 '백오프(Backoff)' 전략이라고 한다고.
인자를 추가한다면 이제 인자를 사용해 재시도 로직을 구현해야 한다. 재시도 로직은 아래와 같이 돌아간다.
- onclose 이벤트가 호출되면 현재까지의 재시도 횟수를 확인한다.
- 재시도 횟수가 아직 최대 횟수(retry)에 도달하지 않았다면 웹소켓 재연결을 시도한다.
- 재연결에 성공하면 재시도 횟수를 초기화한다.
여기서 재시도 횟수는 useRef를 사용해 관리한다.
import { useRef } from 'react';
const retryRef = useRef<number>(0);
평소에 하도 useState를 자주 쓰고, useRef하면 어쩐지 DOM 노드 조작이 먼저 떠올라 자꾸 잊어버리곤 하는데... useRef는 리렌더링을 일으키지 않으면서 값을 저장하고 참조할 수 있는 훅이다. 이렇게 렌더링에 직접적으로 사용되지 않는 값이 아니라면 useRef를 통해 제어할 수 있다.
여기까지 작성하고 확인해보니, 이미 재시도에 들어갔는데도 onopen 이벤트가 여러 번 호출되는 현상이 일어나는 것을 확인했다. 아무래도 현재 연결 중인지, 아닌지 여부를 나타내는 값도 추가할 필요성이 있으리라 판단하여 isConnecting 이라는 값을 추가하기로 했다.
이렇게 추가하면 아래와 같은 모양이 된다.
import { useEffect, useRef } from 'react';
export const useWebSocket = (
url: string,
callback: () => void,
retry = 5,
reconnectInterval = 1000
) => {
const websocketRef = useRef<WebSocket>(null);
const isConnectingRef = useRef<boolean>(false);
const retryRef = useRef<number>(0);
const connectWebSocket = () => {
if (websocketRef.current) return;
websocketRef.current = new WebSocket(url);
websocketRef.current.onopen = (e: MessageEvent) => {
console.log('Websocket opened');
retryRef.current = 0;
isConnectingRef.current = false;
};
websocketRef.current.onmessage = (e: MessageEvent) => {
callback();
};
websocketRef.current.onerror = (error: Event) => {
// Handle Error...
console.log(error);
};
websocketRef.current.onclose = () => {
if (retryRef.current < retry) {
retryRef.current += 1;
isConnectingRef.current = true;
setTimeout(() => connectWebSocket, reconnectInterval);
}
};
};
useEffect(() => {
// WebSocket 연결 시작
connectWebSocket();
return () => {
if (websocketRef.current) {
websocketRef.current.close();
}
};
}, []);
};
- 웹소켓 연결이 끊기고 onclose 가 트리거될 때, retryRef를 확인하여 현재까지의 재시도 횟수를 확인하고, 이것이 최대값보다 작을 경우 재시도 횟수를 1 추가한 후 isConnectingRef를 true로 바꿔준다.
- setTimeout을 통해 설정한 인터벌(reconnectInterval)만큼의 시간이 지난 후 웹소켓 재연결을 시도한다.
- isConnectingRef 를 통해 연결 상태를 관리함으로써, 여러 번 onopen이 트리거되던 현상이 대강 해결되었다.
- 이 글이 많은 참고가 되었다.
- 되게 간단한 구현에서 시작해서 깊은 곳까지 빠지는 느낌이라 읽는 내내 재밌었던.
'공부 > FE' 카테고리의 다른 글
상태 관리 라이브러리 비교해보기 (2) | 2024.12.19 |
---|---|
프록시 설정을 알아보자 (0) | 2024.11.23 |
Hydration이란? (0) | 2024.11.07 |
실시간으로 서버에서 데이터를 가져와보기 (feat. Polling/Websocket/SSE) (0) | 2024.11.03 |
PostMessage 알아보기 (3) | 2024.10.20 |