JAMstack으로 90년대 게스트북을 되살리다: 내 정적 11ty 사이트에 동적 댓글을 추가한 방법

발행: (2025년 12월 22일 오전 10:41 GMT+9)
8 min read
원문: Dev.to

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) 이었습니다.

나에게 교훈을 준 레이스 컨디션

  1. 사용자가 폼을 제출함 → Netlify가 즉시 저장합니다.
  2. 웹훅이 즉시 트리거됨 → 사이트 재빌드가 시작됩니다.
  3. 사이트가 Netlify API에 제출 데이터를 조회함너무 이릅니다!
  4. API가 오래된 데이터를 반환합니다 (제출이 아직 인덱싱되지 않음).
  5. 사이트가 새로운 항목 없이 재빌드됩니다.

데스크톱 테스트는 보통 운이 좋았지만, 모바일 네트워크에서는 타이밍 문제가 드러났습니다.

해결책: 인내와 재시도

  • 재빌드 지연 – 빌드 훅을 호출하기 전에 짧고 설정 가능한 대기시간(예: 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. → 단순함은 강력하다: 작은 파일 세 개가 전체 백엔드를 대체합니다.

전체 흐름

  1. 사용자가 양식을 작성하면 → Netlify가 제출을 저장합니다.
  2. Netlify가 웹훅을 전송하면 → 서버리스 함수가 이를 수신합니다.
  3. 함수가 5초 대기한 후 → 빌드 훅을 트리거합니다.
  4. 11ty가 사이트를 빌드하고 → 재시도 로직으로 제출을 가져옵니다.
  5. 사이트가 배포되면 → 새 메시지가 자동으로 표시됩니다.

직접 해보세요

11ty 사이트에 방명록을 추가하고 싶으신가요? 필요한 것이 여기 있습니다:

  • Forms가 활성화된 Netlify 사이트.
  • build hook URL (Site settings → Build & deploy → Build hooks).
  • forms:read 권한이 있는 Netlify Personal Access Token.
  • 위에서 언급한 세 개의 코드 파일.

환경 변수

Netlify에 다음을 설정하세요:

  • NETLIFY_FORMS_ACCESS_TOKEN
  • NETLIFY_SITE_ID
  • NETLIFY_BUILD_HOOK_URL

이게 전부입니다. 데이터베이스도, 서버도, 유지보수도 필요 없습니다—그냥 JAMstack의 마법이죠.

이 글은 원래 my blog에 게재되었습니다. IndieWeb 최전선에서의 이야기를 더 보고 싶다면 팔로우해주세요!

Back to Blog

관련 글

더 보기 »