Skip to content

[공통] 배포 환경에서 웹소켓 에러 및 구조 개선

SeongHyeon edited this page Dec 3, 2024 · 1 revision

배포 환경에서 업비트 웹소켓에 연결되지 않는 문제가 발생했다.

우선 로컬 환경에서는 다음과 같이 매우 잘 작동한다.

image

하지만 배포한 주소로 접속해서 배포 환경에서 확인하면 화면이 보이지 않고

다음과 같이 웹소캣 연결에 실패했다는 에러가 발생한다.

image

에러의 종류는

  1. CORS
  2. 429 Too Many Requests (요청 제한)

왜 에러가 발생했는가 ?

  1. CORS

    • 로컬 환경(localhost)에서는 웹 브라우저의 보안 정책이 다소 유연하게 적용
    • 배포 환경에서는 더욱 엄격하게 CORS 규칙 적용
    • 그런데, 업비트에 배포 사이트 ip 주소를 등록했음에도 CORS 에러가 발생함.
    • 고민을 더 해봐야겠음.
  2. 429 에러

    • 배포된 사이트는 다수의 사용자가 동일한 API로 요청을 보내기 때문에 다음과 같은 에러가 발생한 것으로 추측
    • 아키텍처를 다음과 같이 리버스 프록시로 하면 되지 않을까 고민중

현재 아키텍처 구조는 다음과 같다.

image

다음과 같이 아키텍처로 개선하려 한다.

image

🛠 마이그레이션 과정

에러가 발생했던 이유를 정리하자면 클라이언트마다 업비트 웹소캣에 연결을 시도하려 했기에

발생했다.

예를 들어, 100명의 사용자가 로그인 중이면 동일한 IP로 업비트에 100개의 웹소캣 요청을 보내는

것이다.

이러한 문제를 해결하고자, 서버에서만 업비트와 웹소캣 연결을 한다. 즉, 1개의 웹소캣 연결만 하고

서버는 웹소캣을 통해 받은 데이터를 클라이언트에 전해주면 된다.

이때, 클라이언트는 SSE 방식으로 서버에게 데이터를 전송 받는다. 클라이언트는 서버에게

실시간 데이터를 전송할 필요가 없기 때문에 SSE 방식으로 충분할 것이라 판단하였다.

필자의 포지션은 FE 이므로 서버 쪽 코드는 생략한다.

서버가 업비트와 웹소캣 연결을 하고, SSE 환경을 구축에 성공한 상황에서 아래 프론트엔드

마이그레이션 과정을 작성한다.

기존에 클라이언트가 서버를 거치지 않고 업비트와 직접 웹소캣을 연결한 코드는 아래와 같다.

useWSTicker 커스텀 훅을 사용하였다.

function useWSTicker(targetMarketCodes: MarketData[]) {
	**const SOCKET_URL = 'wss://api.upbit.com/websocket/v1';**
	const socket = useRef<WebSocket | null>(null);
	const [isConnected, setIsConnected] = useState<boolean>(false);
	const [socketData, setSocketData] = useState<SocketDataType | null>();

	useEffect(() => {
		if (!socket.current) {
			socket.current = new WebSocket(SOCKET_URL);

			const socketOpenHandler = () => {
				setIsConnected(true);
				if (socket.current?.readyState === 1) {
					const sendContent = [
						{ ticket: 'test' },
						{
							type: 'ticker',
							codes: targetMarketCodes.map((code) => code.market),
						},
					];
					socket.current.send(JSON.stringify(sendContent));
				}
			};

			const socketCloseHandler = () => {
				setIsConnected(false);
				setSocketData(null);
			};

			const socketMessageHandler = async (event: MessageEvent) => {
				try {
					const buffer = await event.data.arrayBuffer();
					const decoder = new TextDecoder();
					const message = decoder.decode(buffer);
					const parsedData = JSON.parse(message);

					setSocketData((prev) => ({
						...prev,
						[parsedData.code]: parsedData,
					}));
				} catch (error) {
					console.error(error);
				}
			};

			socket.current.onopen = socketOpenHandler;
			socket.current.onclose = socketCloseHandler;
			socket.current.onmessage = socketMessageHandler;

			return () => {
				if (socket.current) {
					if (socket.current.readyState != 0) {
						socket.current.close();
						socket.current = null;
					}
				}
			};
		}
	}, [targetMarketCodes]);

	return { socket: socket.current, isConnected, socketData };

가장 핵심은 SOEKET_URL = 'wss://api.upbit.com/websocket/v1' 이다.

직접 위의 URL로 웹소캣 요청을 보내고 데이터를 받아왔던 것이다.

아래는 더 이상 업비트와 직접 웹소캣 요청을 하지 않고, 서버를 통해 SSE 방식으로 데이터를

받아오는 코드이다.

export function useSSETicker(targetMarketCodes: { market: string }[]) {
	**const BASE_URL = SERVER_URL;**
	const eventSource = useRef<EventSource | null>(null);
	const [isConnected, setIsConnected] = useState<boolean>(false);
	const [sseData, setSSEData] = useState<SSEDataType | null>();

	useEffect(() => {
		**const queryString = targetMarketCodes
			.map((code) => `coins=${encodeURIComponent(code.market)}`)
			.join('&');
		const url = `${BASE_URL}?${queryString}`;**

		if (!eventSource.current) {
			eventSource.current = new EventSource(url);

			const sseOpenHandler = () => {
				setIsConnected(true);
			};

			const sseErrorHandler = (error: Event) => {
				console.error('SSE Error:', error);
				setIsConnected(false);
				if (eventSource.current) {
					eventSource.current.close();
					eventSource.current = null;
				}
			};

			const handlePriceUpdate = (event: MessageEvent) => {
				try {
					const parsedData = JSON.parse(event.data);
					setSSEData((prev) => ({
						...prev,
						[parsedData.code]: parsedData,
					}));
				} catch (error) {
					console.error('Failed to parse SSE message:', error);
				}
			};

			eventSource.current.onopen = sseOpenHandler;
			eventSource.current.onerror = sseErrorHandler;
			**eventSource.current.addEventListener('price-update', handlePriceUpdate);**

			return () => {
				if (eventSource.current) {
					eventSource.current.removeEventListener(
						'price-update',
						handlePriceUpdate,
					);
					eventSource.current.close();
					eventSource.current = null;
					setIsConnected(false);
					setSSEData(null);
				}
			};
		}
	}, [targetMarketCodes]);
	return {
		eventSource: eventSource.current,
		isConnected,
		sseData,
	};
}

const BASE_URL = SERVER_URL; 해당 부분을 위의 웹소캣 방식과 비교하면 이제 더 이상

클라이언트는 직접 업비트와 통신하지 않고 서버와 통신을 한다.

queryString은 데이터를 전달 받고 싶은 코인 종목을 결정하는 query이다.

서버에서는 전달 받은 queryString에 적힌 코인에 대한 정보만 SSE 방식으로 보내준다.

예를 들어 SERVER_URL/coins=BTC&coins=DOGI 로 요청하면 BTC,DOGI 종목에 대한

실시간 데이터를 SSE 방식으로 받아오게 된다.

참고로 위의 eventSource.current.addEventListener('price-update', handlePriceUpdate);

이 코드를 보면 price-update 라는 이벤트 타입을 이벤트 핸들러에 할당한 것을 볼 수 있다.

해당 타입은 서버에서 정해준다.

클라이언트는 원하는 이벤트 타입만 SSE 통신으로 데이터를 받을 수 있다.

image

💻 개발 일지

💻 공통

💻 FE

💻 BE

🙋‍♂️ 소개

📒 문서

☀️ 데일리 스크럼

🤝🏼 회의록

Clone this wiki locally