본문으로 건너뛰기
개발 머꼬
개발 노트Vue
hohyeon.dev17

scoped 스타일인데 자식 컴포넌트 루트에만 적용된 이유

  • #CSS
  • #Engineering Note
  • #Vue

문제 발생

부모에서 자식 컴포넌트의 버튼 여백을 조정하려 했는데 적용되지 않았습니다.

<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()가 필요합니다.

해결 방안

  1. 자식 내부까지 필요하면 :deep()입니다.
<style scoped>
.card :deep(.btn) { padding: 4px; }
</style>

문서 예시대로 .a[data-v-...] .b로 컴파일되어 해시가 조상에만 붙습니다.

  1. 슬롯 내용은 :slotted()로 겨냥합니다. 어느 쪽 스코프인지 헷갈리면 이 두 의사 클래스 중 무엇이 필요한지를 먼저 정합니다.

  2. 한 규칙만 전역으로 내보내려면 :global()을 씁니다. 별도 <style> 블록을 추가하지 않아도 됩니다.

  3. 더 나은 방법은 자식이 API를 제공하는 것입니다. :deep()은 자식의 내부 클래스 이름에 의존하는 결합입니다. 자식이 리팩터링되면 조용히 깨지므로, 자주 바꿔야 하는 값이라면 자식에 prop이나 CSS 변수를 열어주는 편이 낫습니다.

  4. 성능 주의도 문서에 있습니다scoped 스타일이 클래스를 대체하지는 않습니다. 브라우저가 선택자를 처리하는 방식 때문에 p { color: red }는 scoped일 때(속성 선택자와 결합될 때) 몇 배 느립니다. 요소 선택자 대신 클래스를 씁니다.

  5. v-html로 넣은 내용은 scoped 스타일을 받지 않습니다. 문서가 명시하며, 이 경우에도 deep 선택자로 스타일링할 수 있습니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.