PC 및 IT관련.

(개인연구) Blogger + Firebase 실시간 채팅 가젯 제작 가이드 Feat. GPT,Gemini

ジーエムクン지하블로그 2026. 5. 23. 06:09

브금용

 

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, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}

/* ======================================================= */
/* 마우스 & 터치 드래그 통합 이동 시스템 */
/* ======================================================= */
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 코드

등.


최종 결과

최종적으로:

  • 실시간 채팅
  • 모바일 대응
  • 드래그 이동
  • 위치 저장
  • 자동삭제
  • 숨김 기능
  • 무료 사용

까지 구현 가능.

사실상:

블로그용 미니 디스코드 느낌

으로 사용 가능함.

 

이정두로 다 떠먹여줫는대 key 어디에 넣을줄 모르겠다면 걍 접는게 좋..

 

모바일 페이지도 잘 대응 된 다.