판다 CSS 도입: Emotion.js 대체와 최신 스타일링 패러다임 탐구

KyoungMin Lee
IHFB  R&D 팀블로그
22 min readFeb 14, 2024
Photo by Sid Balachandran on Unsplash

안녕하세요. 밀당R&D 본부 FrontEnd팀 이경민입니다.

밀당 FE 팀에선 React, Next.js 를 사용해서 제품을 제작하고 있습니다.
작년 말 React와 Next.js 의 버젼 업데이트를 진행했습니다.

  • React → 18 version
  • Next → 14 version

Next.js 14 버젼에선 꽤 많은 부분이 달라지는데, 그중 하나가 RSC(React-Server-Component)입니다. Next.js 14 버젼에서 app 폴더 안의 모든 React 컴포넌트는 기본적으로 RSC입니다. 저희는 MUI 기반으로 디자인시스템을 구축했는데, 많은 부분에서 부가적인 스타일링을 Emotion.js로 작성했습니다. MUI 도 내부적으로 Emotion을 사용하고 있습니다.
버젼업 과정에서 pages에 있던 컴포넌트들을 app router로 수정하진 않았기 때문에 모든 컴포넌트를 클라이언트 컴포넌트로 사용하고 있긴 하지만, RSC를 사용한다고 가정했을 때 Emotion 관련한 이슈가 있습니다.
경량화를 위해 상태를 제거한 RSC의 특성상 Context를 사용할 수 없기 때문에 React의 Context를 사용해서 스타일을 주입하는 Emotion을 사용할 수 없습니다. (https://github.com/mui/material-ui/pull/37656 처럼 사용자들이 우회법을 만들고 있는 듯 합니다.)

그렇기에, Emotion의 대체제로 아래와 같은 특징을 가진 라이브러리를 원했습니다. 라이브러리의 선정 조건은 아래와 같습니다.

  1. RSC에서도 사용이 가능한가 ?
  2. 개발자 경험(DX)를 현재와 같이 유지할 수 있는가 ?
  3. Buildtool Agnostic ? (빌드 툴에서 자유로운가? (바벨 설정X, TypeScript 설정X))
  4. 최신 스타일링 패러다임을 지향하는가 ?

대체할 수 있는 라이브러리를 찾기 전에, 왜 CSS-IN-JS 가 많이 사용되는지 알아보겠습니다.

WHY CSS-IN-JS

CSS-IN-JS는 웹개발에서 스타일링을 다루는데 사용되는 패러다임입니다.
장단점은 대략 아래 내용과 같습니다.

장점

  1. 캡슐화와 스코프: 컴포넌트 기반 스타일링을 통해 스타일이 컴포넌트 스코프 내에서 유지되어, 스타일 충돌을 방지하고 전역 스코프 오염을 막을 수 있습니다.
  2. 동적 스타일링: JavaScript를 사용하여 동적으로 스타일을 변경할 수 있어, 상태에 따라 스타일을 동적으로 조절할 수 있습니다.
  3. 코드 스플릿팅과 트리 쉐이킹: 필요한 스타일만 불러와 사용자에게 전달하여 빌드된 번들 크기를 줄이고 페이지 로딩 속도를 향상시킬 수 있습니다.
  4. 컴포넌트 기반 스타일링: 각 컴포넌트가 자체적으로 스타일을 갖게 되어 재사용성을 높이고 유지보수를 쉽게 만듭니다.

단점

  1. 성능의 우려: 일부 CSS-IN-JS 라이브러리는 런타임에서 스타일을 생성하는데, 이 때 성능 이슈가 발생할 수 있습니다.

컴포넌트와 같은파일위치에서 스타일을 작성해나가는게 js 파일 — css 파일 옮겨가면서 작업하는것보다 작업 효율이 훨씬 뛰어나다고 생각합니다. 하나의 파일에서 관리를 할 수 있고, 스타일을 javascript 코드로 작성하니 export 할수도 있어서 재사용 하기도 편합니다. css도 재사용이 가능하지만 JS로 작성하는 코드에 비해 다른 개발자들이 작업할때도 편하게 찾아서 쓰기 힘듭니다. 그래서 CSS-IN-JS를 사용하는게 작업효율면에서 좋다고 생각합니다.

라이브러리 선정조건

  • TypeSafe
  • RSC 지원
  • Zero-runtime
  • 테마 지원
  • 동적 스타일링 지원
  • 커뮤니티 지원 (optional)

필수는 아니지만 커뮤니티가 크면 클수록, 업데이트가 꾸준히 있다면 좋겠죠.이런 조건을 거쳐서 선정해본 라이브러리가 Panda CSS 입니다.

Panda CSS

https://www.adebayosegun.com/blog/the-future-of-chakra-ui

탄생 배경

panda css는 chakra ui 팀에서 chakra ui를 디자인 시스템 인프라으로 발전하기 위해서 고안 되었습니다.

chakra ui에서는 도입 당시 앱과 디자인 시스템을 구축하는데 가장 인기있는 도구 였던 Emotion를 사용했었고 Styled system로 부터 직관적이고 유연한 방식의 chakra 기초 스타일링을 만드는데 많은 아이디어를 얻었습니다.

하지만 사용하는 Emotion이 Runtime CSS-in-JS 라는 점이고 이를 제거하면 성능이 향상되고 초기 JS 페이로드를 줄이고 RSC에서 사용할 수 있기 때문에 여러 사용자들로 부터 제거해달라는 많은 요청이 있었습니다. 또한 최신 트랜드들은 headless 컴포넌트, 디자인 토큰, 서버 컴포넌트 중심으로 변화 하였으며 Emotion은 이러한 변화에 안정적으로 작동하지 않거나 사용할 수 없게 되었으며 컴포넌트 라이브러리에 대한 기대치도 변화 하기 때문에 이런 새로운 패러다임을 만족 시키기 위함도 새로운 프레임워크를 만드는 계기가 되었습니다.

다른 이유로는 react hook, 컴포넌트, 테마 시스템, 다형성 타입들이 하나의 거대한 모놀리스로 되어 있고 여러 프레임워크를 다뤄야 하는 팀에 일관성 떨어지는 DX와 번아웃을 야기하는 기존 문제점들을 해결하고자 문제들을 더 관리하기 쉽도록 작은 단위로 나누었으며 framework agnostic, 직관적인 styled props, 유지 보수 워크로드 감소, 대부분 사용사례에서 단순하게 사용가능한 디자인 토큰 등이 문제 해결을 위한 요구사항으로 정했습니다.

이러한 요구사항을 해결하고자 독립적인 Styling system, Design tokens, State machines, Headless UI 컴포넌트 프로젝트들로 나누게 되었고 여기서 Styling system을 담당하는 프로젝트가 panda css 입니다.

특징

공식문서에 소개된 특징은 아래와 같습니다.

빌드타임에 정적분석을 해서 CSS 파일을 생성합니다. (이 과정에서 PostCSS 플러그인이 사용됩니다.)

정적 분석: Panda는 빌드 시간에 스타일을 파싱하고 분석하기 위해 정적 분석을 사용하며, JavaScript 프레임워크에서 사용할 수 있는 CSS 파일을 생성합니다.

코드 생성: Panda는 스타일을 작성하는 데 사용되는 가벼운 런타임 JS 코드를 생성합니다. 이것은 객체의 키-값 쌍을 최적화된 함수로 결합하는 것으로 생각할 수 있습니다. 이는 브라우저에서 스타일을 생성하거나 <head>에 스타일을 주입하지 않습니다.

타입 안전성: Panda는 css 속성과 디자인 토큰에 대한 타입 안전성을 제공하기 위해 csstype와 자동 생성된 유형을 결합합니다.

좋은 개발자 경험

최신 CSS: Panda는 생성된 스타일에서 캐스케이드 레이어, CSS 변수, :where 및 :is와 같은 현대적인 selector와 같은 최신 CSS 기능을 사용합니다.

소개된 특징들에 대해 좀 더 찾아봤습니다.

Atomic CSS

Atomic CSS는 종종 Utility-First CSS 라고 부르기도 하며 스타일 전달을 위한 페이로드 크기를 줄이고 쉽게 스타일 조합과 재사용을 하게 하는 스타일링 접근 방식입니다. Atomic CSS 는 각자 CSS rule들이 정확히 하나의 선언(atom)을 가지게 CSS를 작성하는 방법입니다.
모양새는 아래와 같습니다.

/** 일반적인 class */
.myClass {
background: red;
width: 100%;
height: 100%;
}
/** 원자적으로 작성할 수 있다 */
.a {
background: red;
}
.b {
width: 100%;
}
.c {
height: 100%;
}

Panda는 정적분석을 통해 Atomic CSS 를 생성합니다.

.flex {
display: flex;
}
.bg-blue {
background-color: blue;
}
.font-16 {
font-size: 16px;
}

하나의 클래스는 한가지 역할만을 수행합니다.

Atomic CSS를 구현하는 라이브러리중에 인기있는 tailwindcss, unocss 처럼 이러한 특성을 활용하여 네이밍도 직관적으로 생성합니다.

특징

  • 사용하는 클래스들을 미리 지정해놓기 때문에 프로젝트가 거대해지더라도 css파일은 일정 파일크기 이상으로 커지지 않습니다.
https://www.youtube.com/watch?v=9JZHodNR184&t=229s
  • CSS 파일 캐싱이 편합니다.
  • 불변성, 구성성, 예측 가능성, 부작용 방지 등 함수형 프로그래밍의 좋은 장점들을 가져갈 수 있습니다.

Panda css와 비슷한 tailwindcss는 아래와같은 단점이 있는데요

  • 사용하는 class의 네이밍을 숙지해야합니다. (Learning Curve)
  • 긴 클래스 조합으로 인해 html 작성 시 태그 하나하나의 클래스가 길어질 수 있습니다.

Panda는 이 점을 보완했습니다.

사용하는 클래스의 네이밍은 몰라도됩니다.
정적분석을 통해 사용되는 Atomic Css들을 자동으로 생성하고, styled-component , emotion과 같은 사용성으로 자바스크립트로 스타일링이 가능합니다.

아래처럼 자바스크립트 코드를 작성하게되면

const styles = css({
backgroundColor: 'gainsboro',
borderRadius: '9999px',
fontSize: '13px',
padding: '10px 15px'
})
<div className={styles}>
<p>Hello World</p>
</div>

아래와 같은 css 파일로 생성해줍니다.

@layer utilities {
.bg_gainsboro {
background-color: gainsboro;
}

.rounded_9999px {
border-radius: 9999px;
}

.fs_13px {
font-size: 13px;
}

.p_10px_15px {
padding: 10px 15px;
}
}

사용자 입장에선 일반적인 CSS-IN-JS 라이브러리를 사용할 때 처럼 편하게 사용하기만 하면 됩니다.

Pattern

패턴은 자주 사용되는 스타일 패턴들을 반복을 줄이고 가독성을 높이기 위해 스타일을 정의할 수 있는 기능입니다.

Panda에서는 미리 정의되어있는 Pattern들을 사용할 수 있고 패턴을 직접 만들 수도 있습니다.

예시1) 수직 FlexBox

import { hstack } from '../styled-system/patterns'

function App() {
return (
<div className={hstack({ gap: '6' })}>
<div>First</div>
<div>Second</div>
<div>Third</div>
</div>
)
}

예시2) GridBox

import { grid } from '../styled-system/patterns'

function App() {
return (
<div className={grid({ columns: 3, gap: '6' })}>
<div>First</div>
<div>Second</div>
<div>Third</div>
</div>
)
}

디자인 시스템 지원

디자인 토큰, recipe 등 DS에서 쓰이는 개념들에 대한 구현이 가능합니다.

CVA

Class Variance Authority, Stitches, Vanilla Extract으로 부터 영감을 받았으며 하나의 컴포넌트가 요구사항에 따라서 다르게 스타일링을 처리하기에 용이하도록 해주는 기능입니다.

아래는 clsx와 cva 방식과 비교한 예시입니다.

clsx

import clsx from 'clsx';
import { ButtonOrLink, Props as ButtonOrLinkProps } from './ButtonOrLink';
export interface Props extends ButtonOrLinkProps {
intent?: 'primary' | 'secondary' | 'danger';
}
export function Button({ intent = 'primary', ...props }: Props) {
return (
<ButtonOrLink
className={clsx(
'flex items-center justify-center px-4 py-2 rounded font-medium focus:outline-none focus:ring-2 focus:ring-offset-white dark:focus:ring-offset-black focus:ring-offset-1 disabled:opacity-60 disabled:pointer-events-none hover:bg-opacity-80',
{
'bg-brand-500 text-white': intent === 'primary',
'bg-gray-200 text-gray-900 dark:bg-gray-700 dark:hover:bg-gray-600 dark:text-gray-100 focus:ring-gray-500':
intent === 'secondary',
'bg-red-500 text-white focus:ring-red-500': intent === 'danger',
},
)}
{...props}
/>
);
}

cva

import { cva, VariantProps } from 'class-variance-authority';
import { ButtonOrLink, Props as ButtonOrLinkProps } from './ButtonOrLink';
const buttonStyles = cva(
'flex items-center justify-center px-4 py-2 rounded font-medium focus:outline-none focus:ring-2 focus:ring-offset-white dark:focus:ring-offset-black focus:ring-offset-1 disabled:opacity-60 disabled:pointer-events-none hover:bg-opacity-80',
{
variants: {
intent: {
primary: 'bg-brand-500 text-white',
secondary:
'bg-gray-200 text-gray-900 dark:bg-gray-700 dark:hover:bg-gray-600 dark:text-gray-100 focus:ring-gray-500',
danger: 'bg-red-500 text-white focus:ring-red-500',
},
fullWidth: {
true: 'w-full',
},
},
defaultVariants: {
intent: 'primary',
},
},
);
export interface Props
extends ButtonOrLinkProps,
VariantProps<typeof buttonStyles> {}
export function Button({ intent, fullWidth, ...props }: Props) {
return (
<ButtonOrLink className={buttonStyles({ intent, fullWidth })} {...props} />
);
}

panda는 cva 를 사용해 variant 기반의 Atomic Recipe 를 구현할 수 있습니다.

import { cva } from '../styled-system/css'

const button = cva({
base: {
display: 'flex'
},
variants: {
visual: {
solid: { bg: 'red.200', color: 'white' },
outline: { borderWidth: '1px', borderColor: 'red.200' }
},
size: {
sm: { padding: '4', fontSize: '12px' },
lg: { padding: '8', fontSize: '24px' }
}
}
})
import { button } from './button'

const Button = () => {
return (
<button className={button({ visual: 'solid', size: 'lg' })}>
Click Me
</button>
)
}

Panda css 에서도 Atomic Recipe 또는 cva라고 부르며 Class Variance Authority 처럼 단순히 조합만 도와주는게 아닌 atomic css까지 생성해줍니다.

SVA

슬롯 레시피를 통해 복잡한 컴포넌트를 구성을 지원합니다.
sva는 컴포넌트 여러부분에 스타일 변형을 줘야할 때 사용합니다.
체크박스의 root, control, label 의 스타일을 설정하는 예시를 보여줍니다.

import { sva } from '../styled-system/css'

const checkbox = sva({
slots: ['root', 'control', 'label'],
base: {
root: { display: 'flex', alignItems: 'center', gap: '2' },
control: { borderWidth: '1px', borderRadius: 'sm' },
label: { marginStart: '2' }
},
variants: {
size: {
sm: {
control: { width: '8', height: '8' },
label: { fontSize: 'sm' }
},
md: {
control: { width: '10', height: '10' },
label: { fontSize: 'md' }
}
}
},
defaultVariants: {
size: 'sm'
}
})

사용시

import { css } from '../styled-system/css'
import { checkbox } from './checkbox.recipe'

const Checkbox = () => {
const classes = checkbox({ size: 'sm' })
return (
<label className={classes.root}>
<input type="checkbox" className={css({ srOnly: true })} />
<div className={classes.control} />
<span className={classes.label}>Checkbox Label</span>
</label>
)
}

Style Props

Styled System에서 인기가 있었던 style props 방식은 매우 직관적이고, 유연한 스타일링 시스템을 구축하는데 도움이 됩니다.

panda.css에서는 jsx ast 분석을 통하여 사용하는 스타일만 atomic하게 스타일을 생성합니다.

import { css } from '../styled-system/css'
import { styled } from '../styled-system/jsx'

// The className approach
const Button = ({ children }) => (
<button
className={css({
bg: 'blue.500',
color: 'white',
py: '2',
px: '4',
rounded: 'md'
})}
>
{children}
</button>
)

// The style props approach
const Button = ({ children }) => (
<styled.button bg="blue.500" color="white" py="2" px="4" rounded="md">
{children}
</styled.button>
)

Cascade Layer

Cascade Layers는 CSS에서 스타일 충돌과 specificity 문제를 보다 의도적으로 관리하기 위해 사용되며, 작성자가 Cascade를 직접 제어할 수 있습니다. 이 기능을 사용하면 specificity 해킹이나 !important 선언에 의존하지 않고도 우선순위가 낮은 재설정부터 우선순위가 높은 재정의까지 명확하고 의도적인 레이어를 사용하여 CSS를 구조화할 수 있습니다. 다양한 소스의 스타일 관리를 간소화하여 CSS 코드베이스를 더 쉽게 유지 관리하고 업데이트할 수 있습니다.

Panda 에서의 cascade layer

1 ~ 5 — 우선순위 낮음 ~ 높음

  1. reset: 기본 HTML을 초기화하는 레이어
  2. base: global 스타일을 담당하는 레이어
  3. tokens: 토큰이나 시멘틱 토큰들을 위한 css variables이 있는 레이어
  4. recipes: config에서 만든 recipe들을 포함하는 레이어
  5. utilities: 특정 utility class를 위한 스타일들

TypeSafe Style

대부분의 스타일 속성은 기본 CSS 속성이나 theme개체에 정의된 대로 정의된 해당 토큰 값에 연결됩니다.

다양한 문법지원

표현을 다양하게 가져갈 수 있다는건 좋게 느껴졌습니다. (이부분은 개개인에 따라 단점으로 느껴질 수도 있겠네요.)

<button
className={css({
bg: { base: 'red.500', _hover: 'red.700' }
})}
>
Hover me
</button>
<button
className={css({
bg: 'red.500',
_hover: { bg: 'red.700' }
})}
>
Hover me
</button>

size 나 variant 도 반응형 처리가 필요하다면 아래처럼 설정할 수 있습니다.
개인적으로 삼항연산자 코드는 보기 지저분해보이는데 아래처럼 깔끔하게 해결할 수 있어서 좋게 느껴집니다.

import { button } from '../styled-system/recipes'

function App() {
return (
<div>
<button className={button({ size: { base: 'sm', md: 'lg' } })}>
Click me
</button>
</div>
)
}

작동 방식

Panda css는 build time에 정적 분석을 통하여 CSS를 만들지만 runtime에서 CSS-in-JS 문법들을 클래스명으로 변환해주는 것이 필요합니다. Panda 또는 panda codegen 명령어를 하면 runtime에 필요한 styled-system 폴더와 파일을 생성합니다.

1. Panda context 불러오기

  • config 파일을 찾아서 분석
  • panda context 생성: config를 통해 코드 생성기를 준비하고 사용자들이 작성한 파일을 AST로 분석

2. 아티펙트 생성하기

  • 경량화된 JS 런타임과 타입들을 output 디렉토리에 작성

3. app code에서 사용하는 스타일 추출

  • 각 사용자의 파일에서 파서를 실행하여 스타일을 식별 및 추출하고, CSS를 계산하고, styles.css에 작성합니다.

아티팩트 생성 단계(codegen)에서는 모든 토큰, 패턴, 레시피, 유틸리티 등이 포함된 해결된 구성을 사용하여 스타일 시스템 폴더를 생성합니다. 사용하는 코드(및 유형)만 포함하도록 앱에 맞는 맞춤형 런타임을 생성합니다.

Panda studio

제공하는 Panda studio를 사용하면 정의된 디자인 토큰들을 볼 수 있는 로컬 사이트도 제공해 줍니다.

용량

필요한 스타일만 추출하기에 적은 용량을 가집니다.

영상 가이드 제공

공식문서도 잘 되어있는데, 영상도 제공해줍니다.

마치며

지금까지 PandaCss에 대해 알아봤는데요.
정보를 찾아보면서 가장 크게 와닿았던 매력은 기존의 CSS관련 라이브러리들(tailwindcss, unocss, chakra-ui, …)의 장점을 최대한 가져가면서 단점은 줄인 점인 것 같습니다.
아직 밀당PT에 도입이 확정된 것은 아니지만, 확정이 된다면 기존의 MUI와 Emotion을 사용하고 있던 부분들을 점진적으로 개선하는것을 계획하고있습니다.
긴 글이지만 읽어주셔서 감사합니다.

참고한 자료들

jbee.io

Next) 서버 컴포넌트(React Server Component)에 대한 고찰

Panda CSS — Build modern websites using build time and type-safe CSS-in-JS

Emotion — @emotion/cache

A Complete Guide to CSS Cascade Layers | CSS-Tricks

In Defense of Utility-First CSS | frontstuff

CSS { In Real Life } | A Year of Utility Classes

“Why would I choose Panda CSS ?”

프론트엔드 팀에서 동료를 찾고 있습니다!

밀당 영어가 최근 급성장하여 에듀테크 유니콘이 되기 위해 새로운 시스템을 개발하고 있습니다. 데이터 파이프라인 구축부터 인프라 개선 등 기존 레거시에 존재하는 다양한 문제를 해결, 개선하고 있습니다. 새로운 도전을 즐기시는 분들과 함께 하면 교육의 혁신을 더욱 빠르고 멋지게 이룰 수 있을 것 같습니다!

밀당과 함께할 동료를 찾습니다.

--

--