[vue3] component style 상속 우선순위 살펴보기

jellykelly·2025년 1월 20일
0

Vue

목록 보기
4/4

Vue에서 scoped 스타일은 해당 컴포넌트의 요소에만 스타일이 적용되도록 제한하지만, 실제로는 이름이 충돌할 경우 특정 규칙에 따라 우선순위가 정해집니다.

HTML+CSS 처럼 cascade 방식으로 스타일이 적용될 줄 알았던 코드가 뱉은 오류와 해결방법을 정리해봅니다.

코드 구조

App.vue

<template>
  <ParentComponent />
</template>

Parent component

<template>
  <ChildComponent />
  <AnotherComponent />
</template>

<script setup>
import ChildComponent from '@/components/ChildComponent.vue'
import AnotherComponent from '@/components/AnotherComponent.vue'
</script>

<style lang="scss" scoped>
.child-wrap {
  background-color: rgba(blue, 0.3);

  .h1 {
    color: blue;
  }
}
</style>

Child component

<template>
  <div class="child-wrap box">
    <div class="h1">CHILD COMPONENT</div>
  </div>
</template>

<script setup>
import AnotherComponent from '@/components/AnotherComponent.vue'
</script>

<style lang="scss" scoped>
.child-wrap {
  background-color: rgba(red, 0.3);

  .h1 {
    color: red;
  }
}
</style>

Another Component

<template>
  <div class="box">
    <div>Another COMPONENT</div>
  </div>
</template>

case 1. Parent와 Child에 동일한 스타일을 적용했을 때

결과

  1. div.child-wrap
    1. ParentComponent.vue의 background-color: rgba(blue, 0.3) 출력
    2. ChildComponent.vue의 background-color: rgba(red, 0.3) 출력
    3. ParentComponent.vue의 background-color: rgba(blue, 0.3)가 우선순위를 가지고 출력됨

  2. div.h1

    1. ParentComponent.vue의 스타일은 출력되지 않음
    2. ChildComponent.vue의 color: red 출력

div.child-wrap의 background-color가 ParentComponent.vue의 스타일이 우선순위를 갖는 이유

Vue에서 scoped 스타일은 해당 컴포넌트의 요소에만 스타일이 적용되도록 제한하지만, 실제로는 이름이 충돌할 경우 특정 규칙에 따라 우선순위가 정해집니다. 이 경우, ParentComponent.vueChildComponent.vue 모두 .child-wrap 클래스를 사용하고 있지만, 각 컴포넌트의 스타일이 scoped로 적용되므로, 실제로 적용되는 스타일의 우선순위는 CSS 셀렉터의 특이성(specificity) 및 선언 순서에 따라 달라집니다.

  • ParentComponent.vue의 스타일은 부모 컴포넌트에서 자식 컴포넌트인 ChildComponent를 감싸는 .child-wrap 요소에 적용됩니다.
  • ChildComponent.vue에서도 .child-wrap 클래스가 선언되어 있으므로, 이 클래스에 대한 스타일이 자식 컴포넌트에 영향을 미칩니다.

우선순위가 결정되는 방식

  • scoped 스타일은 각 컴포넌트마다 고유한 클래스 이름을 자동으로 생성하여 스타일을 적용합니다.
  • 이때 ParentComponent.vue에서 선언한 스타일은 부모 컴포넌트에서 자식 컴포넌트를 감싸고 있는 요소에 적용됩니다.
  • ChildComponent.vue의 스타일은 해당 컴포넌트에만 영향을 미치기 때문에, ParentComponent.vue에서 설정한 background-color는 여전히 자식 컴포넌트의 .child-wrap 요소에 상속되지 않고 겹칠 수 있습니다.

따라서 부모 스타일이 더 우선시되는 이유ParentComponent.vuebackground-color: rgba(blue, 0.3);가 자식 컴포넌트에서 적용되는 ChildComponent.vuebackground-color: rgba(red, 0.3);보다 더 늦게 적용되기 때문입니다.

div.h1에서 ParentComponent.vue의 스타일이 출력되지 않고 ChildComponent.vue의 스타일만 출력되는 이유

여기서 발생하는 이유는 CSS 셀렉터의 특이성(specificity) 때문입니다. 각 컴포넌트의 scoped 스타일은 기본적으로 해당 컴포넌트에만 적용되도록 만들어집니다. 그리고 각 컴포넌트에서 선언된 스타일의 특이성도 다를 수 있기 때문에, 특정 스타일이 더 우선시될 수 있습니다.

  • ParentComponent.vue에서 .h1colorblue로 설정했으나, ChildComponent.vue에서는 .h1colorred로 설정했습니다.
  • ChildComponent.vue에서 선언된 color: redscoped 스타일로 적용되기 때문에 ChildComponent 내의 .h1 요소에만 영향을 미칩니다.
  • ParentComponent.vue에서의 스타일도 scoped로 적용되지만, 이 스타일은 자식 컴포넌트의 .h1에 영향을 미치지 않습니다.

즉, ParentComponent.vue의 스타일이 적용되지 않는 이유는 부모 컴포넌트의 스타일이 자식 컴포넌트에 영향을 미치지 않기 때문입니다. 각 컴포넌트의 scoped 스타일은 기본적으로 해당 컴포넌트에만 국한되므로, 자식 컴포넌트인 ChildComponent.vue에서는 ChildComponent.h1에만 red 색상이 적용됩니다.

case 2. case1의 코드에서 ChildComponet에 를 추가했을 때


Parent component

<template>
  <ChildComponent />
</template>

<script setup>
import ChildComponent from '@/components/ChildComponent.vue'
</script>

<style lang="scss" scoped>
.child-wrap {
  background-color: rgba(blue, 0.3);

  .h1 {
    color: blue;
  }
}
</style>

Child component

<template>
  <div class="child-wrap box">
    <div class="h1">CHILD COMPONENT</div>
  </div>
  <AnotherComponent />
</template>

<script setup>
import AnotherComponent from '@/components/AnotherComponent.vue'
</script>

<style lang="scss" scoped>
.child-wrap {
  background-color: rgba(red, 0.3);

  .h1 {
    color: red;
  }
}
</style>

결과

  1. div.child-wrap
    1. ParentComponent.vue의 스타일은 출력되지 않음
    2. ChildComponent.vue의 background-color: rgba(red, 0.3) 만 출력

  2. div.h1

    1. ParentComponent.vue의 스타일은 출력되지 않음
    2. ChildComponent.vue의 color: red 출력

이 문제의 핵심은 Vue의 scoped 스타일과 컴포넌트 간의 DOM 구조가 어떻게 스타일에 영향을 미치는지에 있습니다.

상황 분석

  1. 기본 구조 (ChildComponent에 <AnotherComponent />가 없을 때):

    • ParentComponent.vue에서 .child-wrapbackground-colorrgba(blue, 0.3)로 설정하고 있습니다.
    • ChildComponent.vue에서 .child-wrapbackground-colorrgba(red, 0.3)로 설정하고 있습니다.

    이때 ChildComponent 내부에는 <AnotherComponent />가 포함되어 있지 않으므로, ParentComponent.vue의 스타일 (background-color: rgba(blue, 0.3))이 ChildComponent.vue.child-wrap에 적용됩니다. 이 스타일은 부모 컴포넌트인 ParentComponent.vue에서 정의한 것이지만, ChildComponent의 DOM 요소에 scoped 스타일을 적용할 때 부모 스타일도 덮어쓰지 않고 그대로 적용됩니다.

  2. <AnotherComponent />ChildComponent에 추가된 경우:

    • 이제 ChildComponent.vue 내에 <AnotherComponent />가 추가되었습니다.
    • 하지만 <AnotherComponent />의 추가가 ParentComponent.vue의 스타일에 영향을 주지는 않습니다. 그럼에도 불구하고, ChildComponent.vue 내에서 .child-wrap에 적용된 background-color: rgba(red, 0.3)가 최종적으로 우선시됩니다.

중요한 차이점

  • scoped 스타일은 기본적으로 해당 컴포넌트 내의 DOM에만 스타일을 적용하게 되어 있습니다. 즉, ParentComponent.vue의 스타일은 ParentComponent.vue의 DOM에만 영향을 미치며, ChildComponent.vue 내에서 .child-wrap 요소에 대한 스타일은 ChildComponent.vue 내에서 선언된 background-color: rgba(red, 0.3)로 덮어씌워집니다.
  • ChildComponent.vue.child-wrap 스타일이 자식 컴포넌트에서 AnotherComponent를 추가했는지 여부와는 관계없이 계속 적용됩니다.

왜 ParentComponent.vue의 background-color는 출력되지 않는가?

ChildComponent.vue에서 .child-wrap 스타일을 background-color: rgba(red, 0.3)로 정의하고 있기 때문에 ChildComponent.vue의 스타일이 우선시됩니다. 이는 scoped 스타일의 우선순위에 따른 결과입니다. ParentComponent.vue의 스타일은 자식 컴포넌트인 ChildComponent에 영향을 미치지 않으며 자식 컴포넌트에서 정의된 스타일이 덮어씌워지게 됩니다.

요약

  • ParentComponent.vuebackground-color: rgba(blue, 0.3)ParentComponent의 스타일로, ChildComponent.vue 내의 .child-wrap 요소에 직접 영향을 미치지 않습니다.
  • ChildComponent.vue 내에서 .child-wrap의 스타일을 background-color: rgba(red, 0.3)로 설정했기 때문에, 이 스타일이 최종적으로 적용됩니다.
  • <AnotherComponent />의 추가는 ParentComponent.vue의 스타일에 영향을 주지 않으며, ChildComponent.vue의 스타일만 적용됩니다.

결론적으로, AnotherComponent를 추가하더라도 ChildComponent.vue.child-wrap 스타일에 영향을 미치지 않으며, ChildComponent.vue의 스타일인 background-color: rgba(red, 0.3)만 적용됩니다.

Vue의 scoped style system의 작동

  1. 단일 자식 컴포넌트일 경우
    • 부모 컴포넌트가 단 하나의 자식 컴포넌트만 포함할 때, Vue는 자동으로 부모의 스코프 속성(예: data-v-parent)을 자식 컴포넌트의 루트 요소에 추가합니다.
    • 이로 인해 부모 컴포넌트의 스코프된 스타일이 자식 컴포넌트의 루트 요소에 적용될 수 있습니다.

  2. 여러 자식 요소나 컴포넌트가 있을 경우
    • 부모 컴포넌트가 여러 자식 요소나 컴포넌트를 포함할 때, Vue는 각 자식 컴포넌트를 독립적으로 취급합니다.
    • 이 경우, 각 자식 컴포넌트는 자신의 고유한 스코프 속성(예: data-v-child)만 가지게 되며, 부모의 스코프 속성은 자동으로 추가되지 않습니다.

  3. 컴포넌트 내부 요소
  • 컴포넌트 내부의 모든 요소들은 해당 컴포넌트의 스코프 속성을 가집니다.
  • 이는 해당 컴포넌트에 정의된 스코프된 스타일이 내부 요소들에 적용될 수 있게 합니다.

이러한 동작의 이유는 Vue가 컴포넌트 간의 스타일 격리를 유지하면서도, 단일 자식 컴포넌트의 경우 부모-자식 관계를 좀 더 밀접하게 유지하려는 의도 때문입니다.따라서, 컴포넌트 구조를 변경하면(예: 단일 자식에서 여러 자식으로, 또는 그 반대로) 스코프된 스타일의 적용 방식이 달라질 수 있습니다. 이는 Vue의 설계된 동작이며, 개발자가 컴포넌트 구조와 스타일 적용을 더 세밀하게 제어할 수 있게 해줍니다.

  1. Vue의 스코프된 스타일 작동 방식:Vue에서 <style scoped>를 사용하면, 해당 컴포넌트의 요소들에 고유한 속성(예: data-v-123abc)을 추가합니다. 그리고 CSS 선택자에도 이 속성을 추가하여 스타일이 해당 컴포넌트에만 적용되도록 합니다.

  2. 컴포넌트 구조 분석:
    • ParentComponent는 ChildComponent를 포함합니다.
    • ChildComponent는 자체 템플릿과 AnotherComponent를 포함합니다.

  3. 스타일 적용 과정:
    • ParentComponent의 스타일: css.childWrap[data-v-parent] { background-color: rgba(blue, 0.3); }
    • ChildComponent의 스타일: css.childWrap[data-v-child] { background-color: rgba(red, 0.3); }

  4. 실제 적용:
    • ChildComponent의 템플릿에 있는 .childWrap 요소는 data-v-child 속성을 가집니다.
    • 이 요소는 ChildComponent의 스타일과 정확히 일치하므로, 빨간색 배경이 적용됩니다.
    • ParentComponent의 스타일은 data-v-parent 속성을 찾지만, ChildComponent 내부에는 이 속성이 없으므로 적용되지 않습니다.

  5. 이전 상황과의 차이:이전에는 ChildComponent가 ParentComponent의 직접적인 자식이었을 때, Vue가 자동으로 ChildComponent의 루트 요소에 data-v-parent 속성을 추가했습니다. 그래서 ParentComponent의 스타일이 일부 적용될 수 있었습니다.

  6. 현재 상황:ChildComponent가 자체적으로 .childWrap을 정의하고 있어, 이 요소는 오직 data-v-child 속성만 가지게 됩니다. 따라서 ParentComponent의 스타일은 더 이상 영향을 미치지 않습니다.

  7. AnotherComponent에 대해:AnotherComponent는 별도의 컴포넌트이므로, ParentComponent나 ChildComponent의 스코프된 스타일의 영향을 전혀 받지 않습니다.

요약하면, Vue의 스코프된 스타일 시스템으로 인해 각 컴포넌트의 스타일이 독립적으로 적용되며, 컴포넌트 구조의 변경으로 인해 ParentComponent의 스타일이 ChildComponent에 영향을 미치지 않게 된 것입니다.

해결방법 : .module 형식으로 스타일 적용

<style module> 을 사용하여 CSS 모듈을 적용하면, 클래스 이름 충돌을 피할 수 있습니다.

<template>
  <ul :class="$style.list">
    <!-- 내용 -->
  </ul>
</template>

<style module>
.list {
  color: green;
  list-style-type: lower-alpha;
}
</style>
profile
Hawaiian pizza with extra pineapples please! 🥤

0개의 댓글