본문 바로가기

가상 클래스와 가상 요소

CSS에서는 기본 선택자 외에도 특별한 상태나 위치를 지정할 수 있는 가상 클래스와 가상 요소라는 강력한 도구를 제공합니다. 이들을 활용하면 HTML 구조를 변경하지 않고도 더욱 세밀하고 동적인 스타일링이 가능해집니다.

1. 가상 클래스 선택자

가상 클래스 선택자(Pseudo-class selectors)는 요소의 특정 상태를 선택할 수 있게 해줍니다. 이들은 콜론(:)을 사용하여 표현됩니다.

1.1 동적 가상 클래스 선택자

동적 가상 클래스는 사용자의 동작에 따라 변화하는 상태를 선택합니다.

아직 방문하지 않은 링크를 선택합니다.

<a href="https://paullab.co.kr/about.html" class="unvisited"
  >방문하지 않은 링크</a
>
.unvisited:link {
  color: blue;
  text-decoration: none;
}

1.1.2 :visited

이미 방문한 링크를 선택합니다.

<a href="https://www.studyin.co.kr/" class="visited">방문한 링크</a>
.visited:visited {
  color: red;
  text-decoration: underline;
}

1.1.3 :hover

마우스 포인터가 올라간 요소를 선택합니다.

<button class="hover-button">마우스를 올려보세요</button>
.hover-button {
  background-color: #3498db;
  color: white;
  padding: 10px 20px;
  border: none;
  transition: background-color 0.3s;
}

.hover-button:hover {
  background-color: #2980b9;
}

1.1.4 :active

마우스로 클릭하고 있는 요소를 선택합니다.

<button class="active-button">클릭해보세요</button>
.active-button {
  background-color: #2ecc71;
  color: white;
  padding: 10px 20px;
  border: none;
}

.active-button:active {
  background-color: #27ae60;
  transform: scale(0.95);
}

1.1.5 :focus

현재 초점을 가진 요소를 선택합니다.

<input type="text" class="focus-input" placeholder="클릭하거나 탭하세요" />
<input type="text" class="focus-input" placeholder="클릭하거나 탭하세요" />
<input type="text" class="focus-input" placeholder="클릭하거나 탭하세요" />
<input type="text" class="focus-input" placeholder="클릭하거나 탭하세요" />
<input type="text" class="focus-input" placeholder="클릭하거나 탭하세요" />
<input type="text" class="focus-input" placeholder="클릭하거나 탭하세요" />
.focus-input {
  padding: 10px;
  border: 2px solid #bdc3c7;
  outline: none;
  transition: border-color 0.3s;
}

.focus-input:focus {
  border-color: #3498db;
  box-shadow: 0 0 5px rgba(52, 152, 219, 0.5);
}

이러한 동적 가상 클래스들을 조합하여 사용하면 사용자 상호작용에 따른 다양한 시각적 피드백을 제공할 수 있습니다. 예를 들어, 링크에 이 모든 상태를 적용할 수 있습니다.

<a href="https://example.com" class="interactive-link">인터랙티브 링크</a>
.interactive-link:link {
  color: blue;
}
.interactive-link:visited {
  color: purple;
}
.interactive-link:hover {
  color: red;
  text-decoration: underline;
}
.interactive-link:active {
  color: orange;
}
.interactive-link:focus {
  outline: 2px solid green;
}

이처럼 동적 가상 클래스는 사용자의 동작에 따라 요소의 스타일을 변경할 수 있습니다.

LVHA 순서 기억하기!

링크 스타일을 정의할 때는 Link → Visited → Hover → Active 순서로 써야 합니다. CSS 우선순위 때문에 순서가 바뀌면 제대로 작동하지 않습니다.

기억법: "Love Vegas? Hate Arizona!"

a:link {
  /* 방문하지 않은 링크 */
}
a:visited {
  /* 방문한 링크 */
}
a:hover {
  /* 마우스 올린 상태 */
}
a:active {
  /* 클릭하는 순간 */
}
a:focus {
  /* 포커스 상태 - 순서 무관 */
}

참고: :focus는 순서와 무관하며 접근성을 위해 항상 포함하는 것이 좋습니다.

2. 가상 요소

가상 요소(Pseudo-elements)는 말 그대로 가상의 요소를 만드는 것입니다. 선택한 요소의 특정 부분에 스타일을 적용할 수 있게 해줍니다. 이들은 이중 콜론(::)을 사용하여 표현됩니다.

2.1 ::before와 ::after

::before와 ::after는 선택한 요소의 내용 앞이나 뒤에 새로운 요소를 만들어 새로운 콘텐츠를 추가할 수 있게 해줍니다. 이들은 주로 content 속성과 짝을지어 장식적인 요소를 추가하거나, 기능적인 목적으로 사용됩니다. 빈태그 img br input 에는 적용할 수 없습니다.

<p class="quote">지식은 힘이다</p>
.quote::before {
  content: '『';
  color: #3498db;
  font-size: 1.2em;
  margin-right: 5px;
}

.quote::after {
  content: '』';
  color: #3498db;
  font-size: 1.2em;
  margin-left: 5px;
}

2.2 ::marker

::marker는 리스트 아이템의 마커를 스타일링 할 수 있게 해주는 가상요소입니다. display: list-item을 가진 요소에만 적용됩니다. 콘텐츠(content), 색상(color), font 관련 속성들 등의 속성은 사용 가능하지만, 박스 모델의 속성인 배경(background), 테두리(border), 안쪽 여백(padding)은 적용되지 않습니다.

<ul class="custom-markers">
  <li>첫 번째 항목</li>
  <li>두 번째 항목</li>
  <li>세 번째 항목</li>
</ul>
.custom-markers li::marker {
  content: '✦';
  color: #e74c3c;
  font-size: 1.2em;
}

이 방식은 list-style-type:none;과 ::before 을 함께 사용하는 방식보다 훨씬 간단하게 작성할 수 있다는 장점이 있습니다. 또한 리스트의 의미 구조를 유지하기 때문에 접근성 측면에서도 더 우수하게 평가됩니다. 따라서 박스 모델 속성의 사용이 필요하지 않다면 ::marker를 사용하는 것을 권장합니다.

[퀴즈]가상 요소를 사용한 리스트 스타일링

가상요소를 이용하여 리스트의 불렛 대신 🍎을 추가해주세요.

<ul class="custom-bullets">
  <li>사과</li>
  <li>바나나</li>
  <li>오렌지</li>
</ul>
정답
.custom-bullets li {
  list-style-type: none;
}

.custom-bullets li::before {
  content: '🍎';
  padding-right: 10px;
}

/* 또는 */

.custom-bullets li {
  padding-left: 10px;
}
.custom-bullets li::marker {
  content: '🍎';
  display: block;
}

이러한 다양한 방법을 사용하여 리스트의 스타일을 원하는 대로 커스터마이즈할 수 있습니다. 각 방법의 장단점을 고려하여 프로젝트에 가장 적합한 방식을 선택하세요.

2.3 ::placeholder

<input>과 <textarea> 요소의 플레이스홀더 텍스트에 스타일을 적용할 수 있게 해줍니다. 플레이스홀더는 사용자에게 입력 필드에 어떤 정보를 입력해야 하는지 안내하는 역할을 합니다.

입력 필드에 텍스트가 입력되면 플레이스홀더는 사라집니다. 접근성을 고려한다면 플레이스홀더에만 의존하지 말고 적절한 레이블도 함께 사용해야 합니다.

<form>
  <input type="text" placeholder="이름을 입력하세요" />
  <input type="email" placeholder="이메일 주소" />
  <textarea placeholder="메시지를 입력하세요"></textarea>
</form>
::placeholder {
  color: #999;
  font-style: italic;
}

input::placeholder {
  font-size: 14px;
}

textarea::placeholder {
  font-size: 16px;
}

input:focus::placeholder,
textarea:focus::placeholder {
  color: transparent;
}
의사 요소 - CSS: Cascading Style Sheets | MDN

3. 구조적 가상 클래스 선택자

구조적 가상 선택자는 문서 구조 내에서 요소의 위치에 따라 선택합니다.

3.1 :first-child와 :last-child

:first-child는 형제 요소 그룹 중 첫 번째 요소를, :last-child는 마지막 요소를 선택합니다.

<ul>
  <li>첫 번째 항목</li>
  <li>중간 항목</li>
  <li>마지막 항목</li>
</ul>
li:first-child {
  color: red;
}
li:last-child {
  color: blue;
}

body의 직계 자손의 경우 :first-child 선택자는 사용가능하지만, :last-child 선택자가 적용되지 않습니다.

<body>
  <p>first-child</p>
  <p>last-child</p>
</body>
/* 적용 o */
body > :first-child {
  color: red;
}

/* 적용 x */
body > :last-child {
  color: red;
}

3.2 :nth-child

부모 요소 내의 모든 자식 요소 중에서 지정된 순서에 있는 요소를 선택합니다. 이 선택자는 요소의 타입과 관계없이 순서만을 고려합니다.

<div>
  <p>첫 번째 문단</p>
  <h2>첫 번째 제목</h2>
  <p>두 번째 문단</p>
  <p>세 번째 문단</p>
</div>
/* 두 번째 자식 요소 선택 (여기서는 h2) */
div :nth-child(2) {
  color: red;
}

/* 홀수 번째 자식 요소 선택 */
div :nth-child(odd) {
  background-color: #f1f1f1;
}

/* 3의 배수 번째 자식 요소 선택 */
div :nth-child(3n) {
  font-weight: bold;
}

:nth-child() 선택자는 괄호 안에 다양한 형태의 값을 사용할 수 있어 매우 유연합니다. 다음은 :nth-child() 괄호 안에 사용할 수 있는 값의 종류입니다.

  • 정수값: 특정 순서의 요소를 선택
  • 키워드
    • odd: 홀수 번째 요소 선택
    • even: 짝수 번째 요소 선택
  • 수식: an+b 형태의 수식 사용 가능 (여기서 n은 0부터 시작하는 정수, a와 b는 정수값)
  • 음수값: 요소의 뒷부분이나 특정 범위의 요소들을 선택할 때 유용
    • 예: :nth-child(-n+3) 처음 3개 요소 선택
<ul>
  <li>항목 1</li>
  <li>항목 2</li>
  <li>항목 3</li>
  <li>항목 4</li>
  <li>항목 5</li>
  <li>항목 6</li>
  <li>항목 7</li>
  <li>항목 8</li>
  <li>항목 9</li>
  <li>항목 10</li>
</ul>
/* 3번째 항목 */
li:nth-child(3) {
  color: red;
}

/* 홀수 번째 항목 */
li:nth-child(odd) {
  background-color: lavender;
}

/* 짝수 번째 항목 */
li:nth-child(even) {
  background-color: lightpink;
}

/* 3의 배수 번째 항목 */
li:nth-child(3n) {
  font-weight: bold;
}

/* 처음 3개 항목 */
li:nth-child(-n + 3) {
  text-decoration: underline;
}

/* 7번째 이후의 항목 */
li:nth-child(n + 7) {
  font-style: italic;
}

3.3 :nth-of-type

부모 요소 내에서 같은 타입의 형제 요소 중 지정된 순서에 있는 요소를 선택합니다. 이 선택자는 요소의 타입을 고려하여 순서를 계산합니다.

<div>
  <h2>첫 번째 제목</h2>
  <p>첫 번째 문단</p>
  <h2>두 번째 제목</h2>
  <p>두 번째 문단</p>
</div>
h2:nth-of-type(1) {
  color: purple;
}

p:nth-of-type(2) {
  color: green;
}

3.4 :only-of-type

:only-of-type는 동일한 유형의 형제가 없는, 형제 요소 중 유일하게 사용된 태그를 선택합니다.

<div>
  <p>유일한 문단</p>
  <h2>첫 번째 제목</h2>
  <h2>두 번째 제목</h2>
</div>
p:only-of-type {
  color: blue;
}

/* h2는 유일하지 않아서 적용되지 않음. */
h2:only-of-type {
  color: blue;
}

3.5 :not

:not은 부정 선택자로, 특정 선택자를 제외한 요소를 선택합니다.

<ul>
  <li>첫 번째 항목</li>
  <li>두 번째 항목</li>
  <li>세 번째 항목</li>
</ul>
/* li 중 첫번째가 아닌 li */
li:not(:first-child) {
  list-style-type: decimal;
}

이러한 구조적 가상 선택자들을 활용하면 HTML 구조를 변경하지 않고도 특정 위치의 요소들에 스타일을 적용할 수 있습니다. 이는 특히 반복적인 구조를 가진 리스트나 그리드 레이아웃에서 유용하게 사용될 수 있습니다.

3.6 :is() and :where()

:is()와 :where()는 여러 개의 선택자를 한꺼번에 지정할 수 있는 간편한 방법을 제공합니다. :is()는 여러 개의 선택자를 지정할 때 사용하며, :where()는 :is()와 유사하지만, 우선순위가 낮습니다.

여러 개의 선택자를 선택하는 것은 기존에도 아래와 같은 방법으로 가능했습니다.

h1,
h2,
h3 {
  color: blue;
}

다만 만약 h1과 h2와 h3 안에 있는 요소를 선택하고 싶다면 아래와 같이 하나씩 선택자를 지정해야 했습니다.

h1 a,
h2 a,
h3 a {
  color: blue;
}

이러한 경우 :is()를 사용하면 아래와 같이 간단하게 표현할 수 있습니다.

:is(h1, h2, h3) a {
  color: blue;
}

역시 익스플로러를 제외하고 대부분의 브라우저에서 지원하고 있으므로 충분히 실무에서 사용할 수 있습니다.

브라우저 지원 여부
<h1><a>hello</a></h1>
<h2><a>hello</a></h2>
<h3><a>hello</a></h3>
<h4><a>hello</a></h4>
:is(h1, h2, h3) a {
  color: blue;
}

where()는 :is()와 유사하지만, 우선순위가 낮습니다. 아래 예제를 보며 is()와 where()를 동시에 사용했을 경우 어떻게 처리되는지 확인해 보도록 하겠습니다. 확인이 되었다면 is()를 삭제해 보세요.

<h1><a>hello</a></h1>
<h2><a>hello</a></h2>
<h3><a>hello</a></h3>
<h4><a>hello</a></h4>
:is(h1, h2, h3) a {
  color: blue;
}
:where(h1, h2, h3) a {
  color: red;
}

3.7 :where() 활용

:where()의 가장 큰 장점은 우선순위(specificity)가 0이라는 점입니다. 이는 스타일 재정의가 필요한 상황에서 매우 유용합니다. 예를 들어, 기본 스타일을 정의하면서도 필요한 경우 쉽게 재정의할 수 있게 해줍니다.

/* 기본 스타일 - 낮은 우선순위 */
:where(button, .btn, [type='submit']) {
  padding: 0.5em 1em;
  background-color: #e0e0e0;
  border: 1px solid #ccc;
}

/* 특정 컨테이너의 버튼에 다른 스타일 적용 - 쉽게 재정의 가능 */
.form-container button {
  background-color: #0066cc;
  color: white;
}

만약 :is() 대신 :where()를 사용하지 않았다면, 두 번째 선택자를 더 구체적으로 만들어야 재정의가 가능했을 것입니다.

3.8 :has()

:has()는 2022년에 도입된 의사 클래스입니다. 이 의사 클래스는 특정 요소가 자식 요소를 포함하고 있는지를 검사할 수 있게 해주며, CSS 선택자의 강력한 기능을 제공합니다. 다른 요소 안에 특정 요소가 포함되어 있는지 여부에 따라 스타일을 적용할 수 있어 매우 유용합니다.

역시 익스플로러를 제외하고 대부분의 브라우저에서 지원하고 있으므로 충분히 실무에서 사용할 수 있습니다.

has() 브라우저 지원 여부

예를 들어, 특정 요소가 자식으로 링크를 포함하고 있는 경우에만 스타일을 적용하고 싶을 때 사용할 수 있습니다. 아래 예시에서는 div 요소가 a 요소를 포함하고 있는 경우에만 배경색을 변경합니다.

<div>
  <p>lorem ipsum</p>
</div>
<div>
  <a href="#">click me</a>
</div>
div:has(a) {
  background-color: lightblue;
}

실용적인 활용

:has() 선택자는 특히 조건부 스타일링에 강력합니다.

  • 이미지가 포함된 카드만 다르게 스타일링
.card:has(img) {
  padding-top: 0;
}
  • 필수 입력 필드(*)가 있는 라벨 강조
label:has(.required) {
  font-weight: bold;
}
  • 빈 목록 표시 방식 변경
ul:not(:has(li)) {
  display: none;
}

이러한 활용 방식은 기존에는 JavaScript로만 가능했던 조건부 스타일링을 순수 CSS로 구현할 수 있게 해줍니다.


JavaScript vs CSS :has() 비교

:has() 선택자가 없었던 시절에는 "이미지가 포함된 카드만 다르게 스타일링"하려면 JavaScript의 도움이 필요했습니다. 어떻게 구현했는지 기존 방식과 새로운 방식을 비교해보겠습니다.

기존 방식 (JavaScript 필요)

// 이미지 있는 카드 찾아서 클래스 추가
document.querySelectorAll('.card').forEach((card) => {
  if (card.querySelector('img')) {
    card.classList.add('has-image');
  }
});
.card.has-image {
  /* 이미지가 있는 카드 스타일 */
  padding-top: 0;
}

새로운 방식 (CSS만)

.card:has(img) {
  /* 이미지가 있는 카드 스타일 - 바로 적용! */
  padding-top: 0;
}

이러한 변화로 코드가 훨씬 간소화되었고, JavaScript에 의존하지 않아도 되므로 런타임 성능이 향상되었습니다. 또한 DOM이 동적으로 변경되어도 CSS가 자동으로 반응하므로 별도의 이벤트 리스너나 업데이트 로직이 불필요합니다.