서버 없는 마을 홈페이지에 신청접수 기능 만들기

요약

마을·단체 홈페이지를 GitHub Pages(무료 정적 호스팅)로 운영하면 서버가 없어서 “신청 폼 → 저장 → 관리자 확인” 기능을 못 붙인다고 생각하기 쉽다. Google Apps Script 웹앱 + 스프레드시트를 백엔드로 쓰면 서버 비용 0원으로 이 전부가 된다. 2026-08-10 망남마을협동조합 사이트의 「망남마을학교」 참가신청(프로그램 안내 → 개인별 신청 → 관리자 집계·CSV)으로 실증했다. 설치는 구글 계정 하나로 10분.

왜 이 기술인가

  • 마을학교·행사·교육 모집은 대부분 “이름·연락처·몇 가지 선택”을 받는 일이다. 이걸 위해 월 몇만 원짜리 서버나 유료 폼 서비스를 쓸 이유가 없다.
  • 접수 데이터가 곧바로 구글 스프레드시트에 쌓이므로, 관리자(마을 활동가)는 익숙한 시트로 바로 명단·식단·숙소 배정 작업을 할 수 있다.
  • 구글 폼과 달리 우리 홈페이지 안에서 신청이 이뤄져 안내문과 신청서가 한 화면에 있고, 디자인·문항 구조를 완전히 통제할 수 있다. (예: “하고 싶지 않은 프로그램” 문항의 보기를 일정표 데이터에서 자동 생성)

구조 한 장

방문자 브라우저 (GitHub Pages의 정적 페이지)
   │  fetch POST (JSON, Content-Type: text/plain)
   ▼
Google Apps Script 웹앱 (무료, 구글이 호스팅)
   │  action=apply → 시트에 행 추가 (+운영진 알림 메일)
   │  action=list  → 비밀번호 검증 후에만 목록 응답
   ▼
Google 스프레드시트 = 데이터베이스

단계별 따라하기

1. Apps Script 백엔드 만들기

  1. sheets.new 로 스프레드시트 생성 (주소창에 sheets.new 입력 — 구글의 새 문서 단축 주소다)
  2. 확장 프로그램 → Apps Script 로 편집기를 열고 doPost(e) 스크립트를 붙여넣는다. 핵심 뼈대:
function doPost(e) {
  var body = JSON.parse(e.postData.contents);
  if (body.action === 'apply') {           // 저장: 누구나 가능
    var lock = LockService.getScriptLock(); // 동시 신청 겹침 방지
    lock.waitLock(20000);
    try { getSheet().appendRow([new Date(), body.name, body.phone, /*...*/]); }
    finally { lock.releaseLock(); }
    return json({ ok: true });
  }
  if (body.action === 'list') {            // 조회: 비밀번호 필요
    var pw = PropertiesService.getScriptProperties().getProperty('ADMIN_PASSWORD');
    if (body.password !== pw) return json({ ok: false, error: '비밀번호가 올바르지 않습니다.' });
    return json({ ok: true, rows: readAllRows() });
  }
}
  1. ⚙ 프로젝트 설정 → 스크립트 속성ADMIN_PASSWORD(관리자 비밀번호), 원하면 NOTIFY_EMAIL(신청 알림 받을 메일)을 등록한다. 비밀번호를 코드나 홈페이지에 적지 않는 것이 이 구조의 요점이다.
  2. 배포 → 새 배포 → 웹 앱: 실행 계정 , 액세스 모든 사용자. 나오는 https://script.google.com/macros/s/…/exec URL이 백엔드 주소다.

2. 홈페이지 폼에서 호출하기

await fetch(ENDPOINT, {
  method: 'POST',
  headers: { 'Content-Type': 'text/plain;charset=utf-8' },  // ← 핵심
  body: JSON.stringify({ action: 'apply', name, phone, ... }),
});

application/json이 아니라 text/plain으로 보내는 것이 CORS를 피하는 요령이다. json으로 보내면 브라우저가 사전 점검(preflight) 요청을 먼저 보내는데 Apps Script가 이에 응답하지 못해 실패한다. text/plain은 점검 없이 바로 간다.

3. 관리자 화면

같은 엔드포인트에 action: 'list', password: 입력값을 보내 목록을 받아 표로 그린다. 비밀번호는 저장하지 말고 매번 입력받는다. CSV 내려받기를 만들 때는 파일 앞에 BOM()을 붙여야 엑셀에서 한글이 안 깨진다.

4. 안전장치 (실전에서 필요했던 것)

  • 폴백: 엔드포인트가 아직 없거나 전송이 실패하면 “작성 내용 복사 → 문의 폼으로 전달” 안내로 자동 전환. 백엔드 설치 전에도 페이지를 먼저 공개할 수 있다.
  • 로컬 백업: 제출 직전 localStorage에 저장해 두면 전송 실패 시 사용자가 다시 쓸 필요가 없다.
  • 개인정보: 건강·금기 정보가 들어가면 시트 공유 범위를 운영진으로 제한하고, 보존·파기 시점(예: 사업 종료 후 3개월)을 신청서에 명시한다.

설치 중 만나는 화면들 (여기서 많이 멈춘다)

  1. “Google hasn’t verified this app” 빨간 경고 — 본인이 만든 스크립트를 본인 계정으로 승인하는 정상 절차다. Advanced → Go to … (unsafe) 로 진행한다.
  2. 권한 체크박스 화면 — “모든 스프레드시트 보기·수정”이 넓어 보여도 Apps Script에 그 범위 하나뿐이다. 체크하지 않고 Continue를 누르면 권한 없이 통과되어 나중에 저장이 실패한다. Select all 후 계속.
  3. “새 배포” 대화상자가 오류로 깨질 때 — 대부분 브라우저에 구글 계정이 여러 개 로그인된 탓. 시크릿 창에서 해당 계정 하나만 로그인하고 재시도.
  4. 코드 수정 후 — “새 배포”가 아니라 배포 관리 → 편집 → 새 버전. 새 배포를 만들면 URL이 바뀐다.

교훈

  • “서버가 없다”는 “기능이 없다”가 아니다. 정적 호스팅 + 무료 웹앱 조합으로 마을 규모의 접수 업무는 충분하다. 비용이 0원이라 사업 종료 후 방치돼도 청구서가 날아오지 않는다.
  • 비밀은 서버 쪽 한 곳에만. 웹앱 URL은 공개되어도 할 수 있는 일이 “신청 1건 넣기”뿐이도록 설계하고, 조회 권한은 스크립트 속성의 비밀번호로만 연다. 유출되면 속성 값만 바꾸면 끝(재배포 불요).
  • 배포 검증은 실제 도메인에서. localhost에서 되던 것이 실도메인 CORS에서 막히는 일이 흔하다. 라이브 페이지에서 실제 신청 1건을 넣어 시트 도착까지 봐야 완료다.

참고