CSS-in-JS와 서버 컴포넌트는 왜 공존하기 어려운지 알아보고, 이를 해결하기 위해 styled-components를 Tailwind CSS로 마이그레이션한 경험을 공유합니다.

이 글을 쓰게 된 계기

Next.js 13이 나온지도 벌써 11개월이 다 되어 갑니다. App Router가 정식으로 출시되면서 React 팀이 발표한 '서버 컴포넌트'를 손쉽게 사용해 볼 수 있게 됐습니다.

평소 개인 프로젝트에서 만큼은 프론트엔드 트렌드를 놓치지 않으려고 하기에, 이 블로그를 Next.js 13으로 올리면서 서버 컴포넌트를 적용해봤습니다.

마이그레이션

13으로의 마이그레이션을 처음 시도한 건 제 커밋 로그에 의하면 5월 말입니다. Next.js 12.2.4에서 13.4.3로 올렸네요. 사실 remote에 push만 안했을 뿐이지 그 전에도 수 차례 시도 했었는데, 그 때마다 실패했었습니다.

실패했던 가장 큰 이유는 '서버 컴포넌트'가 무엇인지 하나도 이해하지 못했기 때문이었습니다. 공식 문서를 조금 훑어보고 App Router의 Layout을 적용해 보고 싶어 /pages에 있던 파일을 막무가내로 /app으로 옮겨놓곤 '왜 안되지??' 하고 있었죠 😂

결국 한 방에 끝내려던 꿈은 접고 점진적 마이그레이션을 위해 Pages로 개발된 부분은 그대로 둔 채 App Router Incremental Adoption Guide를 따라 <Link /><Images />를 최신 API에 맞춰 바꿔주는 것부터 시작했습니다. (가이드가 정말 잘 되어 있으니 저 같은 상황을 겪고 계신 분들은 꼭 읽어보세요!) 그렇게 조금씩 조금씩 바꿔나갔습니다.

지금은 App Router로의 이주를 모두 마친 상태입니다. 마이그레이션과 그 이후 안정화까지, 5월 24일 ~ 6월 12일이니 총 3주 정도 걸렸네요. 다른 일은 아무것도 하고 있지 않던 때라 하루 종일 붙잡고 있었던 걸 감안하면 꽤 오래 걸린 것 같습니다.

마이그레이션 전이였던 지난 5월까지 제 블로그는 CSS-in-JS 스타일링 라이브러리 중 하나인 styled-components를 사용하고 있었습니다. _documents.tsx에서 커스텀 document를 정의해 서버 사이드에서 스타일시트를 생성해 내려주는 방식으로요.

이걸 그대로 App Router로 옮기려다 보니 문제가 많았습니다. 결국엔 Tailwind CSS로 스타일링 방법을 전면 교체하게 됐는데, 그런 결정에 도달하기까지 제가 알게된 내용들을 공유하고자 이 글을 쓰게 됐습니다.

CSS-in-JS와 서버 컴포넌트

styled-components가 동작하는 과정

styled-components 라이브러리는 이런 과정을 거쳐서 동작합니다.

  1. styled.button이 호출되면 컴포넌트의 고유한 ID 를 생성합니다.

    const Button = styled.button`
      padding: 0.5rem 1rem;
      border-radius: 0.25rem;
      color: white;
      background-color: orange;
     
      width: ${({ $buttonWidth }) => $buttonWidth}px;
    `
    const Button = styled.button`
      padding: 0.5rem 1rem;
      border-radius: 0.25rem;
      color: white;
      background-color: orange;
     
      width: ${({ $buttonWidth }) => $buttonWidth}px;
    `
    counter++
    const componentId = hash(counter)
    counter++
    const componentId = hash(counter)
  2. Button의 tagged template을 평가합니다.

    const App = () => {
      return <Button $buttonWidth={50}>버튼</Button>
    }
    const App = () => {
      return <Button $buttonWidth={50}>버튼</Button>
    }

    이 경우엔 $buttonWidth={50}을 줬으니 이렇게 될겁니다.

    const evaluatedCSS = `
      padding: 0.5rem 1rem;
      border-radius: 0.25rem;
      color: white;
      background-color: orange;
     
      width: 50px;
    `
    const evaluatedCSS = `
      padding: 0.5rem 1rem;
      border-radius: 0.25rem;
      color: white;
      background-color: orange;
     
      width: 50px;
    `
  3. 앞서 생성했던 컴포넌트 ID와 평가된 CSS를 가지고 해시를 생성합니다. 이 해시는 곧 className이 됩니다. 실제 구현 코드를 보면 djb2라는 hashing function을 쓰고 있습니다.

    const className = hash(componentId + evaluatedCSS)
    const className = hash(componentId + evaluatedCSS)

    이렇게 생성된 className을 state로 저장합니다.

    const [generatedClassName, setGeneratedClassName] = useState(className)
    const [generatedClassName, setGeneratedClassName] = useState(className)

    generatedClassName은 state이므로, 변경되면 바뀐 className을 가지고 컴포넌트가 리렌더링됩니다.

  4. stylis같은 CSS 프로세서를 사용해 스타일시트로 변환합니다.

    const styleSheet = stylis(`.${generatedClassName}`, evaluatedCSS)
    const styleSheet = stylis(`.${generatedClassName}`, evaluatedCSS)

    정확히 말하면 preprocessor인데, 이 친구 덕분에 nesting이나 vendor prefix 같은 기능을 사용할 수 있습니다.

    그 결과 styleSheet에는 이런 스트링이 들어갑니다.

    /*
      hashed-class에는 hash값이 들어갑니다.
      여기에선 생략하도록 하겠습니다.
    */
    .hashed-class {
      padding: 0.5rem 1rem;
      border-radius: 0.25rem;
      color: white;
      background-color: orange;
     
      width: 50px;
    }
    /*
      hashed-class에는 hash값이 들어갑니다.
      여기에선 생략하도록 하겠습니다.
    */
    .hashed-class {
      padding: 0.5rem 1rem;
      border-radius: 0.25rem;
      color: white;
      background-color: orange;
     
      width: 50px;
    }
  5. 생성된 스타일시트를 <style> 요소로 만들어 DOM에 주입(inject)합니다.

    <style data-styled-components>
      .hashed-class {
        padding: 0.5rem 1rem;
        border-radius: 0.25rem;
        /* ... */
      }
    </style>
    <style data-styled-components>
      .hashed-class {
        padding: 0.5rem 1rem;
        border-radius: 0.25rem;
        /* ... */
      }
    </style>
  6. 마지막으로 generatedClassName을 가지고 컴포넌트를 렌더링합니다.

    const Button = ({ className, generatedClassName }) => {
      return <button className={className + " " + generatedClassName}>버튼</button>
    }
    const Button = ({ className, generatedClassName }) => {
      return <button className={className + " " + generatedClassName}>버튼</button>
    }

많이 간추린 설명이지만, CSS-in-JS 라이브러리는 대략 이렇게 돌아갑니다. 이 모든 과정이 런타임때 이뤄지는 것이죠.

CSS-in-JS와 서버 사이드 렌더링

다시 한번 정리하면, CSS-in-JS 라이브러리는 '클라이언트' 런타임에 스타일시트를 생성하고, <style> 요소로 DOM에 주입합니다.

이 방법을 서버 사이드 렌더링에 적용하려면 어떻게 해야 할까요? 일단 아무 생각 없이 그냥 적용하면 이런 문제가 발생할겁니다.

  1. 서버 사이드 렌더링이 일어나 마크업이 구성됩니다. 그러나, 스타일이 아직 생성되지 않은 상태입니다.
  2. 클라이언트에서 서버로부터 SSR의 결과물을 받아 보여줍니다. 아직 스타일은 없고, 그저 마크업만 보여주고 있습니다.
  3. 클라이언트에서 JS가 돌면서 런타임 스타일을 생성하고, DOM에 삽입됩니다. 이제서야 요소들이 스타일이 들어간 상태로 제대로 보이기 시작합니다.

즉, 2번과 3번 사이에서 이런 이상한 깜빡임이 발생하게 됩니다.

flickering

예전에 이 내용 이슈 해결 방법을 정리한 글을 올렸었는데, 그 글에 있던 이미지를 그대로 가져왔습니다.

간단한 예시로 좀 더 보겠습니다.

import styled from "styled-components"
 
const Box = styled.div`
  width: 200px;
  height: 200px;
  background-color: orange;
`
 
export default function Home() {
  return <Box></Box>
}
import styled from "styled-components"
 
const Box = styled.div`
  width: 200px;
  height: 200px;
  background-color: orange;
`
 
export default function Home() {
  return <Box></Box>
}

주황색 박스를 하나 만들었습니다. 이대로 Next.js에서 돌리면 서버로부터 이런 HTML이 오게 됩니다.

<!DOCTYPE html>
<html lang="ko">
  <head>
    <!-- head 중략 -->
  </head>
  <body>
    <div id="__next">
      <div class="sc-beyTiQ bXfiY"></div>
    </div>
    <!-- script 중략 -->
  </body>
</html>
<!DOCTYPE html>
<html lang="ko">
  <head>
    <!-- head 중략 -->
  </head>
  <body>
    <div id="__next">
      <div class="sc-beyTiQ bXfiY"></div>
    </div>
    <!-- script 중략 -->
  </body>
</html>

서버사이드에서 렌더링은 되기 때문에(SSR) <div class="sc-beyTiQ bXfiY"></div>가 들어가 있는게 보입니다. 그러나 스타일 코드는 없기 때문에 <div>에는 스타일이 적용되지 않은 상태입니다.

그 다음 클라이언트에서 JS가 돌면서 스타일이 생성되고, <style> 요소로 DOM에 들어갑니다. DEV 환경에서는 최종적으로 아래와 같은 형태가 됩니다.

<!DOCTYPE html>
<html lang="ko">
  <head>
    <!-- head 중략 -->
    <style data-styled="active" data-styled-version="6.0.8">
      .bXfiY {
        width: 200px;
        height: 200px;
        background-color: orange;
      }
    </style>
  </head>
  <body>
    <div id="__next">
      <div class="sc-beyTiQ bXfiY"></div>
    </div>
    <!-- script 중략 -->
  </body>
</html>
<!DOCTYPE html>
<html lang="ko">
  <head>
    <!-- head 중략 -->
    <style data-styled="active" data-styled-version="6.0.8">
      .bXfiY {
        width: 200px;
        height: 200px;
        background-color: orange;
      }
    </style>
  </head>
  <body>
    <div id="__next">
      <div class="sc-beyTiQ bXfiY"></div>
    </div>
    <!-- script 중략 -->
  </body>
</html>

이렇게 클라이언트 런타임을 통해 스타일이 주입돼 드디어 화면에 제대로 보이기 시작합니다. 스타일이 적용이 안되고 있다가, 코드가 돌고 스타일이 주입되면서 제대로 보이기 시작하니 위에서 보셨던 깜빡임이 발생하는 것입니다.

Server Side Rendering때 style이 생성되지 않는 이유는 뭘까?

서버 사이드 렌더링때 마크업은 잘 만들어내면서, 스타일이 생성되지 않는 이유는 뭘까요?

React.js를 이용한 SSR은 내부적으로 ReactDOMServer.renderToString을 사용합니다. 이 함수는 컴포넌트를 렌더링하고, 그 결과물을 문자열로 반환합니다.

import ReactDOMServer from "react-dom/server"
 
const html = ReactDOMServer.renderToString(<App />)
import ReactDOMServer from "react-dom/server"
 
const html = ReactDOMServer.renderToString(<App />)

최근엔 renderToPipableStream 같은 메서드로 바뀌긴 했는데, 이건 서버 컴포넌트와 관련된 얘기라 일단 넘어가겠습니다.

이 때, 헷갈리지 말아야 할 것은 ReactDOMServer.renderToStringinitial HTML을 생성한다는 점입니다.

styled-components로 스타일이 생성되려면 1. DOM을 만들고 2. style을 inject하는 두 단계를 거쳐야 합니다. 그러나 ReactDOMServer.renderToString은 1번에서 멈추기 때문에 스타일이 생성되지 않는 것입니다.

styled-components가 프로덕션 환경에서 스타일을 넣는 방법

이건 그냥 재밌는 사실이라 적어봤습니다. 이 글의 내용과는 관련 없습니다.

프로덕션 환경에선 클라이언트 사이드에서 JS가 실행된 후 이런 모습이 됩니다.

<!DOCTYPE html>
<html lang="ko">
  <head>
    <!-- head 중략 -->
    <style data-styled="active" data-styled-version="6.0.8"></style>
  </head>
  <body>
    <div id="__next">
      <div class="sc-beyTiQ bXfiY"></div>
    </div>
    <!-- script 중략 -->
  </body>
</html>
<!DOCTYPE html>
<html lang="ko">
  <head>
    <!-- head 중략 -->
    <style data-styled="active" data-styled-version="6.0.8"></style>
  </head>
  <body>
    <div id="__next">
      <div class="sc-beyTiQ bXfiY"></div>
    </div>
    <!-- script 중략 -->
  </body>
</html>

읭?? 스타일이 어디로 갔을까요? <style> 요소가 생기긴 했는데 내용물이 보이지 않습니다. Chrome Dev Tools로 확인해보면 분명히 저 스타일 요소로부터 스타일이 적용되고 있다고 하는데 말이죠.

style is definitly applied

??? 스타일이 적용되고 있긴 한데, 내용물이 보이지 않습니다.

DEV 환경에서는 DOM API로 스타일 스트링을 주입하고, 프로덕션 환경에서는 CSSOM API로 스타일을 넣어서 그렇습니다.

document.createElement("style").innerHTML =
  ".bXfiY { width: 200px; height: 200px; background-color: orange; }"
document.createElement("style").innerHTML =
  ".bXfiY { width: 200px; height: 200px; background-color: orange; }"

이렇게 넣었던 스타일을

const styleSheet = document.createElement("style").sheet
styleSheet.insertRule(".bXfiY { width: 200px; height: 200px; background-color: orange; }")
const styleSheet = document.createElement("style").sheet
styleSheet.insertRule(".bXfiY { width: 200px; height: 200px; background-color: orange; }")

이런식으로 넣었다는 말입니다. 런타임 성능을 위해 최대한 압축하고자 이렇게 했다는데, 정말 성능 차이가 나는지는 모르겠지만 재밌는 방법인것 같아요.

실제 코드를 보고싶다면 여기

styled-components 스타일을 서버에서 미리 만들어 주면 안되나?

다시 서버 사이드 렌더링과 styled-components의 동작 과정에 대한 얘기로 돌아오겠습니다.

우리가 바라는 건 서버측에서 마크업을 만들면서 스타일도 같이 마크업에 담아주는 것입니다. 그래야 클라이언트에서 첫 DOM과 CSSOM 파싱을 할 때부터 스타일이 제대로 들어가 페이지가 깜빡이지 않을 테니까요.

물론 방법은 있습니다. 서버 사이드에서 미리 모든 스타일을 모아서 하나의 스타일시트를 만들고 SSR된 HTML에 쓱 껴넣어주면 됩니다.

import ReactDOMServer from "react-dom/server"
import { ServerStyleSheet } from "styled-components"
 
const sheet = new ServerStyleSheet()
const html = ReactDOMServer.renderToString(sheet.collectStyles(<App />))
const styleElement = sheet.getStyleElement()
// ...
import ReactDOMServer from "react-dom/server"
import { ServerStyleSheet } from "styled-components"
 
const sheet = new ServerStyleSheet()
const html = ReactDOMServer.renderToString(sheet.collectStyles(<App />))
const styleElement = sheet.getStyleElement()
// ...

ServerStyleSheetcollectStyles 메서드를 통해 서버사이드 렌더링이 진행될 때 스타일시트를 수집합니다. 이렇게 수집된 스타일시트는 getStyleElement 메서드를 통해 <style> 요소로 뽑아낼 수 있습니다. 이제 저 styleElement를 서버에서 넣어주도록 합니다.

이 방법을 Next.js에 적용한 예시는 링크를 참고해주세요. (이게 바로 제가 App Router로 마이그레이션 하기 전까지 사용하고 있던 방법입니다.)

그럼 바라던 대로 서버에서 이런 스타일 요소가 담긴 HTML을 보내줍니다.

<style data-styled="" data-styled-version="6.0.8">
  .bXfiY {
    width: 200px;
    height: 200px;
    background-color: orange;
  }
</style>
<style data-styled="" data-styled-version="6.0.8">
  .bXfiY {
    width: 200px;
    height: 200px;
    background-color: orange;
  }
</style>

참고로 이 경우엔 DEV 환경이든 프로덕션 환경이든 똑같습니다.

클라이언트측 런타임이 아니라 서버측 렌더링이 일어나는 시점에 스타일이 생성돼 initial HTML에 담아 주기 때문에, 이제 깜빡임 없이 페이지가 보이게 됩니다.

hydration missmatch를 방지하기 위해 서버측 렌더링이 일어나는 시점에 생성되는 className은 클라이언트에서 렌더링을 했을 때 만들어내는 className과 같아야 합니다. 이를 보장하기 위해 styled-components에서 제공하는 babel plugin을 사용할 수 있습니다. 이 플러그인은 여러 환경에서 모두 같은 className hash가 생성되도록 도와줍니다.

Server Component

React.js 18 버전부터는 서버에서 async 컴포넌트를 만들어 쓸 수 있습니다. 즉, 클라이언트에서 백엔드 엔드포인트 등에 요청을 날려서 필요한 데이터를 받아오던 기존 방법 대신 서버에서 컴포넌트를 렌더링할 때 직접 데이터를 불러와 넣어줄 수 있게 되었습니다.

const Component = async () => {
  const data = await fetch("https://example.com/api").then((response) => response.json())
 
  return <div>{data}</div>
}
const Component = async () => {
  const data = await fetch("https://example.com/api").then((response) => response.json())
 
  return <div>{data}</div>
}

하지만 그렇다고 서버에서 데이터를 모두 불러올 때까지 기다릴 수는 없겠죠. 이 부분을 보완하기 위해 React는 서버에서 데이터를 불러오고 있는 컴포넌트에 대해서도 Suspense가 가능하게끔 구현됐습니다. 서버로부터 데이터를 stream으로 점진적으로 가져오는 건데, 일단 suspense된 부분을 fallback 컴포넌트로 채운 상태로 서버에서 먼저 보내주고, 데이터를 모두 불러왔다면 suspense된 부분을 갈아끼우는 <script> 요소를 보내주는 식으로 동작합니다. (앞서 잠깐 언급했던 renderToPipableStream 메서드가 이런 역할을 합니다.) 자세한 내용은 React 메인테이너의 discussion에 정말 잘 설명돼 있습니다.

클라이언트에서 데이터를 불러오는 것보다 서버에서 가져오는게 (구조에 따라 다르겠지만) 더 빠르고, 클라이언트가 다운로드 받아야 하는 JS 번들의 사이즈를 줄일 수 있는 등 서버 컴포넌트는 여러 장점을 가지고 있습니다. 서버 컴포넌트를 왜 써야 하는지에 대한 얘기는 이 정도로만 다루겠습니다. (더 많은 이야기는 이 글을 추천드립니다.)

한 편, 이런 특징 때문에 RSC(React Server Component)를 사용할 때는 몇가지 주의할 것이 있습니다.

서버 컴포넌트의 코드는 서버에만 머물고 클라이언트로 전달되지 않습니다. 따라서 클라이언트에서는 이 컴포넌트가 동작하지 않고, 이는 곧 클라이언트에서 리렌더링 되지 않는다는 의미가 됩니다.

클라이언트에서 리렌더링 되지 않는다는 부분 때문에 우리가 일반적으로 작성하던 컴포넌트들처럼 useState를 쓸 수 없고, useEffect도 쓸 수 없습니다. 뭐든 클라이언트에서 돌아가는 무언가가 있어야 한다면 쓸 수 없습니다. 심지어 이벤트 핸들러도 붙여주지 못합니다.

서버 컴포넌트는 이름 그대로 '서버에서만 돌아가는' 컴포넌트입니다.

반대로 우리가 기존에 사용하던 일반적인 컴포넌트는 '클라이언트 컴포넌트'라고 불리고 있습니다. 클라이언트 컴포넌트는 SSR 할 경우 서버에서도 돌아간다는 점을 생각해 보면 굉장히 헷갈리는 네이밍입니다.

Server Component와 CSS-in-JS

여기까지 글을 읽어주신 분이라면 이제 제가 왜 styled-components를 비롯한 CSS-in-JS 라이브러리들이 서버 컴포넌트를 지원할 때까지 기다리지 않고 Tailwind CSS로 마이그레이션 할 수밖에 없었는지 감이 잡히실겁니다.

styled-components는 런타임에 스타일을 생성하고, <style> 요소로 DOM에 삽입합니다. 이런 방식은 서버 컴포넌트와는 맞지 않습니다. 서버 컴포넌트는 클라이언트에서 돌아가는 어떤 것도 사용할 수 없기 때문입니다. CSS-in-JS는 서버 컴포넌트에서 동작하기엔 태생적인 문제를 가지고 있었던 것이죠.

이 문제때문에 잘 나가던 신흥 CSS-in-JS 라이브러리였던 Stitches가 maintain을 포기하기도 했습니다.

And with React 18, the ecosystem has changed and made the future for runtime injection pretty murky.

...그리고 React 18에서 리액트 생태계가 바뀌면서 런타임 주입의 미래가 굉장히 불확실해졌습니다.

- Stitches Issue #1144

한 술 더 떠서 styled-components는 스타일 테마를 주입하기 위해 내부적으로 React Context API를 사용하고 있습니다.

const theme = {
  colors: {
    primary: "orange",
    secondary: "blue",
  },
}
 
const Button = styled.button`
  padding: 0.5rem 1rem;
  border-radius: 0.25rem;
  color: white;
  background-color: ${({ theme }) => theme.colors.primary};
`
 
const App = () => {
  return (
    <ThemeProvider theme={theme}>
      <Button>버튼</Button>
    </ThemeProvider>
  )
}
const theme = {
  colors: {
    primary: "orange",
    secondary: "blue",
  },
}
 
const Button = styled.button`
  padding: 0.5rem 1rem;
  border-radius: 0.25rem;
  color: white;
  background-color: ${({ theme }) => theme.colors.primary};
`
 
const App = () => {
  return (
    <ThemeProvider theme={theme}>
      <Button>버튼</Button>
    </ThemeProvider>
  )
}

ThemeProvider를 통해 주입된 테마를 styled.button의 tagged template에서 사용한 예시입니다.

export const ThemeContext = React.createContext<DefaultTheme | undefined>(undefined)
 
export default function ThemeProvider(props: Props): JSX.Element | null {
  const outerTheme = React.useContext(ThemeContext)
  const themeContext = useMemo(() => mergeTheme(props.theme, outerTheme), [props.theme, outerTheme])
 
  if (!props.children) {
    return null
  }
 
  return <ThemeContext.Provider value={themeContext}>{props.children}</ThemeContext.Provider>
}
export const ThemeContext = React.createContext<DefaultTheme | undefined>(undefined)
 
export default function ThemeProvider(props: Props): JSX.Element | null {
  const outerTheme = React.useContext(ThemeContext)
  const themeContext = useMemo(() => mergeTheme(props.theme, outerTheme), [props.theme, outerTheme])
 
  if (!props.children) {
    return null
  }
 
  return <ThemeContext.Provider value={themeContext}>{props.children}</ThemeContext.Provider>
}

ThemeProvider실제 코드입니다. 간단하게 생겼죠? useContext 훅까지 써서 ThemeProvider를 여러 겹으로 쓸 수 있게 만들어 놓기도 했네요. 덕분에 테마가 바뀌어야 하는 곳에 대해 외부에서 편리하게 주입할 수 있는 구조를 가지고 있습니다.

서버 컴포넌트에서는 이 모든 기능을 사용할 수 없습니다. Context API 또한 클라이언트 런타임이 필요한 API이기 때문입니다.

대안은 없을까?

이 문제는 어떻게 해결할 수 있을까요? 답은 간단합니다. 런타임 이전에 스타일이 결정되면 됩니다.

CSS Modules, Tailwind CSS, Utility Class Components

Next.js 공식 문서에서는 CSS Modules와 Tailwind CSS 이렇게 두가지를 추천합니다. 제일 간단하고 빠르게 적용할 수 있는 방법입니다.

저는 제 개인적인 취향에 따라 Tailwind CSS를 선택했고, 조금이나마 편리하게 마이그레이션 하기 위해 styled-components와 최대한 비슷한 인터페이스를 가지도록 간단한 text concat 라이브러리를 직접 만들어 사용하고 있습니다.

import { utld, ud } from "utility-class-components"
 
const boxStyle = ud`
  bg-red-200
`
 
const Container = utld.div<{ $isRed: boolean }>`
  w-32
  h-32
 
  ${boxStyle}
 
  ${({ $isRed }) => $isRed && "text-red-500"}
`
 
function Page() {
  return <Container $isRed={true}>AWESOME!!</Container>
}
import { utld, ud } from "utility-class-components"
 
const boxStyle = ud`
  bg-red-200
`
 
const Container = utld.div<{ $isRed: boolean }>`
  w-32
  h-32
 
  ${boxStyle}
 
  ${({ $isRed }) => $isRed && "text-red-500"}
`
 
function Page() {
  return <Container $isRed={true}>AWESOME!!</Container>
}

그리고 styled-components를 사용하며 완전히 동적으로 스타일이 들어가던 부분은 모두 inline style로 바꿔주었습니다.

Vanilla Extract, Panda CSS 등

CSS-in-JS의 장점을 가져가면서 정적(zero runtime)으로 스타일을 빌드하는 라이브러리들도 있습니다. 당근에서도 사용하고 있는 vanilla-extract나 chakra-ui 개발팀이 만든 panda 등이 가장 대표적입니다.

이 라이브러리들은 스타일 코드를 빌드타임에 생성합니다. 따라서 정적으로 빌드된 스타일을 HTML에 같이 넣어 클라이언트로 보내주기만 한다면 서버 컴포넌트에서도 충분히 사용할 수 있습니다.

아직 크게 주목받고 있진 못한 것 같지만, 서버 컴포넌트가 좀 더 많은 개발자에게 알려지고 사용된다면 국룰 스타일링 라이브러리 자리를 이 둘이 계승하지 않을까요? 😄