구름에서 진행한 Web IDE 프로젝트에서 실시간 채팅 구현을 맡게 되었고,
해당 기능을 SockJS + STOMP 조합으로 구현했다.
나름 고생했던지라.. 작업 내용과 트러블 슈팅을 기록으로 남기려한다.
실시간 채팅 구현하기
사용 기술 스택
- Next.js
- TypeScript
- Zustand(v 5.0.3) - 상태 관리
- sockjs-client(v 1.6.1) - WebSocket 연결
- @stomp/stompjs(v 7.1.1) - 메시징 프로토콜
타입 스크립트에서 STOMP 사용을 위한 declare
stompjs와 sockjs-client는 브라우저 전용으로 작성되어 있어, TypeScript 환경에선 타입 에러가 발생한다.
`declare module "sockjs-client";`
이렇게 declare 처리하여 타입 오류를 해결했다.
상태 관리 도입 이유
"상태 관리 라이브러리는 왜..?" 라고 생각할 수 있지만
이번 프로젝트에서 채팅 UI는 사이드 패널과 모달 2가지 형태로 존재했고,
동일한 메세지 데이터를 공유하면서도 불필요한 리렌더링을 피해야했다.
또한 채팅 히스토리 API를 통해 서버가 과거 메세지를 받아오기 때문에
이 데이터를 전역에서 관리하는게 더 적절하다 판단해 Zustand를 도입했다.
ChatProvider 구현하기
ChatProvider 내 흐름 요약
1. 프로젝트 입장
2. /chat/join요청 -> sender / nickname / topic, path정보 응답
3. 히스토리 API 호출 -> 기존 메시지 상태 초기화
4. subscribeTopic 구독 시작
5. 입장 메세지 전송
6. 실시간 메세지 수신 시 append
채팅 연결
const connect = () => {
// 입장 API 호출
const res = await fetch("/api/chat/join", {
...
});
const data: ChatJoinResponse = await res.json();
const { sender, nickname, chat } = data;
const { subscribeTopic, sendJoinPath, sendMessagePath } = chat;
const nickNameToUse = resolveNickname(nickname);
setSenderInfo(sender, nickNameToUse);
sendMessagePathRef.current = sendMessagePath;
// History API
const historyRes = await fetch(
`/api/chat/history?projectId=${projectId}`
);
const historyData: ChatMessage[] = await historyRes.json();
setMessages(historyData);
// WebSocket 연결
const socket = new SockJS(`${process.env.NEXT_PUBLIC_API_BASE_URL!}/ws`);
const client = new Client({
webSocketFactory: () => socket,
reconnectDelay: 5000,
debug: () => {},
});
client.onConnect = () => {
// 구독 시작
client.subscribe(subscribeTopic, (message) => {
...
});
// 입장 메세지 전송
const joinPayload: ChatJoinPayload = {
...
};
client.publish({
destination: sendJoinPath,
body: JSON.stringify(joinPayload),
});
};
client.activate();
};
메세지 전송
const sendMessage = (content: string) => {
const client = clientRef.current;
const destination = sendMessagePathRef.current;
if (!client || !client.connected || !destination) return;
const payload: ChatSendPayload = {
...
};
client.publish({
destination: destination,
body: JSON.stringify(payload),
});
};
연결 및 연결 해제 처리
useEffect(() => {
if (!projectId) return;
connect();
return () => {
clientRef.current?.deactivate();
};
}, [projectId, accessToken]);
클라이언트 컴포넌트라 useEffect를 통해 mount/ummount 타이밍에 연결을 관리했다.
트러블 슈팅
1. 새로고침 시 입장 메시지가 여러 번 전송되는 현상
처음엔 useEffect가 두 번 호출되는 개발 모드 이슈인가 했지만,
알고 보니 ChatProvider를 Chat 컴포넌트 바로 외부에서 감싸고 있어서 컴포넌트 리렌더링마다 connect()가 다시 실행되고 있었다.
앱 최상단 Layout에서 ChatProvider를 한번만 감싸도록 수정해서
새로고침 시에도 메시지가 중복 전송되지 않도록 문제를 해결했다.
2. SSL 미적용으로 인한 연결 실패
Vercel 배포 이후 채팅 연결이 안 되는 문제가 발생했는데,
오류 메시지는 정확히 기억나지 않지만 WebSocket 연결 실패와 관련된 내용이었다.
문제 원인을 파악해보니
- 개발 서버는 http://
- 배포 후 클라이언트는 https:// 로 동작
- 서버 연결은 여전히 http:// 로 시도
즉, 프로토콜 불일치(혼합 콘텐츠) 가 원인이었다.
서버에서 https://로 배포하고 난 이후 해결되었다.
3. 배포 후 CORS오류
개발 환경에서는 서버에서 CORS 처리를 미리 해주셔서 문제가 없었지만,
배포 후 WebSocket 연결 시 CORS 오류가 발생했다.
사실 내 환경에서는 발생하지 않았는데,
나중에 확인해보니 개발 서버에 적용된 CORS 설정이 배포 서버에도 우연히 적용되고 있었던 것.
서버는 ngrok을 통해 배포된 상태였고,
- Access-Control-Allow-Credentials 헤더 추가
- SockJS 호출 시 withCredentials: true 설정
등을 적용했지만 여전히 오류가 발생했다.
해당 현상과 유사한 상황은 이 글에서 잘 설명돼 있어서 공유받았고,
확인하려는 찰나 서버 측에서 도메인을 새로 구매해 정식 배포로 전환해버리셔서
결과적으로는 다소 찝찝한 상태에서 문제가 해결되었다.
결론 및 회고
이번 프로젝트를 통해 처음으로 소켓 통신을 직접 구현해보았는데,
SockJS, STOMP 라이브러리와 관련 문서들이 잘 정리되어 있어 비교적 무난하게 도입할 수 있었다.
다만, 개발 마감 시간에 쫓기다 보니 예외 처리나 에러 핸들링이 부족했던 점은 아쉬움으로 남는다.
다음에는 더 여유 있게 방어 로직을 고려하며 작업해보고 싶다.
또한 Zustand도 이번에 처음 사용해봤는데,
코드 구조가 깔끔하고 사용법도 직관적이라 앞으로도 전역 상태 관리에 적극 활용할 것 같다.
배포한 프로젝트 및 깃헙
Even IDE
학습부터 협업, 리뷰까지 하게 웹에서 바로 코드 작성부터 링크 공유, 협업, 실시간 채팅, 알고리즘 학습까지.코드 리뷰와 AI 기반 학습 기능까지, 놓치지 말고 경험해보세요. even ide로 코딩 시작
even-ide.vercel.app
https://github.com/code-is-evenly-cooked/even-IDE-front/blob/main/src/providers/ChatProvider.tsx
even-IDE-front/src/providers/ChatProvider.tsx at main · code-is-evenly-cooked/even-IDE-front
Contribute to code-is-evenly-cooked/even-IDE-front development by creating an account on GitHub.
github.com
'Frontend' 카테고리의 다른 글
[vscode] ESLint, Prettier 적용하기 (0) | 2025.05.02 |
---|---|
[React] Router를 활용하여 모달띄우기 (0) | 2025.01.20 |
[JavaScript] ES모듈로 동적 로드와 버튼 액션 구현하기 (0) | 2025.01.08 |
[VSCode] Live Server 실시간 업데이트 적용하기 (0) | 2024.11.15 |