왜 채팅 구현에 웹소켓을 안쓰고 HTTP Polling 방식을 썼을까?
[실시간 채팅 구현] 필요지식 학습 (1) HTTP, WebSoket
우리는 채팅 서비스의 데이터 전송 방식으로 HTTP를 선택했다.HTTP (HyperText Transfer Protocol)HTTP는 클라이언트가 서버에 요청을 보내면 서버가 응답을 보내는 단방향 통신이다. 따라서 클라이언트의
leeemingyu.tistory.com
일반적으로 채팅 서비스에는 웹 소켓 방식이 많이 쓰이지만 우리는 동아리 내의 소규모 채팅 서비스이기 때문에 예상 사용자가 최대 100명 내외로 트래픽이 많지 않아 polling의 단점인 지연시간 문제도 크게 없을 것 같았고, 기본적인 CRUD도 학습할 겸 HTTP 방식을 선택했다.
1. 채팅 가져오기 (Fetch API)
async function get_chat_data() {
try {
const res = await fetch("api/chat");
const json = await res.json();
const chat_data = json.data;
return chat_data;
} catch (error) {
console.error("Error fetching chat data:", error);
}
}
fetch() 함수로 HTTP 요청을 보내면, 이 함수는 Response 객체를 포함한 Promise를 반환한다. 이 Response 객체는 응답의 상태와 데이터를 포함하고 있고 res.json() 메서드로 데이터를 JSON 형식으로 추출해 사용할 수 있다.
async/await을 사용한 이유는 fetch같은 비동기 작업을 동기 코드처럼 작성할 수 있게 해줘 가독성을 높여주기 때문에 사용했다.
API? Promise? JSON? 비동기? fetch? async/await? 뭔지 잘 모르겠으면 이전 글을 참고하길 바란다.
[실시간 채팅 구현] 필요 지식 학습 (2) API
백엔드 담당 친구가 만든 백엔드에 있는 데이터를 프론트엔드에서 가져와서 사용하기위해 Fetch API를 사용했다.API가 뭔데?API(Application Programming Interface)란 소프트웨어 간의 상호작용을 정의하는
leeemingyu.tistory.com
2. 채팅 보내기
async function create_chat_input(){
const input_wrapper = document.createElement("div");
input_wrapper.setAttribute("id", "chat_input_wrapper");
const chat_input = document.createElement("textarea");
chat_input.setAttribute("id", "chat_input");
const chat_send_btn = document.createElement("button");
chat_send_btn.setAttribute("id", "chat_send_btn");
chat_send_btn.setAttribute("disabled", "");
chat_send_btn.addEventListener("click", function() {
chat_send();
});
chat_send_btn.innerHTML = " 전송 ";
const new_chat = document.createElement("div");
new_chat.setAttribute("id", "new_chat");
input_wrapper.appendChild(chat_input);
input_wrapper.appendChild(chat_send_btn);
main.appendChild(input_wrapper);
chat_input_resize();
sendBtnCtrl();
chat_input.focus();
document
.getElementById("chat_input")
.addEventListener("keydown", function (event) {
submit_textarea(event);
});
}
function chat_send(){
const chat_input = document.getElementById("chat_input");
const chat_value = chat_input.value;
const chat_send_btn = document.getElementById("chat_send_btn");
fetch("api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat: chat_value,
}),
})
.then(function (res) {
return res.json();
})
.catch(function (error) {
console.log(error)
});
}
create_chat_input()함수로 채팅 입력창과 전송 버튼을 만들고, chat_send()함수로 HTTP POST 요청을 통해 채팅 전송 작업을 한다.
채팅 데이터를 가져올 때는 HTTP의 GET메서드가 사용되는데, GET 메서드가 기본값이라서 위에서 따로 명시하지 않았다. 전송할 때는 POST 메소드를 사용해 HTTP를 호출해서 채팅을 전송할 수 있다.
3. 가져온 채팅 데이터 렌더링하기
HTTP를 통해 백엔드와 성공적으로 통신했으면 이제 클라이언트에서 수신한 데이터를 기반으로 채팅을 렌더링해야 한다.
{
"data": [
{
"id": "1",
"chat": "테스트 채팅입니다.",
"date": "2024-08-22 10:00:00",
"user_id": "user123"
},
{
"id": "2",
"chat": "ㅋㅋㅋㅋㅋㅋㅋㅋㅋ",
"date": "2024-08-22 10:05:00",
"user_id": "user456"
}
]
}
반환되는 JSON은 위의 형태이다.
const main = document.getElementById("main_data")
let loaded_chat_num = 0;
let polling_num = 15;
async function generateChatFragments(chatData, startIndex, endIndex) {
const fragment = document.createDocumentFragment();
const myId = await get_my_id();
if (startIndex >= 0) {
for (let i = startIndex; i < endIndex; i++) {
let chatDiv;
let chatId = chatData[i].id;
let chat = chatData[i].chat.replace(/\n/g, "<br>");
let date = format_date(chatData[i].date);
if (chatData[i].user_id === myId) {
chatDiv = `<div id="${chatId}">
<div>
<div>${date}</div>
<div>${chat}</div>
</div>
</div>`;
} else {
const userName = await get_name(chatData[i].user_id);
chatDiv = `<div id=${chatId}>
<div>
<div>
<div>${userName}</div>
<div>${date}</div>
</div>
<div>${chat}</div>
</div>
</div>`;
}
const tempContainer = document.createElement('div');
tempContainer.innerHTML = chatDiv;
fragment.appendChild(tempContainer.firstChild);
}
return fragment;
}
}
async function renderChatData() {
const chatData = await get_chat_data();
if (!chatData) {
logout();
} else {
const chatDataWrapper = document.createElement("div");
chatDataWrapper.setAttribute("id", "chat_data_wrapper");
const fragment = await generateChatFragments(chatData, chatData.length - polling_num < 0 ? 0 : chatData.length - polling_num, chatData.length);
chatDataWrapper.appendChild(fragment);
main.appendChild(chatDataWrapper);
loaded_chat_num = chatData.length - polling_num;
create_chat_input();
chatDataWrapper.addEventListener('scroll', handleScroll);
reset_scroll();
}
}
loaded_chat_num 은 채팅 데이터를 렌더링된 채팅 데이터의 수를 의미하고, polling_num은 한 번에 로드할 채팅 데이터의 수를 의미한다.
반환된 JSON을 generateChatFragments와 renderChatData함수로 렌더링한다. generateChatFragments 함수는 채팅 데이터를 받아 HTML을 만들고, rederChatData함수는 만들어진 HTML 요소를 렌더링하는 역할을 한다.
4. 실시간으로 오가는 채팅 데이터 렌더링하기
처음 채팅에 접속했을 때의 채팅 데이터 렌더링은 가장 최신 채팅 데이터부터 원하는 수만큼 가져와서 렌더링하면 되기 때문에 크게 어렵지않다. 하지만 실시간으로 오가는 채팅을 구현해야하기 위해서는 다른 방법이 필요하다.
if (document.location.pathname === "/chat") {
create_chat_data();
setInterval(async () => {
append_new_chat();
}, 1000);
}
async function append_new_chat(send_who){
const chat_data = await get_chat_data();
const chat_data_wrapper = document.getElementById("chat_data_wrapper");
let renderd_id = 0;
if(chat_data_wrapper.children[chat_data_wrapper.childElementCount-1]){renderd_id = parseInt(chat_data_wrapper.children[chat_data_wrapper.childElementCount-1].id)}
const fetched_id = parseInt(chat_data[chat_data.length-1].id)
if (fetched_id !== renderd_id){
const fragment = await create_chat(chat_data, renderd_id, fetched_id)
chat_data_wrapper.appendChild(fragment);
let objDiv = document.getElementById("chat_data_wrapper");
const scrollPosition = objDiv.scrollTop / (objDiv.scrollHeight - objDiv.clientHeight);
if (scrollPosition >= 0.7 || send_who==='me') {
reset_scroll();
}else {
show_new_message_notification(chat_data[chat_data.length-1]);
}
}
scroll_check();
}
HTTP Polling을 이용하여 실시간 채팅을 구현했다. 이 방식은 일정 시간마다 가장 최근에 프론트에 렌더링된 채팅 데이터의 ID와 백엔드에서 가져온 JSON의 가장 최근 채팅 데이터 ID를 비교하여, ID가 다를 경우 그 차이만큼의 채팅 데이터를 렌더링함으로써 실시간으로 채팅을 업데이트한다.
5. 사용자 경험 개선하기
HTTP polling을 이용한 실시간 채팅 구현은 채팅을 전송해도 즉각적으로 확인할 수 없다는게 단점이다. 예를 들어 5초마다 채팅이 렌더링된다고 한다면, 렌더링되자마자 채팅을 전송하면 사용자는 약 5초간의 시간을 기다려야 자신의 채팅을 확인할 수 있는것이다.
두 가지 방법을 고민해보았다.
첫 번째는 전송버튼 클릭 시에 바로 POST, GET요청을 보내 렌더링하는 방식이고, 두 번째는 일단 클라이언트에 렌더링하고, polling후에 GET한 채팅과 렌더링되어있는 채팅을 비교해서 렌더링되어있지 않은 채팅을 렌더링하는 방식이다. 첫 번째 방식은 서버의 부하가 심해질 수 있다는 단점이 있지만 이는 일정 시간의 텀을 두어 사용자가 짧은 시간안에 많은 채팅을 보내지 못하도록 방식으로 해결할 수 있다. 두 번째 방식은 polling 전에는 내 채팅만 렌더링되어서 polling 후에 리렌더링되어야하고 채팅 순서가 뒤바뀔 수 있기때문에 사용자 경험을 떨어뜨릴 수 있다. 또한 두 번째 방식이 첫 번째 방식에 비해 더 복잡한 구현이 필요하다고 판단되어서 첫 번째 방식을 선택했다.
function chat_send(){
const chat_input = document.getElementById("chat_input");
const chat_value = chat_input.value;
const chat_send_btn = document.getElementById("chat_send_btn");
fetch("api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat: chat_value,
}),
})
.then(function (res) {
return res.json();
})
.then(function (text) {
if (text.req === "ok") {
chat_input.value = '';
chat_send_btn.disabled = true;
append_new_chat('me');
}
})
.catch(function (error) {
console.log(error)
});
}
전송 버튼을 누르면 polling시간을 기다리지 않고 바로 fetch를 되게 바꾸었다.
![]() |
![]() |
개선 전 (polling 후 채팅이 렌더링됨) | 개선 후 (채팅 입력 후 바로 렌더링됨) |
채팅 전송버튼을 눌러서 POST에 성공하면 바로 append_new_chat()함수를 실행시켜 채팅이 렌더링되도록 했다.
'GDSC > 실시간 채팅 구현' 카테고리의 다른 글
[실시간 채팅 구현] API, 비동기 처리 (0) | 2024.08.01 |
---|---|
[실시간 채팅 구현] HTTP, WebSoket (0) | 2024.07.23 |
[실시간 채팅 구현] 프로젝트 개요 (0) | 2024.07.23 |