
Blogger + Firebase 실시간 채팅 가젯 제작 가이드
구글 블로거(Blogger)에
실시간 채팅 가젯을 붙이는 방법 정리.
이번 가이드는 실제 제작하면서 겪었던:
- 모바일 안보임
- 드래그 안됨
- 코드 깨짐
- 위치 저장 문제
등 삽질 내용까지 반영한 최종 정리본.
구현 기능
- 실시간 채팅
- 5분 자동삭제
- 모바일 대응
- 드래그 이동
- 위치 기억
- 최소화 가능
- 우클릭 숨김
- Shift + C 복구
- Firebase 무료 플랜 사용
- 로그인 없이 닉네임 입력 가능
1. Firebase 프로젝트 생성
Firebase 콘솔 접속 후:
프로젝트 추가
프로젝트 이름은 자유.
예시:
blogger-chat
2. Realtime Database 활성화
Firebase 메뉴:
빌드 → Realtime Database
선택.
데이터베이스 만들기
- 지역:
아무거나 가능
(asia-southeast1 추천) - 테스트 모드 시작 선택
3. 데이터베이스 규칙 설정
Realtime Database → 규칙 탭 이동.
기존 내용 삭제 후 아래 입력:
{
"rules": {
".read": true,
".write": true,
"blogger_chat": {
".indexOn": ["time"]
}
}
}
우측 상단:
게시
클릭.

4. 웹 앱 등록
Firebase 홈 → 프로젝트 개요
</> 웹 앱 추가
클릭.
앱 이름은 자유.
예시:
blogger-chat
등록 후 아래 코드가 생성됨:
const firebaseConfig = {
apiKey: "...",
authDomain: "...",
databaseURL: "...",
projectId: "...",
storageBucket: "...",
messagingSenderId: "...",
appId: "..."
};
이 값들을 복사해둔다.
5. Blogger 가젯 생성
블로거 관리자:
레이아웃 → 가젯 추가 → HTML/JavaScript
선택.
중요
가젯 제목은 비우는 걸 추천.
가젯 제목 입력 시:
- 코드 깨짐처럼 보이거나
- 상단에 문자열이 출력될 수 있음.
6. 채팅 코드 붙여넣기
가젯 내용 전체에
채팅 코드 통째로 붙여넣기.
중요
아래 같은 문자는 넣으면 안됨:
```html
또는:
:::
오직 실제 HTML/CSS/JS 코드만 넣기.
7. Firebase 값 교체
코드 내부의:
const firebaseConfig = {
영역 수정.
예시:
기존:
apiKey:"여기에_API_KEY"
↓
변경:
apiKey:"실제_API_KEY"
중요
firebaseConfig를 새로 추가하는게 아님.
기존 placeholder 값만 수정해야 함.
8. 모바일 안보이는 문제
초기 버전은:
top:90px;
같은 고정값 때문에:
- 모바일
- 창모드
- Blogger 모바일 페이지
에서 채팅창이 화면 밖으로 밀릴 수 있음.
해결 방법
- 반응형 위치 계산
- window.innerWidth 사용
- 자동 위치 보정
구조 적용 필요.
9. 드래그 기능
초기 문제:
PC 이벤트만 사용 시:
mousedown
mousemove
mouseup
모바일에서 안움직임.
해결 방법
아래 이벤트 추가 필요:
touchstart
touchmove
touchend
10. 위치 기억 기능
localStorage
사용.
페이지 이동 후에도:
- 위치 유지
- 모바일 유지
- 창모드 유지
가능.
11. 5분 자동삭제 구조
메시지 저장 시:
time: Date.now()
함께 저장.
로드 시:
if(now-msg.time>300000)
이면 자동 삭제.
300000ms = 5분.
12. 무료 플랜 과금 여부
Firebase Spark 무료 플랜 기준:
- 카드 등록 없음
- 자동 과금 없음
- 요금 청구 없음
대신:
무료 한도 초과 시:
- 일시 먹통
- 요청 제한
발생 가능.
하지만 현재 구조는:
- 텍스트만 사용
- 이미지 없음
- 영상 없음
- 자동삭제 있음
이라 트래픽이 매우 적은 편.
일반 블로그 규모에서는 거의 문제 없음.
13. 숨김 기능
우클릭
채팅 버블 우클릭 시:
채팅 숨김
복구
키보드:
Shift + C
입력 시 다시 표시.
14. 실제 삽질 포인트 정리
채팅이 안뜸
원인:
- firebaseConfig 잘못 입력
- 코드 일부만 붙여넣음
코드가 깨져보임
원인:
- 가젯 제목 입력
- 해당 가젯 제목을 지우면 해결된다.
모바일에서 안보임
원인:
- fixed 위치 고정값
- 모바일 CSS 충돌
드래그 안됨
원인:
- touch 이벤트 미구현
위치 저장 안됨
원인:
- localStorage 저장 누락
15. 코드 입력 위치
아래 위치에 직접 삽입:
<style>
/* 블로거 가젯 자체의 부모 레이아웃이 요소를 숨기는 것을 방지 */
.section, .widget, #LinkList1, .sidebar {
overflow: visible !important;
}
#chatBubble {
position: fixed !important; /* 고정 위치 강제 */
top: 16px;
left: 16px;
width: 58px;
height: 58px;
border-radius: 50%;
background: rgba(35,35,35,0.78);
backdrop-filter: blur(16px);
box-shadow: 0 8px 24px rgba(0,0,0,0.35);
z-index: 9999999 !important; /* 메뉴바보다 위에 오도록 z-index 대폭 상향 */
display: flex;
align-items: center;
justify-content: center;
color: white;
cursor: pointer;
user-select: none;
transition: transform 0.25s, opacity 0.25s;
font-size: 24px;
touch-action: none;
}
#chatBubble:hover {
transform: scale(1.05);
}
#floatingChat {
position: fixed !important; /* 고정 위치 강제 */
top: 86px;
left: 16px;
width: min(92vw, 340px);
height: 72vh;
max-height: 540px;
z-index: 9999998 !important; /* 최상위 레이어 유지 */
backdrop-filter: blur(18px);
background: rgba(25,25,25,0.78);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 22px;
overflow: hidden;
box-shadow: 0 12px 40px rgba(0,0,0,0.42);
font-family: sans-serif;
color: white;
display: none;
}
#chatHeader {
height: 56px;
padding: 0 16px;
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(255,255,255,0.05);
cursor: move;
user-select: none;
touch-action: none;
}
#chatTitle {
font-size: 16px;
font-weight: bold;
}
#closeBtn {
font-size: 22px;
cursor: pointer;
opacity: 0.8;
}
#chatMessages {
height: calc(100% - 170px);
overflow-y: auto;
padding: 12px;
}
.chatMsg {
margin-bottom: 10px;
padding: 10px 12px;
border-radius: 16px;
background: rgba(255,255,255,0.07);
font-size: 14px;
line-height: 1.45;
word-break: break-word;
}
.chatNick {
font-weight: bold;
margin-bottom: 4px;
opacity: 0.92;
}
#chatControls {
position: absolute;
bottom: 0;
width: 100%;
padding: 12px;
background: rgba(20,20,20,0.45);
box-sizing: border-box;
}
#nickname,
#messageInput {
width: 100%;
padding: 12px;
border: none;
border-radius: 14px;
outline: none;
background: rgba(255,255,255,0.08);
color: white;
margin-bottom: 8px;
box-sizing: border-box;
font-size: 14px;
}
#sendBtn {
width: 100%;
padding: 12px;
border: none;
border-radius: 14px;
background: rgba(255,255,255,0.14);
color: white;
cursor: pointer;
font-size: 14px;
transition: 0.2s;
}
#sendBtn:hover {
background: rgba(255,255,255,0.2);
}
#chatMessages::-webkit-scrollbar {
width: 6px;
}
#chatMessages::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.15);
border-radius: 999px;
}
@media (max-width: 600px) {
#floatingChat {
max-height: 480px;
}
}
</style>
<!-- 메뉴 구조 탈출을 위해 JavaScript가 이 요소를 바디 직속으로 이동시킬 것입니다 -->
<div id="bloggerChatContainer">
<div id="chatBubble">💬</div>
<div id="floatingChat">
<div id="chatHeader">
<div id="chatTitle">✨ 실시간 낙서방</div>
<div id="closeBtn">×</div>
</div>
<div id="chatMessages"></div>
<div id="chatControls">
<input type="text" id="nickname" placeholder="닉네임">
<input type="text" id="messageInput" placeholder="메시지 입력...">
<button id="sendBtn">전송</button>
</div>
</div>
</div>
<script src="https://www.gstatic.com/firebasejs/10.12.2/firebase-app-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/10.12.2/firebase-database-compat.js"></script>
<script>
/* ======================================================= */
/* 핵심 해결책: 가젯 레이아웃 강제 탈출 후 body 직속 이동 */
/* ======================================================= */
const chatContainer = document.getElementById("bloggerChatContainer");
if (chatContainer) {
document.body.appendChild(chatContainer); // 사이드바/메뉴 영역에서 완전히 분리하여 전체화면 기준으로 배치
}
/* ======================================================= */
/* ↓↓↓↓↓↓↓↓↓ Firebase 값 교체 영역 ↓↓↓↓↓↓↓↓↓ */
/* 여기만 니 Firebase 값으로 교체 */
/* 절대 밑에 또 firebaseConfig 추가하지마 */
/* ======================================================= */
const firebaseConfig = {
apiKey: "AIzaSyAXY3IB24DoPzDuQNqqhRoYkNa_VfAWlKo",
authDomain: "blogger-chat-f4d94.firebaseapp.com",
databaseURL: "https://blogger-chat-f4d94-default-rtdb.asia-southeast1.firebasedatabase.app",
projectId: "blogger-chat-f4d94",
storageBucket: "blogger-chat-f4d94.firebasestorage.app",
messagingSenderId: "762752434450",
appId: "1:762752434450:web:5afdc9750aefcc8eadb3cb",
measurementId: "G-Y83YWP8E45"
};
/* ======================================================= */
/* ↑↑↑↑↑↑↑↑↑ Firebase 값 교체 끝 ↑↑↑↑↑↑↑↑↑ */
/* ======================================================= */
firebase.initializeApp(firebaseConfig);
const db = firebase.database();
const chatRef = db.ref("blogger_chat");
const bubble = document.getElementById("chatBubble");
const chatBox = document.getElementById("floatingChat");
const closeBtn = document.getElementById("closeBtn");
const messages = document.getElementById("chatMessages");
const nickname = document.getElementById("nickname");
const messageInput = document.getElementById("messageInput");
const sendBtn = document.getElementById("sendBtn");
const header = document.getElementById("chatHeader");
/* ======================================================= */
/* 채팅 열기 / 닫기 상태 저장 */
/* ======================================================= */
bubble.onclick = () => {
chatBox.style.display = "block";
localStorage.setItem("chatBoxBoxState", "open");
keepInBounds();
};
closeBtn.onclick = () => {
chatBox.style.display = "none";
localStorage.setItem("chatBoxBoxState", "closed");
};
/* ======================================================= */
/* 닉네임 저장 */
/* ======================================================= */
nickname.value = localStorage.getItem("chatNickname") || "";
nickname.addEventListener("change", () => {
localStorage.setItem("chatNickname", nickname.value);
});
/* ======================================================= */
/* 메시지 전송 */
/* ======================================================= */
sendBtn.onclick = sendMessage;
messageInput.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
sendMessage();
}
});
function sendMessage() {
const nick = nickname.value.trim() || "익명";
const text = messageInput.value.trim();
if (!text) return;
chatRef.push({
nick,
text,
time: Date.now()
});
messageInput.value = "";
}
/* ======================================================= */
/* 메시지 불러오기 */
/* ======================================================= */
chatRef.on("value", (snapshot) => {
messages.innerHTML = "";
const data = snapshot.val();
if (!data) return;
const now = Date.now();
const list = Object.entries(data);
list.forEach(([key, msg]) => {
/* 5분 지나면 자동삭제 */
if (now - msg.time > 300000) {
chatRef.child(key).remove();
return;
}
const div = document.createElement("div");
div.className = "chatMsg";
div.innerHTML = `
<div class="chatNick">${escapeHtml(msg.nick)}</div>
<div>${escapeHtml(msg.text)}</div>
`;
messages.appendChild(div);
});
/* 최근 40개 제한 */
if (list.length > 40) {
const removeCount = list.length - 40;
for (let i = 0; i < removeCount; i++) {
chatRef.child(list[i][0]).remove();
}
}
messages.scrollTop = messages.scrollHeight;
});
/* ======================================================= */
/* HTML 문자 보호 */
/* ======================================================= */
function escapeHtml(text) {
return text
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
}
/* ======================================================= */
/* 마우스 & 터치 드래그 통합 이동 시스템 */
/* ======================================================= */
let activeDrag = null;
let startX = 0, startY = 0;
let initialX = 0, initialY = 0;
function startDrag(e, type) {
if (e.target.tagName === 'INPUT' || e.target.id === 'closeBtn') return;
activeDrag = type;
const clientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
const clientY = e.type.includes('touch') ? e.touches[0].clientY : e.clientY;
startX = clientX;
startY = clientY;
if (type === 'bubble') {
initialX = parseInt(localStorage.getItem("chatBubbleX")) || 16;
initialY = parseInt(localStorage.getItem("chatBubbleY")) || 16;
} else if (type === 'chat') {
initialX = parseInt(localStorage.getItem("chatBoxX")) || 16;
initialY = parseInt(localStorage.getItem("chatBoxY")) || 86;
}
}
function doDrag(e) {
if (!activeDrag) return;
const clientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
const clientY = e.type.includes('touch') ? e.touches[0].clientY : e.clientY;
const deltaX = clientX - startX;
const deltaY = clientY - startY;
let newX = initialX + deltaX;
let newY = initialY + deltaY;
if (activeDrag === 'bubble') {
newX = Math.max(0, Math.min(window.innerWidth - bubble.offsetWidth, newX));
newY = Math.max(0, Math.min(window.innerHeight - bubble.offsetHeight, newY));
bubble.style.left = newX + "px";
bubble.style.top = newY + "px";
localStorage.setItem("chatBubbleX", newX);
localStorage.setItem("chatBubbleY", newY);
} else if (activeDrag === 'chat') {
newX = Math.max(0, Math.min(window.innerWidth - chatBox.offsetWidth, newX));
newY = Math.max(0, Math.min(window.innerHeight - chatBox.offsetHeight, newY));
chatBox.style.left = newX + "px";
chatBox.style.top = newY + "px";
localStorage.setItem("chatBoxX", newX);
localStorage.setItem("chatBoxY", newY);
}
}
function endDrag() {
activeDrag = null;
}
bubble.addEventListener("mousedown", (e) => startDrag(e, 'bubble'));
bubble.addEventListener("touchstart", (e) => startDrag(e, 'bubble'), { passive: true });
header.addEventListener("mousedown", (e) => startDrag(e, 'chat'));
header.addEventListener("touchstart", (e) => startDrag(e, 'chat'), { passive: true });
document.addEventListener("mousemove", doDrag);
document.addEventListener("touchmove", doDrag, { passive: false });
document.addEventListener("mouseup", endDrag);
document.addEventListener("touchend", endDrag);
/* ======================================================= */
/* 모바일 및 화면 리사이즈 시 화면 탈출 방지 함수 */
/* ======================================================= */
function keepInBounds() {
const bX = parseInt(localStorage.getItem("chatBubbleX")) || 16;
const bY = parseInt(localStorage.getItem("chatBubbleY")) || 16;
const cX = parseInt(localStorage.getItem("chatBoxX")) || 16;
const cY = parseInt(localStorage.getItem("chatBoxY")) || 86;
const safeBX = Math.max(0, Math.min(window.innerWidth - bubble.offsetWidth, bX));
const safeBY = Math.max(0, Math.min(window.innerHeight - bubble.offsetHeight, bY));
bubble.style.left = safeBX + "px";
bubble.style.top = safeBY + "px";
const safeCX = Math.max(0, Math.min(window.innerWidth - chatBox.offsetWidth, cX));
const safeCY = Math.max(0, Math.min(window.innerHeight - chatBox.offsetHeight, cY));
chatBox.style.left = safeCX + "px";
chatBox.style.top = safeCY + "px";
}
window.addEventListener('resize', keepInBounds);
/* ======================================================= */
/* 위치 및 열림 상태 복원 */
/* ======================================================= */
const savedBubbleX = localStorage.getItem("chatBubbleX");
const savedBubbleY = localStorage.getItem("chatBubbleY");
const savedBoxX = localStorage.getItem("chatBoxX");
const savedBoxY = localStorage.getItem("chatBoxY");
const savedBoxState = localStorage.getItem("chatBoxBoxState");
if (savedBubbleX && savedBubbleY) {
bubble.style.left = savedBubbleX + "px";
bubble.style.top = savedBubbleY + "px";
}
if (savedBoxX && savedBoxY) {
chatBox.style.left = savedBoxX + "px";
chatBox.style.top = savedBoxY + "px";
}
setTimeout(keepInBounds, 100);
if (savedBoxState === "open") {
chatBox.style.display = "block";
} else {
chatBox.style.display = "none";
}
/* ======================================================= */
/* 우클릭 숨기기 */
/* ======================================================= */
bubble.addEventListener("contextmenu", (e) => {
e.preventDefault();
bubble.style.display = "none";
chatBox.style.display = "none";
localStorage.setItem("chatHidden", "true");
});
/* ======================================================= */
/* Shift + C 로 다시 호출 */
/* ======================================================= */
document.addEventListener("keydown", (e) => {
if (e.shiftKey && e.key.toLowerCase() === "c") {
bubble.style.display = "flex";
localStorage.removeItem("chatHidden");
if(localStorage.getItem("chatBoxBoxState") === "open") {
chatBox.style.display = "block";
}
keepInBounds();
}
});
/* ======================================================= */
/* 숨김 상태 유지 */
/* ======================================================= */
if (localStorage.getItem("chatHidden") === "true") {
bubble.style.display = "none";
chatBox.style.display = "none";
}
</script>
예시:
- HTML/CSS/JS 통합 코드
- Firebase 연결 코드
- 커스텀 UI 코드
등.
최종 결과
최종적으로:
- 실시간 채팅
- 모바일 대응
- 드래그 이동
- 위치 저장
- 자동삭제
- 숨김 기능
- 무료 사용
까지 구현 가능.
사실상:
블로그용 미니 디스코드 느낌
으로 사용 가능함.



'PC 및 IT관련.' 카테고리의 다른 글
| 부 아카이브 블로그 등록 가이드 미니 게임(?)완성. (0) | 2026.05.28 |
|---|---|
| 구글 블로거 뒤로가기 새로고침 없이 이전 페이지 깡로드시키는 BFCache 기능 우회해서 구현. (0) | 2026.05.24 |
| “핵 썼다가 PC 먹통?” 라이엇 초강수 제재에 커뮤니티 난리 (1) | 2026.05.23 |
| 알리 찍먹기) 레이저 바이퍼 얼티메이트 그립 커스텀 완료. (0) | 2026.05.18 |
| 고고모바일/모요) "월 15GB", "월 10원" 7개월 이후 26,400원 (0) | 2026.05.16 |