SOYOYU
블로그로 돌아가기기술 SEO

placeholder는 label이 아닙니다 — label·fieldset·legend 코드 리뷰

placeholder만 있는 폼은 스크린리더에게 '편집 가능'으로만 들리고, AI 에이전트의 자동 입력에서 가장 자주 실패합니다. label·fieldset·legend의 의미론을 코드와 함께 다시 봅니다.

전승엽2026년 6월 13일10 min read
A11y 코드 리뷰시맨틱 HTMLaccessibility treeAI 에이전트웹 접근성label 태그fieldset폼 접근성KWCAG

A11y 코드 리뷰 #002 · 오늘 도감에 등록할 태그 — <label>, <fieldset>, <legend>

표면적으로는 폼 접근성 글이지만, 본질은 AI 에이전트의 폼 자동 입력 성공률과 SEO 신호에 대한 글입니다. 폼은 사이트에서 사용자가 가장 자주 실패하는 지점이고, 그래서 AI에게도 가장 자주 실패하는 지점입니다.

TL;DR

  • placeholder는 label이 아닙니다. 비어 있을 때만 보이고, 색 대비가 약하고, 의미를 보조 기술에 전달하지 못합니다.
  • <label for="id">로 input과 라벨을 명시적으로 연결하면 — 라벨 텍스트를 클릭해도 input에 포커스가 잡히는 무료 기능이 따라옵니다.
  • 라디오·체크박스 그룹은 <fieldset> + <legend>로 묶지 않으면 스크린리더가 그룹 질문을 모릅니다. "성별을 선택하세요" 같은 질문이 사라집니다.
  • AI 에이전트(Anthropic Computer Use, OpenAI CUA, Perplexity 등)의 폼 자동 작성은 name·autocomplete·<label> 세 가지 신호에 의존합니다. 셋 다 없으면 에이전트가 자주 잘못된 필드에 값을 넣습니다.
  • 폼 시맨틱은 a11y인 동시에 conversion rate 최적화입니다. WebAIM 분석은 폼 라벨 누락이 홈페이지 a11y 결함 상위에 매년 등장한다는 사실을 보여줍니다.

1. 어디서나 본 그 폼

다음 네 가지는 한국 웹에서 매일 보는 폼 패턴입니다. 외국 사례도 다르지 않습니다 — 같은 안티패턴이 글로벌하게 똑같이 반복됩니다.

카드 결제 폼 — placeholder만

<input type="text" placeholder="카드 번호 16자리" />
<input type="text" placeholder="MM/YY" />
<input type="text" placeholder="CVC" />

검색창 — label 없음

<input type="search" placeholder="검색어를 입력하세요" />
<button>검색</button>

회원가입 폼 — div로 만든 가짜 라벨

<div className="form-row">
  <div className="label-text">이메일</div>
  <input type="text" />
</div>

성별 라디오 — fieldset 없음

<p>성별</p>
<input type="radio" name="gender" value="m" /> 남
<input type="radio" name="gender" value="f" /> 여

이 네 패턴은 시각적으로는 잘 작동합니다. 마우스로 클릭해서 입력하고 제출하면 평소처럼 동작합니다. 그런데 키보드 사용자, 스크린리더 사용자, AI 에이전트가 같은 폼을 만나면 — 폼이 사실상 텍스트 덩어리로 변합니다.


2. 한 가지 질문

폼은 결국 사이트와 사용자 사이의 가장 명확한 계약 지점입니다. "이름을 알려주세요", "결제 정보를 입력해주세요" 같은 문장이 시각적 라벨로만 존재한다면, 그 계약을 읽지 못하는 모두에게 폼은 닫혀 있는 셈입니다.

같은 input을 사람·스크린리더·AI 에이전트는 각각 무엇으로 인식할까요?

폼에서 이 질문에 답하는 핵심 메커니즘이 <label>, <fieldset>, <legend> 세 태그입니다.


3. label·fieldset·legend — 점진적 해부

3.1 가장 단순한 label

<label for="email">이메일</label>
<input id="email" type="email" />

이 두 줄로 따라오는 동작은 다음과 같습니다 — 스크린리더가 input에 포커스되면 "이메일, 편집"으로 읽습니다. 라벨 텍스트를 클릭하면 input에 포커스가 잡힙니다. 모바일에서 라벨 텍스트의 탭 영역이 자동으로 input의 활성화 영역에 포함됩니다.

3.2 wrapping label (id 없이도 가능)

<label>
  이메일
  <input type="email" />
</label>

input을 label 안에 감싸면 for/id 없이도 같은 효과입니다. 다만 React에서 동적으로 label 위치가 바뀌는 경우가 있어 — id 기반이 더 안전합니다.

3.3 visible label이 디자인적으로 어려운 경우 — aria-label

<input type="search" aria-label="사이트 내 검색" />

검색창처럼 라벨을 시각적으로 숨겨야 하는 경우에만 aria-label을 씁니다. 다만 aria-label이 visible label보다 우선해서는 안 됩니다 — WAI-ARIA의 기본 원칙이기도 합니다.

시각적으로만 숨기고 스크린리더에는 노출하려면 visually-hidden 클래스가 더 정직한 해법입니다.

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
<label for="search" class="sr-only">사이트 내 검색</label>
<input id="search" type="search" placeholder="검색어 입력" />

이렇게 하면 시각적으로 placeholder만 보이고, 스크린리더에게는 "사이트 내 검색, 편집" 라벨이 들립니다.

3.4 fieldset + legend — 그룹의 의미

라디오나 체크박스가 의미상 한 그룹일 때는 <fieldset>으로 묶고 <legend>로 그룹의 질문을 답니다.

<fieldset>
  <legend>배송 방법을 선택하세요</legend>

  <label>
    <input type="radio" name="shipping" value="standard" />
    일반 배송 (3–5일)
  </label>

  <label>
    <input type="radio" name="shipping" value="express" />
    빠른 배송 (1–2일)
  </label>
</fieldset>

스크린리더는 각 라디오를 읽을 때 legend의 문구를 함께 들려줍니다 — "배송 방법을 선택하세요, 일반 배송 3-5일, 라디오 버튼, 2개 중 1번째". legend가 없으면 그룹의 질문 자체가 사라지고, 사용자는 무엇을 고르는 건지 모릅니다.


4. 왜 문제인가 — 3중 영향

4a. 스크린리더 사용자

placeholder-only 폼에서 NVDA가 카드 결제 input에 포커스했을 때 실제로 듣는 소리는 다음과 같습니다.

<input type="text" placeholder="카드 번호 16자리" />
→ NVDA: "편집"
→ VoiceOver: "텍스트 필드, 편집"

placeholder는 라벨로 인식되지 않습니다. 게다가 사용자가 한 글자라도 입력하기 시작하면 placeholder는 사라집니다. 그 시점부터는 "내가 지금 무슨 칸을 채우고 있더라"를 확인할 시각적 단서가 없습니다.

<label>이 붙어 있으면:

<label for="card">카드 번호 16자리</label>
<input id="card" type="text" />
→ NVDA: "카드 번호 16자리, 편집"

라벨이 input과 평생 연결됩니다. 입력 중에도, 폼을 다시 탐색할 때도 동일합니다.

4b. 키보드와 모바일 사용자

라벨이 input과 연결돼 있으면 — 라벨 클릭이 input 포커스를 잡습니다. 이게 모바일에서는 결정적입니다. 손가락이 큰 사용자, 떨림이 있는 사용자, 작은 화면을 쓰는 사용자에게 input 자체보다 라벨 텍스트가 더 넓은 탭 영역을 제공합니다.

라디오·체크박스의 경우는 더 큽니다. 라벨 텍스트를 누르면 라디오가 선택됩니다. label 없이 라디오 버튼만 노출돼 있으면 사용자는 직경 16픽셀짜리 원을 정확히 눌러야 합니다. 폼 이탈률이 여기서 갈립니다.

4c. AI 에이전트

여기가 시리즈 차별화 단입니다.

AI 에이전트의 폼 자동 작성은 다음 세 신호에 의존합니다.

1. <input name="...">         → 필드의 식별자
2. <input autocomplete="...">  → 필드의 의미 (한국에서 가장 자주 누락)
3. <label for="...">           → 필드의 사람 친화 라벨

세 신호가 모두 있으면 에이전트는 다음 같은 폼 자동 입력을 매우 안정적으로 수행합니다.

<label for="cc-number">카드 번호</label>
<input
  id="cc-number"
  name="cardNumber"
  type="text"
  autocomplete="cc-number"
  inputmode="numeric"
/>

autocomplete="cc-number"WHATWG가 정한 표준 값 중 하나로, 브라우저의 패스워드 매니저와 AI 에이전트 모두가 이 값을 읽고 "여기는 카드 번호 칸"으로 인식합니다. 한국 사이트에서 가장 자주 빠지는 속성입니다.

placeholder만 있는 폼에서는 에이전트가 placeholder 텍스트를 OCR로 읽어 추측해야 합니다. 한국어 placeholder는 "카드 번호 16자리", "16-digit card number", "Card #" 등으로 다양해서 추측 정확도가 떨어집니다. 결과적으로 에이전트가 잘못된 칸에 값을 넣거나, 같은 값을 두 번 넣거나, 폼 작성을 중단합니다.

선행 글 구글이 'AI 검색 최적화 가이드'를 냈는데, 정작 핵심은 접근성 트리였습니다에서 봤듯, browser agent들은 accessibility tree를 봅니다. label-input 연결은 그 트리에서 input의 "name" 속성으로 노출됩니다 — ARIA name 말입니다. 라벨이 없으면 이 name이 비어 있고, 에이전트가 필드를 식별할 수 없습니다.


5. SEO/GEO/AEO 영향

5.1 폼이 SEO 신호가 되는 두 지점

폼 자체가 검색엔진에 인덱싱되는 일은 거의 없습니다. 그런데 폼이 있는 페이지는 다음 두 신호로 평가됩니다.

첫째, 페이지의 사용자 경험 품질 신호. Core Web Vitals 외에 구글이 보는 사용성 신호 중 하나가 폼 사용성입니다. 라벨이 없는 폼은 입력 실패율이 높고, 사용자 이탈을 만들고, 결과적으로 페이지의 dwell time과 task success rate를 낮춥니다.

둘째, Schema.org Action과의 일관성. 결제 페이지에 Schema.org/BuyAction 또는 Schema.org/RegisterAction을 마크업했다면 — 그 action의 입력 필드들이 실제 DOM에서 시맨틱하게 노출돼야 합니다. Schema 선언과 DOM 시맨틱이 어긋나면 AI는 둘의 불일치를 학습합니다.

5.2 AEO — 폼이 답변 인용에 미치는 영향

AI Overviews나 Perplexity가 페이지에서 "어떻게 신청하나요?" 같은 답변을 끌어올 때 — 시맨틱하게 정리된 폼이 있는 페이지가 인용 우선순위가 높습니다. 폼의 legend 텍스트와 label 텍스트가 답변에 그대로 인용되는 경우도 봤습니다. "배송 방법을 선택하세요: 일반 배송 3-5일, 빠른 배송 1-2일" 같은 형태로 — 이 정보가 fieldset/legend에 마크업돼 있지 않으면 AI는 정보의 그룹 관계를 모릅니다.

5.3 GEO — Generative Engine의 폼 자동 입력 시대

GEO의 진짜 의미는 — 사용자가 "이 사이트에서 회원가입 해줘"를 에이전트에게 시켰을 때, 그게 성공하느냐의 문제로 옮겨가고 있습니다. 시맨틱 폼이 그 성공률의 기반입니다. 이건 더 이상 a11y만의 문제가 아닙니다 — 전환율 자체의 문제입니다.


6. React 라이브러리에서

6.1 react-hook-form — 가장 흔한 패턴

import { useForm } from "react-hook-form";

function SignupForm() {
  const { register, handleSubmit } = useForm();

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label htmlFor="email">이메일</label>
      <input
        id="email"
        type="email"
        autoComplete="email"
        {...register("email", { required: true })}
      />
      <button type="submit">가입하기</button>
    </form>
  );
}

register("email", ...)가 name 속성을 자동으로 박아줍니다. label-input 연결만 직접 챙기면 됩니다. autoComplete는 react-hook-form이 자동으로 챙겨주지 않으니 명시적으로 추가합니다.

6.2 Headless UI — Field·Label·Description·Input 패턴

import { Field, Label, Description, Input } from "@headlessui/react";

<Field>
  <Label>이메일</Label>
  <Description>가입 확인 메일을 보낼 주소입니다</Description>
  <Input type="email" autoComplete="email" />
</Field>

Headless UI는 Field 컴포넌트가 내부적으로 id를 생성하고 label·description·input을 자동 연결합니다. 즉 시맨틱이 기본값입니다.

6.3 Radix Form — 검증과 시맨틱의 결합

import * as Form from "@radix-ui/react-form";

<Form.Root>
  <Form.Field name="email">
    <Form.Label>이메일</Form.Label>
    <Form.Control asChild>
      <input type="email" autoComplete="email" />
    </Form.Control>
    <Form.Message match="valueMissing">이메일을 입력해 주세요.</Form.Message>
    <Form.Message match="typeMismatch">올바른 이메일 형식이 아닙니다.</Form.Message>
  </Form.Field>
</Form.Root>

Radix Form은 검증 메시지를 aria-describedby로 자동 연결합니다. 에러 발생 시 스크린리더가 "이메일, 편집, 이메일을 입력해 주세요"로 한 번에 읽습니다.

6.4 라디오 그룹 — fieldset의 React 버전

<fieldset>
  <legend>배송 방법</legend>
  {options.map((opt) => (
    <label key={opt.value}>
      <input
        type="radio"
        name="shipping"
        value={opt.value}
        {...register("shipping")}
      />
      {opt.label}
    </label>
  ))}
</fieldset>

CSS reset 때문에 fieldset의 기본 border가 부담스러우면 border: 0만 제거하면 됩니다. 시맨틱은 보존됩니다.


7. 한국 맥락 — 가볍게

KWCAG 2.2의 관련 항목들입니다.

  • 3.3.2 레이블·설명 제공: 사용자 입력이 필요한 콘텐츠는 레이블이나 설명을 제공해야 합니다.
  • 1.3.1 정보·기능·관계의 인식 가능성: 프로그래밍 방식으로 결정될 수 있는 정보·기능·관계가 보조 기술에 노출돼야 합니다 — fieldset/legend가 빠지면 라디오 그룹의 관계가 사라집니다.

한국 결제 폼에서 autocomplete 속성이 거의 누락돼 있다는 점은 한국 특수성으로 한 번 짚을 만합니다. 카카오·네이버 페이 같은 한국 결제 인증 시스템과 native autofill을 함께 쓸 때 — autocomplete가 있으면 native autofill이 먼저 동작하고, 인증 시스템이 그 위에서 추가 단계만 수행합니다. 두 시스템이 충돌하지 않습니다.


8. "placeholder로 안 되나요?"

자주 듣는 반론 — "placeholder가 있으면 라벨처럼 보이는데 충분하지 않나요?"

기능적으로 비슷해 보이지만 다음 네 가지가 깨집니다.

첫째, 입력 시 사라짐. placeholder는 사용자가 한 글자라도 입력하는 순간 사라집니다. 사용자가 폼을 검토하려고 다시 돌아왔을 때 — 빈 칸이 아닌 칸에는 라벨이 없습니다. "내가 지금 뭘 입력했더라"가 사라집니다.

둘째, 색 대비 미준수. placeholder의 기본 색은 회색 #757575 근방입니다. WCAG의 4.5:1 색 대비 기준에 미달입니다. 색 대비를 맞추면 placeholder와 실제 입력 텍스트가 구분되지 않습니다.

셋째, 스크린리더 미인식. 스크린리더는 placeholder를 라벨로 안 읽거나, 일부 환경에서는 읽지만 라벨과는 다른 우선순위로 처리합니다. 환경별로 동작이 일관되지 않습니다.

넷째, AI 에이전트 OCR 의존. 앞에서 봤듯 AI 에이전트가 placeholder 텍스트를 추측해야 합니다. label + autocomplete가 있는 폼 대비 자동 입력 정확도가 떨어집니다.

WebAIM이 매년 발표하는 상위 100만 홈페이지 a11y 분석에서 "input element missing label"은 매년 상위 3개 안에 듭니다. 전세계적으로 가장 흔한 a11y 결함이 라벨 누락이고, 한국도 다르지 않습니다.


9. 체크리스트

  • 모든 input·select·textarea에 연결된 <label>이 있는가 — for/id 또는 wrapping label
  • placeholder를 라벨 대체로 쓰고 있지 않은가
  • 라벨을 디자인적으로 숨겨야 하면 visually-hidden CSS로 — display: none이나 aria-label-only로 도망치고 있지 않은가
  • 라디오·체크박스 그룹이 <fieldset> + <legend>로 묶여 있는가
  • 모든 input에 적절한 nameautocomplete 속성이 있는가 — 카드 번호·이메일·전화번호 등 표준 값 활용
  • 모든 input에 적절한 type이 있는가 — type="email", type="tel", type="number"로 모바일 키보드 최적화
  • 에러 메시지가 aria-describedby로 input에 연결돼 있는가
  • NVDA 또는 VoiceOver로 폼을 들었을 때 모든 input이 라벨과 함께 읽히는가

마무리

이 글이 16편 시리즈의 두 번째 도감 카드입니다. 다음 카드는 <dialog> — 모달을 띄우는 순간 키보드 사용자가 갇히거나 도망가는 그 케이스입니다.

label·fieldset·legend의 단 한 줄 규칙 — input은 혼자 있지 못합니다. 항상 라벨과 짝이고, 그룹이면 fieldset 안에 있습니다. 이 한 줄이 스크린리더 사용자, 키보드 사용자, AI 에이전트의 폼 자동 입력, 검색엔진의 사용성 신호를 동시에 만족시킵니다.

폼 a11y와 AI 자동 입력 최적화 컨설팅이 필요하시면 문의하기로 연락 주세요.


codex:
  primary_tag: label
  secondary_tags: [fieldset, legend, input]
  aria_roles: [group, radiogroup]
  aria_attributes: [aria-label, aria-labelledby, aria-describedby, aria-required, aria-invalid]
  related_patterns: [form-validation, autocomplete-attributes, radio-group, error-message]
  kwcag: ["3.3.2", "1.3.1"]
  wcag: ["3.3.2", "1.3.1", "4.1.2"]
  ai_agent_impact: high
  seo_geo_impact: high
  difficulty: beginner
  test_envs:
    - "NVDA 2024.x + Chrome 125"
    - "VoiceOver macOS 14 + Safari 17"
  references:
    - "WHATWG HTML Living Standard, autofill values"
    - "WebAIM Million Report (annual)"
    - "MDN, <label>: The Label element"
    - "WAI-ARIA Authoring Practices, Form Labels"

다른 글 읽기

기술 SEO

프로그래매틱 SEO: 대규모 페이지 자동 생성과 품질 통제

Google의 Scaled Content Abuse 정책 강화 속에서 프로그래매틱 SEO를 안전하게 실행하는 방법을 다룹니다. Zapier, Canva, Wise의 실제 사례와 30-40% 고유 콘텐츠 기준을 정리합니다.

7 min read
기술 SEO

Next.js layout.tsx에 main이 두 개 — landmark 코드 리뷰

main·nav·header·footer 같은 landmark 태그는 페이지의 지도입니다. 지도가 두 장이거나 없으면 스크린리더 사용자도 AI 에이전트도 길을 잃습니다. 가장 자주 잘못된 케이스부터 정리합니다.

9 min read
기술 SEO

네이버 Search Advisor 기술 감사 워크플로우: GSC와의 차이와 6단계 실전 가이드

네이버 서치어드바이저의 6개 핵심 기능을 분석하고, Google Search Console과 13개 항목을 비교합니다. 네이버에 최적화된 6단계 기술 감사 워크플로우를 제공합니다.

7 min read
기술 SEO

h1은 한 개만이라는 미신과 스크린리더 사용자의 실제 길찾기 — heading 코드 리뷰

SEO 도구는 'h1은 한 개여야 한다'고 외치지만 스크린리더 사용자는 헤딩 목록으로 페이지를 탐색합니다. 헤딩의 본질은 SEO가 아닌 목차이며, 그래서 AI Overviews 인용 신호이기도 합니다.

11 min read
기술 SEO

보안 헤더(CSP/HSTS)가 검색 순위에 미치는 영향

HTTPS는 약 12%의 순위 요인으로 확인되었지만, John Mueller는 보안 헤더가 직접적 순위 요인이 아니라고 밝혔습니다. '상위 사이트 71%가 보안 헤더 사용'이라는 통계의 검증 결과, 실제 채택률 데이터, 상관관계와 인과관계의 구분을 정리합니다.

8 min read
기술 SEO

Edge SEO 실전: 리다이렉트, 헤더 인젝션, HTML 변환 코드 예제

Cloudflare Workers로 구현하는 Edge SEO 실전 코드. 301 리다이렉트, KV 대량 리다이렉트, HSTS/CSP/X-Robots-Tag 헤더 인젝션, HTMLRewriter 메타 태그 삽입, 캐노니컬 인젝션, hreflang 인젝션, A/B 테스트까지.

8 min read

검색 최적화가 필요하신가요?

무료 상담을 통해 비즈니스에 맞는 최적화 전략을 확인하세요.