(번역) CSS in React Server Components
Table of Contents
본문
Josh Comeau의 CSS in React Server Components 를 번역한 문서입니다.
CSS-in-JS와 React의 미래를 이해하기
작년 이맘때, Vercel은 Next 13.4의 안정적인 출시를 발표하며 React 서버 컴포넌트를 기반으로 구축된 최초의 React 프레임워크가 되었습니다.

이는 매우 중요한 일입니다! RSC (React 서버 컴포넌트) 는 React에서 서버 전용 코드를 작성할 수 있는 공식적인 방법을 제공해줍니다. 이는 많은 흥미로운 새로운 가능성을 열어줍니다.
하지만 좋은 일을 하려면 약간의 희생이 필요합니다. RSC는 React 작동 방식에 대한 근본적인 변화이며, 우리가 사용해 왔던 일부 라이브러리와 도구들이 혼란스러워졌습니다. 😅
특히 걱정스러운 것은, 💅 styled-components와 Emotion과 같은 가장 인기 있는 CSS-in-JS 라이브러리들이 React의 이 새로운 비전과 완전히 호환되지 않는다는 점입니다. 또한 앞으로 나아갈만한 명확한 방법이 제시되지 않았습니다.
지난 몇 달 동안 저는 이 문제에 대해 깊이 파고들어, 호환성 문제를 이해하고, 이에 대해 어떠한 선택지가 있는지 배우고 있습니다. 현재 시점에서 저는 전체적인 상황에 대해 꽤 확고한 이해를 하고 있다고 생각합니다. 또한 잘 알려지지 않은 몇 가지 흥미로운 사항도 발견했습니다. ✨
CSS-in-JS 라이브러리를 사용하고 계신다면, 이 블로그 게시물이 많은 혼란을 해소하고, 실질적인 옵션을 제공해주기를 바랍니다.
그리고 CSS-in-JS 라이브러리를 사용하지 않는다고 해도, 이 블로그 게시물은 React 서버 컴포넌트에 대한 이해를 깊게 하는 데 도움이 될 것입니다. 여기서 다룰 많은 문제들은 CSS-in-JS에만 국한된 것이 아닙니다!
그냥 ____을(를) 사용하세요.
이 논의가 온라인에서 발생할 때 가장 흔한 제안 중 하나는 다른 CSS 도구로 전환하라는 것입니다. React 생태계에는 충분한 다른 대안이 있습니다!
하지만 많은 사람들에게 이것은 현실적인 제안이 아닙니다. 저의 블로그와 강의 플랫폼에는 5,000개 이상의 styled-components 가 있습니다. 완전히 다른 도구로 마이그레이션하는 것은 말처럼 쉬운 일이 아닙니다.
그리고 솔직히 말해서, 손가락만 튕기면 완전히 다른 라이브러리로 바꿀 수 있다고 해도 그렇게 하고 싶지 않습니다. 저는
styled
API를 정말 좋아합니다!이 블로그 게시물의 후반부에서는 몇 가지 대체 CSS 라이브러리에 대해 논의하겠지만, styled-components와 유사한 API를 가진 옵션에 중점을 둘 것입니다.
React 서버 컴포넌트 분석하기 (Breaking down React Server Components)
호환성 문제를 이해하려면 React 서버 컴포넌트를 이해해야 합니다. 그러나 그 전에, 우리는 *서버 사이드 렌더링(SSR)*을 이해해야 합니다.
SSR은 여러 가지 전략과 구현 방식을 포함하는 포괄적인 용어이지만, 가장 일반적인 버전은 다음과 같습니다:
사용자가 웹 앱을 방문합니다.
요청이 Node.js에 의해 수신되고, Node.js는 창이 없는 서버 환경에서 React를 실행합니다. 이렇게 하면 애플리케이션이 렌더링되고 초기 UI를 포함한 완전한 HTML 문서가 생성됩니다.
이 HTML 문서가 사용자의 장치에서 로드되면, React는 서버에서 수행된 작업을 반복하여 동일한 컴포넌트를 다시 렌더링합니다. 그러나 새로운 HTML 요소를 생성하는 대신, 서버에서 생성된 HTML 요소를 "채택 (adopt)"합니다. 이를 *하이드레이션(hydration)*이라고 합니다.
React는 상호작용을 처리하기 위해 사용자의 장치에서 실행될 필요가 있습니다. 서버에서 생성된 HTML은 완전히 정적이며, 우리가 작성한 이벤트 핸들러(예: onClick
)를 포함하지 않거나 ref
속성으로 지정한 참조를 캡처하지 않습니다.
그렇다면 왜 동일한 작업을 다시 해야 하나요?? React가 사용자의 장치에서 부팅될 때, 이미 존재하는 많은 UI를 발견하게 되지만, 각 HTML 요소가 어떤 컴포넌트에 속하는지와 같은 맥락에 대한 정보가 없습니다. React는 컴포넌트 트리를 재구성하기 위해 동일한 작업을 수행해야 하며, 이를 통해 기존 HTML을 올바르게 연결하고, 이벤트 핸들러와 ref를 정확한 요소에 부착할 수 있습니다. React는 서버가 끝낸 지점에서 다시 시작할 수 있도록 스스로 지도를 그릴 필요가 있습니다.
이 모델에는 큰 한계가 있습니다. 우리가 작성하는 모든 코드는 서버와 클라이언트에서 실행됩니다. 서버에서만 렌더링되는 컴포넌트를 만들 방법이 없습니다. 데이터베이스에 데이터를 저장하는 풀스택 웹 애플리케이션을 만든다고 가정해 봅시다. PHP와 같은 언어를 사용해본 경험이 있다면, 다음과 같은 작업을 할 수 있을 것이라고 기대할지도 모릅니다:
function Home() {
const data = db.query('SELECT * FROM SNEAKERS');
return (
<main>
{data.map((item) => (
<Sneaker key={item.id} item={item} />
))}
</main>
);
}
이론적으로 이 코드는 서버에서 잘 작동할 수 있지만, 동일한 코드가 사용자의 장치에서 다시 실행될 것입니다. 이는 문제입니다. 클라이언트 측의 React는 데이터베이스에 접근할 수 없기 때문입니다. React에게 "이 코드는 서버에서만 실행하고, 결과 데이터를 클라이언트에서 재사용하라"고 지시할 방법이 없습니다.
React 기반으로 구축된 메타 프레임워크들은 각자의 해결책을 내놓았습니다. 예를 들어, Next.js에서는 다음과 같은 방법을 사용할 수 있습니다:
export async function getServerSideProps() {
const data = await db.query('SELECT * FROM SNEAKERS');
return {
props: {
data,
},
};
}
function Home({ data }) {
return (
<main>
{data.map((item) => (
<Sneaker key={item.id} item={item} />
))}
</main>
);
}
Next.js 팀의 의도는 다음과 같습니다. “좋아, 그러니까 완전 동일한 React 코드가 서버와 클라이언트에서 실행되어야 한다는건데... 그렇다면 React 외부에 서버에서만 실행되는 별도 코드를 추가할 수 있겠구나!”
Next.js 서버가 요청을 받으면 먼저 getServerSideProps
함수를 호출하고, 해당 함수가 반환하는 값이 React 코드에 props로 전달될 것입니다. 완전히 동일한 React 코드가 서버와 클라이언트에서 실행되므로 문제가 없습니다. 꽤 영리한 방법이죠, 그렇죠?
솔직히 저는 지금까지도 이 접근 방식이 꽤 마음에 듭니다. 하지만 이 방식은 React의 한계 때문에 어쩔 수 없이 만들어진 API처럼 느껴집니다. 또한 이 방식은 각 경로의 최상위에 있는 페이지 수준에서만 작동하며, 우리가 원하는 곳 어디에나 getServerSideProps
함수를 넣을 수는 없습니다.
React 서버 컴포넌트 (RSC)는 이 문제에 대해 더 직관적인 해결책을 제공합니다. RSC를 사용하면 데이터베이스 호출이나 다른 서버 전용 작업을 React 컴포넌트 내에서 직접 수행할 수 있습니다:
async function Home() {
const data = await db.query('SELECT * FROM SNEAKERS');
return (
<main>
{data.map((item) => (
<Sneaker key={item.id} item={item} />
))}
</main>
);
}
“React 서버 컴포넌트”의 패러다임에서는, 컴포넌트는 기본적으로 서버 컴포넌트 입니다. 서버 컴포넌트는 서버에서만 동작합니다. 해당 코드는 사용자의 장치에서 다시 실행되지 않습니다. 자바스크립트 번들에도 포함되지 않습니다!
이 새로운 패러다임에는 클라이언트 컴포넌트도 포함됩니다. 클라이언트 컴포넌트는 서버와 클라이언트 모두에서 실행되는 컴포넌트입니다. 지금까지 “전통적인”(RSC 이전) React에서 작성한 모든 React 컴포넌트는 클라이언트 컴포넌트입니다. 이는 익숙한 것을 위한 새로운 명칭입니다.
우리는 파일 상단에 새로운 "use client"
지시어를 사용하여 클라이언트 컴포넌트를 선택합니다:
'use client';
function Counter() {
const [count, setCount] = React.useState(0);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
이 지시어는 “클라이언트 경계”를 생성합니다. 이 파일의 모든 컴포넌트와 이 파일에서 가져오는 모든 컴포넌트는 클라이언트 컴포넌트로 렌더링되어, 처음에는 서버에서, 그 다음에는 클라이언트에서 실행됩니다.
다른 React 기능(예: hooks)과 달리, React 서버 컴포넌트는 번들러와의 깊은 통합이 필요합니다. 2024년 4월 현재, React 서버 컴포넌트를 사용할 수 있는 유일한 실용적인 방법은 Next.js를 사용하는 것이지만, 앞으로는 이 상황이 바뀔 것으로 기대됩니다.
서버 컴포넌트의 한계 (Server Components are limited)
서버 컴포넌트에 대해 이해해야 할 중요한 점은 이들이 "완전한" React 경험을 제공하지 않는다는 것입니다. 대부분의 React API는 서버 컴포넌트에서 작동하지 않습니다.
예를 들어, useState
를 생각해봅시다. 상태 변수가 변경되면 컴포넌트가 다시 렌더링되지만, 서버 컴포넌트는 다시 렌더링될 수 없습니다. 이들의 코드는 브라우저로 전송되지 않기 때문에 React는 상태 변화를 처리할 방법이 없습니다. React의 관점에서, 서버 컴포넌트가 생성한 마크업은 고정되어 클라이언트에서 변경될 수 없습니다.
마찬가지로, 서버 컴포넌트 내에서는 useEffect
를 사용할 수 없습니다. 사이드 이펙트는 서버에서 실행되지 않으며, 클라이언트에서 렌더링된 후에만 실행됩니다. 서버 컴포넌트는 우리의 JavaScript 번들에서 제외되기 때문에, 클라이언트 측의 React는 우리가 useEffect
훅을 작성했다는 사실을 전혀 알지 못합니다.
useContext
훅조차도 서버 컴포넌트 내에서는 사용할 수 없습니다. 이는 React 팀이 React Context를 서버 컴포넌트와 클라이언트 컴포넌트 간에 어떻게 공유할 수 있을지에 대한 문제를 아직 해결하지 못했기 때문입니다.
저는 이렇게 생각합니다: 서버 컴포넌트는 우리가 이전부터 이해해왔던 React 컴포넌트는 아닙니다. 오히려 서버에서 렌더링되어 원본 HTML을 생성하는 PHP 템플릿에 더 가깝습니다. 진정한 혁신은 서버 컴포넌트와 클라이언트 컴포넌트가 동일한 애플리케이션 내에서 공존할 수 있다는 점입니다!
더 깊이 들어가기 (Going deeper)
이 블로그 게시물에서는 React 서버 컴포넌트의 가장 중요한 세부 사항, 즉 CSS-in-JS 프레임워크와의 호환성 문제를 이해하는 데 필요한 내용을 집중적으로 다루고자 합니다.
하지만 React 서버 컴포넌트에 대해 더 알고 싶다면, 이 새로운 세계를 훨씬 더 깊이 탐구한 별도의 블로그 게시물이 있습니다:
CSS-in-JS 라이브러리의 동작 방식 (How CSS-in-JS libraries work)
좋아요, 지금까지 React 서버 컴포넌트의 기본을 다뤘습니다. 이제부터는 💅 styled-components 같은 CSS-in-JS 라이브러리의 기본을 얘기해보겠습니다!
여기 간단한 예시가 있습니다:
import styled from 'styled-components';
export default function Homepage() {
return <BigRedButton>Click me!</BigRedButton>;
}
const BigRedButton = styled.button`
font-size: 2rem;
color: red;
`;
.red-btn
과 같은 클래스에 CSS를 넣는 대신, 우리는 그 CSS를 새로 생성된 React 컴포넌트에 부착합니다. 이것이 styled-components를 특별하게 만드는 점입니다. 클래스가 아니라 컴포넌트가 재사용 가능한 기본 단위입니다.
styled.button
은 동적으로 새로운 React 컴포넌트를 생성하는 함수이며, 우리는 그 컴포넌트를 BigRedButton
이라는 변수에 할당합니다. 그런 다음 우리는 이 React 컴포넌트를 다른 React 컴포넌트와 동일한 방식으로 사용할 수 있습니다. 이 컴포넌트는 큰 빨간색 텍스트를 가진 <button>
태그를 렌더링합니다.
그렇다면 라이브러리는 이 CSS를 이 요소에 정확히 어떻게 적용할까요? 주요한 세 가지 방법이 있습니다:
- 스타일을
style
속성을 통해 인라인으로 적용할 수 있습니다. - 스타일을 별도의 CSS 파일에 넣고
<link>
를 통해 로드할 수 있습니다. - 스타일을
<style>
태그에 넣어 현재 HTML 문서의<head>
에 배치할 수 있습니다.
이 코드를 실행하고 DOM을 검사하면 답이 밝혀집니다:
<html>
<head>
<style data-styled="active">
.abc123 {
font-size: 2rem;
color: red;
}
</style>
</head>
<body>
<button className="abc123">
Click me!
</button>
</body>
</html>
styled-components는 제공된 스타일을 라이브러리가 관리하는 <style>
태그에 작성합니다. 이 스타일을 특정 <button>
에 연결하기 위해 고유한 클래스 이름인 "abc123"을 생성합니다.
이 모든 작업은 React가 최초로 렌더링할 때 실행됩니다.
- 클라이언트 사이드 렌더링 컨텍스트 (예: Parcel, create-react-app) 에서는
<style>
태그가 사용자의 장치에서 동적으로 생성되며, 이는 React가 생성하는 모든 DOM 노드와 동일합니다. - 서버 사이드 렌더링 컨텍스트 (예: Next.js, Remix)에서는 이러한 작업이 서버에서 수행됩니다. 생성된 HTML 문서에는 이
<style>
태그가 포함됩니다.
사용자가 애플리케이션과 상호작용함에 따라 특정 스타일이 생성, 수정 또는 제거될 수 있습니다. 예를 들어, 조건부로 렌더링되는 styled-component가 있다고 가정해 봅시다:
function Header() {
const user = useUser();
return <>{user && <SignOutButton onClick={user.signOut}>Sign Out</SignOutButton>}</>;
}
const SignOutButton = styled.button`
color: white;
background: red;
`;
초기에 user
가 정의되지 않으면 <SignOutButton>
은 렌더링되지 않으므로 해당 스타일은 존재하지 않습니다. 이후 사용자가 로그인하면 애플리케이션이 다시 렌더링되고 styled-components가 작동하여 이러한 스타일을 <style>
태그에 주입합니다.
본질적으로, 모든 styled-component는 일반 React 컴포넌트지만 추가적인 부가 효과가 있습니다. 즉, 그들은 자신들의 스타일을 <style>
태그에 렌더링합니다.
오늘의 목표에서는 이것이 가장 중요한 요점입니다. 하지만 라이브러리의 내부 작동 방식에 대해 더 깊이 알고 싶다면, 제가 작성한 “styled-components 이해하기”라는 블로그 게시물을 참고하세요.
문제의 핵심 (The Crux of the Problem)
지금까지 배운 내용을 요약하자면:
“React 서버 컴포넌트”는 서버에서만 렌더링되는 새로운 유형의 컴포넌트인 서버 컴포넌트를 제공하는 React의 새로운 패러다임입니다. 서버 컴포넌트의 코드는 JS 번들에 포함되지 않습니다.
styled-components 라이브러리는 CSS가 부착된 React 컴포넌트를 동적으로 생성할 수 있게 해줍니다. 이는 컴포넌트가 다시 렌더링될 때 업데이트되는
<style>
태그를 관리함으로써 작동합니다.
기본적인 비호환성 문제는 styled-components가 브라우저에서 실행되도록 설계된 반면, 서버 컴포넌트는 브라우저와 접촉하지 않는다는 점입니다.
내부적으로 styled-components는 useContext
훅을 많이 사용합니다. 이는 React 생명주기에 연결되도록 설계되었지만, 서버 컴포넌트에는 React 생명주기가 존재하지 않습니다. 따라서, 이 새로운 "React 서버 컴포넌트" 환경에서 styled-components를 사용하려면, 단 하나의 styled-component 라도 렌더링하는 모든 React 컴포넌트는 클라이언트 컴포넌트가 되어야 합니다.
당신은 어떨지 모르겠지만, 저는 스타일링이 전혀 포함되지 않은 React 컴포넌트를 가지는 일이 매우 드뭅니다. 제 컴포넌트 파일의 90% 이상은 styled-components를 사용합니다. 이러한 컴포넌트 중 대부분은 완전히 정적이며, 상태나 다른 클라이언트 컴포넌트 기능을 사용하지 않습니다.
이는 즉, 이 새로운 패러다임을 완전히 활용할 수 없다는 것을 의미하므로 실망스러운 일입니다. 하지만 방법이 아예 없는 것은 아닙니다.
React 서버 컴포넌트에서 한 가지를 바꿀 수 있다면, 그것은 "클라이언트 컴포넌트"라는 이름일 것입니다. 이 이름은 이러한 컴포넌트가 오직 클라이언트에서만 렌더링된다는 의미를 내포하지만, 이는 사실이 아닙니다. 기억하세요, "클라이언트 컴포넌트"는 오래된 것에 대한 새로운 이름입니다. 2023년 5월 이전에 생성된 모든 React 애플리케이션의 모든 React 컴포넌트는 클라이언트 컴포넌트입니다.
따라서, styled-components 애플리케이션에서 단 10%의 컴포넌트만 서버 컴포넌트로 변환될 수 있다고 해도, 이는 여전히 개선입니다! 우리의 애플리케이션은 여전히 RSC 이전보다 더 가볍고 빠르게 될 것입니다. 우리는 여전히 서버 사이드 렌더링(SSR)의 모든 이점을 누릴 수 있습니다. 이는 변하지 않았습니다.
그냥 업데이트 하면 안되나요? (Can’t they just update it?)
styled-components나 Emotion의 유지보수자들이 왜 이 라이브러리를 React 서버 컴포넌트와 호환되도록 업데이트하지 않았는지 궁금할 수 있습니다. 우리는 이 변화가 올 것이라는 것을 1년 넘게 알고 있었는데, 왜 아직 해결책을 찾지 못했을까요?
styled-components 유지보수자들은 현재 React에서 API를 제대로 제공하고있지 않아 막혀 있는 상황입니다. 특히, React는 Context에 대한 RSC 친화적인 대안을 제공하지 않았으며, styled-components는 서버 사이드 렌더링 동안 모든 스타일을 올바르게 적용하기 위해 컴포넌트 간에 데이터를 공유할 방법이 필요합니다.
몇 주 전에 꽤 깊이 탐구해 보았는데, 솔직히 React Context 없이 이것이 어떻게 작동할 수 있을지 상상하기 어렵습니다. 제가 알기로는 유일한 해결책은 완전히 다른 접근 방식을 사용하여 라이브러리를 전면적으로 다시 작성하는 것일 것입니다. 이는 상당한 파괴적인 변경을 초래할 뿐만 아니라, 자원봉사로 운영되는 오픈 소스 유지보수 팀에게 기대하기에는 매우 무리한 일입니다.
이에 대해 더 알고 싶다면, styled-components 의 GitHub 이슈를 살펴보세요. 장애물이 뭔지 알 수 있습니다. 비슷한 논의가 Emotion의 저장소에도 있는 것을 봤습니다.
제로 런타임 CSS-in-JS 라이브러리의 세계 (The World of Zero-Runtime CSS-in-JS Libraries)
지금까지 이야기는 다소 암울했습니다. React 컴포넌트와 styled-components 사이에는 근본적인 비호환성이 있으며, 라이브러리 유지보수자들은 지원을 추가하는 데 필요한 도구를 제공받지 못했습니다.
다행히도, React 커뮤니티는 이 문제를 그냥 넘기지 않았습니다! styled-components와 유사한 API를 제공하면서도 React 서버 컴포넌트와 완벽히 호환되는 여러 라이브러리가 개발되고 있습니다! ✨
이 도구들은 React 생명주기에 묶여 있는 대신, 다른 접근 방식을 취했습니다. 모든 처리가 컴파일 타임에 이루어집니다.
현대의 React 애플리케이션은 TypeScript/JSX를 JavaScript로 변환하고 수천 개의 개별 파일을 몇 개의 번들로 패키징하는 빌드 단계를 거칩니다. 이 작업은 애플리케이션이 배포될 때, 프로덕션에서 실행되기 전에 수행됩니다. styled-components를 런타임이 아닌 이 단계에서 처리하는 것은 어떨까요?
이것이 이 섹션에서 논의할 모든 라이브러리의 핵심 아이디어입니다. 자세히 살펴봅시다!
Linaria
Linaria는 2017년에 만들어졌습니다. 이는 styled-components만큼 오래되었습니다! API는 styled-components와 동일하게 보입니다:
import styled from '@linaria/react';
export default function Homepage() {
return <BigRedButton>Click me!</BigRedButton>;
}
const BigRedButton = styled.button`
font-size: 2rem;
color: red;
`;
여기 정말 기발한 부분이 있습니다: 컴파일 단계에서 Linaria는 이 코드를 변환하여 모든 스타일을 CSS 모듈로 이동시킵니다.
Linaria를 실행한 후의 코드는 다음과 같이 보일 것입니다:
/* /components/Home.module.css */
.BigRedButton {
font-size: 2rem;
color: red;
}
/* /components/Home.js */
import styles from './Home.module.css';
export default function Homepage() {
return <button className={styles.BigRedButton}>Click me!</button>;
}
CSS Modules에 익숙하지 않다면, 이는 CSS 위에 구축된 가벼운 추상화 계층입니다. 거의 일반 CSS처럼 취급할 수 있지만, 전역적으로 고유한 이름에 대해 걱정할 필요가 없습니다. 컴파일 단계에서, Linaria가 그 마법을 부린 후, .BigRedButton과
같은 일반적인 이름은 .abc123
과 같은 고유한 이름으로 변환됩니다.
중요한 점은 CSS Modules이 이미 널리 지원되고 있다는 것입니다. 이는 가장 인기 있는 선택지 중 하나입니다. Next.js와 같은 메타 프레임워크는 이미 CSS Modules에 대한 일급 지원을 제공합니다.
따라서, 바퀴를 다시 발명하고 프로덕션에 나갈 수 있는 견고한 CSS 솔루션을 몇년간 구축하는 대신, Linaria 팀은 지름길을 선택했습니다. 우리는 styled-components를 작성할 수 있고, Linaria는 이를 CSS Modules로 사전 처리한 후, 이를 다시 일반 CSS로 처리합니다. 이 모든 과정은 컴파일 타임에 이루어집니다.
런타임 vs 컴파일 타임의 트레이드오프 (Run-time vs compile-time tradeoffs)
RSC가 등장하기 훨씬 전부터, 커뮤니티는 Linaria와 같은 컴파일 타임 라이브러리를 구축해왔습니다. 성능상의 이점은 매우 매력적입니다: styled-components는 JavaScript 번들에 11킬로바이트(gzip)를 추가하지만, Linaria는 모든 작업이 사전에 이루어지기 때문에 0킬로바이트를 추가합니다. 또한, 서버 사이드 렌더링이 조금 더 빨라집니다. 스타일을 수집하고 적용하는 데 시간을 할애할 필요가 없기 때문입니다.
그렇다고 해서 styled-components 런타임이 단순히 불필요한 무게를 더하는 것은 아닙니다. styled-components에서는 컴파일 타임에 불가능한 작업을 수행할 수 있습니다. 예를 들어, styled-components는 React 상태가 변경될 때 동적으로 CSS를 업데이트할 수 있습니다.
다행히도, styled-components가 처음 만들어진 이후 거의 10년 동안 CSS는 훨씬 더 강력해졌습니다. 우리는 CSS 변수를 사용하여 대부분의 동적 사용 사례를 처리할 수 있습니다. 요즘에는 런타임이 일부 상황에서 약간 더 나은 개발자 경험을 제공할 수 있지만, 제 생각에는 더 이상 꼭 필요한 것은 아닙니다.
이는 Linaria 및 다른 컴파일 타임 CSS-in-JS 라이브러리가 styled-components/Emotion을 완전히 대체할 수는 없다는 것을 의미합니다. 동적 컴포넌트를 다시 작업하는 데 약간의 시간이 필요할 것입니다. 그러나 이는 완전히 다른 CSS 도구로 전환하는 것에 비해 아주 작은 작업량입니다.
Linaria로 마이그레이션하기 (Migrating to Linaria)
그렇다면, 우리는 모두 styled-components 애플리케이션을 Linaria로 마이그레이션해야 할까요?
불행히도, 문제가 있습니다. Linaria 자체는 활발하게 유지 관리되고 있지만, Next.js에 대한 공식적인 바인딩이 없으며, Linaria를 Next.js와 함께 작동시키는 것은 간단하지 않습니다.
가장 인기 있는 통합 방법인 next-linaria
는 3년 동안 업데이트되지 않았으며, App Router / React 서버 컴포넌트와 호환되지 않습니다. 더 새로운 옵션인 next-with-linaria
가 있지만, 이 라이브러리를 프로덕션에서 사용하지 말라는 큰 경고가 있습니다. 😅
따라서, 모험심 있는 개발자들에게는 옵션이 될 수 있지만, 저는 이것을 추천하기에는 좀 부담스럽습니다.
Panda CSS

Panda CSS는 인기 있는 컴포넌트 라이브러리인 Chakra UI를 개발한 사람들이 만든 현대적인 CSS-in-JS 라이브러리입니다.
Panda CSS는 여러 가지 다양한 인터페이스를 제공합니다. Tailwind처럼 mb-5
와 같은 단축 클래스를 지정하여 사용할 수 있고, Stitches처럼 variants와 cva를 사용하여 사용할 수도 있습니다. 또는 styled-components처럼 사용할 수도 있습니다.
styled
API를 사용할 경우 다음과 같이 작성할 수 있습니다:
import { styled } from '../styled-system/jsx';
export default function Homepage() {
return <BigRedButton>Click me!</BigRedButton>;
}
const BigRedButton = styled.button`
font-size: 2rem;
color: red;
`;
Linaria와 마찬가지로, Panda CSS도 컴파일되지만 Tailwind 스타일의 유틸리티 클래스들로 컴파일됩니다. 최종 결과는 다음과 비슷하게 보일 것입니다:
/* /styles.css */
.font-size_2rem {
font-size: 2rem;
}
.color_red {
color: red;
}
/* /components/Home.js */
export default function Homepage() {
return <button className="font-size_2rem color_red">Click me!</button>;
}
각각의 고유한 CSS 선언 (예: color: red
) 에 대해, Panda CSS는 중앙 CSS 파일에 새로운 유틸리티 클래스를 생성합니다. 이 파일은 React 어플리케이션의 모든 경로에서 로드됩니다.
저는 Panda CSS를 정말 좋아하고 싶습니다. 이 라이브러리는 관련 경험이 풍부한 견고한 팀에 의해 개발되고 있고, 친숙한 API를 제공하며, 귀여운 스케이트보드를 타는 팬더 마스코트까지 있습니다!
하지만 사용해 본 결과, 이 라이브러리가 저와는 맞지 않는다는 것을 발견했습니다. 몇 가지 문제는 사소하거나 피상적인 것일 수 있습니다. 예를 들어, Panda CSS는 프로젝트 파일을 어지럽히는 많은 것들을 생성합니다. 저에게는 이것이 약간 지저분하게 느껴지지만, 궁극적으로는 큰 문제는 아닙니다.
저에게 더 큰 문제는 Panda CSS가 중요한 기능 하나를 놓치고 있다는 점입니다. 우리는 컴포넌트를 교차 참조할 수 없습니다.
예제로 설명하는 것이 더 쉬울 것입니다. 이 블로그에는 Next.js의 Link
컴포넌트를 스타일링한 TextLink
컴포넌트가 있습니다. 기본적으로는 이렇게 보입니다:
하지만 이 동일한 컴포넌트는 특정 맥락적 스타일을 가집니다. 예를 들어, TextLink
가 Aside
내부에 있을 때는 이렇게 보입니다:
저는 이 Aside
컴포넌트를 부가 정보나 관련 정보에 사용합니다. 기본 TextLink
스타일이 이 맥락에서는 잘 맞지 않는다는 것을 발견했고, 일부 오버라이드를 적용하고 싶었습니다.
이러한 관계를 styled-components 를 통해 표현하면 다음과 같습니다:
import Link from 'next/link';
import { AsideWrapper } from '@/components/Aside';
const TextLink = styled(Link)`
color: var(--color-primary);
text-decoration: none;
${AsideWrapper} & {
color: inherit;
text-decoration: underline;
}
`;
앰퍼샌드 문자 (&
) 는 최근에 공식적인 중첩 문법의 일부로 CSS 언어에 추가되었지만, 많은 해 동안 CSS 전처리기와 도구에서 사용되어 왔습니다. styled-components에서는 현재 선택자에 해당합니다.
이 코드를 렌더링하여 생성한 CSS는 다음과 같을 것입니다.
.textlink_abc123 {
color: var(--color-primary);
text-decoration: none;
}
.aside_def456 .textlink_abc123 {
color: inherit;
text-decoration: underline;
}
CSS를 다룰 때, 저는 하나의 규칙을 따르려고 합니다: 특정 컴포넌트에 대한 모든 스타일은 한 곳에 작성되어야 합니다. 주어진 요소에 적용될 수 있는 다양한 CSS를 찾기 위해 어플리케이션 전체를 뒤질 필요가 없어야 합니다!
이것이 Tailwind가 강력한 이유 중 하나입니다. 모든 스타일이 요소 자체에 함께 위치해 있습니다. 다른 컴포넌트가 소유하지 않은 요소를 "건드려서" 스타일링하는 것에 대해 걱정할 필요가 없습니다.
이 패턴은 그 아이디어의 강력한 버전과 같습니다. 기본적으로 TextLink
에 적용되는 모든 스타일을 나열할 뿐만 아니라, 맥락에 따라 적용되는 스타일도 나열합니다. 그리고 이 모든 스타일이 하나의 위치, 백틱 사이에 함께 모여 있습니다.
안타깝게도, 이 패턴은 Panda CSS에서는 작동하지 않습니다. Panda CSS에서는 요소 자체가 아니라 CSS 선언을 고유하게 식별하기 때문에 이러한 관계를 표현할 방법이 없습니다.
이 패턴에 관심이 없다면, Panda CSS는 당신의 애플리케이션에 좋은 옵션이 될 수 있습니다! 하지만 저에게는 이 점이 결정적인 단점입니다.
styled-components Happy Path
이 "맥락적 스타일" 패턴에 대해 더 알고 싶다면, 제 블로그 게시물 "The styled-components Happy Path"에서 자세히 다루고 있습니다. 이는 styled-components를 사용하면서 오랜 기간 동안 배운 패턴과 팁의 모음입니다.
Pigment CSS
가장 인기 있는 React 컴포넌트 라이브러리 중 하나인 Material UI는 Emotion을 기반으로 구축되었습니다. 그들의 개발 팀은 RSC 호환성과 관련된 모든 동일한 문제들과 씨름해왔으며, 이를 해결하기로 결정했습니다.
그들은 최근에 Pigment CSS 라는 이름의 새로운 라이브러리를 오픈소스로 공개했습니다. 이 라이브러리의 API는 이제쯤 됐으면 익숙하게 보여져야 합니다:
import { styled } from '@pigment-css/react';
export default function Homepage() {
return <BigRedButton>Click me!</BigRedButton>;
}
const BigRedButton = styled.button`
font-size: 2rem;
color: red;
`;
Pigment CSS는 컴파일 타임에 실행되며, Linaria와 동일한 전략을 사용하여 CSS Modules로 컴파일합니다. Next.js와 Vite를 위한 플러그인도 제공됩니다.
사실, Pigment CSS는 WyW-in-JS ("What you Want in JS") 라는 저수준 도구를 사용합니다. 이 도구는 Linaria 코드베이스에서 발전한 것으로, "CSS 모듈로 컴파일"하는 비즈니스 로직을 분리하여 일반화했습니다. 이를 통해 Pigment CSS와 같은 라이브러리가 자체 API를 구축할 수 있습니다.
솔직히, 이게 완벽한 해결책처럼 느껴집니다. CSS 모듈은 이미 충분히 검증되었고 최적화되어 있습니다. 제가 지금까지 본 바로는, Pigment CSS는 훌륭한 성능과 개발자 경험을 제공하는 멋진 라이브러리입니다.
Material UI의 다음 주요 버전은 Pigment CSS를 지원할 예정이며, 궁극적으로 Emotion과 styled-components에 대한 지원을 완전히 중단할 계획입니다. 결과적으로 Pigment CSS는 가장 널리 사용되는 CSS-in-JS 라이브러리 중 하나가 될 가능성이 큽니다. Material UI는 NPM에서 주당 약 500만 회 다운로드되며, 이는 React 자체의 약 1/5에 해당하는 양입니다!
아직 초기 단계입니다. Pigment CSS는 몇 주 전에 오픈소스로 공개되었습니다. 하지만 팀은 이 프로젝트에 상당한 자원을 투입하고 있습니다. 앞으로 어떻게 발전할지 정말 기대됩니다!
목록은 계속됩니다 (The list goes on)
지금까지 다룬 라이브러리 외에도, 생태계에는 흥미로운 일을 하고 있는 많은 프로젝트가 있습니다. 제가 주목하고 있는 몇 가지 프로젝트는 다음과 같습니다:
- next-yak — 스위스 최대 전자 상거래 소매업체를 위해 개발된 next-yak는 컴파일 타임 CSS-in-JS 라이브러리로, styled-components의 대체물에 최대한 가깝도록 설계되었으며, 많은 보조 API를 재구현했습니다.
- Kuma UI — 이 라이브러리는 꽤 야심찬 시도를 하고 있습니다. 대부분의 스타일을 컴파일 타임에 추출하지만, 클라이언트 컴포넌트를 위해 런타임도 제공하는 “하이브리드” 디자인을 목표로 합니다.
- Parcel macros — Parcel은 최근에 "macros"라는 기능을 구현한 번들러로, 컴파일 타임 CSS-in-JS 라이브러리를 포함한 다양한 도구를 구축하는 데 사용할 수 있습니다. 놀랍게도 이 기능은 Parcel에만 국한되지 않으며, Next.js와 함께 사용할 수 있습니다!
앞으로의 길 (The path forward)
좋습니다, 많은 옵션을 탐색해 보았지만, 여전히 질문은 남아 있습니다: "레거시" CSS-in-JS 라이브러리를 사용하는 프로덕션 애플리케이션이 있다면, 실제로 무엇을 해야 할까요?
다소 직관에 반하는 이야기일 수도 있지만, 많은 경우 실제로 아무 것도 할 필요가 없다고 생각합니다. 😅
온라인 토론 중 많은 사람들이 styled-components를 현대적인 React / Next.js 애플리케이션에서 사용할 수 없거나, 이를 사용하는 데 큰 성능 페널티가 있다고 말합니다. 하지만 그것은 사실이 아닙니다.
많은 사람들이 RSC (React 서버 컴포넌트)와 SSR (Server Side Rendering) 을 혼동하고 있습니다. 서버 사이드 렌더링은 여전히 기존 방식과 동일하게 작동하며, 이러한 변화에 영향을 받지 않습니다. Next.js의 App Router나 다른 RSC 구현으로 마이그레이션해도 애플리케이션이 느려지지 않을 것입니다. 사실, 약간 더 빨라질 가능성이 높습니다!
성능 관점에서, RSC와 제로 런타임 CSS 라이브러리의 주요 이점은 TTI ("Time To Interactive") 입니다. 이는 UI가 사용자에게 표시되는 시점과 UI가 완전히 상호작용할 수 있는 상태가 되는 시점 사이의 지연을 의미합니다. 이 지연을 무시하면 사용자 경험이 나빠질 수 있습니다. 사용자는 UI 요소를 클릭하면 작동할 것으로 기대하지만, 애플리케이션이 아직 하이드레이션 과정에 있기 때문에 아무 일도 일어나지 않는 상황이 발생할 수 있습니다.
따라서 현재 애플리케이션이 하이드레이션에 오랜 시간이 걸린다면, 제로 런타임 CSS 라이브러리로의 마이그레이션이 사용자 경험에 긍정적인 영향을 미칠 수 있습니다. 하지만 애플리케이션이 이미 견고한 TTI를 가지고 있다면, 사용자는 이 마이그레이션으로부터 어떤 혜택도 보지 못할 것입니다.
많은 경우, 가장 큰 문제는 FOMO (놓치는 것에 대한 두려움) 라고 생각합니다. 개발자로서 우리는 최신의 최고의 도구를 사용하고 싶어 합니다. 새로운 최적화에서 큰 이점을 얻지 못한다는 것을 알면서도, 많은 "use client"
지시어를 추가하는 것은 재미없습니다. 하지만 이것이 정말로 대규모 마이그레이션을 해야 할 충분한 이유일까요?
제가 하는 일 (What I'm doing)
저는 두 개의 주요 프로덕션 애플리케이션을 유지 관리하고 있습니다: 이 블로그와, 제가 인터랙티브 코스 (CSS for JavaScript Developers와 The Joy of React) 를 위해 사용하는 코스 플랫폼입니다.
제 코스 플랫폼은 여전히 Next.js Pages Router와 styled-components를 사용하고 있으며, 당분간 이를 마이그레이션할 계획이 없습니다. 저는 현재 제공되는 사용자 경험에 만족하고 있으며, 마이그레이션을 통해 얻을 수 있는 성능상의 이점이 크지 않을 것이라고 생각합니다.
제 블로그도 현재 Next.js Pages Router와 styled-components를 사용하고 있지만, Next.js App Router로 마이그레이션하는 과정에 있습니다. 현재로서는 Linaria와 next-with-linaria를 사용하기로 결정했습니다. Pigment CSS가 좀 더 성숙해지면, 그때로 전환할 계획입니다.
React 서버 컴포넌트는 정말 멋집니다. React/Vercel 팀은 서버에서의 React 작동 방식을 재구상하는 데 엄청난 성과를 이루었습니다. 하지만 솔직히 말해서, 제가 직접 이러한 마이그레이션을 시도해본 결과, 대부분의 프로덕션 애플리케이션에는 이를 추천하지 않습니다. App Router가 "안정적"이라고 표시되었지만, 여전히 Pages Router만큼 성숙하지 않으며, 아직 다듬어야 할 부분들이 있습니다.
만약 애플리케이션의 성능에 만족하고 있다면, 업데이트/마이그레이션에 대한 긴급함을 느낄 필요는 없다고 생각합니다 ❤️. 현재 사용 중인 스택은 계속 잘 작동할 것이며, 몇 년 후에 다시 확인하고 상황을 살펴보면 됩니다.
The Joy of React
저는 거의 10년 동안 React를 사용해 왔으며, 이는 동적 웹 애플리케이션을 구축하는 가장 좋아하는 방법이었습니다. 저는 React에 대해 알고 있는 모든 것을 인터랙티브한 자기주도 학습 코스인 The Joy of React에 모았습니다.
이 코스에서는 React의 작동 방식에 대한 멘탈 모델을 구축하고, React를 생산적으로 사용하는 데 도움이 된 "행복한 실천들"을 공유합니다. 이 코스는 React 서버 컴포넌트와 Next.js App Router, Suspense 및 스트리밍 서버 사이드 렌더링과 같은 최신 기능들도 깊이 다룹니다.
자세한 내용은 여기에서 확인할 수 있습니다: