scoped 스타일인데 자식 컴포넌트 루트에만 적용된 이유
문제 발생
부모에서 자식 컴포넌트의 버튼 여백을 조정하려 했는데 적용되지 않았습니다.
<template>
<BaseCard class="card">
<button class="action">저장</button>
</BaseCard>
</template>
<style scoped>
.card { margin: 16px; } /* 적용됨 */
.card .btn { padding: 4px; } /* BaseCard 내부 .btn — 적용 안 됨 */
.action { color: red; } /* 슬롯 내용 — 적용 안 됨 */
</style>.card의 여백만 먹었습니다.
원인 분석
scoped는 마법이 아니라 속성 선택자로의 컴파일입니다. 문서의 예시 그대로 .example { color: red }는 .example[data-v-f3f3eg9]가 되고, 템플릿 요소에는 같은 data-v-... 속성이 붙습니다.
그래서 .card .btn은 실제로 .card .btn[data-v-부모]가 됩니다. BaseCard 안쪽 요소에는 부모의 해시가 없으니 매칭되지 않습니다.
루트만 적용된 것도 문서에 설명이 있습니다 — scoped를 쓰면 부모 스타일이 자식 컴포넌트로 새지 않지만, 자식의 루트 노드는 부모의 scoped CSS와 자식의 scoped CSS 양쪽의 영향을 받습니다. 이는 부모가 레이아웃 목적으로 자식 루트를 스타일링할 수 있게 하려는 의도된 설계입니다.
슬롯 내용이 빠진 것도 별개 규칙입니다. 슬롯으로 넘긴 마크업은 넘긴 쪽의 스코프를 갖기 때문에, 받는 쪽에서 스타일링하려면 :slotted()가 필요합니다.
해결 방안
- 자식 내부까지 필요하면
:deep()입니다.
<style scoped>
.card :deep(.btn) { padding: 4px; }
</style>문서 예시대로 .a[data-v-...] .b로 컴파일되어 해시가 조상에만 붙습니다.
-
슬롯 내용은
:slotted()로 겨냥합니다. 어느 쪽 스코프인지 헷갈리면 이 두 의사 클래스 중 무엇이 필요한지를 먼저 정합니다. -
한 규칙만 전역으로 내보내려면
:global()을 씁니다. 별도<style>블록을 추가하지 않아도 됩니다. -
더 나은 방법은 자식이 API를 제공하는 것입니다.
:deep()은 자식의 내부 클래스 이름에 의존하는 결합입니다. 자식이 리팩터링되면 조용히 깨지므로, 자주 바꿔야 하는 값이라면 자식에 prop이나 CSS 변수를 열어주는 편이 낫습니다. -
성능 주의도 문서에 있습니다 — scoped 스타일이 클래스를 대체하지는 않습니다. 브라우저가 선택자를 처리하는 방식 때문에
p { color: red }는 scoped일 때(속성 선택자와 결합될 때) 몇 배 느립니다. 요소 선택자 대신 클래스를 씁니다. -
v-html로 넣은 내용은 scoped 스타일을 받지 않습니다. 문서가 명시하며, 이 경우에도 deep 선택자로 스타일링할 수 있습니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.