한 줄 요약 — 의사 요소
::before와::after는 HTML에 태그를 더 쓰지 않고도 한 요소의 본문 앞(::before)과 뒤(::after)에 장식을 자동으로 붙이는 CSS다. 단,content속성이 없으면 끈을 매달 자리는 있어도 끈 자체가 없는 셈이라 아무것도 보이지 않는다 — 빈 장식이라도content: ""가 반드시 있어야 나타난다.content에는 글자뿐 아니라 따옴표,data-*값(attr()), 자동 번호(counter)까지 담을 수 있다.
학습 목표#
::before와::after가 각각 본문 앞·뒤에 붙는다는 것을 알고 구분할 수 있다.content속성이 의사 요소의 필수 조건임을 이해하고 빈 값까지 줄 수 있다.content에 글자·따옴표·attr()값을 담을 수 있다.counter로 목록 번호를 직접 적지 않고 자동으로 매길 수 있다.- 장식용 의사 요소가 스크린리더에 잘못 읽히지 않게 다룰 수 있다.
오늘의 비유 — 본문 앞뒤에 자동으로 매다는 작은 장식 끈#
잘 꾸민 책의 한 단락을 떠올려 보자. 단락의 글자(본문)는 그대로 두고, 시작 지점에 작은 장식 끈을, 끝 지점에 또 하나의 끈을 매달아 둔 모습이다. 이 끈은 책에 인쇄된 글이 아니다. 글자를 한 자도 고치지 않고 나중에 매단 장식일 뿐이라, 책 내용을 그대로 옮겨 적으면 끈은 따라오지 않는다.
::before는 본문 앞에 매단 끈, ::after는 본문 뒤에 매단 끈이다. 둘 다 HTML 태그가 아니라 CSS가 만들어 붙이는 가짜 요소라서 "의사(pseudo, 흉내 낸) 요소"라고 부른다. 중요한 건 끈에 무엇이 적혀 있는지다. 끈을 매달 고리(::before)만 만들고 정작 끈에 아무것도 안 달면(=content 없음) 화면엔 아무 일도 일어나지 않는다. 그래서 의사 요소는 늘 "어느 쪽 끈인가(::before/::after)"와 "끈에 뭘 다는가(content)"를 한 쌍으로 생각한다.
핵심 개념#
::before와 ::after — 본문 앞과 뒤, 두 개의 끈#
같은 요소에 끈을 두 개까지 맬 수 있다. 앞쪽은 ::before, 뒤쪽은 ::after다. 둘은 붙는 위치만 다를 뿐 쓰는 법은 똑같다.
.note::before {
content: "📌 "; /* 본문 앞에 붙는 끈 */
}
.note::after {
content: " ←"; /* 본문 뒤에 붙는 끈 */
}
<p class="note">저장 잊지 마세요</p>라고만 적어도, 화면에는 📌 저장 잊지 마세요 ←처럼 앞뒤로 장식이 붙는다. HTML에는 글자만 있고, 앞뒤 끈은 CSS가 매단 것이다. 끈은 기본적으로 인라인(글자처럼 한 줄에 흐름)으로 생기므로, 박스처럼 크기를 주고 싶으면 지난 회차에서 본 display: inline-block을 따로 줘야 한다.
content가 없으면 끈도 없다#
의사 요소의 가장 큰 함정이자 가장 중요한 규칙이다. content 속성이 아예 없으면 의사 요소는 만들어지지조차 않는다. 고리는 걸었는데 끈을 안 단 셈이라, 색을 칠하든 크기를 주든 화면엔 아무것도 안 보인다.
/* (content가 없어 의사 요소 자체가 생기지 않습니다 — 아래 색·크기는 무용지물입니다) */
.line::before {
width: 40px;
height: 2px;
background: #1f2937;
}
장식용 막대·점선처럼 글자가 필요 없는 경우라도, 빈 끈을 뜻하는 content: ""를 반드시 줘야 한다. 그제야 의사 요소가 생기고 크기·배경이 먹는다.
.line::before {
content: ""; /* 빈 끈이라도 있어야 나타난다 */
display: inline-block;
width: 40px;
height: 2px;
background: #1f2937;
}
content에 담을 수 있는 것 — 글자, 따옴표, attr()#
끈에 적을 수 있는 건 평범한 글자만이 아니다. 자주 쓰는 세 가지를 보자.
/* 1) 그냥 글자 */
.tip::before { content: "TIP "; }
/* 2) 따옴표 — 인용구 앞뒤에 큰따옴표를 자동으로 */
blockquote::before { content: "\201C"; } /* “ */
blockquote::after { content: "\201D"; } /* ” */
/* 3) attr() — HTML의 data-* 값을 그대로 끌어와 끈에 출력 */
.badge::after { content: " (" attr(data-count) ")"; }
세 번째 attr()가 특히 쓸모 있다. <span class="badge" data-count="3">알림</span>이라고만 적으면 화면엔 알림 (3)이 나온다. 숫자 3을 CSS에 박지 않고 HTML 속성에서 가져오므로, 값이 바뀌어도 HTML의 data-count만 고치면 된다. 끈에 인쇄될 내용을 본문 쪽에서 정해 주는 셈이다.
counter — 번호를 자동으로 매기는 끈#
목록 항목에 1., 2.처럼 번호를 손으로 적지 않고 CSS가 세어 붙이게 할 수 있다. 부모에서 counter-reset으로 계수기를 0으로 두고, 각 항목에서 counter-increment로 하나씩 올린 뒤 그 값을 content에 출력한다.
.steps { counter-reset: step; } /* 계수기를 0으로 시작 */
.steps li::before {
counter-increment: step; /* 항목마다 1씩 증가 */
content: counter(step) ". "; /* 그 숫자를 앞 끈에 출력 */
font-weight: 700;
}
항목을 중간에 추가하거나 순서를 바꿔도 번호가 자동으로 다시 매겨진다. ol의 기본 번호로 충분할 때는 굳이 쓸 필요가 없지만, 1단계·Q1 같은 꾸민 번호가 필요할 때 유용하다.
함께 따라하기 — 체크 표시 끈과 라벨 뱃지#
::before로 항목 앞에 초록 체크 표시를 붙이고, ::after와 attr()로 빈 칸을 라벨 뱃지로 바꿔 본다.
<ul class="checklist">
<li>의사 요소 이해하기</li>
<li>content 속성 쓰기</li>
</ul>
<span class="tag" data-label="HTML"></span>
.checklist li {
list-style: none; /* 기본 점 제거 */
}
.checklist li::before {
content: "✓ "; /* 앞에 붙는 체크 끈 */
color: #16a34a;
font-weight: 700;
}
.tag::after {
content: attr(data-label); /* data-label 값을 라벨로 */
padding: 2px 10px;
border-radius: 999px;
background: #1f2937;
color: #fff;
font-size: 0.8rem;
}
저장하고 열어 보면, 목록의 각 줄 앞에 초록색 ✓가 붙어 체크리스트처럼 보인다. 그리고 아무 글자도 없던 빈 span은 HTML이라고 적힌 짙은 알약 모양 뱃지로 바뀐다 — 그 글자는 HTML 본문이 아니라 data-label 값을 ::after가 끌어와 매단 끈이다. data-label="CSS"로 바꾸면 뱃지 글자도 따라 바뀐다.
흔한 실수 3가지#
1. content 속성을 빼면 의사 요소가 안 보인다#
가장 흔한 막힘이다. ::before에 색·크기를 다 줬는데 화면엔 아무것도 없다.
/* (content가 없어 끈 자체가 생기지 않습니다) */
.divider::before {
display: inline-block;
width: 60px;
height: 2px;
background: #999;
}
의사 요소는 content가 있어야 비로소 존재한다. 장식용 막대처럼 글자가 필요 없어도 빈 끈인 content: ""를 반드시 넣는다. 이 한 줄이 빠져서 안 보이는 경우가 정말 많으니, "의사 요소가 안 보인다"면 content부터 확인한다.
.divider::before {
content: ""; /* 이 줄이 있어야 나타난다 */
display: inline-block;
width: 60px;
height: 2px;
background: #999;
}
2. 의사 요소를 진짜 img처럼 다루려고 한다#
content: url(...)로 이미지를 넣을 수는 있다. 그래서 로고나 사진 같은 의미 있는 이미지까지 의사 요소로 넣으려는 시도가 나온다.
/* (의미 있는 이미지를 끈에 매달면 alt를 줄 수 없습니다) */
.logo::before {
content: url(/logo.png);
}
문제는 끈에 매단 이미지에는 alt를 줄 수 없다는 점이다. 의사 요소는 어디까지나 장식이고, 본문이 아니다. 로고·제품 사진처럼 내용을 전달하는 이미지는 지난 강의에서 배운 진짜 <img> 태그로 넣고 alt를 적는다. 의사 요소는 점·선·따옴표·작은 아이콘 같은 곁다리 장식에만 쓴다.
<img src="/logo.png" alt="Life is One Coin 로고" />
3. 스크린리더가 읽으면 안 되는 장식을 content로 넣는다#
content에 넣은 글자는 일부 스크린리더가 그대로 읽는다. 그래서 정보를 오직 의사 요소로만 전달하면 위험하다.
/* (이 별표가 "필수"라는 정보를 의사 요소로만 전달하고 있습니다) */
.field-label::after {
content: " *";
color: #dc2626;
}
색만으로 정보를 주지 않는다는 원칙(지난 접근성 메모)과 같은 맥락이다. "필수"라는 사실이 빨간 별표 하나에만 걸려 있으면, 그 별표를 못 읽는 환경에선 정보가 사라진다. 의미가 있는 내용은 HTML 본문에 글자로 두고, 의사 요소는 순수 장식으로만 쓴다. 별표를 시각 장식으로 남기더라도, 본문에 "(필수)" 같은 글자를 함께 두는 식이다.
<label class="field-label">이메일 <span class="req">(필수)</span></label>
오늘 배운 것 체크리스트#
-
::before는 본문 앞,::after는 본문 뒤에 붙는다. -
content가 없으면 의사 요소는 아예 생기지 않는다 — 빈 장식도content: "". -
content에 글자·따옴표·attr()값·counter를 담을 수 있다. -
attr()로data-*값을,counter로 자동 번호를 끌어온다. - 의미 있는 이미지·정보는 본문에 두고, 의사 요소는 장식으로만 쓴다.
자주 묻는 질문#
Q. 인용구 앞뒤에 따옴표를 자동으로 넣으려면 어떻게 하나요?
A. blockquote::before와 blockquote::after의 content에 따옴표 문자를 주면 됩니다. 곡선 따옴표는 유니코드로 content: "\201C"(여는 큰따옴표 “), content: "\201D"(닫는 큰따옴표 ”)처럼 적습니다. 인용문마다 따옴표를 직접 타이핑하지 않아도, blockquote로 감싸기만 하면 CSS가 앞뒤에 따옴표 끈을 매답니다.
Q. 목록 번호를 직접 안 쓰고 CSS로 자동으로 매길 수 있나요?
A. counter를 씁니다. 부모에 counter-reset: 이름으로 계수기를 만들고, 각 항목의 ::before에서 counter-increment: 이름으로 1씩 올린 뒤 content: counter(이름) ". "로 출력합니다. 항목을 추가하거나 순서를 바꿔도 번호가 자동으로 다시 매겨져, 1단계·Q1처럼 꾸민 번호를 만들 때 편합니다.
Q. content에 attr()로 가져온 값은 어디서 정하나요?
A. HTML 요소의 속성에서 가져옵니다. content: attr(data-count)라고 쓰면 그 요소의 data-count 속성 값이 그대로 끈에 출력됩니다. 값을 바꾸고 싶으면 CSS가 아니라 HTML의 data-* 속성만 고치면 되고, 표준 속성(예: attr(title))도 같은 방식으로 끌어올 수 있습니다.
다음 시간 예고#
내일은 Transform — translate, rotate, scale, skew를 한 번에를 다룬다. 요소를 자리에서 떼지 않고 옮기고·돌리고·키우는 변형을, 위치 속성보다 가볍게 처리하는 법을 짚는다.