자바스크립트, 구글 앱스 스크립트, 구글시트를 활용해 비용 0원으로 구독자 전용 콘텐츠 블로그를 만드는 방법을 단계별로 정리했습니다.
블로그를 운영하다 보면 정말 공들여 작성한 양질의 정보를 오직 내 소중한 ‘구독자’분들에게만 특별하게 제공하고 싶을 때가 있습니다. 하지만 전문적인 서버 지식이 없거나, 유료 데이터베이스를 구축하기에는 부담스러운 것이 현실입니다.
데일리허브에서는 이러한 고민을 해결하기 위해 자바스크립트(JavaScript), 구글 앱스 스크립트(Google Apps Script, GAS), 그리고 구글 시트(Google Sheets)를 조합한 콘텐츠 잠금 방법을 적용해 봤습니다. 비용은 0원, 보안은 철저하게, 구독자 관리와 불필요한 매크로 댓글 분석까지 해결해 블로그를 스마트하게 유지할 수 있는 합리적인 방법입니다.
1. 왜 ‘구글 시트’를 데이터베이스로 사용하나요?
일반적인 웹 개발에서 데이터를 저장하려면 MySQL, MongoDB 같은 데이터베이스와 이를 구동할 서버가 필요합니다. 하지만 티스토리나 개인 블로그를 운영하는 분들에게는 배보다 배꼽이 더 큰 상황이 될 수 있죠.
- 완전 무료: 구글 계정만 있다면 무상으로 강력한 클라우드 저장소를 얻게 됩니다.
- 직관적인 UI: 데이터가 어떻게 쌓이는지 엑셀처럼 실시간으로 확인하고 직접 수정할 수 있습니다.
- 서버리스 인프라: Google Apps Script가 API 서버 역할을 대신해주므로, 별도의 호스팅 비용이 들지 않습니다.
2. 작동 아키텍처: 3단계 데이터 과정
사용자가 내 블로그에 접속해서 구독 인증을 완료하기까지, 작업과정은 다음의 세 단계를 거치게 됩니다.
- 프론트엔드 (JavaScript): 사용자가 입력한 블로그 URL을 수집합니다. 브라우저의
fetch()함수를 사용하여 이 데이터를 구글 서버(GAS)로 쏘아 올립니다. - 미들웨어 (Google Apps Script): 브라우저와 구글 시트 사이의 ‘중간 관리자’입니다. 받은 데이터가 유효한지 검사하고, 보안 이슈인 CORS(Cross-Origin Resource Sharing) 문제를 해결하여 시트에 안전하게 전달합니다.
- 백엔드 (Google Sheets): 전달된 데이터가 최종적으로 기록되는 장소입니다. 등록된 구독자 리스트를 확인하거나 새로운 접속 로그를 저장합니다.
3. [1단계] 구글 시트 설정 및 서버 코드(GAS) 작성
먼저 데이터를 담을 그릇을 준비해야 합니다.물런 구독자 데이터 입니다.구독자 데이터는이 링크를 통해 쉽게 자동으로 수집할수 있습니다.
- 구글 시트를 생성하고 제목을 설정합니다 (예: 구독자 인증 관리).
- 상단 메뉴의 확장 프로그램 Apps Script를 클릭합니다.
- 기존 코드를 모두 지우고 아래의 코드(1번 코드)를 붙여넣습니다.
4. [2단계] 웹 앱으로 배포하기
아래 코드(1번)를 작성했으면, 외부(브라우저)에서 접근할 수 있도록 URL을 생성해야 합니다.
- 스크립트 편집기 우측 상단 [배포] [새 배포]를 클릭합니다.
- 유형 선택에서 [웹 앱]을 선택합니다.
- 설정값 확인:
- 다음 사용자 권한으로 실행: 나 (본인 계정)
- 액세스 권한이 있는 사용자: 모든 사용자 (Anyone) – 이 설정을 ‘Anyone’으로 해야 로그인하지 않은 방문자도 데이터를 보낼 수 있습니다.
- 배포 버튼을 누르고 생성된 웹 앱 URL을 안전한 곳에 복사해 둡니다.
5. [3단계] 블로그에 자바스크립트 적용
아래 코드(2번)을 블로그 스킨의 HTML이나 콘텐츠 영역에 팝업 제어와 데이터 전송 기능을 추가합니다.
6. 자바스크립트만 사용하면 안 될까?
자바스크립트로 직접 구글 시트를 수정하려고 하면 브라우저는 빨간색 에러를 내뱉습니다. 그 이유는 보안(Security) 때문입니다.
① CORS(Cross-Origin Resource Sharing) 이슈
브라우저는 abc.com에서 실행 중인 스크립트가 허락 없이 google.com의 데이터를 건드리는 것을 막습니다. 만약 이것이 허용된다면, 악성 사이트가 여러분의 구글 시트 데이터를 마음대로 훔쳐갈 수 있기 때문입니다. GAS는 구글 서버 내부에서 작동하므로 이 제약을 우회할 수 있는 안전한 통로가 됩니다.
② 인증과 권한(OAuth 2.0)
구글 시트는 기본적으로 비공개 상태입니다. 자바스크립트 코드 안에 구글 계정의 비밀번호를 적어둘 수는 없죠. GAS를 사용하면 “내 계정 권한으로 실행하되, 데이터 입구만 외부에 열어둔다”는 안전한 방식의 권한 관리가 가능해집니다.
③ 데이터 포맷의 번역
구글 시트는 ‘표’ 형태의 데이터이고, 자바스크립트는 JSON 형태를 좋아합니다. GAS는 표 데이터를 JSON으로, 혹은 JSON을 표로 변환해 주는 통역사 역할을 수행합니다.
7. 요약 및 추천 전략
블로그나 사이트의 프로젝트 성격에 따라 최적화된 조합을 선택할수도 있습니다.
| 목적 | 추천 조합 | 난이도 |
|---|---|---|
| 단순 데이터 출력 | 시트 웹 게시 + fetch() | 하(Low) |
| 구독 인증 및 저장 | JS + GAS + 구글 시트 | 중(Mid) |
| 대규모 서비스 | JS + Firebase / Supabase | 상(High) |

구글 시트와 GAS를 활용한 시스템은 블로그 운영자에게 가장 쉬운 방법입니다. 별도의 서버 유지비 없이도 전문적인 회원제 시스템과 유사한 기능을 구현할 수 있기 때문입니다. 다만 티스토리 블로그에서는 여러 버전의 블로그가 존재하기 때문에 일부 제약이 있기는 합니다.
데일리 허브에 적용한 코드 예제
아래 코드는 데일리허브에서 직접 설계 테스트한 구독자 전용 콘텐츠 운영 사례를 기반으로 작성되었습니다.
[코드1] 구글 앱스 스크립트(Google Apps Script, GAS)
아래 코드에서 구글 시트 주소와 구독자 데이터 시트 이름을 환경에 맞게 수정하세요
const SPREADSHEET_ID = '구글시트 주소';
const SUBSCRIBERS_SHEET_NAME = '예:나를구독';
function doGet(e) {
const visitorLink = (e && e.parameter && e.parameter.visitorLink) ? e.parameter.visitorLink : null;
let result = { access: false, message: '' };
try {
if (!visitorLink) {
result.message = "Error: visitorLink parameter is missing.";
} else {
const spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID);
let subscribersSheet = spreadsheet.getSheetByName(SUBSCRIBERS_SHEET_NAME);
if (!subscribersSheet) {
subscribersSheet = spreadsheet.insertSheet(SUBSCRIBERS_SHEET_NAME);
subscribersSheet.appendRow(['링크', '저장일시']);
}
const lastRow = subscribersSheet.getLastRow();
const subscriberLinks = lastRow 1 ? subscribersSheet.getRange(2, 1, lastRow - 1, 1).getValues().flat() : [];
const normalizedVisitorLink = normalizeUrl(visitorLink);
const isMySubscriber = subscriberLinks.some(subLink = {
if (!subLink) return false;
return normalizedVisitorLink === normalizeUrl(subLink);
});
result.access = isMySubscriber;
result.message = isMySubscriber ? 'Access granted' : 'Access denied';
result.success = true;
}
} catch (error) {
result.access = false;
result.message = "Error: " + error.message;
}
return ContentService.createTextOutput(JSON.stringify(result))
.setMimeType(ContentService.MimeType.JSON);
}
function normalizeUrl(url) {
if (!url) return '';
let normalized = url.toLowerCase().trim();
normalized = normalized.replace(/^(https?://)?(www.)?/, '');
if (normalized.endsWith('/')) {
normalized = normalized.slice(0, -1);
}
return normalized;
}
[코드2] 블로그에 적용하는 자바스크립트 코드
현재 데일리허브 블로그에 적용한 코드입니다. 자신의 블로그에 어우리는 팝업창 CSS,와 안내 문구를 적용하고, 구글 앱스 스크립트 웹 주소를 넣어주시면 됩니다.
script
(function () {
document.addEventListener('DOMContentLoaded', function () {
const hasPreview = !!document.querySelector('.prime-preview');
if (!hasPreview) return;
/* ===============================
팝업 생성
=============================== */
const popup = document.createElement('div');
popup.id = 'access-popup';
popup.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(10,10,10,0.85);
color: white;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 99999;
padding: 20px;
text-align: center;
overflow: hidden;
backdrop-filter: blur(10px);
`;
popup.innerHTML = `
div style="
background: rgba(20,20,20,0.95);
padding: 40px;
border-radius: 20px;
border: 1px solid #444;
box-shadow: 0 20px 60px rgba(0,0,0,0.9);
max-width: 420px;
width: 95%;
box-sizing: border-box;
z-index: 1;
"
h2 style="
font-size: 1.6rem;
color: #27ae60;
margin: 0 0 15px 0;
font-weight: bold;
" 구독자 전용 콘텐츠/h2
p style="
font-size: 1rem;
margin: 0 0 10px 0;
line-height: 1.6;
color: #eee;
"
이 콘텐츠는 보호되어 있습니다.br
구독 중인 블로그 URL을 입력하세요.
/p
p style="
font-size: 0.85rem;
margin: 0 0 20px 0;
color: #bbb;
line-height: 1.5;
"
※ 구독자임에도 접속이 불가할 경우 또는 비구독자도,br
댓글로 span style="color:#ffcc00;font-weight:bold;"'승인 요청'/span을 남겨주세요.
/p
input
type="text"
id="visitor-url"
placeholder="https://아이디.tistory.com"
style="
width: 100%;
padding: 14px;
margin-bottom: 15px;
border-radius: 8px;
border: none;
outline: none;
text-align: center;
color: #000;
font-size: 1rem;
box-sizing: border-box;
background: #fff;
"
div style="display:flex;flex-direction:column;gap:10px;"
button
id="check-access-btn"
style="
width: 100%;
padding: 14px;
font-size: 1.1rem;
cursor: pointer;
background: #087e3a;
color: white;
border: none;
border-radius: 8px;
font-weight: bold;
transition: 0.3s;
"
접근 확인/button
a
href="홈.이동할 주소"
style="
width: 100%;
padding: 12px;
font-size: 1rem;
cursor: pointer;
background: rgba(255,255,255,0.1);
color: white;
border: 1px solid rgba(255,255,255,0.3);
border-radius: 8px;
font-weight: bold;
text-decoration: none;
display: inline-block;
box-sizing: border-box;
transition: 0.3s;
"
홈으로 이동/a
/div
p
id="access-message"
style="
margin-top: 20px;
font-size: 0.95rem;
min-height: 24px;
font-weight: bold;
"
/p
/div
`;
document.body.appendChild(popup);
document.body.style.overflow = 'hidden';
/* ===============================
사용자 행동 제한
=============================== */
const preventActions = (e) = {
if (e.target.id === 'visitor-url') return;
e.preventDefault();
};
document.addEventListener('contextmenu', preventActions);
document.addEventListener('selectstart', preventActions);
document.addEventListener('dragstart', preventActions);
/* ===============================
접근 확인 로직
=============================== */
const btn = document.getElementById('check-access-btn');
const input = document.getElementById('visitor-url');
const msg = document.getElementById('access-message');
btn.onclick = async () = {
const val = input.value.trim();
if (!val) {
msg.textContent = "URL을 입력해주세요.";
return;
}
btn.disabled = true;
btn.textContent = "확인 중...";
try {
const url = `https://script.google.com/구글GAS주소로 변경?visitorLink=$encodeURIComponent(val)}`;
const res = await fetch(url);
const data = await res.json();
if (data.access) {
msg.textContent = " 확인되었습니다!";
msg.style.color = "#00ff00";
document.removeEventListener('contextmenu', preventActions);
document.removeEventListener('selectstart', preventActions);
document.removeEventListener('dragstart', preventActions);
setTimeout(() = {
popup.remove();
document.body.style.overflow = 'auto';
}, 800);
} else {
msg.textContent = "❌ 구독 정보가 없습니다.";
msg.style.color = "#ff4444";
btn.disabled = false;
btn.textContent = "접근 확인";
}
} catch (e) {
msg.textContent = "⚠연결 오류가 발생했습니다.";
btn.disabled = false;
btn.textContent = "접근 확인";
}
};
input.onkeypress = (e) = {
if (e.key === 'Enter') btn.click();
};
});
})();
/script
[코드 3] 구독자 전용 블로그 글작성시 적용하는 코드
구독잦 전용 블로그 글을 작성할 때 글 상단 또는 하단에 아래 코드를 삽입하면, 자바스크립트가 해당 코드가 포함된 글만 자동으로 감지하여 구독자 전용 콘텐츠로 처리하고 잠금 팝업 창을 표시합니다.
div class="prime-preview"/div