StompJS를 활용하여 실시간 채팅 기능 구현

coggiee
7 min readJul 1, 2024

--

StompJS로 실시간 채팅 기능을 구현했다.

모각GO 프로젝트를 진행하면서, 채팅 기능과 업적 알림 기능(모달)을 맡았다. 두 기능 모두 서버와의 양방향 통신 그리고 실시간 통신이 반드시 필요했기에 일반적으로 사용되는 WebSocket을 사용해야 했다.

WebSocket은 사용해본 경험이 있지만, 단순히 클론 코딩이 전부였기에 사용해봤다고도 할 수 없겠다. 그래서 두려웠지만 꼭 한 번은 해보고 싶었고, 이번 최종 프로젝트에서 개인적인 목표였던 ‘새로운 기술 많이 사용해보기’를 꼭 달성하고 싶었기에 구현을 맡기로 했다.

StompJS를 언급했는데, 갑자기 WebSocket이 무슨 말인가 싶을 수 있겠다. 이에 대해서는 차차 언급하겠다.

실시간 채팅을 어떻게 구현할 수 있을까?

일반적으로 프론트엔드에서 채팅은 WebSocket, SocketIO를 이용해서 구현한다고 알고 있다.

그렇다면 “실시간 채팅은 왜 WebSocket을 사용해야 할까?” 하는 의문이 들었다. 적어도 우리 프로젝트에서는 채팅에 실시간이라는 특징이 있었다. (당연하지만 채팅 자체가 실시간이 아닐까)

물론 API로도 실시간 통신처럼 보이도록 구현은 가능하다. (실제로 2차 팀 프로젝트에서 대부분의 팀이 사용했던 방법이다. 따로 백엔드 파트가 존재하지 않아서, 강사님이 제공해주는 API만을 사용해야 했기에, 모든 팀들이 API로 구현했던 기억이 있다.)

각설하고 API로는 아래와 같이 구현이 가능할 것이다.

  • 메시지 수신 API : /message/receive
  • 메시지 발신 API : /message/send

메시지 발신자 입장에서는 /message/send를 사용해서 메시지를 보내면 된다. 그리고 수신자 입장에서는 /message/receive를 사용해서 메시지를 받을 수 있다.

이때 적어도 실시간 통신에 엇비슷하게 구현하려면 어떻게 해야 할까?

우리는 메시지가 언제 송신(발신)될지 모르기 때문에, 메시지 수신 API를 주기적으로 refetch 하면 된다. 그러나, 단순하게 생각해봐도 이는 비효율적이다. 만약 refetch interval7초로 정했다고 가정하고, 1일 동안 아무런 메시지가 오가지 않는다고 가정해보자.

1일 = 24시간 = 1440분 = 86400초

단순하게 계산해봐도 86400 / 7 = 12,342번의 API 호출이 발생한다. 전혀 메시지가 오지 않을 걸 알면서도 말이다.

위와 같은 이유로 인해 WebSocket, SocketIO 또는 다른 스택을 사용해서 채팅을 구현하는게 좋다고 생각한다. +) 업적 알림 기능(모달)도 마찬가지다. 언제 업적이 달성될지 모른다는 불확실성이 존재하기에, WebSocket이 적절하다.

WebSocket + STOMP

StompJS를 선택한 이유는,

  1. WebSocket에 비해 STOMP 프로토콜은 메시지 규격이 정해져있고 연결 및 해제에 대한 관리가 쉽다.
  2. spring 기반의 백엔드에서 STOMP를 사용

그래서 호환성을 맞추기 위해 StompJS를 사용했다.

구현 중 문제를 마주쳤다.

아래 두 개의 에러코드(error code)를 자주 만났고, 원인은 다음과 같았다.

  1. 1006 : 브라우저에서 비정상적으로 연결이 종료될 때 발생하는 특별한 에러 코드이다.
  2. 1011 : 서버 오류로 인해서 서버에서 연결을 끊을 때 발생하는 에러 코드이다

나의 경우에는 1011 에러가 메시지를 송신할 때 발생했다. 백엔드 로그에서 살펴보니, 메시지는 정상적으로 송신이 되었는데 서버에서 수신한 메시지에 대한 핸들링이 되지 않은 것이 원인이었다. 1006은 아직 원인이 무엇인지 파악하지 못했다. 처음에는 useEffect 내부에서 컴포넌트가 언마운트될 때 disconnect를 호출해서 발생했다고 생각했는데, 이것 때문은 아니었다. 클라이언트 로직에 변경이 없는데도 현재는 정상적으로 동작하는 것을 보아 서버와의 연결 수립에서 어떤 문제가 있는 것으로 추측된다. (아니면 존재하지 않는 서버에 연결하려고 해서?)

배포때는 프로토콜 에러도 마주쳤다. 로컬에서 Stomp 서버 주소의 프로토콜이 ws였고, 로컬도 http였기에 원활하게 동작했지만 배포에서는 Mixed Content error가 발생했다.

사진이 잘렸지만, 요청을 보내는 도메인은 https로 암호화 되어있지만, 요청을 받는 도메인은 ws로 암호화되지 않아서 발생하는 문제였다. 이는 서버쪽에서 wss로 암호화를 적용하니 쉽게 해결되었다.

로직을 어디에 위치시킬 것인가?

채팅과 업적 두 가지 기능을 구현하면서 Stomp 구현부의 적절한 위치에 대한 고민도 필요했다.

구현 위치는 크게 4가지가 있다.

  1. 컴포넌트 내부 (가장 사용하기 쉬운 방법)
  2. 커스텀 훅
  3. Context API
  4. 상위(부모) 컴포넌트 (가장 사용하기 쉬운 방법)

채팅 (Implemented with Custom Hook)

우리 프로젝트에서 채팅 기능은 아래와 같은 특징이 있었다.

  • 채팅방마다 고유한 subscribe 주소가 존재한다.
  • 채팅은 두 개의 페이지에서 가능하다. (채팅 페이지, 카드 관리 페이지)

컴포넌트 내부와 상위 컴포넌트에 구현한다고 생각해보자. 채팅은 가능한 위치가 다를 뿐이지 기능은 완전히 동일하다. 그렇기에, 이 방법은 필연적으로 중복로직이 발생한다.

Context API로 구현한다고 생각해보자. 채팅방마다 고유한 subscribe 주소가 존재한다고 했다. 즉, 여러 서버에 연결을 해야 한다. 이를 고려했을 때 다수의 Context를 생성해야 하는 비용이 발생한다.

커스텀 훅은 어떨까? 각 채팅방의 고유 주소를 Hook에 넘기기만 하면 되고, 중복 로직은 하나의 Hook으로 최소화할 수 있기 때문에 커스텀 훅이 가장 적합하다고 판단했다.

아래와 같이 구현했다.

업적 (Implemented with Context API)

우리 프로젝트에서는 특정 조건을 만족할 때, 업적 진행률이 갱신되거나 업적을 획득하는 이벤트가 발생한다. 다양한 업적이 존재하는데 이는 곧 이벤트가 발생하는 위치가 다양하다고 볼 수 있다. 또한, 업적의 경우 단 하나의 주소만 존재하며, 서버로 송신하는 로직이 없고 수신만 하면 된다.

이를 고려했을 때는 Context API가 가장 적합하다고 판단했다. (다른 방식을 사용한다고 가정하면, 업적 이벤트가 발생가능한 모든 페이지에 커스텀 훅, 로직 자체를 구현해야 한다. 이는 당연하게도 비효율적이며 무엇보다도 유지보수 비용이 증가한다.)

아래와 같이 구현했다.

앞서 언급했듯이, 업적 알림 기능에서는 메시지 송신 기능이 사용되지 않으므로 따로 Provider의 value로 전달하지 않았다.

📍 useState로 Client 객체를 관리하면 안될까? useState로 생성한 state는 리렌더링에 영향을 받는다. 하지만 우리가 사용할 Client 객체는 한 번 생성한 후, 재생성하지 않고 변경하지도 않을 것이다. 만약, 다른 사이드 이펙트로 인해 컴포넌트 자체가 리렌더링 될 때도, Client 객체는 처음과 동일해야 한다. 이를 실현시킬 수 있는게 바로 useRef이다.

구현을 마무리하며

구현에 있어서 생각해볼 만한 것들도 있었고, 무엇보다도 처음이어서 미숙한 부분이 많았다.

처음에는 서버가 만들어지지 않았었다. 내가 구현한 코드가 정상적으로 동작할 것이라는 확신이 없었기에 어떻게든 서버가 필요했다. 그래서 직접 Mock서버를 만들어보기로 했는데, 연결이 제대로 되지 않았다. 거의 이틀은 여기에 시간을 쏟았다. 하지만, 더 이상의 시간 투자는 현명하지 않다고 판단해서, 클라이언트 구현 부만 만들고, 서버를 기다렸다.

결과적으로는 성공적으로 구현했으나, 그 결과에 도달 하기까지 매우 힘들었던 것 같다. 그래도 후회는 없다. 오히려 하길 잘했다는 생각이 든다.

Stomp의 동작 원리는 아직 완벽하게 이해를 하지 못했으나, 쉽게 사용이 가능했다. 순수 WebSocket으로 구현한다면 채팅방에 대한 처리도 따로 해야하고, 재연결에 대한 처리도 해야하지만, StompJS를 사용하면 재연결도 알아서 처리해주고 심지허 헤더를 추가하여 인증 처리도 가능하다.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Unlisted

--

--

coggiee
coggiee

Written by coggiee

0 Followers

Just doing

No responses yet

Write a response