"Problems with CSS at scale" - AdorableCSS는 어떻게 해결했을까?

teo·2021년 11월 21일
3

AdorableCSS

목록 보기
3/3
post-thumbnail

프롤로그

과거 CSS의 가장 큰 이슈는 디자인 변경에 유연한 웹페이지를 만드는 것, 그리고 IE를 필두로 하는 크로스 브라우징이었습니다.

하지만 이제는 IE는 이제 퇴출이 되고있고 웹페이지가 아니라 웹서비스로 발전을 하게 되면서 유연한 디자인이 아니라 하나의 아이덴티티를 가지는 거대한 CSS를 만들고 관리해야 하는 이슈가 새롭게 대두 되었습니다.

그래서 이번 글에서는,

  1. CSS가 어떠한 문제점을 가지고 있고,
  2. 다른 주요 라이브러리/프레임워크들은 어떻게 해결하고 있는지
  3. 그리고 AdorableCSS는 이 문제들을 어떻게 해결하고 있는지

기술적인 이야기를 해볼까합니다.

CSS가 왜 문제가 되는가?

이미 만들어진 웹 스펙은 변하지 않지만 웹 생태계는 항상 변하고 있다.

최초의 웹은 링크가 있는 전자문서로 시작을 했고 포맷은 신문의 형식을 많이 참고했습니다.
당시 필요에 맞게 반복적인 서식적용을 최소화 하기 위해 Cascade라는 방식을 도입하여으로
컨텐츠와 서식을 분리하고 적은 양의 코드로 반복적인 작업을 줄이면서도
같은 컨텐츠에 여러가지 디자인을 갈아 끼울 수 있는 혁신적인 방식이었습니다.

하지만 시간이 흘러
웹은 이제 웹문서에서 웹 사이트로 발전하고 이제 웹서비스, 웹앱으로 발전하게 되었습니다.
웹은 특정 벤더의 소유물이 아니기에
이미 만들어진 스펙은 하위 호환성 유지를 위해 기존 스펙을 없앨 수 없습니다.

그래서 이미 만들어진 기술과 새로운 시대의 변화를 만나면서
웹 페이지를 만들기 위해 좋았던 장점들이
거대한 웹 서비스를 만들기 위해서는 이제는 단점이 되는
상황들이 맞이하게 되는 것입니다.

그리고 늘 그래왔듯 이러한 문제점을 고쳐나가면서 웹은 발전하고 있습니다.

Problems with CSS at scale (Vjeux)

CSS는 물론 여러가지 태생적인 문제들이 존재하지만
이 글에서는 Facebook FE 개발자가 발표해서 유명해진 7가지 문제에 한번 집중해보겠습니다.

참고로 이 자료는 2014년 React 컨퍼런스에서

  • CSS 대신 JS를 통해서 style을 만들어서 inline-style을 넣어보자라고 하는 CSS in JS라는 개념을 소개하고
  • 이걸 시작으로 2017년도에 stylex라는 inline-style대신 atomic css를 만들어서 넣어주는 개념으로 발전하고
  • 이후 이를 바탕으로 styledComponent라고 하는 CSS in JS 방식의 초석이 되는 자료입니다.

https://speakerdeck.com/vjeux/react-css-in-js?slide=2

  • Global namespace: 모든 스타일이 global에 선언되어 중복되지 않는 class 이름을 적용해야 하는 문제
  • Dependencies: css와 JS간의 의존관계를 관리하기 힘든 문제
  • Dead Code Elimination: 기능 추가, 변경, 삭제 과정에서 불필요한 CSS를 제거하기 어려운 문제
  • Minification: 클래스 이름의 최소화 문제
  • Sharing Constants: JS 코드와 상태 값을 공유할 수 없는 문제
  • Non-deterministic Resolution: CSS 로드 순서에 따라 스타일 우선 순위가 달라지는 문제
  • Breaking Isolation: CSS의 외부 수정을 관리하기 어려운 문제(캡슐화)

1. Global namespace

모든 스타일이 global에 선언되어 중복되지 않는 class 이름을 적용해야 하는 문제

모두가 꼽는 CSS의 가장 큰 문제점입니다.

style은 어디에 선언을 하건 import를 하건 항상 global namespace를 가집니다.
기존에 만들어진 스타일에 override를 하기 쉽다는 장점이 있었지만
그로 인해 side-effect와 기존의 이름을 피해서 복잡하게 지어야만 하는 원흉입니다.

특히 jQuery를 이용한 페이지 단위 개발방식에서
React, Svelte, Vue와 같은 컴포넌트 기반으로 바뀌게 되면서
컴포넌트에만 css가 독립적으로 적용되기를 원하게 되었습니다.

이를 피하기 위해서 먼저 생각해 볼 수 있는 간단한 규칙은
모든 스타일앞에 컴포넌트의 클래스명을 붙이는 것입니다.

<style>
.button { ... } (X)
.header { ... } (X)
</style>

<style>
.MyComponent .button { ... } (O)
.MyComponent .header { ... } (O)
</style>

이러한 방법들은 .MyComponent 역시 global scope이기에
어딘가에서 class="MyComponent" 와 같은 구문을 쓴다면 문제가 발생하고

무엇보다 <slot/> 이나 React의 {children}과 같은 Nested Component에서
부모 컴포넌트의 스타일이 강제로 적용이 되기 때문에
컴포넌트의 독립적인 스타일 분리 문제는 여전히 발생하게 됩니다.

<MyComponent>
  <div class="button">외부에서 만든 버튼</button>
</MyComponent>

<!-- Render Result -->
<div class="MyComponent">
  <div class="button">MyComponent의 버튼</div>
  <div class="button">외부에서 만든 버튼</div> <!-- 여기에 컴포넌트에 의해 서식의 영향을 받는다 -->
</div>

그래서 BEM과 같은 방법들이 나타나게 됩니다.

<style>
.MyComponent__button { ... } /* (O) */
.MyComponent__header { ... } /* (O) */
</style>

<!-- Render Result -->
<div class="MyComponent">
  <div class="MyComponent__button">MyComponent의 버튼</div>
  <div class="button">외부에서 만든 버튼</div> <!-- 여기는 서식의 영향을 받지 않는다. -->
</div>

TMI1) 물론 BEM이 꼭 이 문제의 해결책으로 등작한것은 아니었어요.
TMI2) 2014 facebook는 class="MyComponent/button" 이런 식으로 했다고 발표 내용에 있네요.

이후 2015년 쯤에는 다른 해결방식이 나타납니다. style은 그대로 쓰고 알아서 바꿔주자는 방식이죠.
React에서는 CSS Modules가 Vue, Svelte, Angular의 경우 single-file-component를 통해서 컴파일 혹은 런타임때 컴포넌트마다 고유 Hash를 부여해서 독립적인 스타일을 강제로 만들어주는 방식들이 나타나게 됩니다.

/* Button.module.css */
.Button {
  font-weight:bold;
  text-align:center;
}
import React from "react";
import styles from "./Button.module.css";

const Button => <button className={styles.Button}>CSS Module 버튼</button>;
// <button class="_styles__Button_309571057">

이후 나타난 CSS in JS라는 방식에서는 아예 컴파일/런타임시 JS에서 style과 className를 생성하여 컴포넌트마다 독집적인 스타일을 유지 할 수 있도록 인라인 스타일과 같은 느낌으로 작성을 할 수 있게 해줬습니다.

const Button = styled.button`
  font-weight:bold;
  text-align:center;
`

<Button>CSS in JS</Button> 
// => <button class="sc-hBEYos dWjUC">CSS in JS<button>

그러면 AdorableCSS는 Global namespace를 어떻게 해결을 했을까요?

Atomic CSS / Functional CSS라고 불리는 이 방식은 CSS의 Global Scope를 그대로 인정을 합니다.

대신 HTML을 작성하고 CSS를 후술하는 나중의 방식과 달리 변하지 않은 CSS를 먼저 만들어 두는 방식을 택했습니다.

서식의 변경이 필요하다면 CSS를 바꾸지 않고 HTML을 변경하면 그만입니다.

inline-style과 같이 서식의 적용 단위는 Element단위로 이루어지기 때문에 Global Scope의 문제점이 발생하지 않습니다.

<style>
.c\(red\) { color: red; }
.text-center { text-align: center; }
.bold { font-weight: bold}
</style>

<div>
  <div class="c(red) text-center">MyComponent의 버튼</div>
  <div class="bold text-center">외부에서 만든 버튼</div>
</div>

2. Dependencies

css와 JS간의 의존관계를 관리하기 힘든 문제

이 문제는 지금 2021년도에 개발을 하고 있는 사람에게는 그리 와닿지 않는 문제일수도 있겠다고 생각이 듭니다. 이제는 개발을 할때 적어도의 하나의 프레임워크는 쓰고있고 그에 맞는 Webpack이나 Rollup, Vite등 번들툴을 쓰고 있으니 CSS를 JS에서 import를 해주는 기능이 너무 당연하게 느껴질지도 모르겠습니다.

당시 Sass나 Less등 pre-processor기반의 환경에서는 CSS를 모듈로 분리하여 하나의 css파일로 만들어서 사용했는데 그러다 보니
CSS 번들러과 JS의 번들러가 달라서
그래서 CSS의 depencency와 JS의 depencency가 따로 노는 문제가 발생을 했습니다.
그래서 사용하지 않은 컴포넌트의 css가 최종 빌드에 포함이 되거나 필요한 css가 로딩이 되지 않는 문제등이 있었습니다.

현재에 와서는 주류 번들러가 기본적으로 포함하고 있는 환경이며 혹 번들러를 쓰지 않는 환경에서는 애초에 이 문제가 고민거리가 아니기에
현재에 와서는 이미 해결된 문제라고 보고 대략 자세한 설명을 생략하도록 하겠습니다.

https://speakerdeck.com/vjeux/react-css-in-js?slide=11

3. Dead Code Elimination

기능 추가, 변경, 삭제 과정에서 불필요한 CSS를 제거하기 어려운 문제

앞서 CSS depencency를 이제 JS의 import 명령어를 통해서 관리가 되기 때문에 사용하지 않는 모듈이 포함되는 문제는 해결이 되었습니다.

그럼에도 아직까지 Dead Code 문제는 남아있습니다. 같은 CSS내 모듈안에서 전혀 사용하지 않는 CSS가 있더라도 빌드 과정에서는 해당 CSS 라인은 포함이 되게 될 것입니다. HTML과 CSS는 각기 형태로 다른곳에 존재하기 때문에 자칫 전임자가 작성한 코드에서 어느 CSS는 사용하고 어느 CSS는 불필요한지 확인하기란 쉽지 않은 일이죠.

이 문제를 어떻게 해결을 할까요?

이러한 문제를 Lint와 같은 정적분석 툴을 통해서 CSS의 Selector를 이 HTML에 실제 존재하는지를 확인하는 방법을 통해서 해결을 하고자 했습니다.

  • UNCSS
  • PurifyCSS
  • PurgeCSS

하지만 HTML이 JS에서 동적으로 만들어지기도 하고 웹 프레임워크 마다 언어가 다르고 css도 pre-processor의 문법이 복잡해서 실제로는 사용하지만 찾아내지 못하는 경우가 있어 실수로 지웠다가 이슈를 만드는 일도 있었습니다.

이런일들로 css를 다시 순정으로 쓰려는 시도들도 있었고 PostCSS등이 급부상하는 계기도 생겼습니다.

tailwindCSS의 경우에도 이러한 방식을 이용해 만들어진 코드들을 PurgeCSS를 통해서 사용하지 않는 class를 제거하도록 하였습니다. Atomic CSS의 경우에는 selector가 다른 경우에 비해 훨씬 단순하기 때문에 상대적으로 사용하지 않는 class를 잘못 찾아내는 문제가 상대적으로 줄었습니다.

CSS in JS는 어떨까요?
처음부터 css를 만들어두지 않고 런타임으로 만들어내기 때문에 불필요한 CSS코드가 만들어지지 않습니다. 대신 동적으로 생성하는 비용이 발생하기 때문에 런타임이 아니라 빌드시점에 CSS를 생성하는 방법들도 연구되고 있는 중입니다.

그러면 AdorableCSS는 Dead Code Elimination 문제를 어떻게 해결을 했을까요?

AdorableCSS는 on-dand 즉, 주문형 방식으로 해당하는 코드가 존재할때만 필요한 CSS를 생성합니다.
정적 분석 툴을 이용하는 게 번들러의 import CSS를 사용하기 때문에 폴더나 파일 기준이 아니라
실제로 사용하는 코드에서만 생성하므로 사용하지 않는 파일에 대해서는 생성하지 않습니다.
또한 빌드 시점에 최종적인 CSS코드를 생성하기 때문에 런타임시 만들어지는 비용이 없어 보다 나은 퍼포먼스를 제공할 수 있습니다.

4. Minification

클래스 이름의 최소화 문제

CSS는 원래 시작은 적은 코드로 반복적인 서식을 제거하기 위함이었으나 복잡한 디자인과 레이아웃이 만들어지게 되면서 많은 양의 코드를 작성하게 되었습니다

CSS의 클래스네임이나 태그 셀렉터의 경우 전부 string기반이기에 이름을 다른식으로 축약해서 더 작게 만들수가 없게 되었습니다. CssNano등과 같이 최대 가능한 것들이 불필요한 공백을 제거하는 것 정도를 넘어선 압축을 하기가 힘든 문제입니다

CSS in JS의 경우, 스타일을 JS에서 동적으로 생성하기에 클래스이름이 축약되어 적용되며 CSS 사이즈의 이득을 볼 수 있습니다. 반면 그만큼의 문자열이 JS에 포함되며 CSS in JS을 구동하기 위한 라이브러리의 용량 만큼이나 번들에 포함되게 됩니다

그러면 AdorableCSS는 class minification 문제를 어떻게 해결을 했을까요?

ACSS와 같이 .D(b) { display: block} 극단적인 축약을 통해 CSS번들의 크기를 최소화 하는 방법도 있겠으나 그럴 경우 코드의 가독성을 잃어야 했습니다.

제 추구하는 개발의 가치 중 최고로 치는 가독성을 포기하는 것은 있을 수 없었기에 가독성과 축약을 상호보완할 수 있는 방법을 고민했습니다.

첫째로 CSS에서 극단적으로 1글자만 써도 예측이 가능한 것들은 그렇게 했습니다.
class="c(red) bg(blue) w(100) h(100) m(10) p(20) b(#000) r(25) x(10) y(10) z(100)"

둘째로 통상적으로 매우 자주 사용하는 기능만을 짧은 class로 제공하여 반복되는 class 타이핑을 줄였습니다.
hbox { display: flex; flex-direaction: row }
layer { position:absolute; top 0; right: 0; bottom: 0; left: 0;}

나머지는 가독성과 러닝커브를 줄이기 위해서 축약없이 CSS에서 사용하던 이름을 그대로 사용하도록 하여
minification과 clean code의 밸런스를 맞추기 위해 세심하게 고민했습니다.

5. Sharing Constants

JS 코드와 상태 값을 공유할 수 없는 문제

이 역시 2021년에 와서는 고민할 필요가 없는 문제입니다.

CSS Variable이 해당 문제를 해결하기 위한 해결책으로 CSS의 정식 기능으로 이미 탑재가 되었습니다.
물론 해당 기능을 지원하지 않은 IE11은 여전히 문제가 되고 있지만 아마 곧 완전히 퇴출될 거라고 생각합니다.

AdorableCSS의 경우에는 이를 문제로 보고 있지 않으며 CSS Variable을 적극적으로 활용하고 IE11의 경우에도 JS를 이용한 ponyfill등을 사용하기를 권장합니다.
https://jhildenbiddle.github.io/css-vars-ponyfill/#/

6. Non-deterministic Resolution

CSS 로드 순서에 따라 스타일 우선 순위가 달라지는 문제

CSS의 우선순위 문제는 상당히 복잡합니다. CSS 셀렉터마다 고유한 Level과 포인트가 존재하고 그 총 합을 통해 우선수위가 결정되며 같은 우선순위라면 CSS가 나중에 적용되는 순서대로 덮어쓰도록 되어 있습니다.

<div class="red blue">빨간색일까? 파란색일까?</div>
<div class="blue red">파란색일까? 빨간색일까?</div>

위 경우 어떤식으로 화면에 그려질지는 HTML만 보고는 절대로 예측이 불가능합니다.

현대에 와서 이 문제가 중요해진 이유는 코드 스프리팅이 점점 보편화 되고 있기 때문입니다.
해서 CSS에서 의도한 작성순서와 다르게 로딩순서에 따라서 원하지 않는 형태로 그려지는 문제가 발생할 수 있습니다.

그러면 AdorableCSS는 Non-deterministic Resolution 문제를 어떻게 해결하고 있을까요?

AdorableCSS는 CSS를 근간으로 하다보니 Non-deterministic Resolution 문제를 가질 수 밖에 없습니다.
이미 만들어진 브라우저의 CSS의 스펙은 바꿀 수 없으며
run-time이 아니라 build-time에 생성하는 방식이니 이 문제에는 취약할 수 밖에 없습니다.

@NOTE: 대신 build-time이기에 캐시 및 로딩성능이나 번들의 사이즈등에서 run-time에 비해 이점이 있습니다.

그래서
첫번째로는 수동으로 우선수위를 결정할 수 있는 기능을 제공하고 있습니다.

<!-- Adorable CSS도 역시 Non-deterministic Resolution 문제를 가지고 있다 -->
<div class="c(red) c(blue)">빨간색일까? 파란색일까?</div>
<div class="c(blue) c(red)">파란색일까? 빨간색일까?</div>

<!-- !기호를 통해 !important를 이용해 override를 지원한다! -->
<div class="c(red) c(blue)!">파란색이다!</div>
<div class="c(blue) c(red)!">빨간색이다!</div>

그렇다면 둘다 !important면 어떻게 하나요? 아직은 개발중인데 very important!라는 기능으로 !의 개수에 따라 더 우선순위를 높여주는 기능을 제공할 예정입니다.

<!-- 그렇다면 둘다 !important라면?? -->
<div class="c(red)! c(blue)!">빨간색!? 파란색!?</div>
 
<!-- very important!! 기능 제공(예정) -->
<div class="c(red)! c(blue)!!">빨간색! < 파란색!!</div>

하지만 위 방법은 AdorableCSS에 권장하는 방법이 아닙니다.
두번째로는 CSS에서 로딩 순서가 아닌 Selector기반의 우선순위가 만들어진 이유를 최대한 활용하고 있습니다.

<div class="fixed layer">layer는 absolute. fixed가 좀 더 특수한 경우라 우선순위를 더 조정해뒀다.</div>
  
<div class=".selected:c(blue) c(red) selected">클래스 선택자의 경우 항상 기본 서식보다 우선된다.</div>
  
<button class="disabled:bg(gray) hover:bg(red) active:bg(blue)" disabled>disabled > active > hover 순으로 우선순위가 배정되어 있다.</button>

7. Breaking Isolation

CSS의 외부 수정을 관리하기 어려운 문제(캡슐화)

디자인 시스템에 대한 이야기는 많이 들어보셨을 겁니다. 기능의 재사용과 함께 디자인 역시 아이덴티티와 톤 앤 매너를 유지하도록 하기 위해서
마치 잘 만들어진 레고블록 처럼 디자인 블록들을 만들어 원하는 대로 조립을 해도 레고라는 아이덴티티는 유지하면서도 다양한 것들을 만들기 위한 노력이 필요합니다.

이러한 경우 그 잘만들기 위해서는 외부에서 유연하게 수정을 할 수 있는 것과 아이덴티티와 간결함을 유지하기 위한 수정을 하지 못하는 것을 분리하는 장치들이 필요합니다.
이것을 보통 캡슐화라고 부르는데 CSS에는 이점에 있어서 매우 취약합니다.

웹 페이지기반의 같은 데이터를 커스텀이 가능한 여러 디자인을 만들 수 있다는 장점을 가진 방식이
되려 재사용과 Varient를 만들기 위해서는 취약적인 구조인 셈입니다.

이러한 구조화를 하기 위해 BEM과 같은 방법론과, Sass의 Nested Block과 같은 기능들이 제안되었지만,
CSS의 구조적인 한계는 객체지향의 체계나 로직을 심을 수 있는 JS의 체계와 같지 못할 뿐더러
결국 CSS와 JS가 분리되어 있는 구조는 JS기반으로 움직이는 컴포넌트 세계와 함께 관리되기 어렵다는 문제입니다.

Vue나 Svelte등의 경우 이를 single-file-component라는 방식으로 하나의 파일에서 style과 script와 html을 묶어서 관리하는 방식을 제안했고

React에서는 이것을 CSS in JS라는 방식으로 CSS도 JS의 생태계안에 묶어서 관리하자는 방식을 제안하게 됩니다.

그러면 AdorableCSS는 Breaking Isolation를 어떻게 해결을 했을까요?

그것은 바로 주문형 시스템이라는 점입니다. 기존의 CSS와는 다르게 AdorableCSS는 CSS를 직접 작성하지 않습니다.

대개의 웹 프레임워크가 HTML을 저마다의 방식으로 확장하여 사용자에게 DOM API를 직접 쓰지 않고 바인딩을 통해서 관리 할 수 있도록 해줍니다.

AdorableCSS는 HTML의 class문법을 확장하여 사용자가 CSS를 직접 작성하지 않도록 도와 줍니다.

이는 곧 CSS가 별도 관리/작성이 되는 것이 아니라 프레임워크 생태계에 그대로 녹아낼 수 있다는 의미입니다.

한번 만들어진 CSS를 수정하지 않고, 외부에서 CSS를 override하지 않도록 해서 시스템이 망가지지 않도록 하고 있습니다.

마치며

글의 시작이 CSS in JS의 태동과 관련된 내용이다보니 결국 CSS in JS를 하면 이 문제들이 해결된다는 것 아닌가? 라는 식의 결론이 날지도 모르겠다는 생각이 듭니다.
하지만 CSS in JS도 한낱 React 진영에서의 CSS 문제 해결법 중 하나 일 뿐이며 각자의 방식으로 CSS 문제들은 해결해보고자 하고 있다라는 말을 드리고 싶습니다.

뭐가 더 우수한 방법인가 대한 얘기는 아니었습니다.
jQuery에서 웹 프레임워크로 진화해오는 변화만큼은 아직 CSS는 그만큼의 변화는 겪지 않았다고 생각합니다.

태생의 목적과 다르게 요구되는 환경에 맞는 변화에 대한 니즈를 이해해보고
나중에 또 완전히 새로운 CSS의 패러다임이 도래한다면 이러한 문제들을 곧 어떻게 해결했는가를 시작으로 학습을 하시게 될꺼라
본인이 지금 사용하고 있는 CSS에서 이러한 문제점과 그리고 해결법등을 간접적으로 느끼고 생각해볼 수 있는 글이었으면 좋겠습니다.

감사합니다.

profile
Svelte, rxjs, vite, AdorableCSS를 좋아하는 시니어 프론트엔드 개발자입니다. 궁금한 점이 있다면 아래 홈페이지 버튼을 클릭해서 언제든지 오픈채팅에 글 남겨주시면 즐겁게 답변드리고 있습니다.

2개의 댓글

comment-user-thumbnail
2021년 12월 2일

이러한 문제로 인해 독립적인 캡슐화된 구성요소를 만들기 위해 shadow-dom이 적용된 웹 컴포넌트를 사용하고 있는데, 사람은 편해지고 싶은지라 resetCss도 영향을 안받네... 하며 불편함을 호소하고 있습니다 ㅋㅋ

1개의 답글