Why We're Breaking Up with CSS-in-JS
CSS library Emotion의 컨트리뷰터 중 한명이 해당 글을 포스팅하였는데,
CSS-in-JS의 여러 한계점들에 대해 지적하며 SASS Module + Tailwind.css로 마이그레이션 한다는 것이 그 내용이다.
CSS-in-JS의 한계점에 대해 자세히 기술하고 있는데, 그에 대한 반대의견도 적지 않았다.
그래서 직접 다음과 같은 의문에 대한 탐구 과정을 기록하기로 하였다.
1.CSS-in-JS는 어떤 문제를 내포하고 있을까?
2.Atomic CSS는 과연 그 문제를 어떻게 해결하고 있나?
3.Atomic CSS는 그렇다면 충분한 해결책인가?
4.CSS의 미래는 어디에 있을까?
CSS-in-JS와 Atomic CSS에 대해 논하기 이전에 CSS의 역사를 간단하게 정리해 보았다.
최초, 웹은 문서를 공유하기 위해 만들어졌다.
문서를 표현하기 위해 HTML이 만들어졌고,
HTML은 콘텐츠에 의미를 부여한 태그를 붙여주는 방식으로 단순한 형태의 문서를 제공하였다
그러다가 좀 더 다른 형태의 서식으로 문서를 꾸밀 필요성이 제기되고
style 개념이 추가되어
태그에 style을 입력해서 다양한 형태의 문서를 만들 수 있게 되었다.
이를 inline-style이라 한다.
하지만 inline-style은 코드가 너무 비대해져
가독성이 떨어지고, 유지 보수가 어렵게 되는 여러 문제가 발생하였다.
그렇게 1996년 CSS가 고안되었다.
CSS는 별도의 Rule을 선언해 서식이 필요한 곳을 선택해 반복해 적용하는 식으로 만들어졌다.
하지만 HTML이 아닌 CSS만을 수정해 디자인을 적용하기 위해
복잡한 Selector의 사용이 필수불가결해졌다.
이런 복잡한 Selector를 쉽게 만들 수 있는 Sass 같은 css 전처리기들이 등장하였다.
굳이 CSS에서 복잡한 Selector를 잘 쓰는 것보다 HTML 단계에서 class 이름을 잘 지정하는 것이 더 낫다라는 결론에 도달한 개발자들.
이를 기점으로 Selector보다는 어떻게 해야 class이름을 더 잘 지을수 있을까에 대한 고민으로 흐름이 넘어가게 된다.
class 네이밍에는 위 두가지 접근법이 있다.
스타일 변경시 CSS만을 수정하다보니, 시각적인 네이밍을 하게되면 클래스 이름과 실제 CSS간에 차이가 발생할 수 있게 된다.
따라서 class 이름을 의미론적인 관점에서 짓는 것이 주요 원칙으로 자리잡게 되었다.
웹 생태계가 발전하며 웹은 점차 어플리케이션의 형태를 띄게 된다. 그러면서 아래와 같은 여러 문제점이 발생하였다.
Global scope: 코드 수정이 모든 프로젝트에 영향을 주고, 관리 비용, 복잡한 네이밍 필요성 등.
Specificity: class 순서가 아닌 css 순서에 의해 서식 우선순위가 결정됨.
이외에도
Dependencies, Dead code elimination, minification, sharing constants, Non-deterministic Resolution, Breaking Isolation 과 같은 많은 문제가 발생하였다.
Block, Element, Modifer.
Yandex가 고안한 네이밍 컨벤션으로, 원칙을 충실히 준수하면 위의 css의 문제점들을 완벽히 해결할 수 있었다.
하지만 이론은 완벽했으나, 네이밍 실수, 러닝커브 등 휴먼팩터 문제를 완벽히 방지할 수 없는 근본적인 한계가 있었다.
CSS의 문제인 Global Scope를 막기 위해서 Component 단위에서 사용되는 CSS에 hash를 추가하여 CSS가 더 이상 Global 하지 않도록 하는 방식을 통해서 해결을 하고자 하는 방법이 만들어졌다.
본격적으로 JS와 CSS를 결합하려는 시도가 시작된 기점이라 할 수 있다.
CSS-in-JS는 말 그대로 자바스크립트 코드에서 CSS를 작성하는 방법론이다.
2014년 페이스북 개발자인 Christopher Chedeau aka Vjeux가 처음 소개하였고,
이후 StyledComponenet, Emotion등의 라이브러리로 구체화되었다.
'utility-first CSS framework'인 TailwindCSS는 새로운 AtomicCSS라는 패러다임을 들고왔다.
Function, utility-first 로 표현되기도 하는 AtomicCSS는
시멘틱 네이밍 원칙에서 아예 벗어나 시각적인 요소를 표현하는 원자단위로 클래스를 미리 선언한다.
이러한 과정을 거쳐 발전해온 CSS 라이브러리들은 현재 크게 3가지로 분류될 수 있다.
어떤 라이브러리를 사용할 지 확실히 하기위해선, 방법론들마다의 장단점을 확실히 해둘 필요가 있다.
우선 가장 많이 사용되는 CSS-in-js부터 시작하자.
CSS-in-JS 라이브러리에서도 종류가 나뉘는데, 크게 run-time, compile-time 두 분류로 나뉜다.
현업에서 주로 사용되는 Styled-componenet, Emotion은 전자에 속한다.
이들 run-time CSS-in-JS의 대표적인 단점은 다음과 같다.
직렬화 (Style serialization)
CSS 문자열이나 객체 스타일을 plain CSS string으로 변환하여 document에 insert하는 행위.
이 과정은 hash 값(e.x. css-15nl2r3)을 계산하는 과정을 포함한다.
emotion, styled-componenet 등은 리렌더링시 직렬화가 발생하여 런타임 오버헤드로 이어진다.
물론 컴포넌트 외부로 스타일을 이동해(변수로 지정) 렌더링이 발생할 때가 아닌 모듈이 로드될 때 한 번만 직렬화가 발생하도록 할 수 있다. 하지만 이럴 경우 스타일에서 동적인 프로퍼티를 사용할 수 없어, css-in-js의 주요장점을 잃게된다.
라이브러리로 인한 번들사이즈 증가
React Devtools를 복잡하게 만든다.
SEO에 부적합. 런타임에 헤드의 끝에 스타일시트를 삽입하기 때문.
이와 같은 점들로 인해 SSR 환경에서 CSS-in-JS는 퍼포먼스 측면에서 태생적 한계를 가진다는 것이다.
물론 기존의 런타임 CSS-in-JS에 대한 한계를 극복하기 위해 최근 컴파일 타임에 빌드되며 Type-safety까지 보장하는 라이브러리들이 등장하고 있다. Vanilla-extract가 그 대표주자이다.
하지만
컴포넌트가 처음 마운트 될 때, 스타일이 계속 삽입되어 브라우저가 모든 DOM 노드에서 스타일을 다시 계산하도록 함.
빌드 시 추출이 불가능한 동적 프로퍼티를 적용할 때, style 프로퍼티를 사용하여 성능 저하 유발.
해결되지 않는 React Devtools 복잡화.
SEO에 부적합.
성숙하지 않은 커뮤니티 규모
규모있는 라이브러리의 부재
위와 같은 한계는 여전히 있기에, 이와 같은 라이브러리들이 완벽한 대안인가라는 질문에는 답하기 힘들다고 본다.
그렇다면 AtomicCSS는 어떨까
AtomicCSS는 CSS-in-JS(runtime)와 비교하여 다음과 같은 장점을 가진다.
하지만
이라는 한계가 역시 존재한다.
이런 한계점들 때문에 tailwindCSS를 사용하는 서비스들은 tailwind와 함께 다른 라이브러리를 결합해서 사용하고 있는 상황이다.
카카오의 경우에는 TailwindCSS 와 twin.macro (atomic + CSS-in-JS)를 결합해 사용하며.
이 글을 쓴 계기가 되었던 글의 저자는 SASS module에 tailwind를 결합해 사용하고 있다.
지금까지 살펴본 것들을 종합해보면
CSS-in-JS와 AtomicCSS는 어느 하나가 명확한 우위에 있다기 보다는, 각각이 전혀 다른 패러다임을 차용했기에 명확한 각각의 장단점을 갖고있다.
CSS-in-JS쪽에선 현재 컴파일 단계에서 빌드하고, type-safety를 지원하면서 기존의 한계점을 극복하고 있는 상태이고,
AtomicCSS진영 또한 JIT 컴파일러를 도입하고, 다른 방법론과 결합을 시도하는 등 계속해서 진화하고 있는 상황이다.
확실한 것은 퍼포먼스 상의 한계와 SEO에 결점을 가진 런타임 기반 CSS-in-JS는 이제 역사의 뒤안길로 보내줘야 한다는 점이다.
결국 Type-safty를 보장하는 css-in-ts와 atomicCSS가 비교되어져야 할 것이다.
AtomicCSS에 계속 마음이 기울어지긴 하지만 마지막으로, CSS-in-TS 라이브러리와 AtomicCSS도 비교해보자.
앞서 언급했듯이 tailwind에서 동적인 프로퍼티를 적용할려면 in-line 방식을 통해 스타일을 변경해야 한다.
하지만 css-in-ts도 빌드타임에 컴파일된 파일을 통해 스타일링을 하기 때문에, 동적인 프로퍼티에 대한 해결법은 크게 다르지 않다.
CSS-in-TS의 최대 강점은 type-safety에 있다.
잘못된 클래스 네임을 type 검사를 통해 걸러내 에러를 미연에 방지할 수 있다.
하지만 tailwind에는 tailwind-intellisense같은 플러그인을 통해 해당 문제를 해결할 수 있고, 최근에는 typewind같은 type-safty를 자체적으로 지원하는 라이브러리도 나오고 있는 상황이다.
결론적으로 현재 시점에서
런타임 기반 CSS-in-JS는
SEO 성능 메트릭이 중요하지 않은 소-중규모 프로젝트의 경우
적절한 선택지가 될 수 있으나 규모있는 프로젝트에서는 여러 측면에서 한계가 있는 것으로 판단된다.
반면, AtomicCSS는
DX가 중시되고, SSR이 필수적이고, SEO가 중요한 프로젝트에서
가장 좋은 선택지라고 판단된다.
또한, css module, css-in-js 등 다른 방법론과 결합될 경우 대규모 프로젝트에서도 충분히 사용될 수 있는 좋은 선택지가 될 것이다.
CSS-in-TS도 합리적인 결정이 될 수 있으나, 라이브러리의 생태계 측면에서 tailwind에 비해 아쉬운 점이 있기 때문에, 굳이 사용할 이유를 찾지 못하였다.
https://andreipfeiffer.dev/blog/2022/scalable-css-evolution
https://dev.to/srmagura/why-were-breaking-up-wiht-css-in-js-4g9b
https://tech.kakao.com/2022/05/20/on-demand-atomic-css-library/
https://tech.kakao.com/2022/05/24/on-demand-atomic-css-library-2/
https://yozm.wishket.com/magazine/detail/1326/