프론트엔드의 꽃이라고 할 수도 있는 css
에 대해서 오늘은 포스팅해볼까합니다. 프론트엔드 개발자로서 많이 사용하는 styled-components
, tailwindCSS
, emotion
등 이런 것들은 왜 등장하게 되었는지, 오늘은 CSS-in-Js
의 등장배경에 대해서 알아보겠습니다.
1990년, HTML의 탄생
최초의 웹은 문서를 공유하기 위해 만들어졌습니다. 문서를 표현하기 위해 HTML이 만들어졌고, HTML은 콘텐츠에 의미를 부여한 태그를 붙여주는 방식으로 단순한 형태의 문서를 제공하게 되었습니다.
inline-style
그러다가 좀 더 다른 형태의 서식으로 문서를 꾸밀 필요성이 제기되고, style
개념이 추가되어 태그에 style
속성을 입력해서 다양한 형태의 문서를 만들 수 있게 되었습니다. 이를 우리가 흔히 잘알고 있는 inline-style
(인라인 스타일)이라고 합니다.
1996년, CSS의 등장
하지만 inline-style은 코드가 너무 비대해져 가독성이 떨어지고, 유지 보수가 어렵게 되는 여러 문제가 발생하게 되었습니다.
그래서 이에 대한 방안으로 1996년에 CSS
가 고안되었습니다. CSS는 별도의 Rule을 선언하고 서식이 필요한 곳을 선택해 반복적으로 적용하는 식으로 만들어지게 되었습니다.
2006년, Sass의 등장
하지만 CSS
도 아주 큰 문제들이 많았습니다. css module이 있기 이전에, Sass가 먼저 등장하게 되었습니다. CSS
자체 만으로는 클래스 네이밍이나(글로벌 네임스페이스), 복잡한 셀렉터 구조, 사용하지 않는 css 코드 제거가 어려운 것 등 CSS
를 재사용하기 위해서는 그저 복붙하는 수밖에 없었습니다. 이에 따라 등장한게 Sass인데요, Sass는 Selector를 쉽게 만들어 주는 것 뿐만 아니라 재사용성도 높아지고, 시간적 비용도 감소시켜주었습니다.
Ajax 등장과 편집권 변화
개발자들은 셀렉터와 클래스로 CSS
를 사용하다보니 확실히 클래스에서 장점을 느끼게 됩니다. 굳이 CSS에서 복잡한 Selector를 잘 쓰는 것보다 HTML 단계에서 class 이름을 잘 지정하는 것이 더 낫다라는 결론에 도달한 개발자들. 이를 기점으로 Selector보다는 어떻게 해야 class이름을 더 잘 지을수 있을까에 대한 고민으로 흐름이 넘어가게 됩니다.
의미 있는 이름 vs 시각적인 이름
class 네이밍에는 위 두가지 접근법이 있는데요, 스타일 변경시 CSS만을 수정하다보니 시각적인 네이밍을 하게되면 클래스 이름과 실제 CSS간에 차이가 발생할 수 있게 된다고 합니다. 따라서 class 이름을 의미론적인 관점
에서 짓는 것이 주요 원칙으로 자리잡게 되었습니다.
웹 어플리케이션에서 CSS의 한계
웹 생태계가 발전하며 웹은 점차 어플리케이션의 형태를 띄게 됩니다. 그러면서 아래와 같은 여러 문제점이 발생하게됩니다.
- CSS의 대표적인 한계점
- Global namespace: 모든 스타일이 global에 선언되어 별도의 class 명명 규칙을 적용해야 하는 문제
- Dependencies: css간의 의존관계를 관리하기 힘든 문제
- Dead Code Elimination: 기능 추가, 변경, 삭제 과정에서 불필요한 CSS를 제거하기 어려운 문제
- Minification: 클래스 이름의 최소화 문제
- Sharing Constants: JS의 상태 값을 공유할 수 없는 문제
- Non-deterministic Resolution: CSS 로드 순서에 따라 스타일 우선순위가 달라지는 문제
- Isolation: CSS와 JS가 분리된 탓에 상속에 따른 격리가 어려운 문제
2013년, BEM 등 방법론
BEM(Block, Element, Modifer)
은 Yandex가 고안한 class 네이밍 컨벤션으로, 원칙을 충실히 준수하면 위의 css의 문제점들을 완벽히 해결할 수 있었습니다. 이론 자체는 완벽했으나, 네이밍 실수, 러닝커브 등 휴먼팩터 문제를 완벽히 방지할 수 없는 근본적인 한계가 있었습니다.
2015년, CSS Modules
CSS의 문제였던 Global Scope
를 막기 위해서 Component
단위에서 사용되는 CSS
에 hash를 추가하여 CSS가 더 이상 Global 하지 않도록 하는 방식을 통해서 해결을 하고자 하는 방법이 만들어지게 됩니다.
본격적으로 JS와 CSS를 결합하려는 시도가 시작된 기점이라 할 수 있겠습니다.
2016년, CSS-in-JS
CSS-in-JS는 말 그대로 자바스크립트 코드에서 CSS를 작성하는 방법론을 뜻합니다.
대표적으로 StyledComponenet, Emotion등의 라이브러리로 구체화되었습니다.
2017년, AtomicCSS
'utility-first CSS framework'인 TailwindCSS
는 새로운 AtomicCSS라는 패러다임을 가지고 등장하게 됩니다.
Function, utility-first 로 표현되기도 하는 AtomicCSS는 시멘틱 네이밍 원칙에서 아예 벗어나 시각적인 요소를 표현하는 원자단위로 클래스를 미리 선언합니다.
결론적으로 현재시점에서 CSS 라이브러리는 다음과 같이 3가지로 분류됩니다.
- Semantic CSS
- CSS-in-JS
- AtomicCSS
CSS-in-JS의 장단점
-
장점
CSS-in-JS
방식을 사용하게 된다면 CSS의 컴포넌트화로 스타일시트의 파일을 유지보수 할 필요가 없다.- CSS 모델을 문서 레벨이 아닌 컴포넌트 레벨로 추상화할 수 있다.
- CSS-in-JS는 JavaSript 환경을 최대한 활용 할 수 있다.
- JavaScript와 CSS 사이의 상수와 함수를 쉽게 공유 할 수 있다.
- 현재 사용중인 스타일만 DOM에 포함한다.
- CSS-in-JS는 짧은 길이의 유니크한 클래스를 자동으로 생성하기 때문에 코드 경량화의 장점이 있다.
-
단점
- 러닝 커브
- 새로운 의존성
- 별도의 라이브러리를 설치해야 하므로 번들 크기가 커진다
- 인터랙션한 페이지일 경우 CSS 파일을 따로 관리하는 방법에 비해 느린 성능을 보여줄 수 있다.
CSS-in-JS를 항상 사용해야할까?
많은 프로젝트에 참여하면서 CSS-in-JS 방식을 사용하는 styled-components
나 emotion
을 많이 사용하였었는데요, 게다가 많은 유행을 타는 라이브러리다보니 대체로 많이 사용되는 편입니다. 그렇다면 항상 CSS-in-JS방식을 늘 사용하는게 맞을까요?
CSS-in-JS
방식은 개발자에 그냥 쌩! CSS를 사용하는 것보다는 DX를 굉장히 많이 향상시켜줍니다. 게다가 프론트 개발자로서 React
를 사용하는 경우에는 컴포넌트 단위로 관리할 수 있으니 매우 편리하게 느껴지죠.
하지만 자바스크립트로 디자인을 하는 것이다보니 자바스크립트의 코드가 길어질 수 밖에 없다는 단점이 있습니다.
그리고 가장 크게 뽑는 단점으로는 런타임 오버헤드(runtime overhead)
인데요.
오버헤드란?
어떤 처리를 위해 들어가는 간접적인 처리 시간을 뜻합니다. 예를 들어 A라는 처리를 단순하게 실행한다면 10초 걸리는데, 안전성을 고려하고 부가적인 B라는 처리를 추가한 결과 처리시간이 15초 걸렸다면, 오버헤드는 5초가 됩니다. 이글에서 런타임 오버헤드는 런타임에 CSS를 생성함으로써 벌어지는 성능하락을 말합니다.
우리가 브라우저에 웹 페이지가 로드되는 과정을 생각해보면, HTML > CSS > JavaSript 순으로 불러오게됩니다.
CSS-in-JS 방식을 사용한다면 맨 마지막에 디자인을 불러오게 되는 것인데요.
자바스크립트 자체만으로는 디자인을 만들 수 없으니, 자바스크립트가 실행될 때 디자인으로 적용될 수 있게끔 JS가 일반 CSS로 스타일을 직렬화 해야합니다. 즉, 쉽게 말하자면 자바스크립트가 실행될 때 동적으로 스타일이 생성되는 것입니다.
개발 모드에서는 런타임 도중
<style>
tag에 스타일을 생성해서 삽입하고, 배포 모드에서는 stylesSheet를CSSStylesSheet.insertRule 통해 바로CSSOM에 주입하게 됩니다.
런타임에 동작하는 CSS-in-JS 라이브러리는 대표적으로 styled-components
와 emotion
이 있습니다. 따라서 이런 단점이 있기 때문에 언제나 CSS-in-JS를 사용하는게 옳다고 볼 순 없겠죠..?
게다가 CSS가 JS로 작성되다보니 번들의 크기도 커지게 될 것입니다.
이외에도 React Devtools를 복잡하게 만드는 것, 런타임에 헤드의 끝에 스타일시트를 삽입하기 때문에 SEO에 부적합한 것 등 여러 단점이 존재합니다. 그래서 SSR에 부적합합니다.
그래서 대안으로 나온 Zero runtimeZero Runtime
제로런타임은 말 그대로 런타임이 0이라는 말인데요, 어떻게 제로 런타임일까요?
런타임에 CSS로 생성되던 것을 런타임 이전에 미리 CSS파일로 빌드를 해두자라는 취지에서 시작됩니다. 제로 런타임 라이브러리 같은 경우 JS 파일에 작성된 CSS를 별도의 .css
파일로 변환 후 브라우저는 해당 스타일을 읽고 웹 페이지에 적용하는 방식으로 작동하게 됩니다.
또한 zero-runtime이란 말 그대로 앞서 설명한 runtime에서의 동작이 없다는 것을 의미합니다. 즉 동적으로 스타일을 생성하지 않는데, 이러한 문제점을 Linaria
라는 라이브러리에서는 지혜롭게 풀어냈습니다. Linaria
라이브러리는 styled-components
에서 영감을 받아 유사한 API를 가진 CSS-in-JS 라이브러리이고 zero-runtime으로 동작하고 있습니다.
소스코드
import { styled } from '@linaria/react';
import { families, sizes } from './fonts';
const background = 'yellow';
const Title = styled.h1`
font-family: ${families.serif};
`;
const Container = styled.div`
font-size: ${sizes.medium}px;
background-color: ${background};
color: ${props => props.color};
width: ${100 / 3}%;
border: 1px solid red;
&:hover {
border-color: blue;
}
`;
빌드결과
.Title_t1ugh8t9 {
font-family: var(--t1ugh8t-0);
}
.Container_c1ugh8t9 {
font-size: var(--c1ugh8t-0);
background-color: yellow;
color: var(--c1ugh8t-2);
width: 33.333333333333336%;
border: 1px solid red;
}
.Container_c1ugh8t9:hover {
border-color: blue;
}
동적인 스타일에 대응할 수 있는 이유는 css에서 사용하는 variable을 사용하여 변경되는 항목에 대해 css variable만 변경하여 대응할 수 있기 때문이라고 볼 수 있습니다.
제로런타임의 대표적인 라이브러리는 styled-components과 동일한 문법을 사용하는 linaria와 vanilla-extract 등이 있다.
단점으로는 css 파일을 다루기 때문에 플러그인 설치 및 번들러 설정을 건드려야 해서 번거로울 수 있다는 점입니다.
Near-Zero Runtime
런타임에 스타일을 생성하지만 제로 런타임에 근접한 성능을 보장하는 라이브러리들이다. 대표적으로는stitches와 TailwindCSS 가 있다.
예를 들어, 동적 스타일링시, stitches는 사전에 정의한 variants에 의한 스타일링만 가능하도록 제한하여 성능 이점을 챙겼다. 완전한 동적 스타일이 가능한것은 아니지만 적절한 타협점으로 본다.
또 다른 예는, Tailwind CSS는 사전에 설정한 config를 토대로 HTML,js,ts 등 템플릿을 스캔하고 해당 스타일을 생성한 다음 정적 CSS 파일에 작성하는 방식으로 작동한다.
더 전통적인 방식인 CSS module이나 sass도 똑같이 빌드 타임에 스타일시트를 붙이지만 selector 기반 CSS의 한계를 극복하지 못했기 때문에 tailwind css를 콕 집어서 추천해주고 있는 것 같습니다.
Reference Doc
CSS-IN-JS는 어떻게 컴포넌트를 스타일링해줄 수 있는가?
(번역) 우리가 CSS-in-JS와 헤어지는 이유