개요
개발을 하다 보면 무의식적으로 `<div>` 태그를 남발하게 되는 경우가 많다. 당장 스타일링하기 편하고, 별다른 고민 없이 레이아웃을 잡을 수 있기 때문이다. 하지만 `<div>`로 점철된 코드는 Div Soup(div만 가득한 죽)이라고 불리며, 검색 엔진(SEO)과 스크린 리더(접근성)에게는 최악의 구조다. 이와 관련한 글을 이전 포스트로 작성했었다.
이번에 서비스의 SEO 최적화와 웹 접근성 향상을 목표로, 기존의 `<div>`로 떡칠된... 코드를 시맨틱 태그로 대대적으로 리팩토링 했다. (사실 div 태그 제거 작업은 할당된 업무에 없었는데 어차피 해야하는 작업이라 같이 해결했다.) 결과적으로 Lighthouse 점수가 유의미하게 상승했는데, 그 과정과 기준을 정리해본다.
시맨틱 태그 적용과 기준
`<article>` vs `<section>`
가장 먼저 고민한 것은 이 컴포넌트들을 무엇으로 감쌀 것인가?였다. 기존에는 전부 <div>였던 것들을 article과 section으로 나누었다.
- Article: 독립적으로 배포되거나 재사용될 수 있는 콘텐츠. (예: 뉴스 기사, 블로그 포스트, 사용자의 리뷰, 댓글 등)
- Section: 서로 관련 있는 콘텐츠들을 묶은 주제별 그룹. 보통 내부에 heading 태그를 포함한다.
쉽게 말해, 특정 부분만 뚝 떼어내서 다른 곳에 붙여넣어도 말이 되면 article을, 단순히 주제별로 구획을 나눈 것이라면 section을 사용했다. ▼
`<p>` vs `<span>` 태그
텍스트를 감쌀 때도 기준을 세웠다.
- p (Paragraph): 문단이다. 정보 전달이 목적인 텍스트 덩어리에 사용한다. 스크린 리더는 p 태그를 만나면 문단 뒤에서 잠시 멈춰 숨을 고르는 뉘앙스를 준다.
- span: 문단이 아니다. 텍스트 중간에 스타일을 입히거나 짧은 단어를 묶을 때 사용한다. 의미가 없다.
기존에는 긴 설명글도 `<span>`이나 `<div>`로 되어 있어 스크린 리더가 쉬지 않고 읽어버리는 문제가 있었다. 이를 `<p>`로 변경하여 정보 구조를 명확히 했다.
목록은 무조건 `<li>`, `<ol>`, `<ul>`
네비게이션 메뉴나 카드 리스트 등 목록 형태를 띠는 것들은 전부 `<ul>` (순서 없음) 혹은 `<ol>` (순서 있음)과 `<li>`로 변경했다.
스크린 리더가 "목록 5개 항목 중 첫 번째"라고 알려주기 때문에 접근성 측면에서 필수적이다.
`<h1>` vs `<h2>` : 봇들은 `<h2>`를 더 좋아한다?
이번 작업에서 페이지 내의 많은 타이틀을 `<h1>`에서 `<h2>`로 변경했다.
"`<h1>`이 제일 중요한 거니까 섹션마다 `<h1>`을 쓰면 좋은 거 아닌가?"
...라고 생각할 수 있지만, SEO 관점에서는 그렇지 않다. 검색 엔진 봇(Google Bot 등)은 페이지의 구조를 파악할 때 heading 태그의 계층 구조를 중요하게 본다.
- `<h1>`: 책의 제목 (페이지 당 1개 권장)
- `<h2>`: 각 챕터의 소제목
- `<h3>`: 챕터 내부의 문단 제목
한 페이지에 `<h1>`이 여러 개 남발되면 검색 엔진은 "그래서 이 페이지의 진짜 핵심 주제가 뭔데?"라고 혼란스러워한다. 주제의 집중도가 분산되는 것이다. 따라서 페이지 전체를 관통하는 메인 타이틀만 `<h1>`으로 남기고, 각 섹션의 타이틀은 `<h2>`로 계층을 낮춰 구조화했다. 봇들이 `<h2>`를 선호한다기보다는, 잘 잡힌 위계질서(Hierarchy)를 선호한다고 보는 것이 맞다. ▼
<Aside>는 옆에 있어서 Aside가 아니다
이번 리팩토링 과정에서 가장 헷갈렸던 것 중 하나가 바로 `<aside>` 태그다. 이름만 보면 "아, 사이드바(Sidebar) 만들 때 쓰는 거구나!" 하고 단순히 생각하기 쉽다. 나 역시 예전에는 화면의 왼쪽이나 오른쪽에 배치되는 영역은 무조건 <aside>로 감싸곤 했다. 하지만 이는 반은 맞고 반은 틀린 생각이다.
`<aside>`의 핵심 정의는 본문 콘텐츠와 관련이 있긴 하지만, 그 흐름에서 벗어나 있는 내용이다. 즉, 위치가 옆이라서가 아니라 내용이 부수적(Tangential)이라서 Aside인 것이다. 나는 아래와 같은 삭제 테스트를 통해 적용 기준을 세웠다.
이 영역을 지워버려도 본문을 이해하는 데 전혀 지장이 없는가?
- 광고 배너나 추천 게시물 본문 글을 읽다가 중간에 광고가 나오거나, 글 끝에 "이 글과 관련된 다른 글" 목록이 나온다면?
이건 본문의 핵심 내용이 아니다. 없어도 글을 읽는 데는 아무 문제가 없다. → `<aside>` 사용. - 목차 (Table of Contents) 긴 글 옆에 따라다니는 목차는 본문을 요약하거나 내비게이션 역할을 한다.
본문의 직접적인 내용은 아니지만, 본문을 돕는 도구다. → `<aside>` (내부에 <nav>를 포함하는 형태) 사용. - 용어 설명이나 TMI 글을 쓰다가 특정 단어에 대한 주석이나 짤막한 배경지식을 박스 형태로 넣을 때가 있다.
본문 흐름을 끊지 않으면서 부가 정보를 주는 것. → `<aside>` 사용.
주의할 점 : 사이드바 메뉴
만약 사이드바가 페이지 전체의 메인 메뉴(GNB, LNB) 역할을 한다면? 이건 페이지의 핵심 탐색 도구이므로 <aside>보다는 <nav>가 훨씬 더 적절하다. (물론 <aside> 안에 <nav>를 넣는 구조도 가능하지만, 단순히 옆에 있다고 무조건 <aside>를 붙이는 건 지양해야 한다.)
이처럼 <aside>를 명확히 구분해주면, 스크린 리더 사용자는 본문을 읽다가 "부수적인 내용(Complementary Landmark)" 영역을 건너뛰고 바로 본문에 집중할 수 있게 된다.
접근성(A11y) 챙기기
`<div>`와 `<span>`에는 `aria-label`을 쓸 수 없다
(사실 정확히 말하자면 쓸 수 없다 보다는 써도 적용이 안된다의 느낌이 더 강하다.)
가장 많이 했던 실수? 중 하나다. 버튼 역할을 하는 `<div>`에 설명을 넣겠답시고 aria-label을 붙여놨었다. 하지만 `<div>`나 `<span>` 같은 시맨틱하지 않은(Non-semantic) 태그는 기본적으로 접근성 API에 노출되지 않기 때문에, role 속성 없이 aria-label만 붙이면 스크린 리더는 이를 가볍게 무시한다.
그래서 버튼 기능을 하는 요소는 진짜 `<button>` 태그로 바꾸거나, `<a>` 태그로 변경했다. 부득이한 경우 `role="button"`을 명시하고 `tabindex`를 추가했다.
이미지에는 무조건 `alt`
모든 `<img>` 태그에 `alt` 속성을 추가했다.
- 의미 있는 이미지: 이미지가 전달하는 정보를 텍스트로 설명.
- 장식용 이미지: alt="" 빈 값을 주어 스크린 리더가 이미지라고 읽지 않고 넘어가도록 처리.
그러나 장식용 이미지에도 alt에 빈 값을 주긴 했다. 빈값이나 alt를 작성안하나 같은 것이고, 결국엔 스크린 리더가 이미지를 읽을건데 alt가 없다면 읽은 의미가 없기 때문이다. 그래서 장식용이라 할 지 언정 어떤 장식인지를 alt에 넣었다.
Visually Hidden : 보이지 않지만 존재해야 한다
`<div>`와 `<span>`에는 `aria-label`을 적용하지 못한다. 하지만 aria-label을 넣어야할 때가 있다. 예를 들어, 돋보기 아이콘만 있는 검색 버튼이나, 디자인적으로는 제목이 필요 없어서 생략된 섹션들이 그렇다. 눈으로 볼 때는 아이콘만 봐도 검색 기능인지 알 수 있고, 문맥상 섹션 구분이 가지만, 스크린 리더에게는 그저 버튼 혹은 알 수 없는 영역일 뿐이다.
그렇다고 텍스트를 넣자니 디자인 명세에 있지 않다. 넣는다고 해도 디자인이 망가진다. 이런 상황에서 쉽게 저지를 수 있는 실수가 있다. ▼
<button>
<img src="search-icon.png" alt="검색">
<span style="display: none;">검색하기</span>
</button>
바로 위와 같이, 텍스트는 넣되 보이지 않게 해서 보이지 않는 텍스트를 통해 aria-label을 대체하는 것이다. 그러나 문제는 `display: none`이나 `visibility: hidden`을 사용하면 화면에서만 사라지는 게 아니라, 접근성 트리(Accessibility Tree)에서도 아예 제거된다. 즉, 스크린 리더도 이 텍스트를 읽지 못한다. 우리는 화면에는 안 보이지만, 스크린 리더는 읽을 수 있는 무언가가 필요하다.
이를 위해 `visually-hidden` (혹은 sr-only)라는 유틸리티 클래스를 만들어 적용했다. 이 클래스는 요소를 화면에서 1픽셀 크기로 줄이고, 넘치는 부분을 잘라내어(clip) 시각적으로는 완벽하게 숨기지만, HTML 구조상으로는 엄연히 존재하게 만든다. ▼
/* visually-hidden 유틸리티 클래스 */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
이제 이 클래스를 적용하면 디자인과 접근성을 모두 잡을 수 있다. ▼
<button>
<i class="icon-search" aria-hidden="true"></i>
<span class="visually-hidden">검색하기</span>
</button>
<section>
<h2 class="visually-hidden">추천 상품 목록</h2>
<ul>...</ul>
</section>
특히 `<h2>` 같은 헤딩 태그가 디자인상 애매해서 생략된 경우가 많았는데, 이 기법을 통해 문서의 논리적 구조(Outline)를 끊어짐 없이 연결할 수 있었고, 이는 SEO 점수 향상에도 큰 기여를 했다.
DOM 트리 최적화 : 중첩 div 제거
레거시 코드를 뜯어보다 보니 무의미한 `<div>` 중첩이 너무 많았다.
<div class="wrapper">
<div class="container">
<div class="content-box">
<p>안녕하세요</p>
</div>
</div>
</div>
스타일링을 위해 겹겹이 쌓아 올린 `<div>`들을 제거하고, CSS Grid나 Flex를 활용해 불필요한 깊이(Depth)를 줄였다. DOM 트리가 가벼워지면 렌더링 성능에도 미세하게나마 도움이 된다.
그럼 div는 언제 써야 해?
"그럼 <div>는 절대 쓰면 안 되나?"
그건 아니다. `<div>`는 의미 없이 오직 디자인적 배치(Layout)나 스타일링을 위해 그룹핑할 때 사용한다.
- Flex/Grid 컨테이너가 필요할 때
- 배경색을 입히기 위한 래퍼(Wrapper)가 필요할 때
- `<section>`이나 `<article>`로 묶기엔 의미가 너무 없을 때
이럴 때는 여전히 `<div>`가 최고의 선택이다.
성과 및 한계
이번 리팩토링을 통해 Lighthouse 점수가 유의미하게 개선되었다. ▼


- 접근성(Accessibility): 76 → 91 (시맨틱 태그와 aria 속성의 힘)
- SEO: 92 → 100 (Heading 계층 구조 정리 및 메타 데이터 보완)
- 성능(Performance): 50 → 58
- Best Practices: 75 → 75 (변동 없음)
성능 점수(50 -> 58)에 대한 변명
성능 점수가 생각보다 드라마틱하게 오르지 않은 점은 아쉬움으로 남는다. (사실 8점 오른 것도 기적이다.) 이미지 최적화(WebP 전환, 리사이징)와 DOM 최적화를 진행했지만, 프로젝트 자체가 워낙 오래된 레거시 코드로 얽혀있고 무거운 서드파티 라이브러리들이 덕지덕지 붙어있는 상태라, 말단 사원의 권한으로 구조 자체를 갈아엎기엔 역부족이었다. ▼

핵심적인 렌더링 로직이나 번들 사이즈를 줄이는 건 아키텍처 차원의 접근이 필요해 보인다. 그래도 내가 건드릴 수 있는 마크업 영역에서는 최선을 다해 점수를 끌어올렸다는 점에 의의를 둔다.
'Develop > Web' 카테고리의 다른 글
| [React][Issue] 간단한 알고리즘과 함께 말줄임 디테일 챙기기 (0) | 2025.12.01 |
|---|---|
| [React] Spread Attributes를 조심해야 하는 이유 (0) | 2025.11.26 |
| [Issue] Firefox에서 새로고침 시 Input 값이 초기화되지 않는 문제 해결하기 (1) | 2025.11.20 |
| [Web] div 수프를 끓이지 말아야 하는 이유 (0) | 2025.11.02 |
| [React][Error] tailwind css 설치 오류 (4) | 2025.10.05 |