์ผ | ์ | ํ | ์ | ๋ชฉ | ๊ธ | ํ |
---|---|---|---|---|---|---|
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 |
- ์คํ๋ง
- ์ปด๊ณต
- spring
- ๋ฐฑ์๋
- ์ปด๊ณต์
- ์ดํญ๊ณ์
- ๋ฐฑ์ค
- ๋ฆฌ์กํธ
- ๋ฐฑ์ค1436
- SSE
- ๊ทธ๋ฆฌ๋์๊ณ ๋ฆฌ์ฆ
- ์๋ฃ๊ตฌ์กฐ
- ์น๊ฐ๋ฐ๊ธฐ๋ก
- ์ฝ๋ฉ
- ๋จ์ํ ์คํธ
- ChatGPT
- ์๊ณ ๋ฆฌ์ฆ
- ๊ทธ๋ฆฌ๋
- ํ์ด์ฌ
- ํ๋ก๊ทธ๋๋ฐ
- ๊ฐ๋ฐ์
- ๋ชจ๋ฐ์ผ์ฑํ๋ก๊ทธ๋๋ฐ
- ์ฝ๋ฉํ ์คํธ
- ํ๋ก ํธ์ค๋
- ์น๊ฐ๋ฐ
- ์ฐ์ ์์ํ
- ๋ฐฑ์คํ์ด
- boj11653
- ์ปดํจํฐ๊ณตํ
- ๋ฆฌ์กํธ๋ค์ดํฐ๋ธ
- Today
- Total
๐ป๐ญ๐ง๐
๋ฆฌ์กํธ React SSE ์๋ต ์ฒ๋ฆฌํ๊ธฐ (ChatGPT stream) ๋ณธ๋ฌธ
๋ฆฌ์กํธ React SSE ์๋ต ์ฒ๋ฆฌํ๊ธฐ (ChatGPT stream)
adorableco 2024. 2. 13. 22:11๊ธฐ์กด์ ๊ทธ๋ฅ ์์ฑ๋ ๋ต๋ณ์ผ๋ก ๋ฐ์๋ ChatGPT ์๋ต์ SSE(Serve-Sent-Event) ๋ฅผ ์ด์ฉํด ์ค์๊ฐ์ผ๋ก ์์ฑํ๋ ๊ฑธ๋ก ๋ณ๊ฒฝํ๊ฒ ๋์๋ค. ์ด๋ป๊ฒ ๋ฐ์์์ผํ ์ง ์ฌ๋ฌ ๋ฐฉ๋ฒ์ ์ฐพ์๋ณด๋๋ฐ SSE ํ์ค์ ์ ์ด์ POST ๋ฐฉ์์ ์ง์์ ํ์ง ์๋๋ค๊ณ ํ๋ค. (๊ทธ๋์ SSE๋ ๋ณดํต EventSource
๋ฅผ ์ด์ฉํด ์๋ต์ ๋ฐ์ง๋ง ๋จ์ GET์ธ ๊ฒฝ์ฐ์๋ง ๊ฐ๋ฅํ๋ค.) ๊ทธ๋ฐ๋ฐ ๋๋ ๋ด๊ฐ ์์ฒญํ ์ง๋ฌธ์ ๋ํ ๋ต๋ณ์ ๋ฐ์์ผํ๋ ๊ฒ์ด๋ฏ๋ก... ์์ฉ ๋ฐฉ์์ ์ฌ๋ฟ ์ฐพ์๋ณด์๋ค.
๊ทธ๋ฌ๋ ์ค ๋์ ์ํฉ์ ์์ฃผ ๋ฑ ๋ง๋ ์ ํ๋ฒ์ ์ค๋ช
๋ฑ์ฅ... ์ต๊ณ ์ธ์...
https://www.youtube.com/watch?v=JxIQCOrsxxg
๊ธฐ์กด ์ฝ๋
const onSendMsg = async (event) => {
event.preventDefault();
const li = document.createElement("li");
li.className = styles.quest;
li.innerText = msg;
const ul = document.getElementById("msgList");
ul.appendChild(li);
try {
setIsLoading(true);
await axios
.post(
process.env.REACT_APP_API_URL + `/gpt/${chatId}`,
{
prompt: msg,
gptConfigInfo: options,
},
{
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + storedJwtToken,
},
},
)
.then((res) => {
setResult(res.data.answer);
setChatId(res.data.chat_room_id);
setIsLoading(false);
});
} catch (error) {
console.error(error);
alert(error.message);
}
};
- ๋จ์ํ๋ค! ๋ฉ์์ง๋ฅผ body์ ๋ด์ POST ๋ก api์ ์์ฒญ์ ํ๋ฉด response์ ChatGPT์ ๋ต๋ณ, ์ฑํ
๋ฐฉ ๋ฒํธ๊ฐ ๋ด๊ธฐ๊ณ useState ๋ณ์์ธ
isLoading
์ false๋ก ์ ํํ์ฌ ๋ก๋ฉ์ ์ ์งํ๋ค. ChatGPT์ ๋ต๋ณ์ ๋ง์ฐฌ๊ฐ์ง๋ก useState ๋ณ์์ธresult
์ ๋ด๊ธด๋ค.
โ ์ฌ๊ธฐ์์ ์ด๋ฐ์ ์์ฑ๋๋ li ํ๊ทธ๋ ๋ด๊ฐ ๋ณด๋ด๋ ๋ฉ์์ง๋ฅผ ๋์ฐ๊ธฐ ์ํ ์ฉ๋์ด๋ค.
useEffect(() => {
if (result != undefined) {
const li = document.createElement("li");
li.className = styles.response;
li.innerText = result;
document.getElementById("msgList").appendChild(li);
setMsg("");
}
}, [result]);
- ๊ทธ๋ฆฌ๊ณ result ๊ฐ์ ๋ณํ๋ฅผ ๊ฐ์งํ์ฌ, ๋ณํ๊ฐ ์๊ธธ ๋๋ง๋ค, ์ฆ ์๋ก์ด ์๋ต ๋ฉ์์ง๊ฐ ์ฌ ๋๋ง๋ค
li ํ๊ทธ
๋ฅผ ์ถ๊ฐํ๊ณinnerText
๋ฅผresult
๋ก ์ค์ ํด์ฃผ๋ฉด ์๋์ ๊ฐ์ด ๋ฉ์์ง ๋ต๋ณ์ด ์์ฑ๋๋ ํํ์๋ค.
SSE ์ ์ฉํ๊ธฐ
์ ๋ฐ์ ์ธ ํํ๋ ๋น์ทํ๋ ์กฐ๊ธ์ฉ ๊ฝค ๋ง์ด ๋ฌ๋ผ์ก๋ค.
const onSendMsg = async (event) => {
event.preventDefault();
const li = document.createElement("li");
li.className = styles.quest;
li.innerText = msg;
const ul = document.getElementById("msgList");
ul.appendChild(li);
await fetch(process.env.REACT_APP_API_URL + `/gpt/${chatId}`, {
method: "POST",
headers: {
Authorization: "Bearer " + storedJwtToken,
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt: msg,
gptConfigInfo: options,
}),
}).then(async (response) => {
setMsg("");
const reader = response.body
.pipeThrough(new TextDecoderStream())
.getReader();
const li = document.createElement("li");
li.className = styles.response;
setExistLi(li);
document.getElementById("msgList").appendChild(li);
setMsg("");
while (true) {
const { value, done } = await reader.read();
if (done) break;
let str = JSON.stringify(value);
str = str.replace(/\n/gi, "\\r\\n");
let json = JSON.parse(str);
const cleanedStr = json.replace(/data:|\n/g, "").trim();
let chatRoomId;
const matchResult = cleanedStr.match(/\d+/);
if (matchResult) {
chatRoomId = matchResult[0];
}
let simpleText;
simpleText = cleanedStr.replace(/\\r\\n/g, "").trim();
const cleanedText = simpleText.replace(/chat_room_id:.*/, "");
setResult((prev) => prev + cleanedText);
if (chatRoomId) {
setChatId(chatRoomId);
setIsLoading(false);
}
}
setResult("");
setExistLi(null);
});
};
- ๊ธฐ์กด ์ฝ๋์์ ์ฌ์ฉํ๋
axios
๋์fetch
๋ฅผ ์ฌ์ฉํ๋ค.axios
๊ฐ ์คํธ๋ฆผ ์ฒ๋ฆฌ๊ฐ ์์ ์๋๋๊ฑด์ง ๋ชจ๋ฅด๊ฒ ์ง๋ง ์ฐธ์กฐํ ์ฝ๋๋ค ์ค์์axios
๋ฅผ ์ฌ์ฉํ๊ฑด ์์๋ค. - ๊ธฐ์กด ์ฝ๋์์๋ ๋ต๋ณ์ด ์ค๋ฉด ๊ทธ ๋ฉ์์ง๋ก ํ๋์ ๋ต์ฅ ๋ฉ์์ง li๋ฅผ ๋ง๋ค๋ฉด ๋ผ์ ๋ฌธ์ ๊ฐ ์์๋๋ฐ SSE๋ ์ฒซ ์์์๋ li ํ๊ทธ๋ฅผ ์๋ก ๋ง๋ค์ด์ผ ํ๊ณ , ๋ต๋ณ์ด ๋๋ ๋๊น์ง ๊ณ์ ๊ฐ์ li ํ๊ทธ ๋ด์์ innerText๋ฅผ ๋ฐ๊ฟ์ค์ผ ํ๊ธฐ ๋๋ฌธ์ (๊ทธ๋ฆฌ๊ณ ๋ค๋ฅธ ๋ต๋ณ๊ณผ๋ ๊ตฌ๋ถ์ด ํ์ํจ) ์ด๊ฑธ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ ์ง๊ฐ ๊ณ ๋ฏผ์ด์๋ค.
- ๋๋ ์๋์ ๊ฐ์ด ํ์ฌ li ํ๊ทธ๊ฐ ์กด์ฌํจ์ ์ ์ ์๊ฒ, ๊ทธ๋ฆฌ๊ณ ๋ต๋ณ ์คํธ๋ฆผ์ด ์ฌ ๋๋ง๋ค ํด๋น li ํ๊ทธ์ innerText ๊ฐ ๊ณ์ ๋ฐ๋ ์ ์๋๋ก useState๋ก ๋ณ์๋ฅผ ํ๋ ์๋ก ๋ง๋ค์๋ค.
const [existLi, setExistLi] = useState(null);
- ๊ทธ๋ฆฌ๊ณ ๋ต๋ณ ์คํธ๋ฆผ์ done์ด true๊ฐ ๋์ด์ ์ข
๋ฃ๊ฐ ๋ ๋,
existLi
๋ฅผ ๋ค์ null๋ก ๋ง๋ค์ด์ฃผ๋ฉด ๋ค์ ๋ต๋ณ์์๋ ์๋ก์ด li ํ๊ทธ๋ฅผ ๋ง๋ค์ด์ ๋ต๋ณ ๋ฐ๊ธฐ๋ฅผ ์์ํ ๊ฒ์ด๋ค.
๊ทธ๋์ useEffect ์์ result ๊ฐ์ด ๋ฐ๋ ๋๋ง๋ค li ํ๊ทธ๋ฅผ ์๋ก ๋ง๋ค์๋ ๊ธฐ์กด๊ณผ๋ ๋ฌ๋ฆฌ
useEffect(() => {
if (existLi != null) {
existLi.innerText = result;
}
}, [result]);
๋ต๋ณ ๋ฐ๊ธฐ๊ฐ ์์๋ li ํ๊ทธ๊ฐ ์๋ค๋ฉด (existLi
ํ๊ทธ๊ฐ ์กด์ฌํ๋ค๋ฉด), ๊ทธ ํ๊ทธ์ innerText๋ฅผ ๊ฐฑ์ ๋ result๊ฐ์ผ๋ก ์นํํด์ฃผ๋ ๊ฒ์ผ๋ก ๋ณ๊ฒฝํ์๋ค.
์ฌ์ค ์๋ต์์ (ChatGPT์) ๋ต๋ณ์ ์ถ์ถํ๋ ๊ฒ์ด ๊ฐ์ฅ ํ๋ ๊ณผ์ ์ด์๋ค
์๋์ while ๋ฌธ์ reader.read()
์์ ๊ตฌ์กฐ ๋ถํด ํ ๋นํ done ๊ฐ์ด true๊ฐ ๋์ฌ ๋๊น์ง ๋ฐ๋ณต๋๋ ๊ตฌ์กฐ์ด๋ค.
์ฌ๊ธฐ์ reader
์ ์์์ ๋ณผ ์ ์๋ฏ์ด response.body
๋ฅผ pipeThrough(new TextDecoderStream())
์ผ๋ก ์คํธ๋ฆผ์ ํตํด ์ค๋ ํ
์คํธ๋ฅผ ๋์ฝ๋ฉํ๊ณ , .getReader()
๋ก ๋ฐ์ดํฐ๋ฅผ ์ฝ์ ์ ์๋ ๋ฆฌ๋(Reader)๋ฅผ ๊ฐ์ ธ์จ ๊ฒ์ด๋ค.
while (true) {
const { value, done } = await reader.read();
if (done) break;
let str = JSON.stringify(value);
str = str.replace(/\n/gi, "\\r\\n");
let json = JSON.parse(str);
const cleanedStr = json.replace(/data:|\n/g, "").trim();
let chatRoomId;
const matchResult = cleanedStr.match(/\d+/);
if (matchResult) {
chatRoomId = matchResult[0];
}
let simpleText;
simpleText = cleanedStr.replace(/\\r\\n/g, "").trim();
const cleanedText = simpleText.replace(/chat_room_id:.*/, "");
setResult((prev) => prev + cleanedText);
if (chatRoomId) {
setChatId(chatRoomId);
setIsLoading(false);
}
}
setResult("");
setExistLi(null);
});
- ์ ์ฝ๋์ replace ๊ฐ ๊ต์ฅํ ๋ง์ ์ด์ ๋... ์ฒ์์ ๊ทธ๋ฅ
value
๋ฅผ ์ฝ์์ ์ฐ์ด๋ณด๋ ์๋์ ๊ฐ์ด ์์ 'data: ' ๊ฐ ์ฐํ์ ๋์๋ค
์๋ฌด๋ฆฌ ์๊ฐํด๋ ์๋ฒ ์ธก์์ 'data: ' ๋ฅผ ๋๊ฒจ์คฌ์๋ฆฌ๋ ์์ํ
๊ณ ๋น์ฐํ json ํ์์ด๊ฒ ์ง ํ๊ณ value.data
๋ฅผ ์ฐ์ผ๋ ์ด๋ฒ์ undefined
๊ฐ ๋์๋ค. ๊ต์ฅํ ํผ๋์ค๋ฌ์ ๋ค.. ๊ทธ๋์ 'data: '๋ฅผ replace๋ก ์์ ๋ ค๊ณ ํด๋ณด๊ธฐ๋ ํ๊ณ ๋ณ์ง์ ๋คํ๋ค๊ฐ value๋ฅผ JSON.stringfy() ๋ก ๋ฌธ์์ดํํด๋ณด๋ data ๊ฐ์ ๋ต๋ณ๊ณผ ๋ค๋์ ๊ฐํ๋ฌธ์๊ฐ ์์ฌ ์์์ ์๊ฒ ๋์๋ค.....
json์ ๊ฐํ๋ฌธ์๊ฐ ์์ผ๋ฉด json์ผ๋ก ์ธ์์ ํ ์๊ฐ ์๋ค๊ณ ํ๋ค. ์ด๋ฐ ๊ฒฝ์ฐ์๋ JSON.Stringfy() ํ ๋ค์ ๊ฐํ๋ฌธ์๋ฅผ ๋ชจ๋ ์์ ๊ณ ๋ค์ JSON.parse() ๋ก ๋ฐ๊ฟ์ json์ผ๋ก ์ธ์ํ๋๋ก ํ๋ ๋ฐฉ๋ฒ์ด ์๋ค๊ณ ํ๋ค. ์๋์ ๊ฐ๋ค.
let str = JSON.stringify(value);
str = str.replace(/\n/gi, "\\r\\n");
let json = JSON.parse(str);
๊ทธ๋ฆฌ๊ณ ๋ช์ฐจ๋ก ๋ replace๋ก ์นํ์ ํด์คฌ๋๋ฐ ์ฌ์ค ์ผ๋จ ๊ฒ์ผ๋ก ๋ณด์ด๋๊ฒ ์ ์์ผ๋ก ๋ณด์ผ ๋๊น์ง ์ด๊ฒ์ ๊ฒ ํ๊ฑฐ๋ผ ๋ค์ ๋ณด๋ ์ด๋ป๊ฒ ํ๊ฑด์ง ๋ชจ๋ฅด๊ฒ ๋ค..
const cleanedText = simpleText.replace(/chat_room_id:.*/, "");
- chat_room_id ๊ฐ value.data.chat_room_id ๊ฐ์ ํ์์ผ๋ก ์ฐธ์กฐํ๋ฉด ๋ ๊ฒ์ด๋ผ๊ณ ์๊ฐํ๋๋ฐ ์ด์ํ๊ฒ 'data: ์data:chat_room_id:105' ์ ๊ฐ์ด ๋ง์ง๋ง ๋ต๋ณ ๋ฉ์์ง์ ์ฌ์ด ์ข๊ฒ ๊ผญ ๋ถ์ด์ ์๋ฒ๋ ค์ ๊ทธ๋ฅ replace๋ก ์ต์ง๋ก ๋ผ๋ฒ๋ ธ๋ค...^^ ๊ทธ๋ฆฌ๊ณ ๊ทธ๋ฅ ์ํ๋ฒณ d ์ ๋งค์นญ์ด ๋๋ฉด ๋ท ๋ฒํธ๋ฅผ
chatRoomId
๋ณ์์ ์ ์ฅํ๋๋ก ํ๋ค. ์๋ฌด๋๋ ์ผ๋งค์ง๋ง ์ง๊ธ ์ํฉ์์ ์ต์ ์ด๋ผ๊ณ ์๊ฐํ๋ค!
๊ฒฐ๊ณผ๋ฌผ์ด ์์ฃผ ๋ง์กฑ์ค๋ฝ๋ค!