현대의 웹을 아름답게 만들어 내는데 있어 CSS는 필수적인 요소입니다. 그러나 Vanilla CSS(순수 CSS 자체)가 가지고 있는 명확한 한계점이 존재합니다. 이에 따라 CSS를 효과적으로 사용하기 위한 도구들이 생겨났는데요. CSS 문법을 확장한 Sass와 같은 preprocessor, CSS도 JavaScript로 관리하겠다는 CSS in JS, 그러한 CSS in JS의 단점을 극복하면서 떠오르는 Zero-runtime CSS 등 이처럼 CSS를 효과적으로 작성하기 위한 선택지에는 여러가지가 있습니다.
특히 CSS in JS는 Trade-off가 확실한 CSS 작성 방법입니다. JavaScript로 함께 작성하면서 DX(Developer Experiece)에 이점을 취할 순 있지만, 런타임에서 스타일이 생성되어야 한다는 태생적인 한계 때문에 성능은 다른 선택지에 비해 확실히 좋지 않습니다.
그럼에도 CSS in JS를 사용하기로 결정했다면, 그 장점은 살리되 단점은 최소화하는 방법으로 접근해야합니다. 따라서 이 글은 최종적으로 어떻게 하면 더 효과적으로 사용할 수 있을지 Emotion(CSS in JS 라이브러리 중 하나) 사용을 중심으로 6가지 방법을 제안합니다.
긴 글을 읽을 시간이 없다면 효과적인 Emotion(CSS in JS) 사용을 위한 6가지 방법으로 바로 이동하세요.
이 글에서는 Emotion 사용을 중심으로 설명합니다.
CSS in JS 장단점
장점은 살리고, 단점을 줄이기 위해서 장점과 단점에 대해서 먼저 살펴보아야 합니다. 장점은 우리가 왜 CSS in JS를 써야하는지에 대한 특징이며, 단점은 우리가 극복해야할 측면이기 때문입니다.
장점
- CSS는 기본적으로 전역으로 관리됩니다. 따라서 임의의 시점에 작성한 스타일에 대해 classname이 겹치거나 또는 작성된 스타일을 잊어버릴 수 있습니다. 이 때 의도치 않게 기존의 스타일을 덮어씌워서 스타일을 깨뜨려버리거나 같은 코드를 재작성하게 되면서 많은 코드의 중복이 발생할 수 있습니다. 그러나 Emotion에서는 CSS가 컴포넌트 레벨에서 관리되기 때문에 위와 같은 문제를 회피할 수 있습니다.
- 여러 브라우저에 대응할 수 있도도록 Vendor prefix를 자동으로 붙여줍니다.
- 컴포넌트와 같은 위치에 CSS를 작성할 수 있습니다. 일반적인 CSS는 별도의 파일로 분리되어야 합니다. 관련 있는 코드들을 가능한 가깝게 두는 것이 유지보수 관점에서 좋기 때문에 이 또한 Emotion의 장점이 될 수 있습니다. (Colocation)
- CSS를 JavaScript를 작성하는 것 처럼 작성할 수 있습니다. Vanilla CSS에서는 함수처럼 파라미터를 통해 스타일을 작성할 수 없으나, Emotion에서는 JavaScript 변수들을 활용하여 CSS를 완성할 수 있습니다.
단점
- Emotion은 결국 런타임에 CSS를 만듭니다. Vanilla CSS는 작성된 파일을 받았다면 그저 사용하기만 하면 됩니다. 그러나 Emotion은 컴포넌트의 실행과 함께 CSS를 생성하기 때문에 런타임에 오버헤드가 발생합니다. 결국 Vanilla CSS보다 렌더링이 늦을 수 밖에 없습니다.
- CSS in JS는 라이브러리에 의존적입니다. 따라서 라이브러리의 크기만큼 자연스럽게 번들사이즈는 커지게 됩니다.
Emotion은 어떤 과정으로 CSS를 생성하는가?
단점을 극복하기 위한 실마리를 찾기 위해서 Emotion이 어떤 방식으로 CSS을 생성하고 삽입하는지에 대해 이해해야 합니다. 이에 앞서 브라우저의 렌더링 과정을 살펴보면서 Emotion이 Vanilla CSS에 비해 어떤 지점에서 오버헤드가 발생하는지 살펴보겠습니다.
브라우저의 렌더링 과정 (React 기준)
- DOM (Document Object Model): 브라우저는 HTML을 파싱하여 DOM 트리를 구성합니다.
- CSSOM (CSS Object Model): CSS도 파싱하여 CSSOM 트리를 구성합니다.
- JavsScript 실행: 이 과정에서 Emotion과 React를 초기화 합니다.
- React 렌더: React Component를 렌더하고 가상 DOM을 생성합니다. Emotion 사용하는 컴포넌트는 여기에서 스타일을 생성합니다.
- Emotion 처리: 생성된 스타일을 CSS
<style>
태그에 주입합니다. - DOM, CSSOM 업데이트: 3~5번 결과에 따라 DOM과 CSSOM을 업데이트 합니다.
- Render Tree: DOM과 CSSOM이 결합되어 렌더 트리를 형성합니다. 이 트리는 화면에 표시될 요소들을 포함합니다.
- Layout: 렌더 트리의 각 노드에 대해 화면상의 정확한 위치와 크기를 계산합니다.
- Paint: 각 요소의 픽셀을 화면에 그립니다.
- Composite: 여러 레이어를 합성하여 최종 이미지를 생성합니다.
위 과정 중 4번과 5번이 Emotion에서 발생하는 오버헤드입니다. 이 과정에서 스타일이 생성되고 삽입됩니다.
Emotion의 스타일 생성 및 삽입 원리
Emotion의 상세한 동작원리를 이해하려면 'Emotion이 CSS를 생성하는 방법'을 살펴보세요.
그렇다면 브라우저 렌더링 과정 중 4번과 5번을 최대한 빠르게 처리하는 것이 곧 Emotion의 성능을 최대로 가져갈 수 있는 방법입니다. 간략히 정리하자면 Emotion은 아래와 같은 과정을 거쳐서 스타일을 생성하고 삽입하게 됩니다.
@emotion/react
에 의한 JSX 트랜스파일링- 브라우저에서 React 렌더시작
- Emotion cache 생성
- cssProp에 전달된 스타일 직렬화
- 직렬화된 스타일을 캐시에 등록 및 삽입
- stylis로 스타일을 CSSRule로 컴파일
- 컴파일된 CSSrule을 HTML문서에 삽입
효율적인 스타일 생성을 위해 고려해야 하는 것
고려하지 않아도 되는 지점
- 1번은 빌드타임에서 발생하기 때문에 런타임에서의 성능만 고려할 때 관심사가 아닙니다.
- 2번의 React Component를 효율적으로 렌더하는 것은 Emotion 스타일 생성 측면에서 관심사가 아닙니다.
- 3번은 캐시는 효율적인 스타일 생성을 위해 필수적인 과정입니다.
4번에서 7번의 과정에서부터 런타임에서 스타일을 직렬화, 컴파일, 삽입이 발생합니다. 여기에서 목표로 두어야하는 것은 우리가 Emotion으로 스타일을 작성하는 다양한 방법 중 최대한 런타임 성능이 빠른 방법을 발견해내는 것입니다.
4번에서 7번의 과정은 캐시에 의해서 간소화 되거나 생략될 수 있습니다. 하지만 'Emotion이 CSS를 생성하는 방법'에서 설명되어 있듯 결국 모든 컴포넌트는 다음의 과정을 거치게 됩니다.
- 스타일 직렬화: CSS를 Emotion에서 처리할 수 있는 형태로 직렬화합니다.
<Insertion />
렌더: 캐시에 등록하고 문서에 스타일을 삽입합니다.<WrappedComponent \>
렌더: 원래의 컴포넌트를 렌더합니다.
2번과 3번의 캐시 등록과 삽입 그리고 컴포넌트의 렌더링은 CSS를 어떤 방식으로 작성하는지와 관계 없이 Emotion의 컴포넌트에서는 무조건 실행되는 부분입니다. 결국 우리가 효율적을 만들 수 있는 부분은 1번의 스타일 직렬화 하나로 좁혀지게 됩니다.
효과적인 Emotion 사용을 위한 6가지 방법
이제부터 스타일 직렬화를 최대한 덜 발생키시면서도, Emotion의 DX를 살려서 스타일을 작성할 방법들에 대해서 소개하겠습니다. 또한 각 방법이 가지는 장점과 코드 작성예시 그리고 실제로 방법 간의 성능을 측정하고 비교하여 해당 방법에 대해서 검증하겠습니다.
각 방법의 성능측정은 React.Profile 컴포넌트로 측정한 baseDuration을 기준으로 하며 측정 단위는 ms입니다.