JAMstack으로 90년대 게스트북을 되살리다: 내 정적 11ty 사이트에 동적 댓글을 추가한 방법
Source: Dev.to
Source: …
아키텍처: 세 가지 간단한 요소
솔루션은 조화롭게 작동하는 세 개의 상호 연결된 구성 요소로 이루어져 있습니다:
1. 폼 (프론트엔드)
Netlify Forms를 활용한 간단한 HTML 폼:
<form name="guestbook" method="POST" data-netlify="true">
<p>
<label>Name *</label>
<input type="text" name="name" required />
</p>
<p>
<label>Message *</label>
<textarea name="message" required></textarea>
</p>
<p>
<button type="submit">Sign Guestbook</button>
</p>
</form>
여기서 마법 같은 부분은 data‑netlify="true"입니다 – 이 단일 속성이 Netlify에 폼 제출을 가로채고 저장하도록 지시하므로 백엔드가 필요 없습니다.
2. 웹훅 (서버리스 함수)
누군가 폼을 제출하면 Netlify가 웹훅을 트리거하여 새로운 항목으로 사이트를 재빌드합니다:
// netlify/functions/guestbook-webhook.js
const fetch = require('node-fetch');
exports.handler = async function (event, context) {
if (event.httpMethod !== 'POST') {
return { statusCode: 405, body: 'Method Not Allowed' };
}
try {
const payload = JSON.parse(event.body);
if (payload.type === 'submission' && payload.data?.name === 'guestbook') {
console.log('New guestbook submission received:', payload.data);
// Wait a bit before triggering rebuild
console.log('Waiting 5 seconds before triggering rebuild...');
await new Promise(resolve => setTimeout(resolve, 5000));
const buildHookUrl = process.env.NETLIFY_BUILD_HOOK_URL;
if (buildHookUrl) {
const response = await fetch(buildHookUrl, {
method: 'POST',
body: JSON.stringify({ trigger: 'guestbook_submission' }),
headers: { 'Content-Type': 'application/json' }
});
if (response.ok) {
console.log('Build triggered successfully');
}
}
}
return {
statusCode: 200,
body: JSON.stringify({ received: true })
};
} catch (error) {
console.error('Webhook error:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal Server Error' })
};
}
};
3. 데이터 페처 (11ty 데이터 파일)
빌드 시점에 11ty가 Netlify API에서 모든 제출을 가져옵니다:
// src/_data/guestbook.js
const fetch = require('node-fetch');
module.exports = async function () {
const siteId = process.env.NETLIFY_SITE_ID;
const token = process.env.NETLIFY_FORMS_ACCESS_TOKEN;
if (!token || !siteId) {
console.warn('No Netlify API credentials found. Using sample data.');
return getSampleEntries();
}
// Get form ID first
const formsUrl = `https://api.netlify.com/api/v1/sites/${siteId}/forms`;
const formsResponse = await fetch(formsUrl, {
headers: {
Authorization: `Bearer ${token}`,
'User-Agent': 'curl/7.79.1'
}
});
const forms = await formsResponse.json();
const guestbookForm = forms.find(form => form.name === 'guestbook');
// Fetch submissions with retry logic
const url = `https://api.netlify.com/api/v1/sites/${siteId}/forms/${guestbookForm.id}/submissions`;
let response;
let retries = 3;
let delay = 2000;
while (retries > 0) {
const submissionsResponse = await fetch(url, {
headers: {
Authorization: `Bearer ${token}`,
'User-Agent': 'curl/7.79.1'
}
});
if (submissionsResponse.ok) {
response = await submissionsResponse.json();
break;
} else if (retries > 1) {
console.log(`Retrying in ${delay}ms... (${retries} attempts left)`);
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2;
retries--;
}
}
// Transform and return entries
return response.map(submission => ({
name: submission.data.name,
message: submission.data.message,
website: submission.data.website || '',
date: new Date(submission.created_at),
id: submission.id
}));
};
미스터리: 모바일 제출 사라짐
게스트북은 노트북에서는 완벽하게 작동했지만, 모바일에서 제출했을 때 로딩 스피너가 표시되고 성공 페이지에 도착했음에도 실시간 사이트에는 나타나지 않았습니다. 해당 항목은 Netlify의 폼 대시보드에는 있었지만 사이트에는 표시되지 않았습니다.
근본 원인은 분산 시스템에서의 최종 일관성(eventual consistency) 이었습니다.
나에게 교훈을 준 레이스 컨디션
- 사용자가 폼을 제출함 → Netlify가 즉시 저장합니다.
- 웹훅이 즉시 트리거됨 → 사이트 재빌드가 시작됩니다.
- 사이트가 Netlify API에 제출 데이터를 조회함 → 너무 이릅니다!
- API가 오래된 데이터를 반환합니다 (제출이 아직 인덱싱되지 않음).
- 사이트가 새로운 항목 없이 재빌드됩니다.
데스크톱 테스트는 보통 운이 좋았지만, 모바일 네트워크에서는 타이밍 문제가 드러났습니다.
해결책: 인내와 재시도
- 재빌드 지연 – 빌드 훅을 호출하기 전에 짧고 설정 가능한 대기시간(예: 5 seconds) 을 추가합니다.
- 제출물 재시도 – 11ty 데이터 파일에서 지수 백오프 재시도를 구현합니다.
이러한 변경으로 모든 디바이스에서 제출된 내용이 라이브 사이트에 안정적으로 표시됩니다.
왜 이것이 중요한가: IndieWeb 철학
방명록은 향수를 불러일으키며 콘텐츠 소유라는 IndieWeb 원칙을 구현합니다. 타사 댓글 시스템과 달리 모든 데이터는 Netlify 계정에 저장되며, 내보낼 수 있고 다른 플랫폼에 잠기지 않습니다.
Lessons Learned
- Static doesn’t mean static: Serverless functions add dynamic features to static sites. → 정적이 정적이라는 뜻은 아니다: 서버리스 함수는 정적 사이트에 동적 기능을 추가합니다.
- Timing matters: Distributed systems aren’t instantaneous; consider race conditions. → 시점이 중요하다: 분산 시스템은 즉시 작동하지 않으며, 경쟁 상태를 고려해야 합니다.
- Test on real networks: Desktop Wi‑Fi differs from mobile 4G. → 실제 네트워크에서 테스트하라: 데스크톱 Wi‑Fi와 모바일 4G는 다릅니다.
- Simple is powerful: Three small files replace an entire backend. → 단순함은 강력하다: 작은 파일 세 개가 전체 백엔드를 대체합니다.
전체 흐름
- 사용자가 양식을 작성하면 → Netlify가 제출을 저장합니다.
- Netlify가 웹훅을 전송하면 → 서버리스 함수가 이를 수신합니다.
- 함수가 5초 대기한 후 → 빌드 훅을 트리거합니다.
- 11ty가 사이트를 빌드하고 → 재시도 로직으로 제출을 가져옵니다.
- 사이트가 배포되면 → 새 메시지가 자동으로 표시됩니다.
직접 해보세요
11ty 사이트에 방명록을 추가하고 싶으신가요? 필요한 것이 여기 있습니다:
- Forms가 활성화된 Netlify 사이트.
- build hook URL (Site settings → Build & deploy → Build hooks).
forms:read권한이 있는 Netlify Personal Access Token.- 위에서 언급한 세 개의 코드 파일.
환경 변수
Netlify에 다음을 설정하세요:
NETLIFY_FORMS_ACCESS_TOKENNETLIFY_SITE_IDNETLIFY_BUILD_HOOK_URL
이게 전부입니다. 데이터베이스도, 서버도, 유지보수도 필요 없습니다—그냥 JAMstack의 마법이죠.
이 글은 원래 my blog에 게재되었습니다. IndieWeb 최전선에서의 이야기를 더 보고 싶다면 팔로우해주세요!