한 줄 요약 — HTML 트리는 부모·자식·형제로 이어진 가족 관계도입니다. **자손(공백)**은 그 안의 모든 후손, **자식(
>)**은 바로 아래 한 세대만, **인접 형제(+)**는 바로 옆자리, **일반 형제(~)**는 같은 부모 아래 뒤에 선 모두를 가리킵니다. 거기에:hover·:focus같은 상태 가상 클래스와:nth-child같은 위치 가상 클래스를 더하면 "마우스를 올렸을 때만"이나 "짝수 번째만"처럼 한 번 더 좁혀 부를 수 있습니다.
학습 목표
- 자손(공백)·자식(
>)·인접 형제(+)·일반 형제(~) 네 결합 선택자를 구분해 쓸 수 있다. :hover와:focus의 차이를 알고, 둘을 항상 같이 적어야 하는 이유를 말할 수 있다.:nth-child(odd)·:nth-child(even)·:first-child·:last-child로 같은 목록의 일부만 골라 꾸밀 수 있다.:nth-child와:nth-of-type이 어떻게 다른지 설명할 수 있다.
오늘의 비유 — 가족 사진을 찍는 자리
가족 사진을 찍기 위해 모인 자리를 떠올려 봅니다. 한가운데 할아버지·할머니가 앉아 있고, 그 양옆으로 자녀와 사위·며느리가, 그 앞으로 손주들이 줄을 서 있습니다. 사진사가 누군가를 가리킬 때 그냥 "박씨 일가요"라고만 부르면 모두가 한꺼번에 돌아봅니다. 대신 관계로 좁혀 부르면 훨씬 정확합니다.
"박씨네 자녀분들"이라고 하면 자식 세대만 반응합니다. "박씨 댁 후손들"이라고 하면 자식·손주·증손주까지 모두가 반응합니다. "둘째 형 바로 옆에 앉은 동생"이라고 하면 두 사람 사이에 끼인 한 명만 가리키게 됩니다. CSS의 **결합 선택자(combinator)**가 그대로 이런 부름입니다.
거기에 더해 같은 자리에서도 상태나 위치로 한 번 더 좁힐 수 있습니다. 인사하러 다가온 손님이 누군가의 어깨에 손을 얹은 그 순간만 가리키거나, 사진사가 카메라를 들이대 들여다보는 한 명만 가리키거나, 줄서 있는 사람 중 짝수 번째만 부를 수 있다는 식입니다. :hover·:focus·:nth-child 같은 **가상 클래스(pseudo-class)**가 바로 그 역할입니다. "가상"이라는 이름이 붙은 까닭은 HTML에는 적혀 있지 않은데도 브라우저가 그 자리·상태를 알아서 표시해 두기 때문입니다.
핵심 개념
지난 회차에서 태그·클래스·ID 세 선택자로 옷 입힐 대상을 가리키는 법을 익혔습니다. 오늘은 그 선택자들을 둘 이상 이어 붙여 관계로 좁히는 법(결합 선택자)과, 같은 자리 안에서도 상태나 위치로 더 좁히는 법(가상 클래스)을 다룹니다.
자식과 자손 — 한 칸 띄어쓰기 vs >
다음 HTML을 봅니다. <article>이 부모이고 그 안에 <h2>와 <div>가 직계 자식, <div> 안의 <p>는 자식이 아니라 손주입니다.
<article>
<h2>오늘의 메뉴</h2>
<div>
<p>김치찌개와 공깃밥</p>
</div>
</article>
article p {
color: tomato;
}
띄어쓰기로 이어 적은 article p는 "<article>의 모든 후손 <p>"를 가리킵니다. 위 HTML에서 <p>는 <article>의 손주지만 후손이긴 하므로 적용됩니다.
article > p {
color: tomato;
}
>를 사이에 적은 article > p는 "<article>의 직계 자식 <p>만"을 가리킵니다. 위 HTML에는 <article> 바로 아래의 <p>가 없으므로 아무 것도 칠해지지 않습니다. 가족으로 치면 첫 번째는 "박씨 댁 후손들"을 부른 것이고, 두 번째는 "박씨네 자녀들"만 부른 셈입니다.
인접 형제와 일반 형제 — + vs ~
같은 부모 아래에 나란히 줄지어 있는 태그들을 **형제(sibling)**라고 부릅니다. 형제 안에서도 "바로 옆자리"와 "같은 줄에 뒤에 선 모두"를 구분해 부를 수 있습니다.
<section>
<h2>제목</h2>
<p>첫 문단</p>
<p>둘째 문단</p>
<p>셋째 문단</p>
</section>
h2 + p {
font-weight: bold;
}
h2 + p는 "<h2> 바로 다음에 오는 <p> 한 개"만 가리킵니다. 위 HTML에서 첫 문단만 굵게 표시됩니다.
h2 ~ p {
color: gray;
}
h2 ~ p는 "<h2> 뒤에, 같은 부모 아래에 있는 모든 <p>"를 가리킵니다. 세 문단이 모두 회색이 됩니다. "둘째 형 바로 옆 동생"과 "둘째 형 뒤에 줄 선 모든 동생"의 차이입니다.
상태 가상 클래스 — :hover와 :focus는 한 쌍
:hover는 마우스 커서가 그 자리에 올라왔을 때, :focus는 그 자리가 키보드의 Tab 이동으로 선택되었을 때 적용됩니다.
.btn:hover {
background: royalblue;
color: white;
}
.btn:focus {
outline: 3px solid royalblue;
outline-offset: 2px;
}
마우스 사용자에게는 호버가 익숙합니다. 그러나 키보드만 쓰는 사용자(예: 손이 불편하거나, 마우스를 잠시 떼고 Tab으로 양식을 채우는 사람)에게는 호버가 일어나지 않습니다. 그 사람들에게는 포커스가 유일한 단서입니다. :hover만 적고 :focus를 빠뜨리면, 키보드로 페이지를 돌아다니는 사람은 자기가 어디에 와 있는지 모르게 됩니다.
두 상태에 같은 모양을 한 번에 주고 싶다면 쉼표로 이어 적습니다.
.btn:hover,
.btn:focus {
background: royalblue;
color: white;
}
요즘 페이지에서는 :focus 대신 :focus-visible을 더 자주 쓰는 흐름이 자리 잡고 있는데, 이건 시리즈 후반의 접근성 회차에서 따로 다룹니다.
위치 가상 클래스 — :nth-child와 친구들
:nth-child(n)은 같은 부모 아래에서 몇 번째 자식인지로 그 자리를 가리킵니다. n에는 숫자, odd(홀수), even(짝수), 또는 2n+1 같은 식을 적을 수 있습니다.
<ul>
<li>김치찌개</li>
<li>된장찌개</li>
<li>비빔밥</li>
<li>제육볶음</li>
</ul>
li:nth-child(odd) {
background: #f6f8ff;
}
li:nth-child(even) {
background: white;
}
홀수 번째 <li>만 연한 파란 바탕이 되어 줄무늬 효과가 생깁니다. :first-child·:last-child도 자주 씁니다 — 각각 첫째와 막내를 가리키는 짧은 표현입니다.
li:first-child {
font-weight: bold;
}
li:last-child {
border-bottom: 0;
}
함께 따라하기 — 같은 목록을 짝수·첫째·막내만 다르게
day-14 폴더에 index.html과 style.css를 만듭니다. 항목 다섯 개를 줄세우고, 짝수 번째에 다른 배경을, 첫째에는 강조를, 막내에는 아래 경계선 제거를 적용해 봅니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>결합 선택자와 가상 클래스</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<ul class="menu">
<li>김치찌개</li>
<li>된장찌개</li>
<li>비빔밥</li>
<li>제육볶음</li>
<li>순두부찌개</li>
</ul>
</body>
</html>
.menu {
list-style: none;
padding: 0;
max-width: 320px;
}
.menu li {
padding: 12px 16px;
border-bottom: 1px solid #ddd;
}
.menu li:nth-child(even) {
background: #f6f8ff;
}
.menu li:first-child {
font-weight: bold;
color: royalblue;
}
.menu li:last-child {
border-bottom: 0;
}
.menu li:hover {
background: royalblue;
color: white;
cursor: pointer;
}
저장하고 브라우저로 열어 봅니다. 다섯 줄짜리 목록이 보이는데, 첫째 줄(김치찌개)은 굵게 파란 글씨로, 둘째와 넷째는 연한 파란 바탕으로, 마지막 줄(순두부찌개)은 아래 경계선이 사라진 모습입니다. 마우스를 줄 위에 올리면 그 줄만 짙은 파란 바탕에 흰 글씨로 바뀝니다.
:nth-child(even)을 :nth-child(3n)으로 바꿔 보면 셋째와 여섯째만 칠해진다는 것을 확인할 수 있고, .menu li:first-child 블록을 잠시 지워 보면 첫째가 일반 줄로 돌아오는 것도 볼 수 있습니다.
흔한 실수 3가지
1. :nth-child와 :nth-of-type을 같다고 생각한다
비슷해 보이지만 세는 방식이 다릅니다. 다음 HTML을 봅니다.
<section>
<h2>제목</h2>
<p>첫 문단</p>
<p>둘째 문단</p>
</section>
p:nth-child(2) {
color: tomato;
}
(이 코드는 "둘째 문단"을 칠하려는 의도였다면 빗나갑니다.) :nth-child(2)는 같은 부모 아래에서 종류를 가리지 않고 둘째 자식 자리를 찾는데, 위 HTML에서 둘째 자식은 <h2> 다음에 오는 첫 번째 <p>(즉 "첫 문단")입니다. 그 자리가 마침 <p>라 적용은 됩니다만, 의도와 우연이 일치한 것뿐입니다.
p:nth-of-type(2) {
color: tomato;
}
:nth-of-type(2)는 "같은 부모 아래의 <p> 중에서 둘째"를 정확히 가리키므로 둘째 문단이 칠해집니다. 가족으로 치면 :nth-child는 "줄에 선 둘째"이고, :nth-of-type은 "같은 항렬 안에서 둘째"라고 보면 가깝습니다.
2. 자식 선택자(>)와 자손 선택자(공백)를 헷갈린다
띄어쓰기 한 칸이 의미를 완전히 바꿉니다.
<nav class="menu">
<ul>
<li>홈</li>
<li>소개</li>
</ul>
</nav>
.menu > li {
font-weight: bold;
}
(이 코드는 아무 <li>도 굵게 만들지 못합니다.) .menu > li는 ".menu의 직계 자식 <li>"인데, 위 HTML에서 .menu 바로 아래에는 <ul>이 있고 <li>는 손주입니다. 그래서 한 줄도 적용되지 않습니다.
.menu li {
font-weight: bold;
}
띄어쓰기로 적은 .menu li는 ".menu 안의 모든 후손 <li>"를 가리키므로 두 메뉴가 정상으로 굵게 표시됩니다. "박씨네 자녀"와 "박씨 댁 후손"의 차이가 그대로 코드에 옮겨 와 있는 셈이라, 적을 때 그 사이에 한 세대를 건너뛰는지 한 번 더 들여다봅니다.
3. :hover만 적고 :focus를 빠뜨린다
마우스로 페이지를 쓰는 사람만 떠올리고 적으면 자주 빠지는 실수입니다.
.btn:hover {
background: royalblue;
color: white;
}
(이 코드는 키보드 사용자에게는 아무 단서도 주지 못합니다.) Tab 키로 양식을 넘기는 사람이나 보조 기기를 쓰는 사람에게는 호버가 일어나지 않으므로, 자기가 어느 버튼 위에 와 있는지 알 길이 없습니다. 같은 모양을 :focus에도 같이 줘야 합니다.
.btn:hover,
.btn:focus {
background: royalblue;
color: white;
}
쉼표로 묶어 두면 호버와 포커스 두 상태에 같은 옷을 한 번에 입힐 수 있습니다. 실무에서는 :focus-visible로 마우스 클릭 직후의 포커스 링까지 더 정교하게 다듬는데, 이건 시리즈 후반의 접근성 회차에서 따로 다룹니다.
오늘 배운 것 체크리스트
- 자식 결합자(
>)와 자손 결합자(공백)의 차이를 안다. - 인접 형제(
+)와 일반 형제(~)를 구분해 쓸 수 있다. -
:hover와:focus를 항상 같이 적는다는 원칙을 안다. -
:nth-child(odd)·:nth-child(even)·:first-child·:last-child를 골라 쓸 수 있다. -
:nth-child와:nth-of-type의 세는 방식 차이를 말할 수 있다.
자주 묻는 질문
Q. 직계 자식만 선택하는 방법이 있나요?
A. 부모 > 자식 형태로 사이에 >를 적습니다. 자손 전체가 아닌 한 세대만 부를 때 씁니다. 예를 들어 nav > ul은 <nav> 바로 아래의 <ul>만 가리키고, 그 안에 또 들어 있는 더 깊은 <ul>은 건드리지 않습니다.
Q. 홀수·짝수 번째만 칠하려면 어떻게 적나요?
A. :nth-child(odd)와 :nth-child(even)을 씁니다. 표나 목록에 줄무늬 효과를 줄 때 가장 흔하게 쓰는 패턴이고, 더 일반적으로 :nth-child(3n)처럼 적으면 3의 배수 번째만 골라낼 수 있습니다.
Q. :hover와 :focus를 동시에 처리하는 깔끔한 방법은요?
A. 같은 스타일을 두 상태에 함께 주려면 쉼표로 묶어 적습니다. .btn:hover, .btn:focus { … } 형태이면 마우스 사용자와 키보드 사용자 둘 다에게 같은 강조가 보입니다. 큰 페이지에서는 마우스 클릭 직후 포커스 링이 어색하게 남는 문제를 줄이려고 :focus-visible을 함께 쓰는데, 이건 접근성 회차에서 자세히 다룹니다.
다음 시간 예고
내일은 우선순위와 캐스케이드 — 명시도 계산법과 !important의 함정을 다룹니다. 오늘까지 익힌 선택자들을 한 자리에서 충돌시켰을 때 누가 이기는지 정확히 계산하는 법, 그리고 디버깅을 망쳐 버리는 !important 남용을 피하는 법을 정리합니다.