๐ท๏ธ๋ค์ด๊ฐ๋ฉฐ
ํด๋น ๊ธ์ "next": "14.2.6" ๋ฒ์ ์ ๋ฐํ์ผ๋ก ํ ํ๋ก์ ํธ์ ๋๋ค.
SSE๋ HTTP ํ์ค ๊ธฐ์ ์ค ํ๋์ด๋ฏ๋ก ํน์ ๋ฒ์ ์ด ์กด์ฌํ์ง ์์ต๋๋ค.
โSSE๋ฅผ ์ ํํ ์ด์ ๋?
์ฐ๋ฆฌFIS์์นด๋ฐ๋ฏธ์์ ์งํํ ์ต์ด ํ๋ก์ ํธ์์ ์ฐ๋ฆฌ์ ํต์ฌ ๊ธฐ๋ฅ์ '์ ์ฐ'์ด์๋ค. ์ด๋ ์ ์ํ ์ ์ฐ์ ์ํด ‘์ ์ฐ ์์ฒญ’, ‘์ ์ฐ ์์’, ‘์ ์ฐ ์๋ฃ’ ๋ฑ์ ์ ๋ณด๋ฅผ ์ฌํ์ ๊ฐ ์ค์๊ฐ์ผ๋ก ๊ณต์ ํ ํ์์ฑ์ ๋๊ผ๊ณ ์ฐ๋ฆฌ๋ ์ค์๊ฐ์ผ๋ก ์๋น์ค๋ฅผ ๊ตฌํํ ๊ธฐ์ ์ ์ฐพ๊ณ ์๋ค.
SSE(Server-Sent Events) VS Polling VS WebSocket
๋ฐฉ์ | ์ฐ๊ฒฐ ๋ฐฉ์ | ๋ฐ์ดํฐ ํ๋ฆ | ์ค์๊ฐ์ฑ | ์๋ฒ ๋ถํ | ๋ธ๋ผ์ฐ์ ์ง์ | ์ฌ์ฉ ์ฌ๋ก |
SSE (Server-Sent Events) | ๋จ๋ฐฉํฅ (์๋ฒ → ํด๋ผ์ด์ธํธ) | ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก๋ง ๋ฐ์ดํฐ ์ ์ก | ์ค๊ฐ (์ฐ๊ฒฐ ์ ์ง) | ๋ฎ์ | ๋๋ถ๋ถ์ ์ต์ ๋ธ๋ผ์ฐ์ ์ง์ (IE ๋ฏธ์ง์) | ์ฃผ๊ฐ ์ ๋ณด, ์๋ฆผ ์์คํ , ๋์๋ณด๋ |
Polling (Short/Long) | ํด๋ผ์ด์ธํธ๊ฐ ์ฃผ๊ธฐ์ ์ผ๋ก ์์ฒญ | ํด๋ผ์ด์ธํธ → ์๋ฒ ์์ฒญ, ์๋ฒ ์๋ต | ๋ฎ์ (์ง์ฐ ๋ฐ์) | ๋์ (๋ฐ๋ณต์ ์ธ ์์ฒญ) | ๋ชจ๋ ๋ธ๋ผ์ฐ์ ์์ ๊ฐ๋ฅ | ์ฑํ , ๋ฐ์ดํฐ ์๋ก๊ณ ์นจ |
WebSocket | ์๋ฐฉํฅ (์๋ฒ โ ํด๋ผ์ด์ธํธ) | ์๋ฒ์ ํด๋ผ์ด์ธํธ๊ฐ ์์ ๋กญ๊ฒ ๋ฐ์ดํฐ ๊ตํ | ๋์ (์ค์๊ฐ) | ๋ฎ์ (์ฐ๊ฒฐ ์ ์ง, ํจ์จ์ ) | ๋๋ถ๋ถ์ ์ต์ ๋ธ๋ผ์ฐ์ ์ง์ | ์ค์๊ฐ ๊ฒ์, ์ฑํ , ์ฃผ์ ๊ฑฐ๋ |
Polling์ ์ฃผ๊ธฐ์ ์ผ๋ก ์๋ฒ์ ์์ฒญ์ ๋ณด๋ด๋ ๋ฐฉ์, ๋ฆฌ์์ค ๋ญ๋น๊ฐ ์ฌํ๋ค๊ณ ํ๋จ, ๋ํ ์น์์ผ์ญ์ ์๋ฐฉํฅ ํต์ ์ผ๋ก ์ฐ๋ฆฌ๊ฐ ๊ตฌํํ ์ค์๊ฐ ์๋ฆผ์ ์ฐ๊ธฐ์๋ ์ค๋ฒ ์คํ์ด๋ผ๊ณ ์๊ฐํ๋ค. ๊ทธ๋์ ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก ๋จ๋ฐฉํฅ ํต์ ์ธ SSE๋ฅผ ์ ํํด์ ๊ตฌํํ๊ฒ ๋์๋ค.
++ SSE์ WebSocket์ ์ฐจ์ด
SSE๋ ์น ๊ธฐ์ ์ด๊ธฐ ๋๋ฌธ์ HTTP ํ๋กํ ์ฝ ์์์ ๋์ํ๋ค.๋ํ, HTTP ์ฐ๊ฒฐ์ ์ ์งํ ์ํ์์ ์ฌ์ฐ๊ฒฐ์ด๋ ์ถ๊ฐ ์ค์ ์์ด ์๋ฒ๋ก๋ถํฐ ์ง์์ ์ธ ๋ฐ์ดํฐ ์คํธ๋ฆผ์ ๋ฐ์ ์ ์๋ค.
๋ฐ๋ฉด, WebSocket์ ๋ ๋ฆฝ์ ์ธ ํ๋กํ ์ฝ์ ์ฌ์ฉํ๊ณ , HTTP์๋ ๋ณ๋์ ์ฐ๊ฒฐ์ ๋ง๋ค์ด ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ๋๋ค.
SSE๋ CORS(Cross-Origin Resource Sharing)์ ํตํด ๋ค๋ฅธ ๋๋ฉ์ธ์์๋ ๋ฐ์ดํฐ๋ฅผ ์์ ํ ์ ์๋ค. WebSocket๋ ๋์ผํ ๋๋ฉ์ธ ๊ฐ ํต์ ์ ์ ๊ณตํ์ง๋ง, ๋ณด์์์ ์ด์ ๋ก ์ถ๊ฐ ๊ตฌ์ฑ์ด ํ์ํ ์ ์๋ค.
๐ Next.js์ SSE ์ ์ฉํ๊ธฐ
Next.js์์๋ ์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง ์ง์ํด์ SSE API Route๋ ๊ตฌํํ ์ ์๋ค. ํ์ง๋ง ์ฐ๋ฆฌ ํ๋ก์ ํธ์์๋ Spring์ผ๋ก ์๋ฒ๋ฅผ ๊ตฌํํ๊ธฐ ๋๋ฌธ์ ๋๋ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๋ ๋ก์ง์ ๊ตฌํํ๋ฉด ๋๋ค!
์ฐ๋ฆฌ ์๋น์ค์์๋ ์ค์๊ฐ ์๋ฆผ ์๋น์ค๋ฅผ ๋ก๊ทธ์ธ๋ ํ์๋ง ์ด์ฉํ ์ ์๋๋ก ์ค๊ณํ๋ค. ๊ทธ๋์ ์๋ฒ๋ SSE ์ฐ๊ฒฐ ์ Authorization ํค๋์ ํ ํฐ๊ฐ์ ํฌํจํ์ฌ ์ธ์ฆ๋ ์์ฒญ๋ง ์ฒ๋ฆฌ๋๋๋ก ๊ตฌํํด์ผํ๊ณ EventSource API๋ ์์ฒญ์ ๋ณด๋ผ ๋ ์ปค์คํ ํค๋๋ฅผ ์ถ๊ฐํ ์ ์์ด์ event-source-polyfill๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์๋ค.
๋ฌธ์ ์ฝ๋
import { EventSourcePolyfill } from 'event-source-polyfill';
import { auth } from '@withbee/auth-config';
export const connectSSE = async (url: string, onMessage: (data: any) => void, onConnect?: () => void, onError?: (error: any) => void, token?: string) => {
// ์ธ์ฆ ์ ๋ณด ์ป๊ธฐ
const session = await auth();
const accessToken = session?.user!.accessToken;
if (!accessToken) throw new Error('Authentication required');
// SSE ์ฐ๊ฒฐ ์ค์
const eventSource = new EventSourcePolyfill(url, {
headers: { Authorization: `Bearer ${accessToken}` },
heartbeatTimeout: 30000, // 30์ด๋ง๋ค heartbeat ์ฒดํฌ
});
// ์ฐ๊ฒฐ ์ฑ๊ณต ์ด๋ฒคํธ
eventSource.addEventListener('connect', (event: any) => {
if (event.data === 'SSE ์ฐ๊ฒฐ์ด ์๋ฃ๋์์ต๋๋ค.') {
onConnect?.(); // ์ฐ๊ฒฐ ์๋ฃ ์ฝ๋ฐฑ ํธ์ถ
}
});
// ๋ฉ์์ง ์์ ์ด๋ฒคํธ
eventSource.addEventListener('message', (event: MessageEvent) => {
onMessage(event.data); // ๋ฉ์์ง ์ฒ๋ฆฌ
});
// ์๋ฌ ์ฒ๋ฆฌ
eventSource.onerror = (error) => {
console.error('SSE Error:', error);
onError?.(error); // ์๋ฌ ์ฝ๋ฐฑ ํธ์ถ
eventSource.close(); // ์ฐ๊ฒฐ ์ข
๋ฃ
};
return eventSource; // ์ด๋ฒคํธ ์์ค ๋ฐํ
};
์ด๋ ๊ฒ ์ฝ๋๋ฅผ ์์ฑํ ํ์ ๋๋ ค๋ดค๋๋ฐ ๋ค์๊ณผ ๊ฐ์ ์๋ฌ๊ฐ ๋ด๋ค.
headers ๊ฐ์ฒด๊ฐ ์๋ฒ ์์ฒญ ์ค์ฝํ ๋ฐ์์ ํธ์ถ๋จ
์ฒ์์๋ ๋ฌด์จ ์๋ฆฌ์ธ๊ฐ ํ๋๋ฐ ์๋ฒ ์ปดํฌ๋ํธ์์๋ ๋ธ๋ผ์ฐ์ ์ ์ฉ API์ธ EventSource ๋ฅผ ์ฌ์ฉํ ์ ์์ด์ ๋ ์๋ฌ์๋ค!.
๊ทธ๋ ๋ค๋ฉด ์์ฒญ ์ค์ฝํ ๋ฐ์์ ํธ์ถ๋๋ค๋ ๊ฒ ๋ฌด์จ ๋ง์ผ๊น?
- connectSSE ํจ์๊ฐ 'use client' ์ง์๋ฌธ ์์ด ์์ฑ๋จ
- Next.js๋ ์ด ์ฝ๋๋ฅผ ์๋ฒ ์ปดํฌ๋ํธ๋ก ์ธ์
- ๋น๋/์๋ฒ ์์ ์์ ์ ์ด ์ฝ๋๊ฐ ํ๊ฐ๋จ
- ์ด ์์ ์๋ ์ค์ HTTP ์์ฒญ์ด ์์ (= ์์ฒญ ์ค์ฝํ ๋ฐ)
- headers ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ ค๊ณ ์๋ํ์ง๋ง ์ค์ ์์ฒญ ์ปจํ ์คํธ๊ฐ ์์ด์ ์๋ฌ ๋ฐ์
๊ทธ๋ฌ๋๊น ๋๊ฐ ์์ฝํด๋ณด๋ฉด ๋ธ๋ผ์ฐ์ ์ ์ฉ api์์ header๊ฐ ์๋๋ฐ ์๋ฒ์ปดํฌ๋ํธ์์ ์ด๋ฅผ ๋ถ๋ฅผ๋ ค๊ณ ํ๋๊น ์์ฒญ ์ค์ฝํ ๋ฐ์์ header๊ฐ์ฒด๊ฐ ํธ์ถ๋๋ค๋ ๊ฒ์ด๋ค.
๊ผฌ๊ผฌ์ง(๊ผฌ๋ฆฌ์ ๊ผฌ๋ฆฌ๋ฅผ ๋ฌด๋ ์ง๋ฌธ ใ ใ ๋ด๊ฐ ๋ง๋ ์ฉ์ด)
๊ทธ๋ ๋ค๋ฉด ์์ฒญ ์ค์ฝํ๋ ์ ํํ ์ธ์ ํํ๋๋ ๋จ์ด์ผ๊น?
์์ฒญ ์ค์ฝํ๋ ํ๋์ HTTP ์์ฒญ์ด ์์๋์ด ์๋ต์ด ์๋ฃ๋ ๋๊น์ง์ ์๋ช ์ฃผ๊ธฐ๋ฅผ ๊ฐ์ง๋ ์คํ ์ปจํ ์คํธ๋ฅผ ์๋ฏธํ๋ค. Next.js์์๋ ํฌ๊ฒ ์ธ ๊ฐ์ง ์ปจํ ์คํธ๊ฐ ์๋ค.
์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง(SSR) ์ปจํ ์คํธ
// ์๋ฒ์์ ์คํ๋๋ ์ฝ๋
// headers(), cookies() ๋ฑ์ API ์ฌ์ฉ ๊ฐ๋ฅ
export default function Page() {
// ์ฌ๊ธฐ๋ ์์ฒญ ์ค์ฝํ ๋ด๋ถ
}
ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ์ปจํ ์คํธ
'use client'
// ๋ธ๋ผ์ฐ์ ์์ ์คํ๋๋ ์ฝ๋
// window, document ๋ฑ ๋ธ๋ผ์ฐ์ API ์ฌ์ฉ ๊ฐ๋ฅ
์๋ฒ ์ปดํฌ๋ํธ ๋น๋ ํ์ ์ปจํ ์คํธ
// ๋น๋ํ ๋ ์คํ๋๋ ์ฝ๋
// ์์ฒญ ์ค์ฝํ ๋ฐ
const config = {
// ์ฌ๊ธฐ์ headers ์ ๊ทผ ๋ถ๊ฐ
}
ํด๊ฒฐ์ฑ ์ ๊ฐ๋จํ๋ค. EventSourcePolyfill์ด ํ์๋ก ํ๋ ๋ธ๋ผ์ฐ์ ์ปจํ ์คํธ๊ฐ ์์๊ณ headers ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ ค๊ณ ํ์ง๋ง ์ค์ HTTP ์์ฒญ ์ปจํ ์คํธ๊ฐ ์์ด ์๋ฌ๊ฐ ๋ ๊ฑฐ๊ธฐ ๋๋ฌธ์ ์ ์ปดํฌ๋ํธ๋ฅผ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ๋ก ๋ฐ๊ฟ์ฃผ๋ฉด ๋๋ค. ๊ทธ๋ฌ๋๊น 'use client'๋ง ๋ถ์ฌ์ฃผ๋ฉด ๋๋ค.
๋ฌธ์ ํด๊ฒฐ ์ฝ๋
'use client';
// import ์๋ต
export default function RealTimeMsg() {
const { data: session, status } = useSession();
const [notifications, setNotifications] = useState<Notification[]>([]);
const [accessToken, setAccessToken] = useState<string | null>(null);
const [isVisible, setIsVisible] = useState(false);
const [isAnimatingOut, setIsAnimatingOut] = useState(false);
useEffect(() => {
if (
status === 'authenticated' &&
session?.user?.accessToken &&
!accessToken
) {
setAccessToken(session.user.accessToken);
}
}, [session, status, accessToken]);
useEffect(() => {
let eventSource: EventSourcePolyfill;
if (accessToken) {
(async () => {
eventSource = await connectSSE(
`${BASE_URL}/api/notifications/stream`,
(rawData) => {
const parsedNotification: Notification = JSON.parse(rawData);
setNotifications((prev) => [...prev, parsedNotification]);
// ์๋ฆผ ํ์ ๋ฐ ์๋ ์ ๊ฑฐ ํ์ด๋จธ ์ค์
}
);
})();
}
// ํด๋ฆฐ์
return () => {
eventSource?.close();
};
}, [accessToken]);
const handleClose = () => {
setIsAnimatingOut(true);
setTimeout(() => {
setIsVisible(false);
setNotifications((prev) => prev.slice(1));
}, 1000); // ์ ๋๋ฉ์ด์
์๊ฐ๊ณผ ๋ง์ถค
};
if (!isVisible || notifications.length === 0) {
return null;
}
const notification = notifications[0];
if (!notification) {
return null;
}
return (
<div className={styles.background}>
// ์๋ต
</div>
);
}
์ต์ข ํ๋ฉด
๐ซ๋ง์น๋ฉฐ
์ฌ์ค ์๋ฒ ์ปดํฌ๋ํธ๋ฅผ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ๋ก ๋ฐ๊พธ๋ฉด ํด๊ฒฐ๋๋ ๋ฌธ์ ์์ง๋ง ์๋ฌ ๋ก๊ทธ์์ Next.js์ SSR๊ณผ CSR์ ์ถฉ๋ถํ ์ดํด๊ฐ ์์ผ๋ฉด ํท๊ฐ๋ฆฌ๋ ๋ฌธ์ ์๊ธฐ์ ์ถฉ๋ถํ ์๊ฐ์ ๋ค์ฌ์ ๊ณ ๋ฏผํด๋ณด์๋ค,, ๊ทธ๋ฆฌ๊ณ ๊ทธ ๋ค๋ก ๋ฐฐํฌํ๋ ๊ณผ์ ์์ ๋ก์ปฌ์์ ๊ฐ๋ฐํ ๋๋ SSE ์ฐ๊ฒฐ์ด ์๋ฌ ์์ด ์ ์๋ํ๋๋ฐ ๋ฐฐํฌ ํ SSE์ฐ๊ฒฐํ๋ ํ์ด์ง์์ 502 Bad Gateway์๋ฌ๊ฐ ๋ฐ์ํ์๋ค. SSEํต์ ์ ์ง์์ ์ผ๋ก ์ฐ๊ฒฐํ๋ ค๋ฉด nginx์ชฝ์์๋ ๋ฐ๋ก ๋ ์ค์ ์ ํด์ค์ผ ํ๋ค. ๋คํํ ํ์๋ค์ ๋์์ผ๋ก ๋ง๋ฌด๋ฆฌ๋ฅผ ๊น๋ํ๊ฒ ํ ์ ์์๋ ๊ฒ ๊ฐ๋ค. ์๋ก์ด ๊ธฐ์ ํ์ตํด์ ์ง๋ฆฟํ๋ค~~๐ช
์ฐธ๊ณ ์๋ฃ
'Frontend > Next.js' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Next.js - Infinite Scrollํ์ฉํด์ ๋ฐ์ดํฐ ์ฒ๋ฆฌํ๊ธฐ (1) | 2024.11.03 |
---|---|
Next.js - Auth.js( NextAuth.js) ๋ฅผ ์ด์ฉํ Keycloak๊ณผ์ ์ธ์ฆ ์ฒ๋ฆฌ ๊ณผ์ (0) | 2024.09.24 |
Next.js - next.js์์ session ํ์ฉํ๊ธฐ (0) | 2024.09.23 |
Next.js - Data fetching (React vs Next.js) (0) | 2024.02.11 |
Next.js 14๋ฒ์ - Layouts,Metadata,Dynamic Routes (0) | 2024.02.08 |