본문 바로가기

Frontend

Next.js + SockJS + STOMP로 실시간 채팅 구현하기

 

 

 

구름에서 진행한 Web IDE 프로젝트에서 실시간 채팅 구현을 맡게 되었고,

해당 기능을 SockJS + STOMP 조합으로 구현했다.

나름 고생했던지라.. 작업 내용과 트러블 슈팅을 기록으로 남기려한다.

채팅 화면,, 3일 지나면 채팅 내역을 지운터라.. 나 혼자 인사하고 그렇다..

실시간 채팅 구현하기

사용 기술 스택

- Next.js

- TypeScript

- Zustand(v 5.0.3) - 상태 관리

- sockjs-client(v 1.6.1) - WebSocket 연결

- @stomp/stompjs(v 7.1.1) - 메시징 프로토콜

 

타입 스크립트에서 STOMP 사용을 위한 declare

stompjssockjs-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가 두 번 호출되는 개발 모드 이슈인가 했지만,

알고 보니 ChatProviderChat 컴포넌트 바로 외부에서 감싸고 있어서 컴포넌트 리렌더링마다 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도 이번에 처음 사용해봤는데,

코드 구조가 깔끔하고 사용법도 직관적이라 앞으로도 전역 상태 관리에 적극 활용할 것 같다.

 

배포한 프로젝트 및 깃헙

https://even-ide.vercel.app/

 

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