HTML — 시맨틱 태그, 접근성, 브라우저 렌더링까지
HTML은 "쉬운 거 아닌가요?" 하고 넘기다가 깊이 들어가면 헷갈리는 대표적인 주제다. 시맨틱 태그부터 브라우저 렌더링 파이프라인, 접근성까지 한 번에 정리해봤다.
HTML5 시맨틱 태그
div 떡칠이 왜 문제인가
div는 의미가 없다. 아무 의미도 없는 상자를 잔뜩 쌓아놓으면 사람이야 클래스명을 보고 "아, 여기가 네비게이션이구나" 할 수 있지만, 기계는 못 한다. 검색 엔진 크롤러, 스크린 리더 전부 그냥 "상자"로 인식할 뿐이다.
<!-- 안 좋은 예 -->
<div class="header">
<div class="nav">...</div>
</div>
<div class="content">...</div>
<div class="footer">...</div>
<!-- 좋은 예 -->
<header>
<nav>...</nav>
</header>
<main>...</main>
<footer>...</footer>
둘 다 화면에 렌더링되는 결과물은 똑같다. 하지만 두 번째 코드는 브라우저가 접근성 트리(Accessibility Tree)를 구성할 때 각 영역의 역할을 자동으로 부여한다.
주요 시맨틱 태그 정리
| 태그 | 역할 | 비고 |
|---|---|---|
header | 페이지 또는 섹션의 머리글 | 로고, 메인 내비게이션 |
nav | 내비게이션 링크 모음 | 주요 이동 경로 |
main | 문서의 핵심 콘텐츠 | 페이지당 하나만 |
section | 주제별 콘텐츠 그룹 | 보통 제목(h2~h6)을 포함 |
article | 독립적으로 배포·재사용 가능한 콘텐츠 | 블로그 글, 뉴스 기사, 댓글 |
aside | 본문 보조 콘텐츠 | 사이드바, 관련 링크, 광고 |
footer | 페이지 또는 섹션의 맺음말 | 저작권, 연락처 |
section과 article을 헷갈리는 경우가 많은데 — article은 그 자체만 떼어내서 다른 사이트에 옮겨도 의미가 통하는 단위이고, section은 문서 내부를 주제로 나누는 용도다. 블로그 글 전체가 article이라면, 그 안의 "1. 개요", "2. 본론" 같은 구분이 section인 셈.
시맨틱 마크업과 SEO
시맨틱 태그를 쓴다고 검색 순위가 즉시 올라가진 않는다. 하지만 구글 크롤러(Googlebot)가 페이지 구조를 파악하는 데 확실히 유리하다.
영향을 주는 부분:
main태그 안의 콘텐츠를 핵심 본문으로 인식한다.nav를 통해 사이트 구조를 이해한다.article안에time태그의datetime속성이 있으면 게시 날짜를 파악한다.h1~h6계층 구조가 잘 잡혀 있으면 주제 파악이 쉬워진다.
구글은 공식적으로 "시맨틱 태그가 직접적인 랭킹 시그널은 아니다"라고 말하지만, 간접적으로 크롤링 효율성과 콘텐츠 이해도를 높여준다. 그리고 구조가 깔끔하면 리치 스니펫(Rich Snippet)에 노출될 확률도 올라간다.
웹 접근성 (a11y)
접근성(Accessibility)을 줄여서 a11y라고 쓴다. a와 y 사이에 11글자가 있어서. 웹 접근성은 장애가 있는 사용자도 웹 콘텐츠를 동등하게 이용할 수 있도록 보장하는 것이다.
ARIA 속성
시맨틱 태그만으로 역할을 표현하기 어려울 때 ARIA(Accessible Rich Internet Applications) 속성을 사용한다.
<!-- 탭 UI 예시 -->
<div role="tablist">
<button role="tab" aria-selected="true" aria-controls="panel-1">탭 1</button>
<button role="tab" aria-selected="false" aria-controls="panel-2">탭 2</button>
</div>
<div id="panel-1" role="tabpanel" aria-labelledby="tab-1">내용 1</div>
<div id="panel-2" role="tabpanel" aria-labelledby="tab-2" hidden>내용 2</div>
주요 ARIA 속성들:
role— 요소의 역할을 명시 (예:button,dialog,alert)aria-label— 눈에 보이는 텍스트가 없을 때 레이블 제공aria-labelledby— 다른 요소의 텍스트를 레이블로 참조aria-hidden="true"— 스크린 리더에서 숨김 처리aria-live— 동적으로 변경되는 영역 알림 (polite,assertive)aria-expanded— 드롭다운, 아코디언 같은 UI의 열림/닫힘 상태
한 가지 주의할 건, 네이티브 시맨틱 태그로 해결 가능하면 ARIA를 쓰지 않는 게 원칙이다. <button>을 놔두고 <div role="button">을 쓰는 건 안티패턴이다.
alt 텍스트
<!-- 정보를 전달하는 이미지 -->
<img src="chart.png" alt="2025년 매출 추이: 1분기 120억, 2분기 180억">
<!-- 순수 장식 이미지 -->
<img src="decoration.png" alt="">
alt가 빈 문자열이면 스크린 리더가 해당 이미지를 건너뛴다. 장식용 이미지에 의미 있는 alt를 넣으면 오히려 방해가 된다.
키보드 네비게이션
마우스를 사용할 수 없는 사용자를 위해 모든 인터랙티브 요소는 키보드로 접근 가능해야 한다.
Tab으로 포커스 이동,Shift+Tab으로 역순 이동Enter/Space로 버튼 클릭, 링크 이동tabindex="0"— 포커스 순서에 포함tabindex="-1"— 프로그래밍적으로만 포커스 가능 (Tab 순서에서는 제외)tabindex="1"이상 — 자연스러운 DOM 순서를 깨뜨리므로 쓰지 말 것
outline: none으로 포커스 링을 없애는 건 접근성을 망가뜨리는 대표적인 실수다. 커스텀 포커스 스타일을 대신 적용하자.
스크린 리더
스크린 리더는 DOM 트리가 아니라 접근성 트리(Accessibility Tree) 를 읽는다. 브라우저가 DOM을 기반으로 접근성 트리를 별도로 구성하는데, 시맨틱 태그와 ARIA 속성이 이 트리의 품질을 결정한다.
시맨틱 태그 → 브라우저가 자동으로 역할(role)과 이름(accessible name)을 부여 → 스크린 리더가 "탐색 영역", "주요 콘텐츠", "기사" 같은 랜드마크를 인식 → 사용자가 섹션 간 빠르게 이동 가능.
메타 태그
<head> 안에 위치하며 문서 자체에 대한 정보를 제공한다. 화면에 직접 보이진 않지만 브라우저, 검색 엔진, SNS 크롤러에 큰 영향을 미친다.
필수급 메타 태그
<!-- 문자 인코딩 -->
<meta charset="UTF-8">
<!-- 반응형 뷰포트 설정 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 페이지 설명: 검색 결과 미리보기에 노출됨 -->
<meta name="description" content="HTML5 시맨틱 태그와 브라우저 렌더링 정리">
charset을 생략하면 브라우저가 인코딩을 추측하게 되는데, 추측이 틀리면 한글이 깨진다. 반드시 선언하자.
viewport 메타 태그가 없으면 모바일 브라우저가 데스크톱 너비(보통 980px)로 렌더링한 뒤 축소해서 보여준다.
Open Graph 태그
카카오톡이나 슬랙에 링크를 공유했을 때 뜨는 미리보기 카드, 그게 OG 태그에서 가져오는 정보다.
<meta property="og:title" content="HTML 핵심 정리">
<meta property="og:description" content="시맨틱 태그부터 브라우저 렌더링까지">
<meta property="og:image" content="https://example.com/thumbnail.png">
<meta property="og:url" content="https://example.com/html-interview">
<meta property="og:type" content="article">
트위터는 별도로 twitter:card, twitter:title 같은 메타 태그를 사용하기도 한다.
브라우저 렌더링 과정
URL을 입력하고 화면이 뜨기까지, 브라우저가 하는 일을 순서대로 정리하면 다음과 같다.
1. DOM 트리 구성
HTML 바이트를 받아서 토큰화 → 노드 생성 → DOM 트리 구성. 파서가 위에서 아래로 한 줄씩 읽어 내려간다.
2. CSSOM 트리 구성
CSS 파일과 <style> 태그를 파싱해서 CSSOM(CSS Object Model) 트리를 만든다. CSS는 자식이 부모의 스타일을 상속하기 때문에 트리 구조가 필요하다.
3. Render Tree 생성
DOM과 CSSOM을 결합해서 Render Tree를 만든다. 여기서 중요한 건 — display: none인 요소는 Render Tree에 포함되지 않는다. 반면 visibility: hidden은 Render Tree에 포함되지만 화면에 보이지 않을 뿐이다. 자주 헷갈리는 차이점이니 기억해두자.
4. Layout (Reflow)
Render Tree의 각 노드가 화면에서 차지할 정확한 위치와 크기를 계산한다. 뷰포트 크기에 따라 %, em, vh 같은 상대 단위가 실제 픽셀 값으로 변환되는 단계.
5. Paint
Layout 단계에서 계산된 결과를 바탕으로 실제 픽셀을 채운다. 배경색, 테두리, 텍스트, 그림자 같은 시각적 요소를 레이어별로 그린다.
6. Composite
여러 레이어를 합성해서 최종 화면을 만든다. GPU 가속이 적용되는 단계이며, transform이나 opacity 같은 속성은 별도 레이어에서 처리되기 때문에 성능상 유리하다.
Reflow vs Repaint
어떤 CSS 속성이 성능에 안 좋을까? 이 개념을 알면 답할 수 있다.
Reflow
요소의 크기나 위치가 바뀌면 Layout 단계부터 다시 수행된다. 비용이 크다.
Reflow를 유발하는 것들:
width,height,margin,padding,border변경font-size변경- DOM 노드 추가/삭제
offsetWidth,clientHeight같은 레이아웃 속성 읽기 (강제 동기 레이아웃)- 윈도우 리사이즈
Repaint
색상이나 배경만 바뀌면 Layout을 건너뛰고 Paint부터 다시 수행한다. Reflow보다 가볍다.
Repaint만 유발하는 것들:
color,background-color,visibility,outline변경
성능 최적화 팁
transform과 opacity는 Composite 단계에서만 처리된다. Reflow도 Repaint도 일어나지 않는다. 그래서 애니메이션을 구현할 때 top/left 대신 transform: translate()를 쓰라는 거다.
/* 나쁜 예: reflow 발생 */
.box {
transition: left 0.3s;
}
/* 좋은 예: composite만 발생 */
.box {
transition: transform 0.3s;
}
또 하나, JavaScript에서 DOM을 여러 번 변경하면 매번 reflow가 생길 수 있는데, DocumentFragment나 requestAnimationFrame을 사용하면 배치 처리가 가능하다.
script 태그: async vs defer
HTML 파서가 <script> 태그를 만나면 기본적으로 파싱을 멈추고 스크립트를 다운로드한 뒤 실행한다. 이걸 파서 블로킹(Parser Blocking) 이라고 한다.
| 속성 | 다운로드 | 실행 시점 | 실행 순서 보장 |
|---|---|---|---|
| 없음 | 파싱 중단 후 다운로드 | 즉시 실행 | O |
async | 파싱과 병렬 다운로드 | 다운로드 완료 즉시 (파싱 중단) | X |
defer | 파싱과 병렬 다운로드 | HTML 파싱 완료 후 (DOMContentLoaded 직전) | O |
정리하자면:
async— 다른 스크립트에 의존하지 않는 독립적인 스크립트에 적합하다. Google Analytics 같은 것.defer— DOM에 접근해야 하지만 렌더링을 막고 싶지 않을 때. 실행 순서도 보장된다.- 속성 없음 —
<body>맨 끝에 놓거나, 위에 두더라도 파싱 블로킹을 감수해야 한다.
type="module"인 스크립트는 기본적으로 defer처럼 동작한다는 것도 알아두면 좋다.
심화 질문
DOCTYPE의 역할
<!DOCTYPE html>
이 선언이 없으면 브라우저가 쿼크 모드(Quirks Mode) 로 렌더링한다. 쿼크 모드에서는 IE5 시절의 비표준 박스 모델이 적용되고, CSS 해석 방식이 달라진다. <!DOCTYPE html>을 선언하면 표준 모드(Standards Mode) 로 동작하며 W3C 명세에 따라 렌더링한다.
HTML5에서는 <!DOCTYPE html> 이 한 줄이면 충분하다. 이전 HTML4나 XHTML은 DTD를 명시해야 했지만.
data-* 속성
커스텀 데이터를 HTML 요소에 저장할 수 있는 표준 방법이다.
<li data-user-id="42" data-role="admin">홍길동</li>
JavaScript에서는 element.dataset.userId로 접근한다. 하이픈으로 연결된 이름이 카멜케이스로 변환되는 것에 주의.
주의점 — data-* 속성에 민감한 정보를 넣으면 안 된다. DevTools에서 누구나 볼 수 있으니까. 그리고 대량의 데이터를 저장하는 용도로도 적합하지 않다. 그런 건 JavaScript 변수나 상태 관리 라이브러리를 쓰자.
Web Storage vs Cookie
| 항목 | Cookie | localStorage | sessionStorage |
|---|---|---|---|
| 용량 | ~4KB | ~5-10MB | ~5-10MB |
| 만료 | 설정 가능 (Expires, Max-Age) | 영구 (직접 삭제 전까지) | 탭/창 닫으면 삭제 |
| 서버 전송 | 매 요청마다 자동 전송 | 전송 안 됨 | 전송 안 됨 |
| 접근 범위 | 도메인 + 경로 기준 | 같은 출처(Origin) | 같은 출처 + 같은 탭 |
Cookie는 서버에 자동으로 전달되기 때문에 인증 토큰 저장에 쓰이고, HttpOnly 플래그를 설정하면 JavaScript에서 접근이 불가능해 XSS 공격에 상대적으로 안전하다.
localStorage는 새로고침이나 브라우저 종료 후에도 데이터가 유지되므로, 사용자 설정이나 캐시용으로 적합하다.
sessionStorage는 같은 URL을 새 탭에서 열면 별도의 저장소가 생성된다. 탭 간 데이터가 공유되지 않는다는 점이 localStorage와의 핵심 차이.
Web Component
네이티브 브라우저 API로 재사용 가능한 커스텀 엘리먼트를 만드는 기술이다. 프레임워크 없이도 컴포넌트 기반 개발이 가능하다.
세 가지 핵심 기술로 구성된다:
- Custom Elements —
customElements.define()으로<my-button>같은 커스텀 태그를 등록 - Shadow DOM — 컴포넌트 내부의 DOM과 스타일을 외부로부터 캡슐화
- HTML Templates —
<template>과<slot>으로 재사용 가능한 마크업 구조 정의
리액트나 뷰 같은 프레임워크의 컴포넌트와 뭐가 다르냐는 질문이 나올 수 있다. Web Component는 브라우저 표준이라 프레임워크에 의존하지 않고, Shadow DOM을 통해 진짜 스타일 격리가 된다는 점이 다르다. 다만 상태 관리나 반응형 렌더링 같은 건 직접 구현해야 해서 프레임워크만큼 편하진 않다.
파생 개념
이 글에서 다룬 내용은 다른 주제로 자연스럽게 이어진다.
- CSS — 캐스케이딩, 셀렉터 우선순위, 박스 모델, Flexbox/Grid, 반응형 디자인, CSS-in-JS
- JavaScript — 이벤트 루프, 프로토타입 체인, 클로저, ES6+ 문법, 모듈 시스템
- 웹 성능 최적화 — Critical Rendering Path, 코드 스플리팅, 레이지 로딩, 이미지 최적화, Core Web Vitals (LCP, FID, CLS)
HTML만 단편적으로 아는 것보다 연결 지어 이해하는 게 중요하다. "시맨틱 태그를 쓰면 접근성 트리가 개선되고, 접근성 트리가 좋으면 스크린 리더 사용자 경험이 나아지며, SEO에도 간접적으로 도움이 됩니다" — 이렇게 전체 그림을 그릴 수 있으면 된다.
주의할 점
1. div로 모든 것을 감싸면 SEO와 접근성이 모두 나빠진다
<div>는 의미가 없는 컨테이너다. <nav>, <article>, <aside> 등 시맨틱 태그를 사용해야 검색 엔진과 스크린 리더가 페이지 구조를 이해할 수 있다.
2. heading 레벨을 건너뛰면 문서 구조가 깨진다
<h1> 다음에 <h3>를 쓰면 <h2>가 빠진 것이므로 접근성 도구에서 경고가 발생한다.