
솔직히 저는 꽤 오랫동안 순위 번호를 HTML에 직접 손으로 입력했습니다. 번호 하나 바꾸려고 태그를 하나씩 열고 닫는 게 당연한 줄 알았거든요. CSS Counter를 알게 된 건 실수를 반복하고 나서였는데, 알고 보니 이미 CSS 안에 자동 번호 기능이 내장되어 있었습니다.
이 글을 쓰면서 드는 생각이 있습니다. CSS Counter가 존재한다는 것을 늦게 알았다는 사실 자체보다, 그걸 몰랐던 이유가 더 흥미롭습니다. 저는 CSS를 "스타일을 입히는 도구"로만 인식하고 있었고, 번호 매기기는 당연히 HTML이나 JavaScript의 영역이라고 생각했습니다. 그 선입견이 학습의 범위를 좁혀놓고 있었던 겁니다.
하드코딩 번호가 무너지던 날
TOP 10 추천 글을 처음 발행했을 때 저는 모든 순위 번호를 HTML에 직접 박아 넣었습니다. 그 당시에는 별 생각이 없었는데, 문제는 글을 올린 다음 날 생겼습니다. 3위와 4위 항목을 바꿔야 하는 상황이 생긴 겁니다. 텍스트만 옮기면 끝날 것 같았지만, 번호 태그도 하나씩 수동으로 고쳐야 했습니다. 10개 항목 중간을 손으로 수정하다 보니 어느 순간 같은 번호가 두 개 생겼습니다. 독자 분이 댓글로 알려주셔서 알았는데, 그 민망함이 아직도 기억납니다.
이때부터 "번호를 HTML에 직접 쓰는 방식이 맞나?"라는 의문이 들기 시작했습니다. 이 질문은 단순히 CSS Counter를 쓰느냐 마느냐의 문제가 아닙니다. 데이터와 표현의 관계에 대한 질문이기도 합니다. 번호는 콘텐츠의 일부가 아니라 순서라는 구조적 정보입니다. 그 정보를 HTML에 직접 심어놓으면 순서가 바뀔 때마다 HTML도 함께 바뀌어야 합니다. 구조와 표현이 엉켜 있는 설계의 전형적인 문제입니다.
CSS Counter는 CSS 자체에 내장된 카운팅 기능으로, HTML 구조를 건드리지 않고도 요소에 자동으로 번호를 부여하는 속성입니다. counter-reset은 카운터를 초기화하는 속성으로 보통 부모 요소에 선언합니다. counter-increment는 지정한 요소를 만날 때마다 카운터 값을 1씩 올립니다. counter() 함수는 누적된 카운터 값을 화면에 출력하며, 가상 요소의 content 속성 안에서 사용합니다. 이 세 가지가 부모-자식 구조로 얽혀 있다는 걸 직접 목차를 만들어보면서 항목을 추가하고 삭제해보니 그제야 눈에 들어왔습니다. 항목을 하나 지워도 번호가 자동으로 당겨지는 게 확인되는 순간, 이거구나 싶었습니다.
자동번호 구현, 직접 써보니 이랬습니다
CSS Counter의 핵심은 ::before 가상 요소(pseudo-element)와의 조합입니다. ::before란 HTML 요소 앞에 CSS가 콘텐츠를 삽입할 수 있도록 만들어주는 가상의 요소로, 실제 HTML 문서에는 존재하지 않지만 화면에는 렌더링됩니다.
TOP 리스트 글에서 실제로 사용한 방식은 ::before에 counter() 함수를 넣고, :nth-child 선택자로 1위에는 금색, 2위에는 은색, 3위에는 동색 스타일을 입히는 방식이었습니다. 이후 순위 변경 작업은 텍스트만 옮기면 끝났습니다. 번호 태그를 하나씩 고치던 예전과 비교하면 체감 차이가 상당했습니다.
숫자 스타일도 선택지가 있습니다. counter() 함수의 두 번째 인자로 스타일을 지정할 수 있는데, decimal-leading-zero를 사용하면 01, 02, 03처럼 앞에 0이 붙는 형태가 되고, upper-roman을 쓰면 로마 숫자로 출력됩니다. decimal-leading-zero란 숫자 앞에 0을 붙여 자릿수를 맞추는 표기 방식으로, 디자인적으로 정렬감이 필요한 리스트에 쓰면 훨씬 깔끔하게 보입니다.
중첩 번호가 필요한 경우에는 counters() 함수를 사용합니다. counters()는 1.1, 1.2, 2.1처럼 계층 구조를 가진 번호를 자동으로 출력할 수 있습니다. 문서형 글이나 긴 기술 글을 쓸 때 목차에 한 번 적용해봤는데, 계층이 눈에 확 들어오는 느낌이라 가독성이 눈에 띄게 좋아졌습니다. 다만 실무에서는 자주 쓰이지 않고, 구조가 복잡한 글에서 필요할 때 꺼내 쓰는 편입니다.
가상요소 번호, 어디까지 믿어야 할까
CSS Counter가 편리한 건 맞는데, 쓰다 보면 신경 쓰이는 부분이 생깁니다. 제가 실무에서 부딪혔던 제약은 크게 두 가지였습니다.
첫 번째는 복사 문제입니다. ::before로 생성된 번호는 사용자가 드래그해서 복사할 때 포함되지 않습니다. 독자 입장에서 글 내용을 메모하려고 복사했을 때 번호가 빠지면, 문맥이 달라질 수 있습니다. 예를 들어 "3번째 방법이 제일 중요하다"라는 내용을 복사했는데 번호가 사라지면 어느 항목인지 알 수가 없게 됩니다. 이 경우라면 HTML에 직접 번호를 넣는 방식이 더 적합합니다. CSS Counter를 도입할 때 이 복사 동작을 미리 사용자 입장에서 테스트해보는 분이 많지 않습니다. 꽤 중요한 확인 항목인데 놓치기 쉽습니다.
두 번째는 SEO(검색엔진 최적화) 문제입니다. SEO란 검색엔진이 페이지 콘텐츠를 읽고 색인하는 과정에서 해당 페이지가 얼마나 잘 발견되도록 만드는 최적화 작업을 의미합니다. ::before 같은 가상 요소에 들어가는 콘텐츠는 검색엔진이 안정적으로 인덱싱하지 못할 수 있습니다. 번호가 단순한 숫자라면 큰 문제가 없겠지만, 만약 핵심 키워드가 가상 요소 안에 들어가 있다면 검색 노출에 불리하게 작용할 수 있습니다. 핵심 키워드는 반드시 HTML 본문에 직접 넣어야 한다는 점은, CSS Counter를 쓸 때 놓치기 쉬운 부분입니다.
여기서 조금 더 솔직하게 말하자면, 저는 CSS Counter를 처음 알았을 때 "이걸 모든 번호 표시에 써야겠다"는 생각이 먼저 들었습니다. 새로운 기술을 배우면 그걸 모든 상황에 적용하고 싶어지는 경향이 있는데, 그게 오히려 화를 불러오는 경우가 있습니다. 복사 문제와 SEO 제약을 직접 부딪혀보고 나서야 "이 도구가 어떤 상황에 맞고 어떤 상황에 맞지 않는가"를 판단할 수 있게 됐습니다. 도구를 배우는 것과 도구를 제대로 쓰는 것은 다른 일입니다.
CSS Counter를 쓰기 전에 이 세 가지는 먼저 확인하는 게 좋습니다. 번호가 복사 내용에 포함되어야 하는 콘텐츠인지, 가상 요소에 들어가는 내용이 SEO 핵심 키워드는 아닌지, counter-reset이 부모 요소에 counter-increment가 자식 요소에 각각 올바르게 배치되어 있는지. 순위 변경이 잦은 TOP 리스트나 자동 번호가 필요한 목차에는 확실히 효율적입니다. 어떤 방식이든 상황에 맞게 골라 쓰는 것, 그게 결국 실무에서 덜 고생하는 방법이라고 저는 생각합니다.
참고:
MDN Web Docs — CSS Counters
CSS-Tricks — Numbering in Style
W3Schools — CSS counter-reset