이번 편의 결과물: 카드·네브바 컴포넌트 스타일이 @scope (.project-card) to (button[data-open-project]) 형태로 격리되어, 자식 컴포넌트(button[data-open-project] 안)의 스타일이 새지 않습니다. 다루는 개념: 스코프 루트·한계(donut scope) 문법, 컴포넌트 스타일이 다른 곳으로 새지 않게 막기
이 편에서 만드는 파일
web-practice/portfolio/
└─ css/styles.css ~ (components 레이어를 @scope로 재작성)
web-practice/lessons/modern-css/
└─ 07_scope-component-isolation/ + (이번 편 스냅샷)
└─ notes.md개념 정리
@layer(03편)는 “어느 규칙이 이기는가”라는 우선순위 문제를 해결합니다. @scope는 “이 선택자가 어디까지 적용되는가”라는 범위 문제를 해결합니다. 두 문제는 다릅니다 — 레이어를 아무리 잘 나눠도, h2 같은 흔한 타입 선택자는 카드 안에서도 다른 섹션 안에서도 똑같이 걸립니다.
| 문법 조각 | 뜻 |
|---|---|
@scope (.project-card) { ... } | .project-card 자신부터 그 후손까지가 스코프 범위(스코프 루트) |
@scope (.project-card) to (button[data-open-project]) { ... } | .project-card부터 시작하되 button[data-open-project] 안쪽은 제외 |
:scope | 스코프 블록 안에서 스코프 루트 자신을 가리키는 의사 클래스 |
to (...) 뒤에 오는 선택자를 한계(lower boundary)라고 부릅니다. 스코프 루트와 한계 사이의 “도넛” 모양 범위에만 스타일이 적용된다고 해서 이 구조를 도넛 스코프라 부릅니다. 한계 안쪽(button[data-open-project]의 후손)은 이 스코프의 영향을 받지 않으므로, 카드의 “자세히” 버튼이 페이지 다른 곳의 .button과 똑같은 스타일을 그대로 쓰게 됩니다.
흔한 오해: @scope가 :has()처럼 조건을 판단하는 문법이라고 생각하기 쉽지만, @scope는 조건이 아니라 선택자의 유효 범위를 제한하는 블록 문법입니다. @scope 안의 각 규칙은 평소처럼 명시도 계산을 거칩니다.
실습
1. 격리 전 문제 상황 재현하기
/* portfolio/css/styles.css (수정 전) */
@layer components {
h2 {
font-weight: 700;
}
.project-card h2 {
font-size: var(--text-lg);
}
}h2처럼 흔한 타입 선택자는 카드뿐 아니라 페이지 안의 다른 컴포넌트(예: 각 섹션 제목, 프로젝트 상세 dialog의 제목)에도 그대로 걸립니다. .project-card h2라는 후손 선택자는 .project-card 안이라면 몇 단계 아래에 있든 다 걸려, 카드 내부에 다른 컴포넌트를 중첩하면 의도치 않게 스타일이 섞입니다.
2. @scope로 카드 컴포넌트 범위 가두기
/* portfolio/css/styles.css (수정 후) */
@layer components {
@scope (.project-card) to (button[data-open-project]) {
:scope {
display: flex;
flex-direction: column;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
overflow: hidden;
}
h2 {
font-weight: 700;
font-size: var(--text-lg);
padding-inline: var(--space-4);
}
p {
color: var(--color-text-muted);
padding-inline: var(--space-4);
}
img {
aspect-ratio: 3 / 2;
object-fit: cover;
}
}
}:scope는 .project-card 자신을 가리켜, 카드 바깥 레이아웃 규칙과 카드 내부 규칙을 한 블록 안에 같이 둘 수 있습니다. h2·p는 이 스코프 안에서만 유효하므로, 나중에 프로젝트 상세 dialog에 똑같은 제목 요소를 써도 서로 간섭하지 않습니다.
3. button[data-open-project] 안쪽을 한계로 제외하기
/* portfolio/css/styles.css */
@layer components {
/* button[data-open-project] 자체는 01편부터 정의된 공용 .button 스타일을 그대로 쓴다.
카드 스코프가 이 규칙을 덮어쓰지 않도록 한계로 지정한다. */
/* 프로젝트 상세 dialog는 별도의, 페이지 전체에서 재사용되는 스코프에서 관리 */
@scope (#project-dialog) {
:scope {
display: grid;
gap: var(--space-4);
}
.dialog-title {
/* 이 제목은 카드 스코프의 h2와 완전히 별개로 계산된다 */
font-size: var(--text-xl);
font-weight: 700;
}
}
}button[data-open-project]가 한계로 지정되어 있어서, 카드의 “자세히” 버튼이 열어 주는 #project-dialog가 카드 스코프 밖에 있는 별도 컴포넌트임을 CSS로도 분명히 나눌 수 있습니다. 카드 스코프의 h2와 dialog 스코프의 .dialog-title은 서로 다른 범위에서 완전히 독립적으로 계산됩니다.
4. 네브바에도 같은 방식 적용하기
/* portfolio/css/styles.css */
@layer components {
@scope (.site-header) {
:scope {
position: sticky;
top: 0;
height: var(--header-height);
border-block-end: 1px solid var(--color-border);
background: var(--color-bg);
}
.logo {
font-size: var(--text-lg);
font-weight: 700;
}
.site-nav {
display: flex;
gap: var(--space-6);
}
}
}5. 실행 및 확인
1. 브라우저 개발자 도구에서 .project-card 안의 h2 요소를 선택합니다.
2. Styles 패널에서 어떤 규칙이 적용되는지 확인합니다 — 카드 스코프의
h2(font-size: var(--text-lg))만 보이고, #project-dialog 스코프의
.dialog-title 규칙은 목록에 아예 나타나지 않아야 합니다.
3. .site-header 안의 .logo가 --text-lg 크기로, .project-card 안의
h2와 같은 변수를 쓰지만 서로 다른 스코프에서 독립적으로 계산됨을
확인합니다.직접 해보기
contact.html의.faq(아코디언) 영역에@scope (.faq)를 만들어,summary를 이 스코프 안에서만 스타일링해 다른 컴포넌트의summary와 섞이지 않게 해보세요.@scope없이.faq summary로 썼을 때와 비교해, 개발자 도구의 명시도 표시가 어떻게 다른지 확인해 보세요.
풀이 보기
@scope (.faq) {
:scope {
display: grid;
gap: var(--space-4);
}
summary {
font-weight: 600;
}
}@scope 안의 선택자는 스코프 근접도(proximity)만 추가로 계산에 끼어들 뿐, 기존 명시도 규칙 자체는 그대로 유지됩니다.
자주 하는 실수
| 증상 | 원인 | 고치는 법 |
|---|---|---|
:scope 규칙이 카드 바깥에도 적용됨 | @scope (...) 괄호를 빼고 그냥 :scope만 씀 | @scope (스코프 루트 선택자) { } 형태를 갖춘다 |
| 한계 안쪽 요소에도 스타일이 그대로 적용됨 | to (...) 부분을 빠뜨림 | 한계로 제외할 선택자를 to (...)에 명시한다 |
| 두 스코프의 같은 클래스가 서로 영향을 줌 | 스코프 블록을 안 쓰고 후손 선택자로만 작성함 | 컴포넌트 단위로 @scope 블록을 분리한다 |
| 오래된 Firefox에서 스타일이 아예 안 먹힘 | @scope는 2026-03부터 Baseline이라 그 이전 버전 브라우저에서는 미지원 | @supports (@scope) 폴백으로 후손 선택자 버전을 함께 준비하거나, 최신 브라우저로 확인한다 |