백오피스 알림 구현기

Thomas·2023년 12월 9일
0
post-thumbnail

Intro

작업중인 백오피스에서 알림을 구현했습니다.
아직 백오피스에 Role 기능이 없기 때문에 알림받는 타겟은 전체로 설정했고,
소켓을 열어 새로운 업무가 발생하면 메세지를 받아서 알림 컴포넌트에 뿌려주는 식으로 구현했습니다.
그럼 구현했던 과정을 알아보겠습니다!

Tech

먼저 실시간 서비스는 단순 REST 아키텍쳐에서 구현이 불가능합니다. 정확히 말하면 폴링으로 구현은 가능하지만 비효율적 입니다.
HTTP/HTTPS 프로토콜은 상태가 유지되지 않습니다.
그렇기 때문에 실시간 서비스를 구현하려면 폴링 혹은 소켓을 이용해야합니다.
저는 알림 서비스에서 가장 보편적으로 사용되는 소켓을 열기로 했고 프로토콜로 STOMP를 사용했습니다.

STOMP

STMOP 프로토콜이랑 Simple Text Oriented Messaging Protocol 의 약자로 단순 메세지 전송을 위한 프로토콜입니다. STOMP 를 활용한다면 메세지 헤더를 받을 수 있기 때문에 별도의 인증을 구현할 수 있습니다.

제가 활용한 STOMP 프로토콜의 플로우는 아래와 같습니다.

  1. 서버와 클라이언트의 커넥션을 열어줍니다.
  2. 클라이언트에서 토픽을 구독합니다.
  3. 서버에서 토픽에 메세지를 보냅니다.
  4. 클라이언트에서는 구독한 토픽에서 메세지를 받습니다.

구현 과정

라이브러리 설치

클라이언트는 React, Typescript 를 활용해 구현을 했고, stomp 를 쉽게 활용할 수 있는 @stomp/stompjs 라는 라이브러리가 있습니다. 아래의 라이브러리를 설치합니다. 타입스크립트를 사용한다면 타입을 위한 라이브러리를 개발 의존성을 주입해 설치합니다.

yarn add @stomp/stompjs
yarn add @types/stompjs --dev

클라이언트 인스턴스 생성

클라이언트 인스턴스를 생성해줍니다. 웹소켓을 열어주기 때문에 url 은 ws:// 혹은 wss:// 로 시작합니다.
백오피스는 소켓을 연결하기 전 로그인을 하여 사용자 인증을 받습니다. 그렇기 때문에 웹소켓 커넥션을 연결할 때 별도의 인증을 하지 않았습니다. 만약 인증 정보가 필요하다면 client 의 connectHeaders 을 통해 데이터를 넘겨줄 수 있습니다.
reconnectDelay 옵션을 통해 다시 연결될 때 까지의 딜레이 시간을 정해줄 수 있고, 그 외에 heatbeat 가 오고 가는 시간을 정해줄 수 있습니다.

const client = new Client({
  brokerURL: import.meta.env.VITE_WS_BASE_URL + '/ws',
  reconnectDelay: 10000,
  // 별도의 인증이 필요하다면 사용할 connectHeader
  connectHeaders: {
    login: 'user',
    passcode: 'password',
  },
  heartbeatIncoming: 4000,
  heartbeatOutgoing: 4000,
  debug: function (str) {
    console.log(str);
  }
});

핸들링 함수 작성

클라이언트에서 웹 소켓이 연결된 후 핸들링 할 함수를 작성합니다. connect 후 특정 토픽을 구독하여 메세지를 전달받을 계획입니다.

const handleConnect = () => {
  client.subscribe('/topic/notice', (message) => {
    // 로직 작성
  });
};

useEffect 를 통해 클라이언트 활성화

React 컴포넌트 자체에서 HTTP 요청을 보내거나 WS 를 활성화 시킬 땐 useEffect 내부에서 해야합니다. 작성한 connect 함수를 client 의 onConnect 에 바인딩하고 activate 메서드를 통해 클라이언트를 활성화합니다.
컴포넌트가 언마운트 되었을 때 소켓을 해제하기 위해 반드시 클린업 함수를 리턴해주세요.

const closeSocket = () => {
  if (client) client.deactivate();
};


useEffect(() => {
  client.onConnect = handleConnect;
  client.activate();
  return () => closeSocket();
}, []);

상태값은 101로 커넥션이 잘 연결되어 있는 것을 확인할 수 있습니다.

알림 리스트 활용하기

여기까지 소켓을 연결하는 것은 쉽습니다.
전달받을 메세지 프로토콜을 백앤드 개발자와 정의를 하고 메세지를 받게되면 어떻게 해야할까요?
먼저 구현할 알림의 UI 는 다음과 같습니다.

알림-ui

소켓을 통해 실시간으로 데이터를 반영하는 로직 이외에 추가로 알림 리스트 데이터가 필요합니다.
해당 데이터는 별도의 api 를 통해 list 를 get 합니다. 데이터의 형태는 다음과 같습니다.

type Notification = {
  backofficeNoticeId: number;
  backofficeNoticeType: NotiType;
  title: string | null;
  id: number;
  isRead: boolean;
  createdAt: number;
};

해당 Notification 의 List 를 react-query 를 활용해 데이터를 가져오고 뿌려줍니다.

const { data: notiList } = useGetNotiList();

웹 소켓 메세지 react-query 에 반영하기

웹 소켓 데이터와 react-query 를 통해 관리하는 데이터는 엄연히 다릅니다. 데이터를 받기위해 사용하는 프로토콜 마저 다릅니다.
그럼 WS 를 통해 받은 데이터를 react-query 와 싱크를 맞추는 방법을 알아봅시다.
저는 queryClient 의 setQueryData 를 활용하기로 했습니다.
웹소켓을 통해 실시간으로 최신 데이터를 서버로부터 받고 있습니다. 그렇기 때문에 WS 로 넘어온 데이터를 최초에 패칭해온 리스트에 넣어주는 경우 항상 해당 리스트는 최신 리스트가 됩니다. 이 과정을 통해 List 를 또 다시 Get 하지 않고 데이터를 클라이언트에서 변경시킴으로써 최신 데이터를 유지하도록 하겠습니다.

const queryClient = useQueryClient();
const { data: notiList } = useGetNotiList();

const handleConnect = () => {
  client.subscribe('/topic/notice', (message) => {
    const body = JSON.parse(message.body) as NotificationType;

    if (body) {
      queryClient.setQueryData<NotificationType[]>([KEY], (data) => [body, ...(data ? data : [])]);
    }
  });
};

setQueryData 는 웹소켓의 subscribe 안에 들어가는 메세지 콜백함수에서 작성해줍니다. 해당 콜백 함수는 메세지를 전달받았을 때 실행이 됩니다.

String 데이터를 파싱해주고 JSON 형태로 파싱해주고, 해당 List 데이터에 매핑된 키를 활용해 List 에 넣어줍니다.

이 과정을 통해 항상 최신의 데이터를 유지시키게 되었습니다.

마무리하며

알림 전체를 구현한 이야기는 남겨놓지 않았지만 모두 읽음, 읽음 표시, 삭제 등 다양한 기능을 구현했습니다.

알림 기능을 통해 백오피스 사용자가 업무를 놓치지 않게 된다면 비즈니스 적으로 아주 가치가 있는 기능이라고 생각이 들었습니다.
읽어주셔서 감사합니다.

profile
안녕하세요! 주니어 웹 개발자입니다 😆

0개의 댓글