IT/WEB

[CSS] Scoped CSS에 있어서의 CSS 설계 방법

개발자 두더지 2023. 8. 21. 22:45
728x90

일본의 한 블로그 글을 번역한 포스트입니다. 오역 및 의역, 직역이 있을 수 있으면 틀린 내용은 지적해주세요.

 

 Vue.js에서는 CSS를 이용할 때에 Scoped CSS나 CSS Modules, CSS-in-JS등의 방법을 사용할 수 있니다. 어쨌든 Scoped CSS는 가볍게 이용할 수 있으므로, 이용할 기회가 많이 생길거라고 생각한다. 

 Scoped CSS가 있다면 CSS설계를 사용하지 않아도 된다는 의견도 있지만, 실제로 그럴까? 이 포스트를 통해서 우리가 생각한 방법에 대해서 소개하도록 하겠다.  참고로, 여기 나와있는 예시들은 Vue.js의 SFC(싱글 파일 컴포넌트)로 Scoped CSS를 이용하고 있는 것을 상정하고 쓰여졌다. 

 

 

CSS 설계란?


 기본적으로 CSS는 항상 모든 페이지에서 읽어들여, 늘어나면 늘어날 수록 상호 덮어쓰는 상태가 일어나기 쉬워 무너지기 쉽다. 

 그렇게 등장한 것이 CSS설계로  BEM이나 SMACSS, FLOCSS, PRECSS등 다양항 CSS 설계 방법이 등장했다. 각각 상세하게 보면 차이점이 있지만 설계를 통해 CSS를 관리를 쉽게 하기 위함이라는 공통의 목표를 가지고 있다.

 구체적으로 "예측 가능성", "재이용성", "보수성", "확장성" 이 네 가지를 만족시키는 것을 목적으로 한다. 이것은 Google 엔지니어인 Philip Walton가 작성한 포스트에서 제안한 방식이다. 

 

 

Scoped CSS이란?


 Scoped CSS이란 어떠 특정의 범위만 CSS를 적용할 수 있는 기능이다. Vue.js에서 Scoped CSS를 어떻게 실현하고 있는가에 대해서 이야기하자면 HTML 쪽에 컴포넌트마다 커스텀 데이터 속성을 부여해, CSS 쪽에도 대응하는 데이터 속성을 부여하는 방법으로 사용한다. 

 커스텀 데이터 속성을 이용해 CSS의 영향 범위를 한정지어, 동일한 클래스명이나 셀렉터라고 해도 영향을 받지 않도록 한다.

 

 

Scoped CSS의 독자적인 주의점


 Vue.js의 Scoped CSS에는 알아차리기 어려운 함점 두 가지가 존재한다. 

 리셋 CSS등이 적용되도록 글로벌 스타일은 모든 컴포넌트에 적용된다. 의도하지 않은 스타일이 적용되지 않도록, 글로벌 CSS에 정의하고 있는 클래스명등은 주의해야한다.

 더욱이, 스타일이 자식 컴포넌트의 루트 요소에 적용시킬 수 있는 사양이 있다. 위에서 해설했듯, 커스텀 데이터 속성의 부여에 의해 Scoped를 실현하고 있으므로 자식 컴포넌트에는 커스텀 데이터 속성이 두 개가 부여되므로, 자식 컴포넌트의 루트 요소에 스타일을 적용할 수 있는 것이다.

 스타일을 자식 컴포넌트의 루트 요소에 적용할 수 있다는 사양에 의해 의도하지 않게 자식 컴포넌트의 스타일 덮어쓰기가 일어나버릴 가능성이 있다. 

 이와 같이 Scoped CSS에는 외부 CSS의 영향을 받기 쉽다는 특징이 있기 때문에 주의해야할 필요가 있다.

 

 

Scoped x CSS 설계


 여기서 부터가 본론인데, Scoped CSS를 이용하면 CSS 설계 필요하지 않냐는 의견에 대해서 반대하는 것이 필자의 의견이다. 왜 필요한가에 대해서 "예측 가능성", "재이용성", "보수성", "확장성" 이 네 가지 관점으로 해설하고자한다.

 

예측 가능성

 "예측 가능성"이라는 것은 스타일을 변경했을 때의 영향 범위가 제대로 예측할 수 있는가에 대한 시점이다. 기존의 코드를 편집했을 때나 스타일을 추가했을 때 의도하지 않은 곳에서 스타일의 영향을 받는 것을 원치않는다.

 Scoped CSS를 이용하면 컴포넌트 간의 영향을 예방할 수 있으나, 컴포넌트 내에서의 "예측 가능성"을 담보하는 것은 아니다. 실제 코드를 살펴보자.

<template>
  <div>
    <div class="newsBlock">
      <p class="text">...</p>
    </div>
    <div class="aboutBlock">
      <p class="text">...</p>
    </div>
  </div>
</template>
<style lang="scss" scoped>
.newsBlock {
  .text {
    color: red;
  }
}
.aboutBlock {
  .text {
    color: blue;
  }
}
</style>

 예를 들어 위와 같은 코드에서 HTML에 수정을 하고자 한다고 하자. .newBlock이 .aboutBlock에 감싸져 있다. 

<template>
  <div>
    <div class="aboutBlock">
      <!-- .newsBlock을 이동 -->
      <div class="newsBlock">
        <p class="text">...</p>
      </div>
      <p class="text">...</p>
    </div>
  </div>
</template>
<style lang="scss" scoped>
.newsBlock {
  .text {
    color: red;
  }
}
.aboutBlock {
  .text {
    color: blue;
  }
}
</style>

 CSS만을 본다면, .newBlock .text는 빨간색이 적용되어 있고 .aboutBlock .text에는 파란색이 적용되어 한다고 예측할 것이다. 그러나 실제로는 .newBlock .text에도 파란색이 적용되어, 둘 다 파란색 CSS가 되어 버리고 만다. 이러한 결과는 "예측 가능성"을 충족시키지 못한다.

 이와 같이, 작은 스코프안에서도 괸리를 잘못하면 혼란스러운 CSS가 탄생해버릴 가능성이 있다. 이러한 경우 가장 간단한 해결 방법이 아로애 같이 자식 셀렉터(>)를 추가하는 것이다.

<style lang="scss" scoped>
.newsBlock {
  > .text {
    color: red;
  }
}
.aboutBlock {
  > .text {
    color: blue;
  }
}
</style>

 그러나 이 해결 방법이라면 나중에 설명하겠지만, 다른 문제가 발생해버린다.

 

재이용성

 다음은 기존의 컴포넌트를 다른 부분에서도 사용하고 싶을 때, 코드를 다시 쓰거나 덮어 쓸 필요가 없도록 하는 것이 "재이용성"의 관점이다.

  Scoped CSS를 이용하면 컴포넌트 단위로 지정이 가능하며 컴포넌트간에 스타일은 기본적으로는 영향을 받지않는다. 그러므로 "재이용성"은 충족한다고 생각하기 쉽다.

 그러나 주의점에서 설명했듯 스타일이 자식 컴포넌트의 루트 요소에 적용할 수 있다는 사양이 있다. 이 사양의 영향으로 "재이용성"이 무너질 가능성이 있다.

 아래와 같이 자식 컴포넌트의 루트 요소에 정의되어 있는 클래스명과 동일한 클래스명을 부모 컴포넌트에서 호출할 때에 정의해버리면 자식 컴포넌트의 스타일이 덮어쓰여지게 된다.

<!-- 부모 컴포넌트 -->
<template>
  <div class="parent-component">
    <h1>부모 컴포넌트</h1>
    <childComponent class="child-component"></childComponent>
  </div>
</template>
<style lang="scss" scoped>
.child-component {
  color: red;
}
</style>
<!-- 자식 컴포넌트(childComponent) -->
<template>
  <div class="child-component">
    <h2>자식 컴포넌트</h2>
  </div>
</template>
<style lang="scss" scoped>
/* 이 경우 부모 컴포넌트에 의해 덮어씌여져, color: red;가 적용된다. */
.child-component {
  color: blue;
}
</style>

 컴포넌트 간에 의존관계가 되어버려 특정 컴포넌트내에서만 스타일이 변화하는 "재이용성"을 충족시키기 못하는 컴포넌트가 생길 위험이 존재한다.

 특정 컴포넌트에서만 스타일이 변화하는 케이스의 경우 독자적인 룰을 설계해 부모 컴포넌트에서의 스타일 적용을 회피하자. 예를 들면 다음과 같은 룰이 유용하다.

  • 다음 두 가지 룰을 동시에 채용한다
    • 자식 컴포넌트의 루트 요소에는 유니크한 class명을 정의한다.
    • 부모 컴포넌트에서 호출될 때에는 유니크한 class명을 정의하지 않으면 안된다.
  • 루트 요소는 class명을 정의하지 않는다.

 그러나 위의 룰만으로는 부족하다. .parent-component > div와 같은 지정에 의해 의도치 않게 스타일이 덮여쓰여질 가능성이 있기 때문이다. 방금 봤던 "예측 가능성"에서 언급했던 > (자식 셀럭터)를 기재했을 때 생길 수 있는 문제가 바로 이것이다. 

 

보수성

 컴포넌트를 추가하거나 업데이트, 재배치할 때 기존 코드의 리팩터링이 필요 없도록 하는 '보수성' 관점이다.이것은 Scoped CSS의 특기 분야이기 때문에, 위의 「재이용성」이나 「예측 가능성」의 관점을 지키고 있으면 문제가 없다고 생각된다.

 

확장성

 프로젝트 자체가 커지고 개발자가 늘었을 때도 쉽게 관리할 수 있도록 하는 것이 '확장성'의 관점이다 .

 Scoped CSS으로 인해 CSS 설계를 그만두는 선택을 했다고 해도, 어느 정도의 룰이 없으면 CSS는 점점 카오스가 된다.그렇기에 독자 룰을 책정한다고 했다고 해도 프로젝트가 커지고 개발자도 늘어나, 자신이 아닌 다른 사람이 코드를 작성하게 되면 어려움을 겪을 것이다.

 

 

우리가 도입한 CSS 설계


 우리의 경우는 BEM 시스템의 Block, Element, Modifier로 분류하여 구성된 규칙을 채택하고 있다. SMACSS나 FLOCSS 등과 같은 CSS 설계에서는 레이아웃 등을 접두사를 붙여 관리하지만, 그것은 컴포넌트를 세세한 단위로 관리할 수 있기 때문에 BEM을 채용했습니다.

 Element는 언더 스코어 1개, Modifier는 언더 스코어 2개로 표현하,  PRECSS의 명명 규칙을 적용하고 있다.

.block {
  &_element {} /* Element */
  &__modifier {} /* Modifier */
} /* Block */

스타일을 적용싴리 요소에는 모두 클래스 이름을 붙이는 것이 좋다.이 규칙은 의도하지 않은 자식 컴포넌트의 스타일이 덮어쓰여지는 문제의 회피와 '예측 가능성'을 확보하여 li에서 다른 컴포넌트로 꺼낼 경우 작업을 편하게 하려는 의도가 있다.

▼NG의 예

<template>
  <ul class="todo-list">
    <li>리스트1</li>
    <li>리스트2</li>
    <li>리스트3</li>
  </ul>
</template>
<style lang="scss" scoped>
.todo-list {
  > li {}
}
</style>

▼OK의 예

<template>
  <ul class="todo-list">
    <li class="todo-list_item">리스트1</li>
    <li class="todo-list_item">리스트2</li>
    <li class="todo-list_item">리스트3</li>
  </ul>
</template>
<style lang="scss" scoped>
.todo-list {
  &_item {}
}
</style>

 각 컴포넌트의 루트 요소에는 커ㅏㅁ포넌트명에 해당하는 class명을 붙인다.

<!-- PostBlock.vue의 경우 -->
<template>
  <div
    class="post-block"
  >
    ...
  </div>
</template>
<style lang="scss" scoped>
  .post-block {}
</style>

 부모 컴포넌트에서 자식 컴포넌트에 클래스 명을 정할 때는 컴포넌트 이름에 해당하는 class명을 붙이면 안된다. 자식 컴포넌트에 클래스 이름을 붙일 때는 element로 취급하는 등 컴포넌트 이름에 해당하는 class명을 붙이지 않도록 하자.

▼NG의 예

<!-- AboutPage.vueの의 경우-->
<template>
  <div
    class="about-page"
  >
    <!-- 부모 컴포넌트에서 컴포넌트명을 class에 붙이는 것은 금지 -->
    <PostBlock class="post-block" />
  </div>
</template>

▼OK의 예

<!-- AboutPage.vue의 경우 -->
<template>
  <div
    class="about-page"
  >
    <PostBlock class="about-page_item" />
  </div>
</template>

 글로벌 CSS는 모든 페이지에 공통하여 읽어들여지는 것이므로, 클래스를 다둘 때에는 구체적인 명명을 하자. 범용적인 이름은 컴포넌트내에서츼 충돌 위험이 있기 때문이다.

▼NG의 예

.button {}
.card {}
.slider {}

▼OK의 예

.send-mail-button {}
.post-card {}
.thumbnail-slider {}

참고자료

https://ics.media/entry/200515/#scoped-css%E3%81%A8%E3%81%AF

728x90