HTML 기본 태그 | 텍스트, 링크, 이미지, 리스트
이 글의 핵심
태그 이름만 외우면 div만으로도 화면은 만들 수 있지만, 브라우저와 검색 엔진, 스크린 리더는 태그의 의미를 읽습니다. 이 글은 HTML이 토큰화와 트리 구축을 거쳐 DOM이 되는 과정을 먼저 짚고, 시맨틱 태그를 썼을 때 접근성과 검색에 어떤 차이가 생기는지 설명한 뒤 텍스트, 링크, 리스트, 테이블 태그를 차례로 다룹니다.
시리즈 안내
#02 | 📋 전체 목차 | 이전: #01 시작하기 · 다음: #03 CSS 기초
들어가며
HTML 태그는 브라우저와 검색 엔진이 읽을 구조와 의미를 표시합니다. 이후 박스 모델에서 다루듯, 대부분의 요소는 화면에서 상자로 그려집니다.
심화: HTML 파싱, DOM 구축, 시맨틱 마크업의 실제 효과
태그 문법을 넘어서면, 브라우저는 HTML을 문자열이 아니라 토큰 스트림과 트리 연산으로 처리합니다. 이 관점이 있어야 “왜 이렇게 써야 하는지”, “왜 이 버그가 생기는지”가 설명됩니다.
파싱 파이프라인: 인코딩, 토큰화, 트리 구성
-
바이트 스트림과 인코딩
네트워크로 도착한 바이트는 먼저 문자 인코딩(UTF-8 등)이 확정되어 코드 포인트 시퀀스로 변환됩니다.<meta charset>이 늦게 오거나 잘못되면 이미 파싱된 앞부분과 뒷부분의 디코딩이 어긋나 깨진 글자·잘못된 태그 인식으로 이어질 수 있으므로, 문서 앞에 올바른 선언을 두는 것이 구조적으로 중요합니다. -
토큰화(Tokenization)
HTML 파서는 입력을 읽으며 시작 태그, 종료 태그, 문자, 주석, DOCTYPE 등의 토큰을 생성합니다. 여기서 HTML은 XML과 달리 오류에 관대합니다. 닫히지 않은 태그, 잘못된 중첩 등은 HTML 명세의 “파싱 알고리즘”에 정의된 복구 규칙으로 보정됩니다. 개발자가 “브라우저가 알아서 고쳐 주겠지”라고 느끼는 현상의 대부분은 이 단계에서 의도와 다른 트리가 만들어진 결과입니다. -
트리 구성(Tree construction)
토큰은 열린 요소 스택(open elements stack)과 활성 포맷팅 요소 목록 등의 상태를 갖고 DOM 트리로 반영됩니다. 예를 들어<p>안에 블록 레벨 요소가 들어오면 명세에 따라p가 먼저 닫히는 식으로 트리가 재구성됩니다. 따라서 검증된 마크업은 렌더링 결과의 안정성과 디버깅 비용을 동시에 줄입니다.
DOM(Document Object Model)이 갖는 의미
DOM은 HTML 문서의 객체 표현이며, JavaScript의 document API와 CSS 선택자가 모두 이 트리를 대상으로 동작합니다. 노드 타입(요소, 텍스트, 주석 등), 부모·자식·형제 관계, 속성(attribute)과 프로퍼티(property)의 대응은 JavaScript DOM에서 조작할 때 그대로 체감됩니다.
- 점진적 파싱: 문서 앞부분부터 순서대로 DOM에 반영되므로, 상단의 레이아웃·스크립트가 하단 렌더링에 영향을 줄 수 있습니다.
<script>와 파싱 차단: 클래식 동기 스크립트(기본script)는 파싱을 일시 중지하고 스크립트를 가져와 실행한 뒤 파싱을 재개합니다. 따라서 스크립트가 DOM에서 아래에 있는 요소를 찾지 못하는 타이밍 이슈가 생깁니다.defer(파싱 후 순서 실행),async(병렬 로드·실행),type="module"(기본 defer 유사) 등으로 실행 시점을 설계해야 합니다.innerHTML등으로 삽입한 문자열은 별도의 HTML 파서 경로를 타며, 기존에 손으로 쓴 마크업과 동일한 복구 규칙이 적용됩니다. XSS 방지를 위해 신뢰할 수 없는 문자열을 그대로 넣으면 안 됩니다.
시맨틱 HTML이 엔진·접근성·검색에 주는 영향
시맨틱 태그(<main>, <nav>, <article>, <section> 등)는 시각적으로는 div와 비슷해 보일 수 있으나, 의미 레이어를 분리합니다.
- 접근성(Accessibility): 보조 기술은 랜드마크 역할, 제목 계층(
h1–h6), 목록·폼 레이블 등을 통해 문서를 순회할 경로를 얻습니다. 의미 없는div만 겹치면 “어디가 본문인지”를 사용자가 구조적으로 파악하기 어렵습니다. - SEO: 검색 엔진은 제목·본문·날짜·작성자 등의 힌트를 마크업에서 추출합니다.
article+time, 적절한h1구조는 콘텐츠의 주제와 범위를 기계가 읽기 쉽게 만듭니다(구체적인 랭킹 알고리즘은 공개되지 않지만, 명확한 구조는 크롤링·인덱싱 품질에 유리하다는 점은 업계 관행입니다). - 유지보수와 스타일링: 의미 단위로 나뉜 DOM은 CSS 선택자와 디자인 시스템 컴포넌트 경계와 맞추기 쉽으며, E2E 테스트에서 역할 기반 쿼리(
getByRole)와도 잘 맞습니다.
정리하면, HTML은 “보이는 모양”을 적는 언어가 아니라 브라우저가 트리를 만들고, 스크립트·스타일·보조 기술이 그 트리에 반응하게 하는 계약에 가깝습니다. 태그를 고를 때마다 “이 노드가 DOM에서 어떤 부모 밑에 붙으며, 스크립트 실행 순서와 시맨틱 레이어에 어떤 영향을 주는가”를 함께 떠올리면 설계 수준이 한 단계 올라갑니다.
제목·문단·강조 텍스트 태그
제목 태그
<h1>가장 큰 제목 (페이지당 1개 권장)</h1>
<h2>두 번째 제목</h2>
<h3>세 번째 제목</h3>
<h4>네 번째 제목</h4>
<h5>다섯 번째 제목</h5>
<h6>가장 작은 제목</h6>
중요: <h1>은 페이지의 주제를 나타내는 제목이므로 보통 하나만 둡니다.
“h1은 페이지당 하나”는 흔히 SEO 규칙으로 소개되지만, 실제로 더 중요한 이유는 문서 구조입니다. 스크린 리더 사용자는 제목 목록을 불러와 원하는 부분으로 건너뛰는 방식으로 페이지를 훑는데, 제목 계층이 h1 → h3처럼 건너뛰거나 글자 크기를 키우려고 h2를 쓰면 이 목차가 엉망이 됩니다. 제목 태그는 크기가 아니라 계층을 표시하는 것이므로, 글자를 키우고 싶다면 CSS로 하고 태그는 내용의 깊이에 맞춰 골라야 합니다. 구글 역시 h1이 여러 개인 페이지를 문제 삼지 않는다고 밝힌 바 있으므로, 개수보다 계층이 논리적인지가 핵심입니다.
문단과 줄바꿈
<p>문단입니다. 자동으로 위아래 여백이 생깁니다.</p>
<p>또 다른 문단입니다.</p>
<!-- 줄바꿈 -->
<p>첫 번째 줄<br>두 번째 줄</p>
<!-- 수평선 -->
<hr>
텍스트 강조
<!-- 굵게 -->
<strong>중요한 내용</strong>
<b>단순히 굵게</b>
<!-- 기울임 -->
<em>강조</em>
<i>단순히 기울임</i>
<!-- 밑줄 -->
<u>밑줄</u>
<!-- 취소선 -->
<s>취소선</s>
<!-- 작은 글씨 -->
<small>작은 텍스트</small>
<!-- 형광펜 효과 -->
<mark>하이라이트</mark>
<br>은 주소나 시처럼 줄바꿈 자체가 내용인 경우에만 씁니다. 문단 사이 간격을 만들려고 <br><br>를 반복하면, 스크린 리더는 이를 한 문단으로 읽고 CSS로 간격을 조절할 수도 없게 됩니다. 간격은 <p>를 나누고 margin으로 조절하는 것이 맞습니다.
<strong>과 <b>, <em>과 <i>는 화면에서 똑같이 보이지만 의미가 다릅니다. <strong>은 “중요하다”, <em>은 “이 단어에 강세를 두어 읽어라”는 뜻이라 문장의 의미를 바꾸고, <b>와 <i>는 제품명이나 외래어처럼 관례상 다르게 표시할 뿐 중요도를 더하지 않습니다. <u>도 “밑줄 긋기”가 아니라 맞춤법 오류 표시 같은 주석을 뜻하며, 웹에서 밑줄은 링크로 오해받기 쉬우므로 강조 목적으로는 피하는 것이 좋습니다. 삭제된 내용을 편집 이력으로 표시할 때는 <s>보다 <del>(삭제)과 <ins>(추가)가 의미상 더 정확합니다.
링크 만들기
기본 링크
<!-- 외부 링크 -->
<a href="https://google.com">구글로 이동</a>
<!-- 새 탭에서 열기 -->
<a href="https://google.com" target="_blank">새 탭에서 열기</a>
<!-- 같은 페이지 내 이동 -->
<a href="#section1">섹션 1로 이동</a>
<h2 id="section1">섹션 1</h2>
<!-- 이메일 링크 -->
<a href="mailto:[email protected]">이메일 보내기</a>
<!-- 전화 링크 -->
<a href="tel:010-1234-5678">전화 걸기</a>
href에 쓰는 경로는 세 종류로 구분됩니다. https://로 시작하는 절대 URL, /about처럼 사이트 루트 기준 경로, about.html이나 ../index.html처럼 현재 문서 위치 기준 상대 경로입니다. 로컬 파일로 열 때는 잘 되던 링크가 서버에 올리면 깨지는 경우는 대개 이 기준을 헷갈린 것입니다. 루트 기준 경로(/about)는 file://로 연 페이지에서는 드라이브 루트를 가리키므로 로컬에서만 깨지고, 반대로 하위 폴더로 배포하면 상대 경로가 어긋날 수 있습니다.
target="_blank"는 예전에는 반드시 rel="noopener noreferrer"와 함께 써야 했습니다. 새 탭의 페이지가 window.opener로 원래 탭을 조작해 피싱 사이트로 바꿔치기할 수 있었기 때문입니다. 현재 주요 브라우저는 target="_blank"에 noopener를 기본 적용하지만, 오래된 브라우저를 고려하거나 의도를 명확히 하려면 여전히 붙여 두는 편이 안전합니다. 또 모든 외부 링크를 새 탭으로 여는 것은 사용자의 뒤로 가기 흐름을 끊으므로, 작성 중인 폼을 잃을 수 있는 경우처럼 이유가 있을 때만 쓰는 것이 좋습니다. 링크 텍스트로 “여기를 클릭”을 쓰면 링크 목록만 훑는 스크린 리더 사용자는 무엇으로 가는 링크인지 알 수 없으므로, 목적지를 설명하는 문장을 링크로 만드는 것이 좋습니다.
링크 스타일링
<style>
a {
color: #3498db;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>
이미지와 figure
기본 이미지
<!-- 기본 -->
<img src="image.jpg" alt="이미지 설명">
<!-- 크기 지정 -->
<img src="image.jpg" alt="이미지 설명" width="300" height="200">
<!-- 반응형 이미지 -->
<img src="image.jpg" alt="이미지 설명" style="max-width: 100%; height: auto;">
이미지 링크
<a href="https://example.com">
<img src="logo.png" alt="로고">
</a>
Figure와 Figcaption
<figure>
<img src="photo.jpg" alt="사진">
<figcaption>사진 설명</figcaption>
</figure>
width와 height 속성은 이미지를 늘리거나 줄이는 용도보다 자리 예약 용도가 더 중요합니다. 브라우저는 이미지 파일을 받기 전에는 크기를 모르므로, 속성이 없으면 처음에 높이 0으로 그렸다가 이미지가 도착하는 순간 아래 내용을 밀어냅니다. 글을 읽던 중 문단이 갑자기 내려가는 이 현상이 Core Web Vitals의 CLS(누적 레이아웃 이동) 점수를 나쁘게 만듭니다. 최신 브라우저는 두 속성으로 가로세로 비율을 계산해 두므로, CSS로 max-width: 100%; height: auto;를 함께 줘도 반응형 크기 조절과 자리 예약이 모두 됩니다. 화면 아래쪽 이미지에 loading="lazy"를 붙이면 스크롤해서 가까워질 때까지 다운로드를 미룰 수 있습니다. 다만 첫 화면에 보이는 대표 이미지에까지 붙이면 오히려 표시가 늦어집니다.
<figure> 안의 alt와 <figcaption>은 역할이 다릅니다. alt는 이미지를 볼 수 없는 사람을 위한 대체 텍스트이고, figcaption은 모두에게 보이는 설명입니다. 둘에 같은 문장을 넣으면 스크린 리더는 같은 내용을 두 번 읽으므로, 캡션이 이미 내용을 설명한다면 alt는 캡션이 말하지 않는 시각 정보에 집중하는 것이 좋습니다.
순서 없는·순서 있는·설명 리스트
순서 없는 리스트
<ul>
<li>항목 1</li>
<li>항목 2</li>
<li>항목 3</li>
</ul>
순서 있는 리스트
<ol>
<li>첫 번째</li>
<li>두 번째</li>
<li>세 번째</li>
</ol>
<!-- 시작 번호 지정 -->
<ol start="5">
<li>다섯 번째</li>
<li>여섯 번째</li>
</ol>
중첩 리스트
<ul>
<li>과일
<ul>
<li>사과</li>
<li>바나나</li>
</ul>
</li>
<li>채소
<ul>
<li>당근</li>
<li>브로콜리</li>
</ul>
</li>
</ul>
설명 리스트
<dl>
<dt>HTML</dt>
<dd>웹 페이지의 구조를 만드는 마크업 언어</dd>
<dt>CSS</dt>
<dd>웹 페이지의 스타일을 정의하는 언어</dd>
</dl>
리스트 태그를 쓰는 이유는 점이나 번호를 찍기 위해서가 아닙니다. 스크린 리더는 <ul>을 만나면 “목록, 항목 3개”라고 먼저 알려 주므로, 사용자는 끝까지 들을지 건너뛸지 판단할 수 있습니다. 내비게이션 메뉴를 <ul>로 만드는 관례도 이 때문입니다. 반대로 CSS에서 list-style: none으로 점을 없애면 Safari의 VoiceOver가 목록으로 인식하지 않는 동작이 알려져 있어서, 메뉴처럼 목록 의미가 중요한 곳에는 role="list"를 명시하기도 합니다. <ul>과 <ol>의 직계 자식은 <li>만 올 수 있고, 중첩 리스트는 위 예제처럼 반드시 <li> 안에 넣어야 합니다. <ul> 바로 아래에 <ul>을 두면 표시는 비슷해 보여도 유효하지 않은 구조입니다.
테이블과 셀 병합
기본 테이블
<table>
<thead>
<tr>
<th>이름</th>
<th>나이</th>
<th>직업</th>
</tr>
</thead>
<tbody>
<tr>
<td>홍길동</td>
<td>25</td>
<td>개발자</td>
</tr>
<tr>
<td>김철수</td>
<td>30</td>
<td>디자이너</td>
</tr>
</tbody>
</table>
테이블 스타일링
<style>
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ddd;
padding: 12px;
text-align: left;
}
th {
background-color: #4CAF50;
color: white;
}
tr:hover {
background-color: #f5f5f5;
}
</style>
셀 병합
<table>
<tr>
<th colspan="2">이름</th>
<th>나이</th>
</tr>
<tr>
<td>성</td>
<td>이름</td>
<td rowspan="2">25</td>
</tr>
<tr>
<td>홍</td>
<td>길동</td>
</tr>
</table>
테이블은 표 형식의 데이터를 위한 태그입니다. CSS 레이아웃 기능이 부족하던 시절에는 페이지 전체를 테이블로 배치했지만, 지금은 레이아웃에 테이블을 쓰면 스크린 리더가 “3행 2열” 같은 표 정보를 읽어 혼란을 주고, 작은 화면에서 반응형으로 재배치하기도 어렵습니다. 배치는 Flexbox와 Grid로 하고, 테이블은 행과 열 모두에 의미가 있는 데이터에만 씁니다.
접근성을 위해 테이블에 더해 두면 좋은 것이 두 가지 있습니다. <caption>은 표의 제목을 제공해 스크린 리더가 표에 들어가기 전에 무엇에 관한 표인지 알려 주고, <th scope="col">이나 scope="row"는 머리글이 열을 설명하는지 행을 설명하는지 명시합니다. 셀 병합이 들어가면 이 관계가 복잡해지므로, 위 예제처럼 colspan과 rowspan이 섞인 표는 보조 기술로 읽었을 때 의미가 전달되는지 확인해 보는 것이 좋습니다. 또 <tbody>를 생략해도 파서가 자동으로 넣어 주기 때문에, CSS 선택자 table > tr은 실제 DOM에서 아무것도 선택하지 못합니다. 초보자가 자주 겪는 “선택자가 안 먹는” 문제의 원인 중 하나입니다.
header·main·article 같은 시맨틱 태그
문서 구조
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>시맨틱 HTML</title>
</head>
<body>
<!-- 헤더 -->
<header>
<h1>사이트 제목</h1>
<nav>
<ul>
<li><a href="#home">홈</a></li>
<li><a href="#about">소개</a></li>
<li><a href="#contact">연락처</a></li>
</ul>
</nav>
</header>
<!-- 메인 콘텐츠 -->
<main>
<article>
<h2>글 제목</h2>
<p>글 내용...</p>
</article>
<aside>
<h3>관련 링크</h3>
<ul>
<li><a href="#">링크 1</a></li>
<li><a href="#">링크 2</a></li>
</ul>
</aside>
</main>
<!-- 푸터 -->
<footer>
<p>© 2026 회사명</p>
</footer>
</body>
</html>
시맨틱 태그 종류
| 태그 | 의미 |
|---|---|
<header> | 헤더 영역 |
<nav> | 네비게이션 |
<main> | 메인 콘텐츠 |
<article> | 독립적인 콘텐츠 |
<section> | 섹션 |
<aside> | 사이드바 |
<footer> | 푸터 영역 |
<section>과 <article>, <div>를 구분하는 기준은 “떼어 내도 말이 되는가”입니다. <article>은 블로그 글, 댓글, 상품 카드처럼 그 자체로 독립적으로 배포해도 의미가 통하는 단위이고, <section>은 제목을 가진 주제 묶음입니다. 스타일을 입히기 위한 묶음일 뿐 의미가 없다면 <div>가 맞습니다. 모든 <div>를 <section>으로 바꾸는 것은 시맨틱을 개선하는 것이 아니라 의미 없는 랜드마크를 늘려 오히려 탐색을 어렵게 만듭니다. <main>은 페이지에 하나만(보이는 것 기준) 두어야 하고, 보조 기술의 “본문으로 건너뛰기” 기능이 이 요소를 기준으로 동작합니다.
기본 폼 요소
HTML 마크업 예제입니다.
<form>
<!-- 텍스트 입력 -->
<label for="name">이름:</label>
<input type="text" id="name" name="name">
<!-- 이메일 입력 -->
<label for="email">이메일:</label>
<input type="email" id="email" name="email">
<!-- 비밀번호 입력 -->
<label for="password">비밀번호:</label>
<input type="password" id="password" name="password">
<!-- 버튼 -->
<button type="submit">제출</button>
</form>
<label for="name">과 <input id="name">을 연결하면 레이블 글자를 눌러도 입력창에 포커스가 가고, 스크린 리더가 입력창에 들어갈 때 “이름, 편집 가능한 텍스트”처럼 레이블을 함께 읽어 줍니다. placeholder만 두고 레이블을 생략하는 디자인이 흔하지만, 입력을 시작하면 안내 문구가 사라져 무엇을 입력하던 중인지 알 수 없게 되므로 레이블을 대체하지 못합니다. name 속성은 서버로 보낼 때의 키이며, id만 있고 name이 없는 입력은 폼을 제출해도 전송되지 않습니다.
이 예제 폼에는 한 가지 함정이 있습니다. action과 method를 지정하지 않으면 폼은 현재 URL로 GET 요청을 보내므로, 제출하는 순간 주소창에 ?name=...&password=...처럼 비밀번호가 그대로 드러나고 브라우저 기록과 서버 로그에도 남습니다. 실제 로그인이나 회원가입 폼은 반드시 method="post"를 지정하고 HTTPS로 전송해야 합니다. 폼 요소와 검증은 시리즈 뒤쪽 글에서 더 자세히 다룹니다.
시맨틱 태그로 프로필 페이지 만들기
예제: 프로필 페이지
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>프로필</title>
<style>
body {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.profile-img {
width: 200px;
height: 200px;
border-radius: 50%;
object-fit: cover;
}
.skills {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.skill-tag {
background: #3498db;
color: white;
padding: 5px 15px;
border-radius: 20px;
}
</style>
</head>
<body>
<header>
<img src="profile.jpg" alt="프로필 사진" class="profile-img">
<h1>홍길동</h1>
<p><em>프론트엔드 개발자</em></p>
</header>
<main>
<section>
<h2>소개</h2>
<p>
안녕하세요! 웹 개발에 열정을 가진
<strong>프론트엔드 개발자</strong>입니다.
</p>
</section>
<section>
<h2>기술 스택</h2>
<div class="skills">
<span class="skill-tag">HTML</span>
<span class="skill-tag">CSS</span>
<span class="skill-tag">JavaScript</span>
<span class="skill-tag">React</span>
</div>
</section>
<section>
<h2>프로젝트</h2>
<ul>
<li>
<a href="#">포트폴리오 웹사이트</a>
<p>개인 포트폴리오 사이트 제작</p>
</li>
<li>
<a href="#">쇼핑몰 프로젝트</a>
<p>React를 활용한 쇼핑몰 구현</p>
</li>
</ul>
</section>
<section>
<h2>연락처</h2>
<ul>
<li>이메일: <a href="mailto:[email protected]">[email protected]</a></li>
<li>GitHub: <a href="https://github.com/hong" target="_blank">github.com/hong</a></li>
<li>블로그: <a href="https://blog.example.com" target="_blank">blog.example.com</a></li>
</ul>
</section>
</main>
<footer>
<hr>
<p><small>© 2026 홍길동. All rights reserved.</small></p>
</footer>
</body>
</html>
HTML 기본 태그 요약
핵심 요약
- 텍스트:
<h1>~<h6>,<p>,<strong>,<em> - 링크:
<a href="URL"> - 이미지:
<img src="URL" alt="설명"> - 리스트:
<ul>,<ol>,<li> - 테이블:
<table>,<tr>,<th>,<td> - 시맨틱:
<header>,<nav>,<main>,<footer>
다음 단계
같이 보면 좋은 글
- HTML/CSS 시작하기 | 웹 개발 첫걸음
- JavaScript 시작하기: 개발 환경, 기본 문법, 브라우저에서 실행하기
- JavaScript DOM 조작 | 웹 페이지 동적으로 제어하기
- Flask 기초 | Python 웹 프레임워크 시작하기
- Django 기초 | Python 풀스택 웹 프레임워크 시작하기
- 반응형 웹 디자인 | 미디어 쿼리와 모바일 최적화
자주 묻는 질문 (FAQ)
Q. <img>의 alt는 꼭 써야 하나요? 장식용 이미지는 어떻게 하나요?
A. 내용을 전달하는 이미지라면 스크린 리더가 읽어 줄 설명으로 alt를 반드시 써야 하고, 이미지가 로드되지 않을 때도 이 텍스트가 대신 보입니다. 배경 장식처럼 의미가 없는 이미지는 alt를 빼는 대신 alt=""로 비워 두어야 스크린 리더가 파일명을 읽지 않고 건너뜁니다. 함께 width와 height를 지정해 두면 이미지가 늦게 로드되어도 레이아웃이 밀리지 않습니다.