웹 개발자를 위한 GDPR 준수 실전 가이드 (2026년판, 코드 예시 포함)

발행: (2026년 6월 11일 PM 07:40 GMT+9)
7 분 소요
원문: Dev.to

출처: Dev.to

원래는 Reddit의 r/webdev에 올린 글이며, dev.to 커뮤니티를 위해 여기서 공유합니다.
저는 독일에 거주하는 개발자입니다. 클라이언트 사이트가 Google Fonts를 외부에서 로드한다는 이유로 €900 짜리 Abmahnung(경고 서한)을 받으면서 GDPR/DSGVO 준수 문제에 깊이 파고들게 되었습니다. 아래에 제가 배운 내용을 실천 가능한 기술 단계별로 정리했습니다.

법률 자문이 아닙니다. 2026년 현재 EU 법원 판결을 기준으로 실제 현장에서 작동하는 방법을 소개합니다.

문제

fonts.googleapis.com을 로드하면 사용자의 IP가 Google 서버로 전송됩니다. 뮌헨 지방 법원은 이것이 동의 없이 이루어지는 데이터 처리에 해당한다는 판결을 내렸습니다(LG München, 2022).

해결책

폰트를 직접 호스팅하세요.

Google Fonts 로컬에 다운로드하기

npx google-font-download "Inter:wght@400;600;700" --output ./fonts
# 또는 google-webfonts-helper API 사용
curl "https://gwfh.mranftl.com/api/fonts/inter?subsets=latin" | jq -r '.variants[] | .fontFiles[]'
/* Before (ILLEGAL in EU) */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap');

/* After (DSGVO compliant) */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: local('Inter Regular'),
       url('/fonts/inter-v12-latin-regular.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153;
}

Vite 혹은 webpack을 사용할 경우

// vite.config.js — Google CDN 대신 폰트를 직접 호스팅
export default defineConfig({
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@use "src/styles/fonts" as *;`
      }
    }
  }
});

문제

ePrivacy 지침에 따라 비필수 쿠키를 설정하기 전에 사용자의 동의를 받아야 합니다.

해결책

세부 옵션을 제공하는 적절한 쿠키 동의 배너를 구현합니다.

// localStorage에서 동의 상태 확인
const consent = JSON.parse(localStorage.getItem('cookie-consent') || '{}');

if (consent.analytics) {
  // 동의가 있을 때만 분석 스크립트 로드
  const script = document.createElement('script');
  script.src = '/js/analytics.js'; // 직접 호스팅!
  script.async = true;
  document.head.appendChild(script);
}

// 쿠키 동의 배너 로직
function setConsent(type) {
  const consent = JSON.parse(localStorage.getItem('cookie-consent') || '{}');
  consent[type] = true;
  consent.timestamp = new Date().toISOString();
  localStorage.setItem('cookie-consent', JSON.stringify(consent));
  document.getElementById('cookie-banner').style.display = 'none';

  if (consent.analytics) loadAnalytics();
  if (consent.marketing) loadMarketing();
}

문제

GDPR 제13/14조는 데이터 처리에 대한 구체적인 정보를 요구합니다.

해결책

실제 데이터 처리 상황을 반영한 동적 개인정보 처리방침을 작성합니다.

// privacy-policy-data.js — 개인정보 처리방침을 실제와 동기화
const dataProcessing = {
  cookies: {
    essential: [
      { name: 'session', purpose: 'Login session', duration: '24h', provider: 'First-party' }
    ],
    analytics: [
      { name: '_pa', purpose: 'Page analytics', duration: '13 months', provider: 'Self-hosted Matomo' }
    ]
  },
  thirdPartyServices: [
    { name: 'Hetzner', purpose: 'Server hosting', location: 'Germany', data: 'Server logs' },
    { name: 'Stripe', purpose: 'Payment processing', location: 'EU/US (SCCs)', data: 'Payment data' }
  ],
  dataSubjectRights: ['access', 'rectification', 'erasure', 'portability', 'restriction', 'objection']
};

문제

암호화되지 않은 이메일 전송이나 동의 없이 데이터 저장을 하는 폼이 있습니다.

해결책

DSGVO에 맞는 폼 처리 로직을 구현합니다.

// DSGVO‑compliant form handler
app.post('/contact', rateLimit({ windowMs: 15 * 60 * 1000, max: 5 }), async (req, res) => {
  const { name, email, message, privacyConsent } = req.body;

  // 1. 동의 여부 확인
  if (!privacyConsent) {
    return res.status(400).json({ error: 'Privacy consent required' });
  }

  // 2. 동의 로그와 타임스탬프 저장
  await db.query(
    'INSERT INTO consent_log (email, purpose, timestamp, ip_hash) VALUES ($1, $2, NOW(), $3)',
    [email, 'contact_form', hashIP(req.ip)]
  );

  // 3. 자동 삭제 정책 적용 (Art. 5(1)(e) — 저장 제한)
  await db.query(
    'INSERT INTO inquiries (name, email, message, created_at, delete_after) VALUES ($1, $2, $3, NOW(), NOW() + INTERVAL \'30 days\')',
    [name, email, message]
  );

  // 4. 오래된 데이터 자동 삭제 (cron 작업으로 실행)
  // DELETE FROM inquiries WHERE delete_after < NOW();
  res.json({ success: true });
});
// ConsentManager 예시
class ConsentManager {
  constructor() {
    this.handlers = {};
    window.addEventListener('storage', (e) => {
      if (e.key === 'cookie-consent') this._runHandlers();
    });
    this._runHandlers();
  }

  onConsent(type, callback) {
    if (!this.handlers[type]) this.handlers[type] = [];
    this.handlers[type].push(callback);
    this._runHandlers();
  }

  _runHandlers() {
    const consent = JSON.parse(localStorage.getItem('cookie-consent') || '{}');
    Object.entries(this.handlers).forEach(([type, callbacks]) => {
      if (consent[type]) callbacks.forEach(cb => cb());
    });
  }
}

// 사용 예시
const consent = new ConsentManager();

// 사용자가 지원 쿠키에 동의했을 때만 Intercom 로드
consent.onConsent('support', () => {
  window.Intercom('boot', { app_id: 'YOUR_ID' });
});

// 사용자가 분석 쿠키에 동의했을 때만 로드
consent.onConsent('analytics', () => {
  // GA 대신 자체 호스팅 Matomo 사용
  const _paq = window._paq || [];
  _paq.push(['trackPageView']);
  _paq.push(['enableLinkTracking']);
});

출시 전 체크리스트

  • 모든 폰트를 직접 호스팅 (Google Fonts CDN 사용 금지)
  • 세부 옵션을 제공하는 쿠키 동의 배너 구현
  • 분석 스크립트는 동의 후에만 로드 (자체 호스팅 Matomo 사용)
  • 개인정보 처리방침에 모든 데이터 처리 활동 명시
  • 연락 폼은 동의와 타임스탬프를 로그에 기록
  • 데이터는 30일 후 자동 삭제
  • 모든 페이지에 SSL/TLS 적용
  • 동의 받기 전 제3자 스크립트 로드 금지
  • .de 도메인용 실제 연락처가 포함된 Impressum
  • 서버가 EU 내에 위치하거나 적절한 SCCs 확보

수동으로 확인하는 것이 번거로워 스캐너를 만들었습니다. 회원가입 없이 무료로 이용할 수 있습니다:
nevik.de/guard/ — URL을 입력하면 다음을 검사합니다.

  • 외부 리소스 로드 여부(폰트, 스크립트, CDN)
  • 쿠키 동의 상태
  • SSL/TLS 설정
  • 법적 페이지 누락 여부(Impressum, Datenschutz)
  • 제3자 트래커 탐지

테스트한 독일 웹사이트 73%에서 위반 사항을 발견했으며, 대형 사이트에서도

0 조회
Back to Blog

관련 글

더 보기 »