기본 콘텐츠로 건너뛰기

시맨틱 마크업은 왜 접근성과 SEO의 출발점이 되는가

시맨틱 마크업은 왜 접근성과 SEO의 출발점이 되는가

빠른 답

  • 시맨틱 마크업은 화면 모양보다 콘텐츠의 역할과 관계를 HTML 구조에 남기는 작성 방식이다.
  • 접근성에서는 제목, 랜드마크, 버튼과 링크의 역할이 스크린 리더와 키보드 탐색의 기준이 된다.
  • SEO에서는 주요 콘텐츠, 제목 계층, 링크 구조가 검색 엔진이 문서를 이해하는 단서가 된다.
  • CSR에서도 최종 DOM의 의미 구조는 중요하지만, 공개 콘텐츠라면 초기 HTML에 무엇이 들어 있는지도 함께 봐야 한다.

점검 매트릭스

문서 구조
머리말, 탐색, 본문, 보조 영역, 바닥글의 역할이 구분되는지 확인해 페이지의 큰 지도를 만든다.
제목 계층
글자 크기가 아니라 문서의 포함 관계에 맞게 제목 수준이 이어지는지 확인한다.
상호작용 요소
클릭 가능한 요소가 키보드와 보조 기술에서 버튼, 링크, 입력 필드로 인식되는지 확인한다.
주요 콘텐츠
반복되는 메뉴를 지나 본문으로 바로 이동할 수 있는지 확인해 탐색 비용을 줄인다.
렌더링 시점
CSR 페이지에서 초기 HTML과 최종 DOM이 각각 어떤 정보를 제공하는지 확인한다.
자동 검증
린트, 접근성 검사, E2E 스모크 테스트가 배포 전 최소 기준을 막는지 확인한다.

시맨틱 마크업은 태그 이름보다 의미 구조에 가깝다

시맨틱 마크업은 특정 요소 이름을 많이 쓰는 기법이 아니다. 페이지 안의 콘텐츠가 어떤 역할을 하는지 브라우저, 보조 기술, 검색 엔진이 해석할 수 있도록 구조를 남기는 방식이다.

같은 박스처럼 보이는 영역도 문서 안에서는 서로 다른 의미를 가진다. 어떤 영역은 사이트 전체 탐색이고, 어떤 영역은 본문이며, 어떤 영역은 독립적으로 읽을 수 있는 글이고, 어떤 영역은 단순한 장식일 수 있다. CSS는 화면 표현을 바꾸지만, 콘텐츠의 역할까지 항상 전달하지는 않는다.

예를 들어 큰 글씨와 굵은 스타일은 사람 눈에는 제목처럼 보일 수 있다. 그러나 문서 구조에서 제목으로 노출되지 않으면 스크린 리더의 제목 목록에도 잡히지 않고, 자동 테스트에서도 제목 역할로 찾기 어렵다. 반대로 올바른 제목 구조와 랜드마크를 가진 문서는 스타일이 일시적으로 깨져도 문서의 뼈대는 유지된다.

실제로 자주 막히는 지점은 컴포넌트 이름과 최종 DOM의 의미가 다르다는 데 있다. 파일명이 Header라고 해서 접근성 트리에서 머리말 역할을 한다고 보장되지는 않는다. 디자인 시스템의 Text 컴포넌트가 화면에서는 제목처럼 보이더라도, 최종 결과가 일반 텍스트라면 문서 구조는 제목을 잃는다.

점검할 때는 다음 질문이 도움이 된다.

  • 이 영역은 페이지 전체에서 어떤 역할을 하는가?
  • 제목만 훑어도 문서의 큰 흐름이 보이는가?
  • 사용자가 키보드만으로 주요 기능에 도달할 수 있는가?
  • 링크와 버튼의 차이가 실제 동작과 맞는가?
  • 클라이언트 렌더링 이후에도 같은 의미가 유지되는가?

브라우저와 보조 기술은 구조를 기준으로 해석한다

브라우저는 HTML을 파싱해 DOM을 만들고, 스타일과 레이아웃을 계산해 화면에 그린다. 접근성 측면에서는 여기서 한 단계 더 나아가 접근성 트리가 만들어진다. 접근성 트리는 모든 노드를 그대로 복사한 결과가 아니라, 사용자가 이해하고 조작해야 하는 역할, 이름, 상태, 관계를 중심으로 구성된다.

사용자는 스크린 리더의 제목 목록, 랜드마크 목록, 링크 목록을 통해 페이지를 빠르게 훑는다. 문서 구조가 흐리면 사용자는 페이지를 처음부터 끝까지 선형으로 들어야 하고, 원하는 본문이나 기능으로 이동하는 비용이 커진다. 키보드 사용자도 비슷하다. 시각적으로는 버튼처럼 보이지만 포커스가 가지 않는 요소는 실제 조작 흐름에서 빠져 버린다.

검색 엔진이 보조 기술과 동일하게 동작한다고 볼 수는 없지만, 문서 이해에 필요한 단서는 겹친다. 대표 제목, 본문 영역, 내부 링크, 이미지 대체 텍스트, 페이지 간 연결은 검색 엔진이 문서의 주제와 구조를 추론하는 데 도움을 준다. 시맨틱 마크업만으로 검색 순위가 보장되지는 않지만, 문서를 오해하게 만드는 불필요한 손실은 줄일 수 있다.

관련 기준을 더 확인할 때는 MDN의 HTML 요소 레퍼런스, W3C WAI의 접근성 기초 문서, Google 검색의 JavaScript SEO 가이드를 함께 보면 좋다.

점검 순서는 제목, 랜드마크, 상호작용부터 잡는다

시맨틱 마크업을 점검할 때 요소 이름 목록부터 외우면 적용 기준이 흔들리기 쉽다. 먼저 페이지를 읽는 순서와 역할을 본 뒤, 그 역할에 맞는 기본 요소를 선택하는 흐름이 더 안정적이다.

첫 번째는 제목 구조다. 페이지의 대표 제목이 있고, 그 아래 하위 제목들이 논리적인 포함 관계로 이어지는지 확인한다. 제목 수준은 글자 크기를 정하는 도구가 아니라 문서 목차를 만드는 도구에 가깝다. 작게 보이는 제목이 필요하면 제목 수준을 낮추기보다 스타일을 조정하는 편이 구조를 유지하기 쉽다.

두 번째는 랜드마크 구조다. 전역 탐색, 주요 콘텐츠, 보조 영역, 바닥글이 모두 같은 일반 컨테이너로만 되어 있으면 페이지의 큰 지도가 사라진다. 사용자가 반복되는 메뉴를 지나 본문으로 바로 이동할 수 있는지, 자동화 도구가 주요 콘텐츠를 역할 기준으로 찾을 수 있는지 확인해야 한다.

세 번째는 상호작용 요소다. 페이지 이동은 링크, 현재 화면에서 동작을 실행하는 기능은 버튼이라는 기준을 지키면 키보드 조작과 보조 기술 안내가 단순해진다. 일반 컨테이너에 클릭 이벤트만 붙이면 포커스, Enter 또는 Space 동작, 비활성 상태, 접근성 이름을 직접 챙겨야 한다.

품질 기준은 테스트 종류별로 나누면 운영하기 쉽다.

  • 단위 테스트: 메뉴 생성 함수나 제목 목록 생성기처럼 순수 로직이 있을 때 구조 규칙을 확인한다.
  • 통합 테스트: 렌더링된 페이지에 대표 제목, 탐색, 본문 역할이 존재하는지 확인한다.
  • E2E 테스트: 키보드 이동, 주요 링크 이동, 폼 제출처럼 사용자가 실제로 수행하는 흐름을 검증한다.
  • 스모크 테스트: 배포된 URL에서 제목, 주요 탐색, 본문 영역이 깨지지 않았는지 빠르게 확인한다.
  • 회귀 테스트: 이전에 발생한 접근성 이슈가 다시 생기지 않도록 실패 케이스를 고정한다.

문서 구조를 먼저 모델링해 본다

블로그 상세 페이지를 예로 들면, 화면 구현 단위와 문서 역할 단위가 항상 일치하지 않는다. 상단에는 사이트 이름과 전역 탐색이 있고, 본문에는 글 제목, 작성 정보, 본문 섹션이 있으며, 하단에는 관련 글이나 정책 링크가 있을 수 있다.

처음부터 코드로 들어가기보다 구조를 먼저 적어보면 역할이 더 잘 보인다.

페이지
  머리말
    사이트 이름
    전역 탐색
  주요 콘텐츠
    글
      글 제목
      작성 정보
      첫 번째 본문 섹션
      두 번째 본문 섹션
    관련 글
  바닥글
    저작권 정보
    정책 링크

이 구조에서 글은 독립적으로 읽을 수 있는 콘텐츠 단위이고, 본문 섹션은 글 내부의 하위 주제를 나누는 단위다. 전역 탐색은 사이트 전체 이동을 담당하고, 관련 글은 본문 이해를 보조하지만 본문 자체는 아니다.

반대로 모든 영역을 박스로만 바라보면 다음처럼 의미가 흐려진다.

페이지
  박스
    박스
    박스
  박스
    박스
      텍스트
      텍스트
    박스
  박스

사람은 위치와 스타일을 보고 대략 이해할 수 있지만, 키보드 사용자나 스크린 리더 사용자는 구조적 단서를 잃는다. 검색 엔진도 주요 콘텐츠와 반복 영역을 구분하기 어려워질 수 있다. 시맨틱 마크업은 이 차이를 줄이는 기준선이다.

버튼처럼 보이는 것과 버튼으로 동작하는 것은 다르다

시맨틱 마크업의 문제는 레이아웃보다 상호작용에서 더 자주 드러난다. 아래 예시는 버튼처럼 보이지만 실제 의미 정보가 부족한 UI를 단순화한 것이다.

const weakUi = {
  node: "generic-container",
  text: "저장",
  style: "button-like",
  onClick: "saveForm"
};

이 구조는 마우스로 클릭할 때는 동작할 수 있다. 그러나 키보드 포커스가 가능한지, Enter나 Space로 실행되는지, 보조 기술이 버튼으로 읽는지, 비활성 상태가 전달되는지는 별도로 확인해야 한다.

의미 중심으로 보면 같은 UI는 다음 조건을 만족해야 한다.

const semanticUi = {
  role: "button",
  accessibleName: "저장",
  action: "submit-current-form",
  keyboard: ["Enter", "Space"],
  disabledState: false
};

실제 구현에서는 가능한 한 기본 상호작용 요소를 사용하는 편이 좋다. 기본 요소는 역할, 키보드 동작, 상태 전달의 상당 부분을 브라우저가 이미 처리한다. 커스텀 컴포넌트가 필요하더라도 최종 결과가 사용자에게 어떤 역할과 이름으로 노출되는지 확인해야 한다.

자동 테스트도 이 기준에 맞춰 작성할 수 있다. CSS 클래스나 내부 구현 이름보다 사용자가 인식하는 역할과 이름으로 요소를 찾으면 접근성 문제를 더 빨리 발견할 수 있다.

import { test, expect } from "@playwright/test";

test("저장 동작은 사용자에게 버튼으로 노출된다", async ({ page }) => {
  await page.goto("/settings");

  const saveButton = page.getByRole("button", { name: "저장" });

  await expect(saveButton).toBeVisible();
  await saveButton.press("Enter");
  await expect(page.getByText("설정이 저장되었습니다")).toBeVisible();
});

이 테스트는 버튼이 화면에 있는지만 보지 않는다. 브라우저의 역할 기준으로 찾을 수 있는지, 키보드 입력으로 동작하는지까지 확인한다. 배포 전 품질 게이트로 쓰기에 좋은 형태다.

자동 점검과 CI 품질 게이트를 둔다

시맨틱 마크업은 코드 리뷰만으로 유지하기 어렵다. 페이지가 늘어나고 컴포넌트가 재사용되면 제목 계층, 랜드마크, 버튼 역할, 링크 이름이 쉽게 흔들린다. 그래서 정적 점검과 브라우저 기반 점검을 CI에 함께 넣어 두는 편이 운영 부담을 줄인다.

다음은 접근성 린트와 E2E 테스트를 함께 실행하는 간단한 구성 예시다.

name: quality-gate

on:
  pull_request:
  push:
    branches: [main]

jobs:
  semantic-accessibility:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22

      - run: npm ci
      - run: npm run lint
      - run: npm run test:e2e
      - run: npm run test:smoke

프로젝트마다 도구는 달라질 수 있지만, 역할은 나누는 편이 좋다. 린트는 대체 텍스트 누락, 잘못된 역할 사용, 클릭 이벤트만 가진 비상호작용 요소처럼 정적으로 잡을 수 있는 문제에 적합하다. E2E와 스모크 테스트는 실제 브라우저에서 렌더링된 결과를 기준으로 제목, 탐색, 본문, 키보드 흐름을 확인한다.

명령 구성은 다음처럼 단순하게 시작할 수 있다.

{
  "scripts": {
    "lint": "eslint src",
    "test:e2e": "playwright test",
    "test:smoke": "playwright test tests/smoke"
  }
}

CI가 남겨야 하는 실패 증거도 중요하다. 실패한 URL, 스크린샷, 접근성 리포트, 브라우저 콘솔 로그가 남아야 원인을 좁히기 쉽다. “버튼을 찾지 못했다”보다 “역할이 button이고 이름이 저장인 요소를 찾지 못했다”는 증거가 훨씬 다루기 쉽다.

배포 후 스모크 테스트는 범위를 좁게 잡는 편이 좋다. 대표 제목이 있는지, 주요 탐색과 본문이 역할 기준으로 조회되는지, 핵심 CTA가 키보드로 실행되는지 정도를 빠르게 확인한다. 이 검사는 모든 접근성 문제를 해결해 주지는 않지만, 큰 구조가 깨진 배포를 막는 방어선이 된다.

import { test, expect } from "@playwright/test";

test("문서의 기본 구조가 유지된다", async ({ page }) => {
  await page.goto("/articles/semantic-markup");

  await expect(page.getByRole("heading", {
    name: "시맨틱 마크업은 왜 접근성과 SEO의 출발점이 되는가"
  })).toBeVisible();

  await expect(page.getByRole("navigation")).toBeVisible();
  await expect(page.getByRole("main")).toBeVisible();
});

CSR 환경에서는 초기 HTML과 최종 DOM을 나누어 본다

CSR 환경에서도 시맨틱 마크업은 여전히 중요하다. 사용자가 실제로 조작하는 것은 렌더링 이후의 DOM이고, 스크린 리더와 브라우저 자동화 도구도 이 결과를 기준으로 페이지를 해석하기 때문이다.

다만 SEO 관점에서는 한 가지를 더 나누어 봐야 한다. 검색 엔진이나 소셜 미리보기 수집기, 사내 검색 인덱서, 모니터링 도구가 JavaScript 실행 전의 초기 HTML만 읽을 수 있기 때문이다. Google처럼 JavaScript 렌더링을 처리하는 검색 엔진도 있지만, 모든 크롤러가 같은 수준의 렌더링을 보장하지는 않는다.

따라서 CSR 페이지는 두 질문을 분리해서 점검하는 편이 좋다.

  • 최종 DOM: 사용자가 보는 화면에 제목, 본문, 탐색, 버튼과 링크의 의미가 남아 있는가?
  • 초기 HTML: 공개 콘텐츠의 제목, 설명, 주요 본문, 링크가 JavaScript 실행 전에도 어느 정도 읽히는가?
  • 메타 정보: 검색 결과와 공유 미리보기에 필요한 제목, 설명, 대표 이미지 정보가 서버 응답에 포함되는가?
  • 콘텐츠 성격: 로그인 뒤 대시보드인지, 공개 문서인지, 상품 상세인지에 따라 SEO 기준을 다르게 적용하는가?

로그인 뒤에만 보이는 대시보드는 SEO보다 접근성과 키보드 탐색 기준을 우선해도 된다. 반면 공개 문서, 블로그, 상품 상세, 검색 노출이 필요한 목록 페이지는 초기 응답에 핵심 콘텐츠가 비어 있지 않은지 확인해야 한다. 필요하다면 SSR, SSG, 프리렌더링 같은 전략을 함께 검토할 수 있다.

운영 중 자주 깨지는 지점을 미리 본다

시맨틱 구조는 처음 작성할 때보다 변경 과정에서 더 자주 무너진다. 디자인 시스템을 도입하면서 모든 텍스트가 같은 컴포넌트로 감싸지거나, 라우팅을 바꾸면서 대표 제목이 사라지거나, 모달을 추가하면서 포커스 흐름이 끊기는 식이다.

주의할 만한 지점은 다음과 같다.

  1. 제목 수준을 글자 크기 기준으로 고르는 경우
  2. 링크와 버튼을 시각적 스타일만으로 구분하는 경우
  3. 일반 컨테이너에 클릭 이벤트만 붙여 상호작용을 만드는 경우
  4. ARIA 속성으로 기본 HTML 의미를 과하게 대체하는 경우
  5. CSR 전환 후 초기 HTML에서 공개 콘텐츠가 비어 버리는 경우

ARIA는 의미를 보충하는 도구이지 기본 요소의 의미를 매번 대체하는 수단은 아니다. 기본 요소로 해결할 수 있는 상호작용을 직접 재구현하면 키보드 이벤트, 상태 전달, 접근성 이름, 포커스 관리를 모두 책임져야 한다. 복잡도가 높아질수록 누락 가능성도 커진다.

운영 기준은 너무 거창할 필요가 없다. 공개 페이지에는 대표 제목과 주요 콘텐츠 영역이 있어야 한다. 전역 탐색은 역할 기준으로 탐색 가능해야 한다. 클릭 가능한 요소는 키보드로도 조작 가능해야 한다. 배포 전에는 정적 검사와 최소 스모크 테스트를 통과해야 한다. 이 정도만 품질 게이트로 두어도 반복되는 실수를 상당히 줄일 수 있다.

시맨틱 마크업은 코드 컨벤션보다 사용자와 문서 사이의 계약에 가깝다. 눈으로 보는 사용자, 키보드로 탐색하는 사용자, 스크린 리더로 듣는 사용자, 문서를 수집하는 검색 엔진은 같은 페이지를 서로 다른 방식으로 읽는다. 그 차이를 견디려면 시각적 배치만이 아니라 의미 구조를 함께 남겨야 한다.

원문 참고

https://www.maeil-mail.kr/question/59

점검 매트릭스

시맨틱 마크업의
시맨틱 마크업의 핵심은 태그 이름이 아니라 콘텐츠의 역할을 HTML 구조에 드러내는 것이다.
접근성에서는 스크린
접근성에서는 스크린 리더와 키보드 탐색이 페이지 구조를 이해하는 기준이 된다.
SEO에서는 제목
SEO에서는 제목 구조, 주요 콘텐츠 영역, 링크 구조가 검색 엔진의 문서 이해를 돕는다.
CSR에서도 최종
CSR에서도 최종 렌더링된 DOM의 의미 구조는 중요하지만, 초기 HTML과 서버 렌더링 전략까지 함께 봐야 한다.

댓글

이 블로그의 인기 게시물

아이콘 폰트 (icomoon 사용법)

 장난감 프로젝트를 만들다 보면, 아이콘이 필요한 경우가 있다. 간단하게 아이콘을 인터넷에서 검색하여, 이미지로 넣어두고 이미지 태그를 이용하여, 사용하는 경우가 일반적이였지만...  요즘에는 대부분 폰트를 이용하여 아이콘을 노출 한다. 나 같은 경우에도 기본적으로  https://material.io/resources/icons 를 참고하여 아이콘 폰트를 이용할 수 있도록 처리하고, 추가적으로 필요한 아이콘이고, 일상적으로 사용 되지 않는 아이콘의 경우에는  https://icomoon.io 에서 제작하여, 아이콘 폰트로 이용 하곤 한다.  그래서 이번에는 아이콘  https://icomoon.io 의 사용법을 간단히 공유하고자 한다.   들어가자 마자 위의 icoMoonApp버튼을 누르면 아래와 같은 화면이 나타난다.  icomoon에서 무료로 제공하는 아이콘들이 보이면 위에 파란색으로 표시 되어있는 집 모양 세가지를 선택한 후, 아래의 빨간색으로 표시되어있는 Generate Font를 눌러보자.  그리고 나서 바로 다운로드를 요청해보자. icomoon.zip이 다운로드가 될텐데, 압축을 해제해 보면, 아래의 폴더 및 파일들이 있다. 아래에서 중요한 것은 font 폴더와 style.css이다. demo-files fonts demo.html Read Me.txt selection.json style.css <!doctype html > <html> <head> <link rel ="stylesheet" href ="style.css" ></head> </head> <body> <span class ="icon-home" ></span> <span class ="icon-home2" ></span> <span class ="icon-hom...

Chart js와 amchart 비교

Chart js 특징은 위의 그림으로 대체 할 수 있을 듯 하다. 오픈 소스이고, 기본으로 제공하는 차트 종류가 8가지 Canv a s를 이용해서 차트를 그리고, 반응형을 지원한다. amchart amchart는 기본적으로 유료이며, 기본으로 제공하는 차트 종류가 기본적인 차트 + 주식 처럼 보이는 차트 + 지도에 관련된 차트(?) 까지 하면, 기본 제공 하는 종류가 20개 내외 이려나, 일일이 세기에는 양이 좀 많아 보인다. 렌더링은 svg를 통하여 그려지고, 당연 반응형도 지원이 된다. 그러면, 이 둘중에 어떤것이 내 프로젝트에 적합 하냐는 것이 문제이다. 일단, 주식 처럼 보이는 차트나 지도에 관련된 차트(?)가 필요하면, amchart를 선택해야 되는 것은 맞다. 그건 당연한 것이니 빼고 얘기 해보자! 여러 종류의 차트가 필요하다면, 일단은 amchart를 염두해 두는 것이 좋다. 돈 낸 만큼은 하는 듯 하다. 하지만, 기본적인 막대 그래프, 도넛 차트 등, 아주 기본적인 차트들인데, Chart js도 amchart도 그러한 차트가 없을 때가 문제가 된다. 그렇다면, 조금이라도 커스텀이 용이한 것을 찾는 것이 좋을 것이다.  일단 amchart에서 custom이라고 검색 하였을 때, 검색 결과가 61가지가 나온다. 차트의 종류도 많고, 각 차트마다 들어가는 속성이 매우 많기 때문에, 웬만한 내용들은 속성 값을 어떻게 주느냐에 따라서 변경이 가능 하게 된다. 커스텀의 예를 들면, 기본적으로 도넛 파이의 형태를 띄면서, 화살표로 목표를 표시해주는 차트가 필요하다고 생각 해보자. 이것은 amchart로 만든 그래프이고 이것은 chart js로 만든 그래프이다. 모양이 살짝 다르긴 하지만, 완벽하게 똑같이 구현 할 수도 있다. amchart로 만든 그래프의 경우, 저것은 도넛그래프가 아닌 guage 그래프이다. 원래 게이지 그래프는 이와 같...

javascript 압축 파일 다운로드

이번에는 전 게시글의 응용판? 이라고 해야하나....? 어쨋든! 우리는 각각의 파일들을 다운로드 해보았다. 그런데 생각보다 귀찮음?을 느꼇을 것이다. 파일을 각각 다운 받아야 한다는 현실때문에! 그래 파일 두개야 뭐 그렇다 치지... 하지만, 개발자도 사용자도 게으름뱅이이다. 자 결국, 우리가 해야 하는 것은 파일을 한 번에 둘다! 다운 받는 것이다. 물론, 클릭 한번에 여러개의 함수를 엮어서 다운받게 하면 되지만! 크롬에서 자주 봤듯이, 여러개의 파일을 다운로드를 시도하면 <- 여러개의 파일을 다운로드 합니다. 허용 합니까? 하고 물어보는 것을 볼 수 있다. 게다가 다운로드 한 파일들을 찾기도 귀찮다는 것. 자 해결책을 제시해보자면, https://github.com/Stuk/jszip 클라이언트 단에서 파일을 zip파일로 압축을 할 수가 있다! 필요한 작업은 아래와 같다. 0. 데이터 준비 1. BLOB(binary large object)를 만든다. 2. Blob을 URL.createObjectURL을 사용하여, 해당 binary의 주소를 생성. 3. 다운로드가 필요한 파일들을 Zip 객체에 셋팅! 4. a태그를 이용하여, 해당 url 셋팅 하고, 다운로드. 전 게시물과 별로 달라진게 없네... 자 그럼 샘플! 샘플을 보자! http://embed.plnkr.co/NMprnRxqYG0fkHa2J55D/ var util = {} function fixBinary(bin) { //binary to arrayBuffer var length = bin.length var buf = new ArrayBuffer(length) var arr = new Uint8Array(buf) for (var i = 0; i < length; i++) { arr[i] = bin.charCodeAt(i) } return buf } window.onload = function() { ...