본문 바로가기
컴퓨터 인터넷 모바일 it

CSS의 :not() 셀렉터 활용, 티스토리 BookClub li 디스크 텍스트 겹침 해결

by 하누혀누IT 2025. 4. 25.
반응형

CSS의 :not() 셀렉터 활용, 티스토리 BookClub li 디스크 텍스트 겹침 해결

웹 문서를 마크업하다 보면 “특정 요소만 빼고 전부 디자인하고 싶다”는 요구가 자주 등장합니다. 예를 들어 ol > li에만 들여쓰기를 적용했더니 ol > li > ul > li의 불릿 아이콘이 텍스트와 겹쳐 보이는 난감한 상황을 겪어 보셨을 겁니다. 이때 ‘선택적 제외(Selective Exclusion)’를 구현해 주는 것이 CSS :not() 셀렉터입니다.

HTML 구조가 복잡해질수록 하위 요소마다 새로운 클래스를 주는 방식은 관리 비용이 기하급수적으로 커집니다. :not()을 적절히 결합하면 불필요한 클래스 증식을 막고, 유지보수성이 높은 스타일 시트를 작성할 수 있습니다. 이 글에서는 :not() 셀렉터의 문법적 특성과 구체적 사용법, 그리고 실전에서 만나게 되는 퍼포먼스 이슈까지 전부 다룹니다.

:not() 셀렉터 기본 개념

구조적 부정(Structural Negation)

:not()은 “괄호 안 조건을 만족하지 않는 모든 요소”를 선택합니다.

  • 문법 A:not(selector)
    A 가 선택한 집합에서 selector에 해당하지 않는 노드를 남기는 식으로 동작합니다.
  • 예시 .btn:not(.primary).btn 클래스를 가진 요소 중 primary 클래스가 없는 버튼만 선택.

단일 인자 원칙(Selectors Level 3)

CSS Selectors Level 3에 정의된 :not()은 괄호에 단 하나의 간단 선택자(Simple Selector)만 허용합니다. 콤마로 여러 조건을 나열하는 기능은 Level 4부터 지원됩니다. 따라서 브라우저 호환성을 넓게 가져가려면 복수 조건은 중첩해서 써야 합니다.

/* 구식 브라우저 호환 – 중첩 */
a:not(.disabled):not(.external) { ... }

:not() 셀렉터와 선택자 우선순위(Specificity)

Specificity 값 계산

  • :not() 자체는 0,0,0 값을 갖지만 괄호 안 선택자의 점수를 그대로 승계합니다.
  • .box:not(#hero)는 클래스(0,0,1,0) + 아이디(0,1,0,0) → 최종 0,1,1,0.

우선순위 충돌 예방법

신중하지 않으면 :not()에 넣은 복합 선택자가 전체 스타일 시트를 뒤엎을 수도 있습니다. 설계 초기 단계에서 우선순위를 표로 정리해 두고, :not()을 이용할 땐 항상 “보다 낮은 점수의 선택자로 덮어쓰겠다”는 원칙을 세우면 안전합니다.

:not() 셀렉터 결합 테크닉

자식 결합자 >

리스트 들여쓰기 예처럼 부모 ol만 타깃할 때 다음 패턴이 유용합니다.

.entry-content ol:not(ul) > li {
  text-indent: -15px;
}
  • ol:not(ul) : ul의 하위 ol을 제외한 **루트 ol**만 남김
  • > : 직계 li에만 적용해 후손 li 겹침 방지

형제 결합자 +, ~

h2:not(.no-border) + p { border-top: 1px solid #ddd; }

no-border 클래스가 없는 h2 다음 절친 p에만 보더를 주는 식으로, 챕터 타이틀 사이 간격을 자동화할 때 자주 쓰입니다.

:not() 성능 인사이트

렌더 트리 탐색 모델

브라우저는 선택자 우측부터 좌측 순으로 매칭합니다(Right-to-Left Matching). :not() 내 조건이 복잡해질수록 트리 검사가 늘어나므로:

  • 작은 범위부터 제외하고,
  • 동일한 필터가 반복될 경우 공통 클래스로 치환
    이 두 가지 원칙으로 페인트 누수를 최소화할 수 있습니다.

리플로(Reflow)를 줄이는 패턴

/* Good */
.article *:not(img):not(video) { line-height: 1.6; }

/* Bad – 모든 요소 필터 후 속성 추가 */
.article *:not(.skip) { ... }

와일드카드 * 뒤에 :not()을 걸면 DOM 전체를 한 번 훑어야 하므로, 특정 요소에만 긍정 선택자를 써서 범위를 먼저 줄이는 편이 훨씬 빠릅니다.

티스토리 BookClub 스킨 사례 분석

문제 진단

기본 스킨 코드:

.entry-content ol li {
  position: relative;
  margin-bottom: 10px;
  list-style: inherit;
  text-indent: -15px;
}
  • ol > li > ul > li 구조에서 disc 아이콘이 텍스트 위로 겹침
  • 절댓값 -15px 이 직계 li뿐 아니라 손자 li에도 적용된 탓

개선 코드

/* 1. 공통 초기화 */
.entry-content ol li {
  position: relative;
  margin-bottom: 10px;
  list-style: inherit;
  text-indent: 0;      /* 초기화 */
}

/* 2. 직계 ol > li에만 음수 들여쓰기 */
.entry-content ol:not(ul) > li {
  text-indent: -15px;
}
  • .entry-content ol:not(ul)ul 내부 ol 제어 X
  • > 로 직계 li에만 영향
    이렇게 하면 하위 ul > li의 disc가 정상적으로 표시되고, 루트 ol > li만 들여쓰기가 적용됩니다.

자주 쓰이는 :not() 패턴 5가지

  1. 다크 모드 토글
  2. body:not(.dark) .dark-only { display: none; }
  3. 모바일 전용 숨김
  4. @media (max-width: 767px) { .nav-item:not(.mobile-keep) { display: none; } }
  5. 폼 검증 하이라이트
  6. input:required:not(:valid) { border-color: red; }
  7. 데이터 속성 필터링
  8. [data-role]:not([data-role="admin"]) { opacity: .4; }
  9. 접근성 포커스 링 제거
  10. button:not(:focus-visible) { outline: none; }

접근성과 유지보수 전략

의미론적 클래스 우선

class="skip" 같은 중립 네이밍을 권장합니다. 부정을 정의하는 이름(not-this)은 동료 개발자에게 혼란을 주기 쉬우므로, 의미론으로 그룹을 묶되 :not()으로 원치 않는 상황을 제외하는 방식이 이해하기 쉽습니다.

스타일 문서화

스타일 가이드를 작성할 때 :not() 로직은 “예외 규칙” 챕터로 별도 구분하면 좋습니다. 코멘트만으로는 시간이 지나면 원래 목적을 잊기 쉽기 때문에, 마크다운 가이드 혹은 Storybook에 “적용 시나리오 – 제외 조건 – 우선순위”를 명시적으로 기록하세요.

:not() 셀렉터, Level 4 그리고 미래

CSS Selectors Level 4부터 :not()콤마 구분 복합 조건을 허용합니다.

/* Level 4 문법 – 최신 브라우저만 지원 */
a:not([href^="http"], [href^="//"]) { color: red; }

ESR(Extended Support Release) 브라우저, 혹은 IE 잔존 환경까지 커버해야 한다면 트랜스파일(예: PostCSS)이나 폴리필 전략을 고려해야 합니다.

결론

:not()은 “조건을 제외한다”는 단순 발상으로부터, 유지보수성·접근성·성능을 한 번에 챙길 수 있는 강력한 도구입니다. 특히 리스트·내비게이션·폼처럼 반복 구조가 깊어질수록 클래스를 남발하지 않고 구조적인 제어를 가능케 해 줍니다. 다만 Specificity와 렌더링 성능을 충분히 고려하여,

  1. 좁은 범위부터 제외하고,
  2. 요소 트리를 최소 탐색하도록 선택자를 설계한다면, :not()은 프론트엔드 생산성을 극적으로 끌어올려 줄 것입니다. 이제 프로젝트 스타일 시트에서 ‘제외하고 싶은 상황’이 생긴다면, CSS :not()을 먼저 떠올려 보세요.

반응형

댓글