-
Notifications
You must be signed in to change notification settings - Fork 1
[공통] 배포 환경에서 웹소켓 에러 및 구조 개선
배포 환경에서 업비트 웹소켓에 연결되지 않는 문제가 발생했다.
우선 로컬 환경에서는 다음과 같이 매우 잘 작동한다.
하지만 배포한 주소로 접속해서 배포 환경에서 확인하면 화면이 보이지 않고
다음과 같이 웹소캣 연결에 실패했다는 에러가 발생한다.
에러의 종류는
- CORS
- 429 Too Many Requests (요청 제한)
왜 에러가 발생했는가 ?
-
CORS
- 로컬 환경(localhost)에서는 웹 브라우저의 보안 정책이 다소 유연하게 적용
- 배포 환경에서는 더욱 엄격하게 CORS 규칙 적용
- 그런데, 업비트에 배포 사이트 ip 주소를 등록했음에도 CORS 에러가 발생함.
- 고민을 더 해봐야겠음.
-
429 에러
- 배포된 사이트는 다수의 사용자가 동일한 API로 요청을 보내기 때문에 다음과 같은 에러가 발생한 것으로 추측
- 아키텍처를 다음과 같이 리버스 프록시로 하면 되지 않을까 고민중
현재 아키텍처 구조는 다음과 같다.
다음과 같이 아키텍처로 개선하려 한다.
에러가 발생했던 이유를 정리하자면 클라이언트마다 업비트 웹소캣에 연결을 시도하려 했기에
발생했다.
예를 들어, 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 통신으로 데이터를 받을 수 있다.
- [FE] TailwindCSS @apply
- [FE] 캐러셀 구현
- [FE] 사이드 바 상태관리 도전기
- [FE] axios interceptor로 로그인 필요한 api 개선하기
- [FE] Tanstack Query API 최적화 도전기
- [FE] Tanstack Query로 구현하는 무한 스크롤 차트 도전기
- [FE] 차트 무한 스크롤링 최적화 도전기
- [FE] 차트 실시간 등락 구현 도전기
- [FE] 검색 구현 및 검색 API 호출 최적화 도전기
- [FE] 고차 컴포넌트를 활용한 인증 접근 제어
- [FE] 코드 스플릿팅으로 최적화 도전기
- [BE] Server 생성
- [BE] CI/CD
- [BE] GitAction 학습 정리
- [BE] ssh터널링으로 db연결
- [BE] 배포환경에서 DB 연결 및 테스트 완료
- [BE] https 적용
- [BE] upbit api 연결 및 SSE api
- [BE] SSE 구현
- [BE] SSE 에러
- [BE] redis 설치 및 연동
- [BE] 트랜잭션 락 구현과 최적화
- [BE] Oauth CORS
- [BE] QueryRunner 사용 시 발생한 문제점과 해결방안
- [BE] Git Action 학습 정리
- [BE] NestJS 학습 정리
- [BE] 로그인 기능 및 리프레시토큰
- [BE] 비회원 체험 기능
- [BE] Nginx 학습 정리
- [BE] Mixed Content와 HTTPS 보안 구현하기
- [BE] 매수/매도 로직 구현 및 개선 과정
- [BE] Queue, Load Balancing, Redis