SSE(Server Sent Events)
- SSE์ ๊ฒฝ์ฐ ์นํ์ด์ง(ํด๋ผ์ด์ธํธ)์ ์์ฒญ ์์ด๋ ์ธ์ ๋ ์ง ์๋ฒ๊ฐ ์๋ก์ด ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ด๋ ๊ฒ์ด ๊ฐ๋ฅ
- ์ ํต์ ์ผ๋ก ์นํ์ด์ง(ํด๋ผ์ด์ธํธ)๋ ์๋ก์ด ๋ฐ์ดํฐ๋ฅผ ๋ฐ๊ธฐ ์ํด ์๋ฒ๋ก ์์ฒญ์ ๋ณด๋ด์ผ ํจ
- SSE(Server-Sent Events)๋ ์ค์๊ฐ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ตฌํํ๋๋ฐ ์ฌ์ฉ๋ ์ ์๋ Web API
- ์ค์๊ฐ์ผ๋ก ์์ ๋ถ๋ฌ์ค๊ธฐ, ์๋ฆผ ๋ฐ๊ธฐ ๋ฑ ์ค์๊ฐ์ผ๋ก ๋ชจ๋ํฐ๋ง ํด์ผํ๋ ์ํฉํด์ ์ฌ์ฉํ ๋ ์ ์ฉํ๊ฒ ์ฌ์ฉ
๋์ ํ๋ฆ

- ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ์๊ฒ SSE๋ก ํต์ ํ์๋ ์์ฒญ์ ๋ณด๋. (Connection Request)
- ์๋ฒ๋ ์ด ์์ฒญ์ ์์ ํ๊ณ ์๋ฝํ์์ ์๋ฆฌ๋ ๋ฉ์์ง๋ฅผ ๋ณด๋. (Connection Response)
- ํด๋ผ์ด์ธํธ๋ ์ด๋ฅผ ๋ฐ๊ณ , ์ง๊ธ๋ถํฐ ์๋ฒ๊ฐ ๋ณด๋ด์ฃผ๋ ๋ฐ์ดํฐ๋ค์ ๋ฐ์ํ ์ค๋น
- ์ด์ ์๋ฒ๋ ์ ํด์ง ์ด๋ฒคํธ๊ฐ ์์ ๋๋ง๋ค ํด๋ผ์ด์ธํธ์๊ฒ ๋ฉ์์ง๋ฅผ ๋ณด๋. (Event Response)
์ด๋, ํด๋ผ์ด์ธํธ๋ ๋จ๋ฐฉํฅ ํต์ ์ด๋ฏ๋ก ์ด์ ์๋ตํ ์๋ ์์! - ํด๋ผ์ด์ธํธ๋ ์๋ฒ๋ก๋ถํฐ ๋ฉ์์ง๊ฐ ๋์ฐฉํ ๋๋ง๋ค ์ด์ ๋ฐ์ํ์ฌ ํ๋ฉด์ ์ ๋ฐ์ดํธํ๋ ๋ฑ ํ์ํ ์์ .
- ์ด ๊ณผ์ ๋ค์ ํ๋์ ์ฐ๊ฒฐ ์์์ ๊ณ์ ์ด๋ค์ง๊ณ , ๋ง์ฝ ์ฐ๊ฒฐ์ด ๋๊ธฐ๋ฉด ํด๋ผ์ด์ธํธ๋ ์๋์ผ๋ก ์ฌ์ฐ๊ฒฐ์ ์์ฒญํ์ฌ ํต์ ์ ์ฌ๊ฐ.
- ํ์ํ ์์ ์ ๋ง์น๋ฉด ํด๋ผ์ด์ธํธ ๋๋ ์๋ฒ์์ ์๋๋ฐฉ์๊ฒ ์ข ๋ฃ๋ฅผ ํต๋ณดํ๋ ๋ฉ์์ง๋ฅผ ๋ณด๋์ผ๋ก์จ ์ฐ๊ฒฐ์ด ๋.(Connection Closed)
๋ฉ์์ง ๊ตฌ์กฐ
content-type : text/event-stream
id: 123
event: message
data: Hello World
data: This is another message line
๊ฐ ๋ฉ์์ง๋ ์ฌ๋ฌ ํ๋๋ก ๊ตฌ์ฑ๋๋ฉฐ, ๊ฐ ํ๋๋ ์ค๋ฐ๊ฟ(\\n)์ผ๋ก ๊ตฌ๋ถ๋๊ณ , ๋ฉ์์ง์ ๋์ ๋ ์ค๋ฐ๊ฟ(\\n\\n)์ผ๋ก ํ์.
1. Data
- ์ญํ : ์ค์ ์ ์กํ ๋ฐ์ดํฐ๋ฅผ ๋ด๋ ํ๋
- ํน์ง:
- ํ ๋ฉ์์ง์ ์ฌ๋ฌ data: ํ๋๋ฅผ ํฌํจ
- ๊ฐ data: ๋ผ์ธ์ ์ค๋ฐ๊ฟ(\\n)์ผ๋ก ๊ตฌ๋ถ
- ๋ธ๋ผ์ฐ์ ์์ ์์ ์, ์ฌ๋ฌ data: ์ค์ ํ๋์ ๋ฌธ์์ด๋ก ํฉ์ณ์ ธ ์ ๋ฌ
- ์์:
data: Hello data: World
→ ํด๋ผ์ด์ธํธ์์๋ "Hello\\nWorld"๋ก ์์
2. Event
- ์ญํ : ๊ธฐ๋ณธ message ์ธ์ ์ปค์คํ ์ด๋ฒคํธ ํ์ ์ ์ ์
- ํน์ง:
- event: ํ๋๋ฅผ ์ฌ์ฉํด ์ด๋ฒคํธ ์ด๋ฆ์ ์ง์ ํ๋ฉด,
- ํด๋ผ์ด์ธํธ์์ addEventListener('customEventName', ...) ํ์์ผ๋ก ์์ ๊ฐ๋ฅ
- ์์:
event: update data: New data available
→ ํด๋ผ์ด์ธํธ์์๋ eventSource.addEventListener('update', handler)๋ก ์ฒ๋ฆฌ
3. ID
- ์ญํ : ๋ฉ์์ง์ ๊ณ ์ ID๋ฅผ ์ง์
- ์ฉ๋:
- ์ฌ์ฐ๊ฒฐ ์์ ๊ด๋ฆฌ์ ์ฌ์ฉ
- ๋ธ๋ผ์ฐ์ ๊ฐ ๋๊ธด ํ ๋ค์ ์ฐ๊ฒฐํ ๋, ๋ง์ง๋ง์ผ๋ก ๋ฐ์ id ๊ฐ์ Last-Event-ID ํค๋์ ํฌํจํ์ฌ ์ ์กํ๋ฏ๋ก, ์๋ฒ๋ ์ค๋จ๋ ์์ ์ดํ์ ๋ฐ์ดํฐ๋ง ๋ค์ ๋ณด๋ผ ์ ์์.
- ์์:
- id: 123 data: New message
4. ๋ฉ์์ง ์ข ๋ฃ
- ๊ฐ ์ด๋ฒคํธ ๋ฉ์์ง๋ ๋ฐ๋์ ๋ ์ค๋ฐ๊ฟ(\\n\\n)์ผ๋ก ๋๋์ผ ํจ.
- ์ด ๊ตฌ๋ถ์๊ฐ ์์ผ๋ฉด ๋ธ๋ผ์ฐ์ ๋ ๋ฉ์์ง๋ฅผ “์์ง ๋๋์ง ์์ ์ํ”๋ก ๊ฐ์ฃผ
โ ์ข ํฉ ์์
id: 42
event: update
data: {"status": "ok", "message": "Data refreshed"}
\\n
→ ํด๋ผ์ด์ธํธ ์ธก ๋์ ์์
const eventSource = new EventSource('/events');
eventSource.addEventListener('update', (e) => {
console.log('Received event:', e.data);
// {"status": "ok", "message": "Data refreshed"}
});
์ฆ, SSE ๋ฉ์์ง๋ id, event, data ํ๋๋ฅผ ์กฐํฉํ ๋จ์ํ ํ ์คํธ ๊ธฐ๋ฐ ํ๋กํ ์ฝ์ด๋ฉฐ, ์ค๋ฐ๊ฟ๊ณผ ๋ฉ์์ง ๊ตฌ๋ถ ๊ท์น์ ํตํด ๋ธ๋ผ์ฐ์ ๊ฐ ์ค์๊ฐ ์ด๋ฒคํธ๋ฅผ ์์ ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์๋๋ก ์ค๊ณ๋จ.
์์์ผ ํ ๊ฐ๋
- Event ID ๋ฐ Last-Event-ID ํค๋
- ์ด๋ฒคํธ์ ์์๋ฅผ ๋ณด์ฅํ๊ธฐ ์ํด event ID๋ฅผ ์ฌ์ฉ๊ฐ๋ฅ
- ํด๋ผ์ด์ธํธ๋ Last-Event-ID ํค๋๋ฅผ ํตํด ๋ง์ง๋ง์ผ๋ก ์์ ํ ์ด๋ฒคํธ ID๋ฅผ ์๋ฒ์ ์ ๋ฌํ์ฌ, ์ฐ๊ฒฐ์ด ์ฌ์ค์ ๋ ๊ฒฝ์ฐ์๋ ๋ฐ์ดํฐ ์์ค ์์ด ์ด์ด์ ๋ฐ์ ์ ์์
- Custom Event Types
- ๊ธฐ๋ณธ ์ด๋ฒคํธ ํ์ ์ธ์๋, ์ปค์คํ ์ด๋ฒคํธ ํ์ ์ ์ ๊ฐ๋ฅ
- ์๋ฒ์์ ํน์ ์ด๋ฒคํธ ํ์ ์ ์ค์ ํ๊ณ , ํด๋ผ์ด์ธํธ์์๋ ํด๋น ํ์ ์ ๋ํ ํธ๋ค๋ฌ๋ฅผ ์ค์ ํ์ฌ ๋ค์ํ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌ
- ๋ฉ์์ง์ ์ด๋ฒคํธ ์ด๋ฆ์ด ์ง์ ๋์ง ์์ ๊ฒฝ์ฐ onmessage ํธ๋ค๋ฌ๊ฐ ํธ์ถ
- Retry Mechanism
- SSE๋ ์๋์ผ๋ก ์ฌ์ฐ๊ฒฐ์ ์๋ํ๋ฉฐ, ๊ธฐ๋ณธ ์ฌ์ฐ๊ฒฐ ๊ฐ๊ฒฉ์ 3์ด
- ์ ์ ๊ฐ์ด ์๋ ๊ฒฝ์ฐ retry ํ๋๋ ๋ฌด์๋จ
- ์๋ฒ๋ retry ํ๋๋ฅผ ํตํด ํด๋ผ์ด์ธํธ์ ์ฌ์ฐ๊ฒฐ ๊ฐ๊ฒฉ์ ์กฐ์
- res.write(`retry: 5000\\n`); // 5์ด ํ ์ฌ์ฐ๊ฒฐ ์๋
- ๋ธ๋ผ์ฐ์ ์ง์ ๋ฐ ํด๋ฆฌํ
- ๋๋ถ๋ถ์ ํ๋์ ์ธ ๋ธ๋ผ์ฐ์ ๋ SSE๋ฅผ ์ง์ํ์ง๋ง, Internet Explorer์ ๊ฐ์ ๊ตฌํ ๋ธ๋ผ์ฐ์ ์์๋ ์ง์๋์ง ์์ ์ ์์
- ์ด๋ฐ ๊ฒฝ์ฐ ํด๋ฆฌํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ ํธํ์ฑ์ ๋ณด์ฅ
ํ๊ณ์
- Open connection ์ ํ๊ณผ HTTP/2
- ํ ๋ธ๋ผ์ฐ์ domain ๋น open connection ์ ํ
- ์ฃผ์: HTTP/2 ๋ฅผ ์ฌ์ฉํ์ง ์์ ๋ SSE๋ ํ์ฑํ๋ ์ฐ๊ฒฐ์ ์ต๋ ๊ฐ์ ์ ํ์ผ๋ก ์ธํ ํ๊ณ๋ฅผ ๊ฒช์ ์ ์์ผ๋ฉฐ, ์ด ์ ํ์ ๋ธ๋ผ์ฐ์ ๋น ์ ์ฉ๋ ๋ฟ๋ง ์๋๋ผ ๋งค์ฐ ๋ฎ์ ์(6)๋ก ์ค์ ๋์ด ์์ด ํนํ ์ฌ๋ฌ ํญ์ ์ด ๋ ๋ฌธ์ ๋ฅผ ๊ฒช์ ์ ์์ต๋๋ค. ์ด ๋ฌธ์ ๋ Chrome๊ณผ Firefox์์ ์์ ๋์ง ์์ ๊ฒ์ผ๋ก ํ์๋์์ต๋๋ค. ์ด ์ ํ์ ๋ธ๋ผ์ฐ์ ์ ๋๋ฉ์ธ๋ณ๋ก ์ ์ฉ๋๋ฏ๋ก www.example1.com ์ ๋ํด ๋ชจ๋ ํญ์์ 6๊ฐ์ SSE ์ฐ๊ฒฐ์ ์ด ์ ์๊ณ , www.example2.com ์ ๋ํด์๋ 6๊ฐ์ SSE ์ฐ๊ฒฐ์ ์ด ์ ์์ต๋๋ค. (์ถ์ฒ: Stackoverflow). HTTP/2๋ฅผ ์ฌ์ฉํ ๋๋ ๋์์ ์ด ์ ์๋ HTTP ์คํธ๋ฆผ์ ์ต๋ ๊ฐ์๊ฐ ์๋ฒ์ ํด๋ผ์ด์ธํธ ๊ฐ์ ํ์๋๋ฉฐ, ๊ธฐ๋ณธ๊ฐ์ 100์ ๋๋ค.
- EventSource์ ํ๊ณ์ polyffils
- Web API๋ก ์ฃผ์ด์ง๋ EventSource๋ ํธ๋ฆฌํ์ง๋ง, GET ์์ฒญ ๋ฐ์ ์๋๋ ์ , header ์์ ์ด ์๋๋ ์ ๋ฑ์ ํ๊ณ
- polyfill ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํตํด ๊ทน๋ณต ๊ฐ๋ฅ
๊ธฐ๋ณธ ๊ตฌ์ฑ
ํด๋ผ์ด์ธํธ - ํ๋ก ํธ ๊ตฌ์ฑ
- ์ด๋ฒคํธ ๋ฆฌ์ค๋ ๋ฑ๋ก ๋ฐฉ์ - addEventListner ํ์ฉ ์
// ์ด๋ฒคํธ๋ฅผ ์ ๋ฌ ๋ฐ๊ธฐ ์ํด์ ์๋ฒ๋ก ์ ์์ ์์ํ๋ ค๋ฉด ์ฐ์ ,
// ์ด๋ฒคํธ๋ฅผ ์์ฑํ๋ ์๋ฒ์ธก ์คํฌ๋ฆฝํธ๋ฅผ URI๋ก ์ง์ ํ์ฌ ์๋ก์ด EventSource ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ค
// EventSource ์์ฑ์์ ์ ๋ฌ ๋ URL์ด ์ ๋ URL ์ธ ๊ฒฝ์ฐ
// ํด๋น ์ถ์ฒ (scheme, domain, port)๊ฐ ํธ์ถ ํ์ด์ง์ ์ถ์ฒ์ ์ผ์นํด์ผ ํ๋ค.
const eventSource = new EventSource(`/subscribe`); // sse์ฝ๋๊ฐ ์๋ ์๋ฒ๋จ ํ์ผ
// ์๋ฒ๋ก๋ถํฐ ๋ฐ์ดํฐ๊ฐ ์ค๋ฉด
eventSource.addEventListener('message', function(e) {
console.log(e.data);
});
// connection๋๋ฉด
eventSource.addEventListener('open', function(e) {
// Connection was opened.
});
// error ๋๋ฉด
eventSource.addEventListener('error', function(e) {
if (e.readyState == EventSource.CLOSED) {
// Connection was closed.
}
});
- ํ๋กํผํฐ ํธ๋ค๋ฌ ๋ฐฉ์ - on
const eventSource = new EventSource('http://localhost:3000/events');
eventSource.onopen = () => {
console.log('SSE ์ฐ๊ฒฐ ์ฑ๊ณต');
setError(null);
};
eventSource.onmessage = (event: MessageEvent) => {
const eventData = JSON.parse(event.data);
const newNotification: Notification = {
id: Date.now(),
message: eventData.message,
};
setMessages((prevMessages) => [...prevMessages, newNotification]);
};
eventSource.onerror = (err) => {
console.error('SSE ์๋ฌ:', err);
setError('์ค์๊ฐ ์๋ฆผ ์ฐ๊ฒฐ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค.');
// ์ด๋ฒคํธ ์คํธ๋ฆผ ๋ซ๊ธฐ
eventSource.close();
};
โeventSource์ addEventListner vs on (ํ๋กํผํฐ ํธ๋ค๋ฌ) ๋ฐฉ์์ ์ด๋ค ์ฐจ์ด๋ฅผ ๊ฐ์ง๊น?
- ํ๋กํผํฐ ํธ๋ค๋ฌ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ๊ณผ addEventListner๋ฅผ ํ์ฉํ ์ด๋ฒคํธ ๋ฆฌ์ค๋ ๋ฑ๋ก ๋ฐฉ์์ ์๋์ ๊ฐ๋ค. ํ์ง๋ง, ์ค์ ๋ก๋ addEventListner๊ฐ ํจ์ฌ ๋ ์ ์ฐํ๊ณ ํ์ฅ์ฑ ์๋ ๋ฐฉ๋ฒ
- onmessage / onopen / onerror → ๊ธฐ๋ณธ ์ด๋ฒคํธ 3์ข ๋ง ์ง์
- addEventListener() → ์ปค์คํ ์ด๋ฒคํธ๊น์ง ํฌํจํด ๋ชจ๋ ์ด๋ฒคํธ ์์ ๊ฐ๋ฅ
- ์ฃผ์ ์ฐจ์ด์ ์์ฝ
๊ตฌ๋ถ onmessage / onopen / onerror addEventListener() ์ง์ ์ด๋ฒคํธ 'open', 'message', 'error' 'open', 'message', 'error', ์ปค์คํ ์ด๋ฒคํธ ์ค๋ณต ํธ๋ค๋ฌ ๋ฑ๋ก โ ๋ถ๊ฐ๋ฅ (๋ง์ง๋ง ํ ๋น๋ง ์ ํจ) โ ๊ฐ๋ฅ (์ฌ๋ฌ ๊ฐ ๋ฑ๋ก ๊ฐ๋ฅ) ์ด๋ฒคํธ ์ ๊ฑฐ โ ์ง์ ์ ๊ฑฐ ๋ถ๊ฐ๋ฅ โ removeEventListener()๋ก ์ ๊ฑฐ ๊ฐ๋ฅ ์ฝ๋ ๊ตฌ์กฐ ๊ฐ๋จ ๋ ์ ์ฐ, ํ์ฅ์ฑ ๋์ ์ปค์คํ ์ด๋ฒคํธ ์์ โ ๋ถ๊ฐ๋ฅ โ ๊ฐ๋ฅ (event: customName) - ์ํฉ ๋ณ ์ถ์ฒ ๋ฐฉ์
์ํฉ ์ถ์ฒ ๋ฐฉ์ ๋จ์ํ ์ฐ๊ฒฐ / ๊ธฐ๋ณธ ๋ฉ์์ง ์ฒ๋ฆฌ๋ง ํ์ํ ๋ onmessage, onopen, onerror ์ปค์คํ ์ด๋ฒคํธ(event: ํ๋) ์ฌ์ฉ / ์ฌ๋ฌ ๋ฆฌ์ค๋ ํ์ / ์ ์ง๋ณด์ ์ค์ addEventListener() ๋ฐฉ์ ๊ถ์ฅ
์๋ฒ - ๋ฐฑ์๋(node.js) ๊ตฌ์ฑ
import express from 'express';
import cors from 'cors';
const app = express();
const FRONTEND_URL = '<http://localhost:5173>';
const corsOptions = {
origin: FRONTEND_URL,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
// CORS ์์ฒญ ์ ์ธ์ฆ์ ๋ณด(์ฟ ํค, ์ธ์
, Authorization ํค๋ ๋ฑ) ํฌํจ ์ฌ๋ถ ์ค์
// false(๊ธฐ๋ณธ๊ฐ): ๊ต์ฐจ ์ถ์ฒ ์์ฒญ ์ ์ฟ ํค ๋ฑ ์ธ์ฆ์ ๋ณด๋ฅผ ์ ์กํ์ง ์์
// true: ์๋ฒ์์ Access-Control-Allow-Credentials: true ์๋ต ์ ์ธ์ฆ์ ๋ณด ํฌํจ ๊ฐ๋ฅ
// ์ฃผ์: credentials: true ์ฌ์ฉ ์ origin์ ๋ฐ๋์ ๋ช
์ํด์ผ ํ๋ฉฐ, '*'์ ํจ๊ป ์ฌ์ฉํ ์ ์์
**credentials: true,**
};
app.use(cors(corsOptions));
const port = 5000;
app.listen(port, () => {
console.log(`Listening on port ${port}`);
});
app.get('/events', (req, res) => {
**// Content-Type์ 'text/event-stream'์ผ๋ก ์ ์กํด์ผ SSE ๋ฉ์์ง ์ ์ก ๊ฐ๋ฅ"
res.setHeader('Content-Type', 'text/event-stream');**
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
setInterval(()=>{
const data = { message: `5์ด์ 1๋ฒ์ฉ ๋ณด๋ด๋ ๋ฉ์์ง : ${new Date().toISOString()}` };
res.write(`data: ${JSON.stringify(data)}\\n\\n`);
},5000)
});
- Content-Type text/event-stream
- ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก ์ค์๊ฐ ๋ฐ์ดํฐ๋ฅผ ์ ์กํ๊ธฐ ์ํด ์ฌ์ฉํ๋ MIME ํ์
- Cache-Control no-cache
- ํด๋ผ์ด์ธํธ๊ฐ ํญ์ ์ต์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ ์ ์๊ฒ ํ๊ธฐ ์ํด ์๋ต์ ์บ์ฑํ์ง ์๋๋ก ์ค์
- Connection keep-alive
- HTTP์ฐ๊ฒฐ ์ธ์ ์ ์ง์์ ์ผ๋ก ์ ์ง ์ํค๊ธฐ ์ํด ์ฌ์ฉ
์ปค์คํ ์ด๋ฒคํธ ํ์ ์ค์
// ์๋ฒ์์ ์ด๋ฒคํธ ํ์
์ค์
res.write(`event: customEvent\\n`);
res.write(`data: ${JSON.stringify({ message: 'Custom Event Data' })}\\n\\n`);
- ํด๋ผ์ด์ธํธ์์๋ addEventListner๋ฅผ ํตํด์ ์ด๋ฒคํธ ํ์ ์ ์ค์ ํ๋ค.
Reference
https://sanghee01.tistory.com/186
https://developer.mozilla.org/ko/docs/Web/API/Server-sent_events/Using_server-sent_events
https://techblog.woowahan.com/23199/
https://phsun102.tistory.com/202
https://velog.io/@blessoms2017/Event-Source-Polyfill๋ก-๋ชจ๋ -๋ธ๋ผ์ฐ์ ์์-SSE-๊ตฌํํ๊ธฐ
https://itsfuad.medium.com/understanding-server-sent-events-sse-with-node-js-3e881c533081
https://velog.io/@overslept/React์์-์์ ํ-SSEServer-Sent-Events-์ฐ๊ฒฐ-๊ด๋ฆฌํ๊ธฐ
https://cocococo331.tistory.com/76
https://boseong.dev/post/2025-09-05-sse-deep-dive
https://github.com/Azure/fetch-event-source?tab=readme-ov-file
๋๊ธ