일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- TypeScript
- 이더리움
- 가상화
- 이슈
- AWS
- 백엔드
- 솔리디티
- 프론트엔드
- kubernetes
- VUE
- 백준
- 컴퓨터공학
- 알고리즘
- 웹
- 쿠버네티스
- CSS
- 파이썬
- docker
- k8s
- 리액트
- 블록체인
- 클라우드
- next.js
- es6
- 자바스크립트
- HTML
- JavaScript
- react
- 타입스크립트
- BFS
- Today
- Total
즐겁게, 코드
Server-Sent Event로 데이터 구독하기 본문
웹 서비스를 사용하다 보면 때때로 '이 기능은 어떻게 구현한 걸까?' 라는 의문이 들 때가 있지 않나요?
저는 최근 인스타그램을 사용하다가 "내가 올린 게시물에 좋아요가 달리면, 어떻게 실시간으로 알림이 오는 걸까?" 라는 궁금증을 가지게 되었는데요, 오늘 다룰 Server-Sent Event (SSE) 라는 키워드가 약간의 해답이 되었던 것 같아요.
그런데 ‘서버에서 보내는 이벤트’ 라는 명칭만 들어서는 이게 어떤 기술인지 금방 알아차리기 어려울 수 있는데요,
Server-Sent Event에 대해 더 이해할 수 있도록 이번 글을 작성해 보았습니다.
목차
- Server-Sent Event란?
- Server-Sent Event 구현하기
- SSE를 응용해 친구 초대 알람 만들어보기
1. Server-Sent Event (SSE) 란?
- 주어진 API에 요청을 보내고, 요청의 응답을 출력하고 있는 간단한 예시에요.
const { data } = await axios("https://jsonplaceholder.typicode.com/todos");
console.log(data);
프론트엔드 또는 백엔드 개발을 경험해본 분이시라면 한번쯤은 요청 - 응답 형태의 HTTP 통신을 구현해본 적이 있을 거에요.
그런데 요청 - 응답 모델은 가장 단순하면서도 강력한 방법이지만, 실시간성이 필요한 경우에는 적합하지 않은 경우가 종종 있습니다.
예를 들어 기획자가 이런 요청사항을 요청한다면 어떨까요?
👩🏻💼 (SNS 서비스 기획자) : “친구 추가” 요청이 들어왔을 때, 실시간 알람과 함께 UI에 반영해 주세요!
저는 두 가지 방법이 떠오르네요.
방법 1. 롱 폴링 사용하기
- 특징 : 일정 주기마다 HTTP 요청을 보내고, 응답이 올 때까지 기다린다.
- 장점 : 다른 프로토콜에 대한 지식이 필요하지 않고, 구현이 비교적 간편하다.
- 단점 : 서버에서 보낼 내용이 없을 때도 계속해서 요청을 보내야 한다.
방법 2. 웹소켓 사용하기
- 특징 : 웹소켓 프로토콜로 클라이언트와 서버 간 커넥션을 유지한다.
- 장점 : 클라이언트와 서버가 실시간으로 데이터를 주고받는 상황에 효과적이다.
- 단점 : 구현이 상대적으로 복잡하고, 웹소켓 프로토콜에 대한 이해가 필요하다.
롱 폴링과 웹소켓 모두 서버에서 내려주는 응답을 실시간으로 구독해야 할 때 고려해볼 수 있는 방법이지만, 이런 상황에서는 오늘 다룰 SSE가 효율적일 수 있습니다.
- 서버에서 실시간으로 데이터를 수신해야 할 때
- 클라이언트에서 데이터를 전송할 필요 없이, 데이터를 수신하기만 하면 될 때
SSE는 웹소켓보다 구현이 간단하면서 롱 폴링보다 서버 자원을 덜 소모하는데요, 서버와의 연결은 유지한 채 서버에서 보내주는 데이터를 스트림 형태로 받아볼 수 있기 때문에 위와 같은 상황에 적절합니다.
2. Server-Sent Event 구현하기
SSE를 요약하면 "실시간성을 제공하면서 웹소켓보다 단순한" 방법이 될 것 같은데요, 이제 한번 코드를 작성해 볼까요?
Node.js와 리액트를 사용해 간단한 서버와 클라이언트를 구현하는 코드를 작성해 보겠습니다.
💡 글을 작성하다 보니 코드가 길어지게 되어, 완성된 코드를 미리 올려두었어요.
다소 번거로우시더라도 아래 링크에서 완성된 코드를 참고해주시면 감사드려요.
2. 1. 서버 준비하기
Node.js와 Express.js로 간단한 서버를 준비해 보겠습니다.
// index.js
const express = require("express");
const cors = require("cors")
const app = express();
const PORT = 5500;
app.use(cors())
app.listen(PORT, () => {
console.log(`Server is running at ${PORT}`)
})
위와 같이 간단한 코드를 작성한 다음, 아래 커맨드로 서버를 시작할 수 있습니다.
npm install
node index.js
서버가 실행되었다면 SSE 요청을 처리할 수 있는 경로를 만들어줘야 할 텐데요, SSE로 데이터를 전송할 때 몇 가지 지켜야 할 규칙이 있습니다.
SSE를 구현할 때 지켜져야 할 형식
- 응답 헤더의 Content-Type 속성은 text/event-stream 으로 설정되어야 합니다.
- SSE로 전송되는 데이터는 반드시 텍스트여야 하며, 항상 “data:” 로 시작해야만 합니다.
- SSE로 전송된 데이터의 끝에는 반드시 끝을 알리는 "\n\n" 를 붙여주어야 합니다.
- (예시 : "data: ${JSON.stringify(데이터)}\n\n")
이제 SSE 요청을 처리할 수 있는 첫 번째 경로를 만들어 보겠습니다.
아직은 실습 단계니, 예시로 3초마다 현재 시각을 전송하는 간단한 로직을 추가해 보겠습니다.
// index.js
const express = require("express");
const cors = require("cors")
const app = express();
const PORT = 5500;
app.use(cors())
app.get("/friend/check-request", (req, res) => {
res.setHeader("Content-Type", 'text/event-stream');
res.setHeader("Cache-Control", 'no-cache');
res.setHeader("Connection", 'keep-alive');
const intervalId = setInterval(() => {
const date = new Date();
// 1. SSE로 전송할 데이터는 반드시 텍스트 형식이어야 합니다.
// 2. 데이터의 끝은 \n\n로 구분되어야 합니다.
const data = `data: ${date.toISOString()}\n\n`;
// res.send()가 아닌 res.write() 메서드를 사용하도록 주의해 주세요.
res.write(data);
}, 3000);
})
app.listen(PORT, () => {
console.log(`Server is running at ${PORT}`)
})
2. 2. 클라이언트 준비하기
이제 서버에서 전송한 데이터를 수신할 수 있도록 클라이언트를 구현할 차례입니다.
서버에서 text/event-stream 이라는 특별한 헤더 값을 사용했던 것처럼 클라이언트에서도 특별한 방법을 통해 요청을 보내야 하는데요, 바로 eventSource 객체를 사용한 방법입니다.
// ❌ : 일반 HTTP 요청을 보내는 예시
axios.get("http://localhost:5500/friend/check-request")
// ✅ : EventSource 객체를 사용해 SSE 요청을 보내는 예시
new EventSource("http://localhost:5500/friend/check-request")
이번 예제에서는 리액트를 사용하고 있기 때문에 SSE 요청을 보내는 간단한 훅을 작성해 보겠습니다.
import { useEffect } from "react";
import ENDPOINT from "../constant/api";
export const useCheckFriendRequest = () => {
useEffect(() => {
const eventSource = new EventSource(
`${ENDPOINT.BASE}${ENDPOINT.CHECK_FRIEND_REQUEST}`
);
/*
onmessage 리스너를 작성하면 SSE 요청의 응답을 다룰 수 있습니다.
*/
eventSource.onmessage = (message) => {
console.log(message.data);
};
/*
EventSource 객체는 자체적으로 에러를 throw하지 않기 때문에 try-catch의 영향을 받지 않습니다.
따라서, 리스너를 작성해야 SSE 요청이 실패했을 때의 상황을 다룰 수 있습니다.
*/
eventSource. = (error) => {
eventSource.close();
};
/*
Graceful close를 위해 컴포넌트 언마운트 시점에 연결을 종료해 주세요.
*/
return () => {
eventSource.close();
};
}, []);
};
위에서 만든 훅을 리액트 프로젝트의 아무 컴포넌트에서나 불러와 보면 요청이 수행되는 것을 확인할 수 있어요.
3. SSE 형태 살펴보기
SSE 요청을 보내게 되면 어떤 형태로 응답이 돌아오는지 크롬 개발자 도구를 통해 살펴보겠습니다.
먼저 요청의 유형이 fetch나 XHR이 아닌 eventsource로 설정되어 있고, 요청 헤더와 응답 헤더의 Content-Type 속성은 text/event-stream 으로 설정된 것을 확인할 수 있습니다.
흥미로운 점은 일반적인 HTTP 요청을 보내면 “Response” 라는 탭이 존재해야 하는데 SSE는 “EventStream” 이라는 탭이 존재하는 모습인데요, “EventStream” 탭을 확인해 보면 서버에서 전송한 데이터와 타임스탬프가 존재하는 것을 확인할 수 있습니다.
3. SSE를 응용해 친구 초대 알람 만들어보기
이제 SSE가 무엇인지는 어느 정도 알았으니, 기획자의 요청을 들어주러 가볼까요?
시나리오 : 유저 A가 유저 B에 '친구 초대' 를 보내면 유저 B에게 알람을 보내주는 로직 구현하기
SSE를 알기 전까지는 이런 로직을 구현해야 했을 거에요.
- 유저 A가 서버에 “친구 초대” 요청을 보낸다.
- 서버에서 해당 요청을 검증하고 유저 A의 요청에 대한 응답을 보낸다.
- 유저 B는 N분마다 요청을 보내 새로운 친구 요청이 있는지 확인한다.
이제 우리는 SSE가 무엇인지 알고 있기 때문에, 새로운 방법을 시도해볼 수 있어요.
- 유저 A가 서버에 “친구 초대” 요청을 보낸다.
- 서버에서 해당 요청을 검증하고 유저 A의 요청에 대한 응답을 보낸다.
- 유저 B에게 친구 초대 정보가 포함된 데이터를 SSE로 보낸다.
3. 1. Express.js로 구현한 예시
친구 요청에 사용하는 /friend/request API로 요청이 들어오면 이벤트를 발생시켜 클라이언트에게 SSE로 데이터를 내려줄 수 있도록 작성한 코드에요.
// index.js
app.post("/friend/request", (req, res) => {
// 친구 초대를 처리하는 로직
// ....
// 친구 초대 처리를 마치면 SSE 응답을 전송하는 이벤트를 발생시킨다.
eventEmitter.emit("newFriendRequest", {
friendName: req.body.friendName,
});
res.send("친구 요청이 전송되었습니다.");
});
app.get("/friend/check-request", (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
// 초대를 보낸 친구의 이름을 클라이언트에 전송한다.
const sendEvent = (e) => {
res.write(`data: ${JSON.stringify({ friendName: e.friendName })}\n\n`);
};
eventEmitter.on("newFriendRequest", sendEvent);
});
결과
- 좌측은 유저 A, 우측은 유저 B의 화면이라고 가정하고 제작한 예제에요.
- 유저 A가 "친구 신청하기" 버튼을 눌렀더니, 유저 B의 화면에서 실시간으로 알림이 노출되는 모습이에요.
마치며
만약 SSE를 모르는 상황이었다면 우리는 웹소켓과 롱 폴링, 또는 새로운 방법을 찾아야 했을 텐데요, 이번 글을 통해 비슷한 상황에서 여러분들이 고를 수 있는 선택지가 늘어났으면 하는 바램이 있어요.
"아는 것이 힘이다" 라는 격언처럼, 앞으로도 여러분들에게 힘이 되어줄 수 있는 다양한 기술들을 더 들고 찾아올게요.
그럼, 즐거운 코딩 하세요!
'🎨 프론트엔드' 카테고리의 다른 글
XSS 공격의 위험성 체험하기 (1) | 2024.11.14 |
---|---|
페이지 노출 여부를 판별하는 Page Visibility API 살펴보기 (0) | 2024.06.02 |
electron-react-boilerplate에 테일윈드 끼얹기 (0) | 2023.09.30 |
import 영역을 읽기 좋게 다듬는 세 가지 방법 (0) | 2023.06.18 |
바벨과 함께 트랜스파일 해보기 (4) | 2021.08.20 |