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

div는 클릭되지만 버튼은 아닙니다 — button vs a 코드 리뷰

div onClick은 클릭되지만 button이 아닙니다. 스크린리더, 키보드 사용자, AI 에이전트 모두 그 차이를 압니다. 구글 공식 가이드(2026-05-15)가 흘린 단서로부터 button과 a의 의미론을 다시 봅니다.

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

A11y 코드 리뷰 #001 · 오늘 도감에 등록할 첫 태그 — <button> (그리고 그 사촌 <a>)

이 시리즈는 한국 프론트엔드 팀을 위한 실전 a11y 코드 리뷰입니다. 표면적으로는 접근성 시리즈지만, 본질은 SEO·GEO·AEO 최적화 시리즈입니다. 왜 그런지는 4번 섹션에서 구글 공식 문서가 직접 말합니다.

TL;DR

  • div onClick은 클릭됩니다. 그런데 button은 아닙니다. 사용자도, 스크린리더도, AI 에이전트도 그 차이를 압니다.
  • 키보드 사용자에게 button은 Enter와 Space로 발화되지만, 링크는 Enter만 발화됩니다. div는 둘 다 무반응이 기본값입니다.
  • 구글 공식 가이드(2026-05-15)는 browser agent가 페이지를 이해할 때 accessibility tree를 본다고 명시했습니다. div onClick은 거기에 안 잡힙니다.
  • 라우팅이 일어나면 <a>, 현재 페이지의 상태가 바뀌면 <button>. 단 한 줄 규칙입니다.
  • 이 글의 체크리스트는 다음 PR 한 번이면 적용 가능합니다.

1. 어디서나 본 그 div

다음 네 가지 코드는 모두 한국 웹 어딘가에서 흔히 보는 패턴입니다. 회사 이름은 굳이 거론하지 않겠습니다. 너무 흔해서 의미가 없습니다.

결제 화면의 "결제하기"

<div
  className="checkout-btn"
  onClick={handleCheckout}
  style={{ cursor: "pointer" }}
>
  결제하기
</div>

FAQ 영역의 토글

<div className="faq-row" onClick={toggle}>
  <span>{question}</span>
  <span>{open ? "−" : "+"}</span>
</div>

헤더의 햄버거 메뉴

<div onClick={openSidebar} className="hamburger">
  <svg>{/* 메뉴 아이콘 */}</svg>
</div>

약관 페이지로 이동하는 "약관 보기"

<div className="link-like" onClick={() => router.push("/terms")}>
  약관 보기
</div>

네 패턴 모두 동작은 합니다. 화면에는 잘 보이고, 마우스 클릭으로는 잘 작동합니다. cursor도 pointer로 잡혀 있어서 "이게 클릭되는 거구나" 직관도 줍니다.

그런데 같은 페이지를 키보드로 탐색하거나 스크린리더로 듣거나, 요즘 등장한 browser agent로 자동화해 보면 — 네 패턴 모두 한 가지 공통점을 드러냅니다. 이 요소들의 의미를 컴퓨터가 모릅니다.


2. 한 가지 질문

이 글 전체를 관통하는 질문은 한 줄입니다.

같은 div를 사람, 스크린리더 사용자, 키보드 사용자, AI 에이전트는 각각 어떻게 인식할까요?

이 질문에 답을 하다 보면 자연스럽게 시맨틱 HTML이 나옵니다. 그래서 이 글은 "그냥 button 쓰세요"라는 결론을 향해 가지 않습니다. 그건 너무 빠른 결론입니다. 대신, 왜 그래야 하는지가 사용자별로 어떻게 다르게 체험되는지를 봅니다.


3. button과 a — 점진적 해부

가장 단순한 형태부터 한 줄씩 진화시켜 봅니다.

<button>결제하기</button>

이게 전부입니다. 이 한 줄로 다음이 자동으로 따라옵니다 — Tab 키 포커스, Enter/Space 발화, 스크린리더 "버튼" 라벨, :focus-visible 기본 스타일, disabled 속성 지원, form 안에 들어가면 자동 submit 동작.

form 안에서 submit이 되지 않게 하려면 한 글자만 더 추가합니다.

<button type="button">결제하기</button>

type 기본값은 submit입니다. 폼 외부에서는 차이가 없지만, 안에 들어가면 의도치 않은 submit이 발생합니다. 그래서 form 외부에서도 명시하는 게 안전합니다.

이번엔 라우팅용입니다.

<a href="/terms">약관 보기</a>

Adrian Roselli는 이 차이를 한 문장으로 정리합니다.

Link는 사용자를 다른 URL로 옮깁니다. Button은 현재 페이지의 상태를 바꿉니다.

Adrian Roselli, Links, Buttons, Submits, and Divs, Oh Hell (2016)

이 한 줄이 전체 결정 규칙입니다. 현재 URL이 바뀌면 <a>, 안 바뀌면 <button>. 사이드바를 여는 햄버거는 button입니다(상태 변경). 약관 페이지로 이동하는 "약관 보기"는 a입니다(URL 변경). 결제 처리는 button입니다(트랜잭션). FAQ 토글은 button입니다(상태 변경).

이게 모호한 경우는 거의 없습니다. 모호하다고 느낀다면 보통은 그 디자인 자체가 사용자에게 모호하다는 신호입니다.


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

이제 div onClick이 누구에게 어떤 식으로 닿지 않는지를 봅니다.

4a. 스크린리더 사용자

NVDA(Windows)나 VoiceOver(macOS)가 페이지를 읽을 때, 시맨틱 HTML은 자동으로 의미가 입혀집니다.

<button>결제하기</button>
→ NVDA: "결제하기, 버튼"
→ VoiceOver: "결제하기, 버튼"

<a href="/terms">약관 보기</a>
→ NVDA: "약관 보기, 링크"
→ VoiceOver: "약관 보기, 링크"

<div onClick={...}>결제하기</div>
→ NVDA: "결제하기"
→ VoiceOver: "결제하기"

마지막 케이스가 가장 위험합니다. 존재하지 않는 것처럼 들리지도 않습니다. 그냥 텍스트입니다. 사용자는 이 텍스트가 클릭 가능하다는 것을 알 길이 없습니다. 시각적으로 cursor: pointer로 표시돼 있더라도 스크린리더 사용자는 그 정보를 받지 못합니다.

스크린리더에는 "버튼 목록 보기", "링크 목록 보기" 같은 단축 기능이 있습니다. 시각장애 사용자가 가장 자주 쓰는 탐색 방식 중 하나입니다. div onClick은 둘 다에 잡히지 않습니다. 사이트가 통째로 사라진 것과 같은 효과입니다.

4b. 키보드 사용자

마우스를 쓰지 않는 사람은 시각장애 사용자만이 아닙니다. 운동 장애, 손목 통증, 효율을 추구하는 파워 유저, 그리고 — 이 글의 다음 섹션에서 다룰 — AI 에이전트도 키보드로 페이지를 조작합니다.

Adrian Roselli가 짚은 키보드 동작의 차이는 외워둘 가치가 있습니다.

하이퍼링크는 Enter 키로 발화됩니다. 진짜 버튼은 Enter 또는 Space로 발화됩니다.

— Adrian Roselli

<button>       → Tab 포커스 ○ / Enter ○ / Space ○
<a href="...">  → Tab 포커스 ○ / Enter ○ / Space ×
<div onClick>   → Tab 포커스 × / Enter × / Space ×

div에 tabindex="0"을 추가하면 포커스는 받습니다. 그래도 Enter/Space에는 반응하지 않습니다. 별도 onKeyDown 핸들러를 붙여야 하는데, 이때 button을 emulate하려면 Enter와 Space를 모두 처리하고, Space는 keypress 시점이 아니라 keyup 시점에 발화시키며, repeat을 막아야 하고, Esc로 취소도 지원해야 합니다. button 태그 하나가 무료로 제공하는 동작을 흉내 내려면 수십 줄의 코드가 필요합니다.

같은 페이지에 button처럼 보이는 것들이 섞여 있을 때 — 어떤 건 Space로 작동하고 어떤 건 안 한다면 — 사용자는 사이트 전체에 대한 신뢰를 잃습니다. Roselli의 표현으로는, "일관성 없는 인터페이스에서 사용자가 페이지의 동작 방식에 자신감을 가질 수 없다"는 얘기입니다.

4c. AI 에이전트

여기가 이 시리즈가 다른 a11y 글과 갈리는 지점입니다.

구글이 2026-05-15 공식 문서로 낸 Generative AI Search Optimization Guide의 마지막 섹션은 짧지만 결정적입니다.

Browser agent들은 작업을 수행하기 위해 사이트에 접근할 수 있습니다 — 시각적 렌더링 분석(스크린샷 같은), DOM 구조 검사, 그리고 accessibility tree 해석 같은 방식으로.

Google, Generative AI Search Optimization Guide (2026-05-15)

브라우저 에이전트가 페이지를 이해하는 입력값으로 구글이 명시한 세 가지가 있습니다 — 스크린샷, DOM, 그리고 accessibility tree. 마지막의 그 트리는 원래 시각장애 사용자용 스크린리더가 쓰던 의미론적 쌍둥이입니다. 시맨틱 HTML, ARIA, label-input 연결, 헤딩 위계 같은 걸 잘 챙기면 자동으로 만들어집니다.

이게 왜 중요한가 — 다음 두 가지 사실 때문입니다.

첫째, AI 에이전트의 자동화는 이미 시작됐습니다. Anthropic Computer Use는 공식 문서상 스크린샷 기반으로 동작합니다. 그러나 스크린샷에서 클릭 가능 요소를 식별하는 일은 OCR + 시각적 단서(border, cursor)에 의존합니다. 같은 페이지에 시맨틱 button과 div onClick이 섞여 있으면, 에이전트는 후자를 정적 텍스트로 분류할 확률이 올라갑니다. 결과적으로 "왜 결제 버튼이 안 눌리지?" 같은 실패가 발생합니다.

둘째, 미래의 browser agent들은 효율 때문에라도 accessibility tree를 봅니다. 스크린샷 OCR보다 트리 파싱이 훨씬 가볍습니다. 구글이 가이드에 트리를 명시했다는 사실 자체가 트리 기반 agent가 곧 늘어난다는 신호입니다.

선행 글에서 더 다뤘습니다 — 구글이 'AI 검색 최적화 가이드'를 냈는데, 정작 핵심은 접근성 트리였습니다Google I/O 2026: AI Mode와 Agents.

요약하면 — div onClick은 accessibility tree에서 "generic" 또는 "text"로 보입니다. button과 a는 각각 "button"과 "link"로 보입니다. 에이전트에게 던지는 "이 페이지에서 결제 버튼을 눌러" 같은 자연어 지시의 성공률이 여기서 갈립니다.


5. SEO/GEO/AEO 영향 — 왜 a11y가 검색 최적화인가

이 시리즈의 정체성을 만드는 단입니다.

작년부터 한국 GEO 시장에서 "AI 시대의 새로운 SEO"라며 팔리던 것들이 있었습니다. llms.txt를 만들어야 한다, 콘텐츠를 청크로 잘게 쪼개야 한다, AI 전용으로 글을 다시 써야 한다. 구글은 2026-05-15 가이드에서 이 세 가지를 한 줄씩으로 거절합니다.

llms.txt에 대해: "Generative AI search에 나타나기 위해 새로운 기계 판독 파일이나 AI 텍스트 파일, 마크업, Markdown을 만들 필요는 없습니다."

콘텐츠 청킹에 대해: "AI가 더 잘 이해하도록 콘텐츠를 작은 조각으로 쪼갤 필요는 없습니다."

AI 전용 글쓰기에 대해: "Generative AI search를 위해 특별한 방식으로 글을 쓸 필요는 없습니다."

그리고 이렇게 못 박습니다.

구글 검색의 관점에서, generative AI search 최적화는 검색 경험 최적화이며, 따라서 여전히 SEO입니다.

이게 무슨 뜻인가요. "AI 검색을 위한 별도 기술 스택은 없다, 사람을 위한 최적화가 곧 AI를 위한 최적화다"라는 입장입니다. 그리고 그 가이드의 마지막 섹션에서 accessibility tree를 흘렸습니다. 두 메시지를 합치면 — 사람을 위한 시맨틱 HTML이 곧 AI를 위한 시맨틱 HTML이다.

이 시리즈의 모든 글이 이 명제를 태그 단위로 검증합니다.

button vs a 케이스에서 구체적으로는:

  • AI Overviews / Perplexity / ChatGPT 인용: 인용 가능성에는 콘텐츠의 시맨틱 구조가 영향을 줍니다. 자세히는 도구별 알고리즘 차이가 있지만, 공통점 하나는 명확합니다. div 덩어리보다 시맨틱 마크업이 된 페이지가 인용 후보로 더 자주 떠오릅니다. 답변 가능한 단위로 잘려 있기 때문입니다.
  • Schema.org Action과의 보완: Schema.org/Action은 페이지에서 가능한 액션을 마크업합니다. 그런데 그 액션의 트리거가 시맨틱 button이 아니면 — 즉 Schema는 "결제하기"라는 액션을 선언했지만 실제 DOM의 그 텍스트는 div onClick이면 — 일관성이 깨집니다. AI는 둘의 불일치를 학습합니다.
  • AI Overviews에 노출된 페이지의 공통 신호: 우리가 컨설팅 현장에서 본 신호 중 하나는, AI Overviews에 인용된 페이지들이 헤딩 위계와 시맨틱 버튼 비율이 평균보다 높다는 점입니다. 인과는 단정 못 합니다만 상관은 일관적입니다. 이 부분은 시리즈 후속 글에서 데이터를 정리할 예정입니다.

한 줄 결론 — button 태그를 쓰는 것은 시각장애 사용자만을 위한 일이 아닙니다. 같은 결정이 AI 검색에서의 인용 가능성, browser agent의 자동화 성공률, SEO 기본기를 동시에 끌어올립니다.


6. 점진적 진화 — React 라이브러리에서

이론은 그렇고, 실제 React 코드에서는 어떻게 적용되는지 단계별로 봅니다.

6.1 단순한 button + 상태

function CheckoutButton({ onCheckout, loading, disabled }: Props) {
  return (
    <button
      type="button"
      onClick={onCheckout}
      disabled={disabled || loading}
      aria-busy={loading}
    >
      {loading ? "처리 중..." : "결제하기"}
    </button>
  );
}

여기서 disabled 속성은 button 태그가 무료로 제공하는 동작입니다 — 포커스에서 제외하고, 클릭을 막고, 스크린리더가 "비활성" 상태를 알립니다. div onClick으로 같은 동작을 만들려면 aria-disabled + tabindex 동적 변경 + onClick 가드 + CSS pointer-events: none을 모두 손으로 처리해야 합니다.

aria-busy는 비동기 처리 중임을 보조 기술에 알려줍니다. AI 에이전트도 이 신호를 읽어 "지금 클릭하면 안 되겠다"를 판단할 수 있습니다.

6.2 라우팅 분기 — Next.js Link

같은 페이지에 액션과 라우팅이 섞일 때가 진짜 결정 지점입니다.

// ❌ 안티패턴 — 라우팅인데 button
<button onClick={() => router.push("/terms")}>약관 보기</button>

// ❌ 안티패턴 — 액션인데 a
<a href="#" onClick={(e) => { e.preventDefault(); handleCheckout(); }}>
  결제하기
</a>

// ✅ 라우팅 — Next.js Link
<Link href="/terms">약관 보기</Link>

// ✅ 액션 — button
<button type="button" onClick={handleCheckout}>결제하기</button>

<Link>를 button onClick + router.push로 대체하면 표면적으로는 같아 보이지만 — 우클릭으로 새 탭 열기가 안 되고, Ctrl+클릭이 안 되고, 가운데 클릭이 안 되고, 검색엔진이 링크로 인식 못 합니다. 즉 사용자가 가진 기본 기대 동작 다섯 가지가 사라집니다. SEO 기본기에서 가장 흔한 손실 지점입니다.

6.3 Headless UI / Radix / shadcn

세 라이브러리의 Button 구현 철학은 미묘하게 다릅니다.

  • Headless UI <Button>: 기본적으로 <button type="button">을 렌더합니다. as="a" prop으로 a로 바꿀 수도 있고, 그 경우 button의 키보드 동작을 emulate하는 코드가 들어갑니다. 즉 안전한 fallback.
  • Radix UI: 별도의 Button 컴포넌트는 없습니다. 대신 컴포넌트별로 (예: Dialog.Trigger) asChild 패턴으로 사용자가 직접 button을 넘기게 합니다. 책임을 사용자에게 위임하는 대신 시맨틱 컨트롤은 보장합니다.
  • shadcn/ui: Radix 위에 Tailwind 클래스를 입힌 형태입니다. <Button> 컴포넌트는 native button을 렌더합니다. asChild로 a로 바꾸면 button 스타일 + a 시맨틱이 됩니다.

세 라이브러리 모두 기본값은 시맨틱 button입니다. 깨지는 케이스는 보통 라이브러리 안이 아니라, 라이브러리를 div로 감싸 추가 onClick을 붙이거나, 자체 컴포넌트를 만들 때 시맨틱을 떨군 경우입니다.

6.4 div onClick으로 흉내 내기 — 그러면 안 되는 이유의 코드 증거

// ⚠️ button을 흉내 내려면 이 정도가 필요합니다
function FakeButton({ onClick, disabled, children }: Props) {
  const handleKey = (e: React.KeyboardEvent) => {
    if (disabled) return;
    if (e.key === "Enter") {
      e.preventDefault();
      onClick();
    }
    if (e.key === " ") {
      e.preventDefault();
      // Space는 keyup에 발화시켜야 native button과 일치
    }
  };
  const handleKeyUp = (e: React.KeyboardEvent) => {
    if (disabled) return;
    if (e.key === " ") {
      e.preventDefault();
      onClick();
    }
  };
  return (
    <div
      role="button"
      tabIndex={disabled ? -1 : 0}
      aria-disabled={disabled}
      onClick={disabled ? undefined : onClick}
      onKeyDown={handleKey}
      onKeyUp={handleKeyUp}
      style={{ cursor: disabled ? "not-allowed" : "pointer" }}
    >
      {children}
    </div>
  );
}

이게 native button 하나로 대체 가능합니다.

<button type="button" onClick={onClick} disabled={disabled}>
  {children}
</button>

코드 분량의 차이만이 아니라 — 위쪽 버전은 form 안에서 submit 동작이 빠져 있고, :focus-visible 기본 스타일이 빠져 있고, 미래의 commandcommandfor 같은 새 속성도 못 받습니다.


7. 한국 맥락 — 가볍게

한국 웹 접근성 표준 KWCAG 2.2의 관련 항목은 다음과 같습니다.

  • 2.1.1 키보드 사용 보장: 콘텐츠는 키보드만으로 사용할 수 있어야 합니다. div onClick은 기본적으로 이 항목 위반입니다.
  • 4.1.2 이름, 역할, 값: 사용자 인터페이스 요소는 이름(label)과 역할(role)을 보조 기술이 읽을 수 있어야 합니다. div onClick은 역할이 없습니다.

KWCAG는 글로벌 WCAG 2.x를 기반으로 한 한국 표준이라 항목 번호와 의미가 거의 같습니다. 즉 위 두 항목은 외국 사례와 한국 사례가 똑같이 적용됩니다. 이 시리즈에서 한국 맥락은 KWCAG 항목 매핑 정도로 짧게 가고, 본문은 글로벌 표준 위에서 풀어가겠습니다.


8. "그래도 div onClick으로 안 되나요?"

이런 질문이 가끔 옵니다 — "role='button' + tabindex + onKeyDown 다 붙이면 button과 같지 않나요?"

기능적으로 비슷한 케이스를 만들 수는 있습니다. 그런데 다음 네 가지가 깨집니다.

첫째, form 안에서의 submit 동작. native button은 form 안에 들어가면 자동으로 submit입니다. div는 안 됩니다. 그래서 type="submit"으로 명시할 수도 없습니다. enter 키로 폼이 제출되는 경험이 그대로 사라집니다.

둘째, 브라우저별·OS별·보조기술별 동작 일관성. native button은 운영체제·브라우저·스크린리더가 합의한 동작을 보장합니다. div + ARIA는 보장이 아닙니다. 우연히 동작하는 것에 가깝습니다. 새로운 브라우저 버전이나 스크린리더 업데이트에서 미묘하게 깨질 수 있습니다.

셋째, 미래 HTML 사양 호환성. 최근 추가된 commandcommandfor 속성, popover API 등은 native button 위에서 동작하도록 정의돼 있습니다. div로는 이 새 기능들에 올라타지 못합니다.

넷째, AI 에이전트와 검색엔진의 신호. 앞에서 봤듯 browser agent는 accessibility tree를 봅니다. div role="button"은 트리에 button으로 노출되지만, 시맨틱 button만큼의 신뢰 신호는 아닙니다. ARIA spec 자체가 "No ARIA is better than Bad ARIA"를 명시합니다. native HTML 위에 ARIA를 더하는 것은 의미가 있지만, native HTML을 ARIA로 흉내 내는 것은 안티패턴입니다.

Adrian Roselli의 표현을 빌리면 — "여러분과 여러분의 프로젝트는 특별한 눈송이가 아닙니다." 모든 프로젝트가 한 번씩은 div가 정당한 예외인 줄 알고 길을 갑니다. 거의 항상 그게 아닙니다.


9. 체크리스트

다음 PR에서 코드 리뷰할 때 한 줄씩 점검할 수 있는 항목입니다.

  • 모든 클릭 가능한 div를 grep으로 찾아 <button> 또는 <a>로 바꿨나
  • 라우팅이 일어나는 곳에 <button onClick={router.push}>를 쓰고 있지 않나 — <Link> 또는 <a>로 교체
  • 액션을 트리거하는 곳에 <a href="#" onClick={preventDefault}>를 쓰고 있지 않나 — <button>으로 교체
  • 모든 button에 type="button"을 명시했나 (form 안에서 의도치 않은 submit 방지)
  • 아이콘만 있는 button에 aria-label을 달았나 (햄버거 메뉴, 닫기 X 버튼 등)
  • disabled 상태를 native 속성으로 표현하고 있나 — CSS만으로 비활성화 표시하고 있지 않나
  • Tab 키로 모든 인터랙티브 요소에 포커스가 가는가 — 마우스 없이 페이지 끝까지 이동 가능한가
  • NVDA 또는 VoiceOver로 페이지를 들었을 때 모든 버튼이 "버튼"으로, 링크가 "링크"로 읽히는가

마무리 — 단 한 줄 규칙

이 글이 16편짜리 시리즈의 첫 번째 도감 카드입니다. 다음 카드는 <label>/<fieldset>/<legend>로 — 한국 결제 폼에서 가장 자주 깨지는 그 폼 시맨틱입니다.

button vs a의 단 한 줄 규칙을 다시 적습니다 — URL이 바뀌면 <a>, 바뀌지 않으면 <button>. 그리고 둘 중 하나여야 합니다, div가 아니라.

이 규칙 하나가 스크린리더 사용자, 키보드 사용자, AI 에이전트, 검색엔진 인용 가능성, 미래 HTML API 호환성을 동시에 만족시킵니다. 표면적으로는 접근성이지만 본질은 SEO·GEO·AEO 최적화라는 시리즈의 명제 — 첫 번째 태그에서 이미 증명됐습니다.

XEO 진단 또는 코드 리뷰 컨설팅이 필요하시면 문의하기로 연락 주세요.


codex:
  primary_tag: button
  secondary_tags: [a]
  aria_roles: [button, link]
  aria_attributes: [aria-label, aria-pressed, aria-disabled, aria-busy, aria-current]
  related_patterns: [routing-action-split, icon-only-button, loading-state, disabled-state]
  kwcag: ["2.1.1", "4.1.2"]
  wcag: ["2.1.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:
    - "Adrian Roselli, Links, Buttons, Submits, and Divs, Oh Hell (2016)"
    - "Google, Generative AI Search Optimization Guide (2026-05-15)"
    - "MDN, <button>: The Button element"
    - "WAI-ARIA Authoring Practices, Button Pattern"

다른 글 읽기

기술 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

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

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