메시지 브릿지
매뉴얼 [웹] 기본 엔진 소스 설명
페이지 정보
본문
WebSocket 엔진(상단 핵심 로직) 설명서
※ 이 영역은 전체 시스템의 핵심 엔진이므로 가능한 수정 없이 그대로 사용하는 것을 권장합니다.
※ 필요한 기능 확장은 하단 UI 영역 또는 별도 함수에서 처리하는 것을 권장합니다.
1. 기본 정보 설정
const domain = window.location.hostname;
const MB_ID = "아이디";
const USER_IP = "ip주소(해당 언어 참고)"
const myKey = MB_ID || USER_IP;domain
현재 접속한 웹사이트의 도메인 이름.
WebSocket 서버로 전송되는 group 파라미터로 사용되며, 같은 도메인끼리만 접속자 그룹을 분리하여 관리하는 역할을 한다.
MB_ID
로그인된 사용자의 회원 ID.
비회원이라면 빈 문자("") 또는 null.
USER_IP
비회원 사용자를 구분하기 위한 식별자.
myKey
현재 사용자를 대표하는 고유 키.
우선순위: 회원 → MB_ID
비회원 → USER_IP
이 값은 이후 접속자 Map에서 Key로 사용되며, 접속자 중복 여부 판단 등에 매우 중요하므로 수정하지 않을 것을 권장.
2. 접속 사용자 목록 저장소
let ws;
let activeUsers = new Map(); // {key: data}
let userListString = "";ws
WebSocket 인스턴스를 저장하는 변수.
재연결 또는 종료 시 동일 객체에 접근하기 위해 전역으로 선언.
activeUsers (Map 구조)
현재 WebSocket 그룹에 접속 중인 사용자 목록을 저장하는 저장소.
key: 사용자 식별자(MB_ID 또는 IP)
value: 사용자 정보 객체(접속시간, id, ip 등 서버에서 내려주는 데이터)
userListString
화면에 보여주기 좋은 문자열 형태(“아이디1 | 아이디2 | …”)로 만든 접속자 목록.
activeUsers와 userListString은 실시간 UI 업데이트에 활용되므로 구조 변경은 권장하지 않음.
3. WebSocket 연결 함수
function connectWebSocket() {
ws = new WebSocket('wss://designonex.com:14147/?group=' + encodeURIComponent(domain));WebSocket 서버에 연결하는 엔트리 포인트.
접속 시 group=도메인 값을 전달하여 도메인 단위의 접속자 방(그룹)을 자동으로 형성한다.
이 그룹 구조는 서버 설계에 맞춰져 있으므로 반드시 수정 없이 유지해야 한다.
4. WebSocket 이벤트 처리
4-1. 접속 성공 (onopen)
ws.onopen = () => {
sendEvent({type: "join", mb_id: MB_ID, ip: USER_IP});
};서버에 “현재 사용자가 접속했다”는 정보를 전송.
서버는 이를 기반으로 전체 접속자 목록을 갱신한다.
4-2. 서버 메시지 수신 (onmessage)
ws.onmessage = (event) => {
let data;
try { data = JSON.parse(event.data); } catch { return; }서버에서 받은 데이터를 JSON 형태로 변환한다.
이후 메시지 타입에 따라 다른 동작을 수행한다.
메시지 타입별 처리
① init — 전체 접속자 목록을 최초로 받는 경우
if (data.type === "init") {
activeUsers.clear();
for (const u of data.users) {
const key = u.mb_id || u.ip || Math.random();
activeUsers.set(key, u);
}
}서버가 현재 유지하고 있는 전체 접속 사용자 목록을 배열로 전달함.
Map을 초기화한 뒤 새 목록을 모두 다시 저장.
② join / update — 누군가 접속했거나 정보가 갱신된 경우
else if (data.type === "join" || data.type === "update") {
const key = data.mb_id || data.ip || Math.random();
activeUsers.set(key, data);
}
새 사용자가 들어온 경우(join)
닉네임‧멤버 정보가 바뀐 경우(update)
activeUsers에 새로운 정보로 갱신함.
③ leave — 특정 사용자가 나간 경우
else if (data.type === "leave") {
const key = data.mb_id || data.ip;
activeUsers.delete(key);
}후처리
rebuildUserString();
if (typeof updateDisplay === "function") {
updateDisplay(activeUsers, data);
}userListString을 다시 구성
UI가 필요할 경우 개발자가 직접 만든 updateDisplay()에 데이터를 전달
updateDisplay는 “하단 사용 UI 로직”에서 작성하는 것을 권장.
상단 엔진은 건드리지 않고 UI만 교체할 수 있는 구조로 설계됨.
4-3. 연결 종료 (onclose)
ws.onclose = () => {
setTimeout(connectWebSocket, 2000); // 자동 재연결
};WebSocket이 끊기면 2초 후 자동 재연결을 시도.
서버 재부팅·네트워크 불안정 상황에서도 지속 접속을 보장.
4-4. 오류 발생 (onerror)
ws.onerror = () => {
ws.close();
};오류 발생 시 즉시 연결을 닫고, onclose → 자동 재연결 동작이 이어짐.
5. 메시지 전송 함수
function sendEvent(jsonData) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(jsonData));
}
}WebSocket이 정상적으로 열려 있는 경우에만 JSON 데이터 전송.
서버로 join/update/chat 등 다양한 이벤트 메시지를 보낼 때 사용한다.
엔진에서 사용하는 기본 전송 구조이므로 변경하지 않는 것을 권장한다.
6. 안전한 종료 처리 (gracefulDisconnect)
function gracefulDisconnect() {
const leaveData = {type: "leave", mb_id: MB_ID, ip: USER_IP};6-1. WebSocket으로 정상 종료 신호 보내기
if (ws && ws.readyState === WebSocket.OPEN) {
try {
ws.send(JSON.stringify(leaveData));
ws.close();
} catch(e) {}
}6-2. 모바일 대비 sendBeacon 보조 전송
try {
const blob = new Blob([JSON.stringify(leaveData)], {type: "application/json"});
navigator.sendBeacon("/ws-leave", blob);
} catch(e) {}모바일 브라우저는 탭 닫힘 시 WS 전송이 실패하는 문제가 빈번하기 때문에 sendBeacon으로 백엔드에 안전하게 한 번 더 전송하여 접속자 정보가 꼬이지 않도록 보완한다.
이 종료 처리 구조는 필수 요소이며, 엔진의 안정성을 위해 그대로 유지하는 것을 강력 권장.
7. 브라우저 종료·백그라운드 이동 감지
window.addEventListener("beforeunload", gracefulDisconnect);
window.addEventListener("pagehide", gracefulDisconnect);
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
gracefulDisconnect();
}
});PC와 모바일에서 브라우저가 페이지를 떠나는 다양한 상황을 모두 감지하여, 접속 종료 정보가 빠짐없이 서버에 전달되도록 설계됨.
8. 접속자 문자열 생성
function rebuildUserString() {
userListString = Array.from(activeUsers.values())
.map(u => u.mb_id || u.ip)
.join(" | ");
}activeUsers(Map)을 기반으로 “아이디 | 아이디 | IP…” 형태의 문자열을 구성.
UI에서 접속자 리스트를 간단히 표현할 때 활용.
9. 최초 실행
connectWebSocket();페이지 로드와 동시에 WebSocket 연결을 시작함.
기본엔진 전체소스
const domain = window.location.hostname;
const MB_ID = "아이디";
const USER_IP = "ip주소(해당 언어 참고)"
const myKey = MB_ID || USER_IP;
let ws;
let activeUsers = new Map(); // {key: data}
let userListString = "";
// ---------------------------
// 웹소켓 연결
// ---------------------------
function connectWebSocket() {
ws = new WebSocket('wss://designonex.com:14147/?group=' + encodeURIComponent(domain));
ws.onopen = () => {
sendEvent({type: "join", mb_id: MB_ID, ip: USER_IP});
};
ws.onmessage = (event) => {
let data;
try { data = JSON.parse(event.data); } catch { return; }
// 초기 접속자 목록
if (data.type === "init") {
activeUsers.clear();
for (const u of data.users) {
const key = u.mb_id || u.ip || Math.random();
activeUsers.set(key, u);
}
}
// 접속 또는 업데이트
else if (data.type === "join" || data.type === "update") {
const key = data.mb_id || data.ip || Math.random();
activeUsers.set(key, data);
}
// 접속 종료
else if (data.type === "leave") {
const key = data.mb_id || data.ip;
activeUsers.delete(key);
}
rebuildUserString();
if (typeof updateDisplay === "function") {
updateDisplay(activeUsers, data);
}
};
ws.onclose = () => {
setTimeout(connectWebSocket, 2000); // 자동 재연결
};
ws.onerror = () => {
ws.close();
};
}
// ---------------------------
// 메시지 전송
// ---------------------------
function sendEvent(jsonData) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(jsonData));
}
}
// ---------------------------
// 안전한 종료 처리
// ---------------------------
function gracefulDisconnect() {
const leaveData = {type: "leave", mb_id: MB_ID, ip: USER_IP};
// 1️⃣ WebSocket으로 전송 (가능하면)
if (ws && ws.readyState === WebSocket.OPEN) {
try {
ws.send(JSON.stringify(leaveData));
ws.close();
} catch(e) {}
}
// 2️⃣ 모바일 대비 — sendBeacon으로 보조 전송
try {
const blob = new Blob([JSON.stringify(leaveData)], {type: "application/json"});
navigator.sendBeacon("/ws-leave", blob);
} catch(e) {}
}
// ---------------------------
// 브라우저 이벤트 감지
// ---------------------------
// PC 및 일부 모바일 브라우저
window.addEventListener("beforeunload", gracefulDisconnect);
// iOS / Android에서 동작 안정적인 이벤트
window.addEventListener("pagehide", gracefulDisconnect);
// 탭 비활성화 시 (백그라운드 이동 등)
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
gracefulDisconnect();
}
});
// ---------------------------
// 문자열 갱신
// ---------------------------
function rebuildUserString() {
userListString = Array.from(activeUsers.values())
.map(u => u.mb_id || u.ip)
.join(" | ");
}
// ---------------------------
// 최초 실행
// ---------------------------
connectWebSocket();
</script>
<script>
function updateDisplay(activeUsers, lastMessage) {
// 접속자 수 표시
$("#userConCount").text("(" + activeUsers.size + ")");
// 사이드 패널에 접속자 리스트 표시
const userListString = Array.from(activeUsers.values())
.map(u => {
// mb_id가 null, undefined, 빈 문자열이 아니면 mb_id 표시
if (u.mb_id && u.mb_id.trim() !== "") {
return u.mb_id;
}
return u.ip; // 회원이 아니면 IP 표시
})
.join(" | ");
$("#sidePanelContent").text(userListString);
}
댓글목록
등록된 댓글이 없습니다.
