CSS 라이브러리는 크게 세 가지로 구분될 수 있습니다: 컴포넌트 기반(Component-based), CSS-in-JS, 그리고 CSS 프레임워크(CSS Framework)입니다. 컴포넌트 기반은 UI 요소를 컴포넌트로 제공하며, CSS-in-JS는 런타임에 JavaScript를 통해 head 태그 내에 스타일 태그를 직접 생성합니다. 반면 CSS 프레임워크는 CSS 사용을 더 편리하게 만들어 줍니다.
제 경험상, 초기 스타트업을 제외하고는 컴포넌트 기반 접근법을 널리 사용하는 경우를 본 적이 없습니다. 이는 커스텀이 많이 필요할수록 비용이 상승하기 때문입니다. 최근에는 CSS-in-JS나 CSS 프레임워크 중 하나를 선택하여 사용하는 경향이 더욱 두드러집니다.
싱글 페이지 애플리케이션(SPA)이 처음 유행했을 때에는 CSS-in-JS가 인기였습니다. 하지만 여러 단점들 때문에, 초기 요청에서 전통적인 CSS를 로딩하는 방향으로 돌아가는 추세입니다.
CSS-in-JS의 단점으로는 런타임 오버헤드, 캐싱 문제, 번들 크기 증가 등이 있습니다. 실제로 복잡하지 않은 스타일링에는 인라인 스타일을 사용하는 편입니다. 가독성과 성능에 대한 우려가 있음에도 불구하고, 눈에 띄는 성능 차이가 없는데 개발 경험이 좋다면 기술을 쉽게 변경하지 않습니다.
CSS 모듈은 좋은 선택지이지만, 어떤 이유에서인지 emotion-vanilla-v11의 성능이 더 우수한 것으로 나타났습니다. 벤치마킹이 100% 신뢰할 수는 없지만, 일관된 결과를 보여주었습니다. 그러나 렌더링 속도가 20ms 이하로 나타나 오버헤드와 관련된 지표는 크게 중요하지 않았습니다.
벤치마크: https://simnalamburt.github.io/css-in-js-benchmark/
앞서 css-in-js의 단점들을 살펴보았습니다. 하지만 이를 넘을만큼 설정이 간단하고 러닝커브가 적은 emotion css를 써보겠습니다. 사심이 가득하긴 했으나 이모션 10 버전부터 SSR을 지원하여서 위의 단점들 대다수를 상쇄할 수 있을 것으로 보입니다.
이대로 끝내긴 아쉬우니 실제로 Next.js 14 App router 환경의 SSR에서 Emotion을 사용해보겠습니다.
이 두 명령어를 입력하고 Next.js 14 프로젝트를 만들어줍니다.
npx create-next-app@latest
npm i @emotion/react @emotion/cache
그리고 아래의 코드를 만들어줍니다.
useServerInsertedHTML은 스타일을 사용하기 전에 삽입할 수 있도록 도와주는 훅입니다.
// app/emotion.tsx
"use client";
import { CacheProvider } from "@emotion/react";
import createCache from "@emotion/cache";
import { useServerInsertedHTML } from "next/navigation";
import { ReactNode, useState } from "react";
export default function RootStyleRegistry({
children,
}: {
children: ReactNode;
}) {
const [cache] = useState(() => {
const cache = createCache({ key: "css" });
cache.compat = true;
return cache;
});
useServerInsertedHTML(() => {
return (
<style
data-emotion={`${cache.key} ${Object.keys(cache.inserted).join(" ")}`}
dangerouslySetInnerHTML={{
__html: Object.values(cache.inserted).join(" "),
}}
/>
);
});
return <CacheProvider value={cache}>{children}</CacheProvider>;
}
다음은 layout.tsx 파일을 수정해봅시다.
이곳에 이전에 만들었던 RootStyleRegistry 컴포넌트를 넣어주어야 합니다.
// app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import RootStyleRegistry from "./emotion";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<head></head>
<body className={inter.className}>
<RootStyleRegistry>{children}</RootStyleRegistry>
</body>
</html>
);
}
page.tsx 파일의 최상단에 아래의 코드를 추가하고, className으로 지정된 모든 부분을 css로 변경합니다. 이때, 기존의 초기 스타일을 그대로 유지하기 위해 @emotion/react의 Global 컴포넌트를 사용하여 전역 스타일을 적용할 수 있습니다. 또한, 기존에 CSS 모듈을 사용했던 부분을 키-값 형태로 변경해야 합니다. 이 과정에서 @emotion/react의 CSS 리터럴 템플릿을 활용하면 기존 CSS 코드를 쉽게 이전할 수 있습니다.
Next.js는 SWC를 적극적으로 사용하고 있으므로, .babelrc 파일을 사용한 커스터마이징 대신 JSX pragma를 활용하여 CSS prop을 사용할 수 있게 설정합니다. 이는 스타일 컴파일 시 React.createElement 대신 emotion의 jsx() 함수를 사용하기 위한 설정입니다.
이러한 변경을 통해, 코드의 가독성과 유지보수성을 높이며, @emotion/react를 통한 스타일링 방식으로 원활하게 전환할 수 있습니다.
// app/page.tsx
/** @jsxImportSource @emotion/react */
"use client";
다음과 같이 SSR이 잘 적용된 것을 볼 수 있습니다.
코드 참고: https://github.com/emotion-js/emotion/issues/2928#issuecomment-1293012737
반전으로는 사실 위의 일련의 과정은 정식 지원이 아닌 우회하여 적용하는 방법으로 실제 서비스를 만드실 때에는 Next.js 공식 홈페이지에서 권장하는 스타일 방법들로 사용하시는게 좋을 것 같습니다. 저는 개인적으로 사용법이 간단한 Emotion을 SPA에서 좀 더 적극적으로 쓸 것 같습니다.
마지막으로 CSS 라이브러리를 잘 정리한 사이트 두 곳을 남기고 글을 마무리 짓도록 하겠습니다.
https://www.freecodecamp.org/news/best-css-frameworks-for-frontend-devs/
https://dev.to/avinashvagh/react-ecosystem-in-2024-418k
잘읽었습니다~ 👍