SSE | WebSocket |
---|---|
SSE를 구현하기 위해서는 서버 측과 클라이언트 측에서의 설정과 처리 로직을 구현해야 한다.
Next.js + Spring Boot로 진행했던 프로젝트에서 실시간 알림 기능을 구현하였는데, 클라이언트(Next.js)에서 작성했던 코드를 공유하고자 한다.
slideInRight
을 적용한다.slideOutRight
을 적용한다.@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOutRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
.background {
position: fixed;
bottom: 5px;
right: 5px;
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
width: 400px;
}
.slide-in {
animation: slideInRight 0.5s ease forwards;
}
.slide-out {
animation: slideOutRight 0.5s ease forwards;
}
event-source-polyfill
이라는 라이브러리를 사용하였다.heartbeatTimeout
은 SSE 연결 시간을 의미하며, 적용해주지 않으면 45초가 기본 값으로 설정된다.newNotice
, statusChange
, newApply
)에 대한 이벤트 리스너를 작성하였다.slide-in
애니메이션을 적용하고, react-query 의 invalidateQueries
메서드를 활용하여 특정 상태들을 무효화하여 자동으로 업데이트 하도록 설정하였다.slide-out
애니메이션을 적용하여 알림이 자동으로 사라지도록 구현하였다.useEffect
훅 return 값으로 eventSource.close()
를 작성하여 컴포넌트가 언마운트 될 때 SSE 연결을 닫도록 설정하였다.import React, { useEffect, useState } from 'react';
import { useQueryClient } from 'react-query';
import Cookies from 'js-cookie';
import { EventSourcePolyfill, NativeEventSource } from 'event-source-polyfill';
import { INewNotice, IEmergency, INoticeAdmin } from '@/types/Notice';
import styles from './NewNotice.module.scss';
function NewNotice() {
const accessToken = Cookies.get('accessToken');
const queryClient = useQueryClient();
const [newNotice, setNewNotice] = useState<INewNotice>();
const [newStatus, setStatus] = useState<IEmergency>();
const [newApply, setNewApply] = useState<INoticeAdmin>();
const [animationClass, setAnimationClass] = useState(styles['slide-in']);
useEffect(() => {
const EventSource = EventSourcePolyfill || NativeEventSource;
const eventSource = new EventSource('API_URL', {
headers: {
Authorization: `Bearer ${accessToken}`,
Connetction: 'keep-alive',
Accept: 'text/event-stream',
},
heartbeatTimeout: 86400000,
});
// eslint-disable-next-line
eventSource.addEventListener('connect', (event: any) => {
const { data: receivedConnectData } = event;
if (receivedConnectData === 'SSE 연결이 완료되었습니다.') {
console.log('SSE CONNECTED');
} else {
console.log(event);
}
});
// eslint-disable-next-line
eventSource.addEventListener('newNotice', (event: any) => {
const newNoticeInfo: INewNotice = JSON.parse(event.data);
setNewNotice(newNoticeInfo);
setAnimationClass(styles['slide-in']); // 슬라이드 애니메이션
queryClient.invalidateQueries('noticeCnt'); // 쪽지수 업데이트
queryClient.invalidateQueries('noticeList'); // 쪽지리스트 업데이트
queryClient.invalidateQueries(['unreadReceiveList', 0]); // 안읽은 쪽지리스트 업데이트
const slideOutTimer = setTimeout(() => {
setAnimationClass(styles['slide-out']);
const clearNoticeTimer = setTimeout(() => {
setNewNotice(undefined);
}, 500);
return () => clearTimeout(clearNoticeTimer);
}, 5000);
return () => clearTimeout(slideOutTimer);
});
// eslint-disable-next-line
eventSource.addEventListener('statusChange', (event: any) => {
const newNoticeInfo: IEmergency = JSON.parse(event.data);
setStatus(newNoticeInfo);
setAnimationClass(styles['slide-in']); // 슬라이드 애니메이션
queryClient.invalidateQueries('noticeCnt'); // 쪽지수 업데이트
queryClient.invalidateQueries('noticeList'); // 쪽지리스트 업데이트
queryClient.invalidateQueries(['unreadReceiveList']); // 안읽은 쪽지리스트 업데이트
queryClient.invalidateQueries('apiCount'); // 상태 수 업데이트
queryClient.invalidateQueries('apiStatuslist 전체'); // 상태 리스트 업데이트
queryClient.invalidateQueries(['apiStatus']); // 상태 리스트 업데이트
// 5초 후에 알림 언마운트하고 상태 비우기
const slideOutTimer = setTimeout(() => {
setAnimationClass(styles['slide-out']);
const clearStatusTimer = setTimeout(() => {
setStatus(undefined);
}, 500);
return () => clearTimeout(clearStatusTimer);
}, 5000);
return () => clearTimeout(slideOutTimer);
});
// eslint-disable-next-line
eventSource.addEventListener('newApply', (event: any) => {
const newApplyInfo: INoticeAdmin = JSON.parse(event.data);
setNewApply(newApplyInfo);
setAnimationClass(styles['slide-in']); // 슬라이드 애니메이션
queryClient.invalidateQueries('noticeCnt'); // 쪽지수 업데이트
queryClient.invalidateQueries('noticeList'); // 쪽지리스트 업데이트
queryClient.invalidateQueries(['unreadReceiveList']); // 안읽은 쪽지리스트 업데이트
queryClient.invalidateQueries(['provideApplyList']); // 제공 신청 리스트 업데이트
queryClient.invalidateQueries(['useApplyList']); // 제공 신청 리스트 업데이트
// 5초 후에 알림 언마운트하고 상태 비우기
const slideOutTimer = setTimeout(() => {
setAnimationClass(styles['slide-out']);
const clearStatusTimer = setTimeout(() => {
setStatus(undefined);
}, 500);
return () => clearTimeout(clearStatusTimer);
}, 5000);
return () => clearTimeout(slideOutTimer);
});
return () => {
eventSource.close();
console.log('SSE CLOSED');
};
// eslint-disable-next-line
}, []);
const handleClose = () => {
setAnimationClass(styles['slide-out']);
};
if (!newNotice && !newStatus && !newApply) {
return null;
}
if (newNotice) {
return (
<div className={`${styles.background} ${animationClass}`}>
...
</div>
);
}
if (newStatus) {
return (
<div className={`${styles.background} ${animationClass}`}>
...
</div>
);
}
if (newApply) {
return (
<div className={`${styles.background} ${animationClass}`}>
...
</div>
);
}
}
export default NewNotice;
References
실시간 데이터 전송 방법 Server-Sent Events(SSE)와 웹소켓 차이
알림 기능을 구현해보자 - SSE(Server-Sent-Events)!
도움이 많이 되었습니다.