CSS-in-JS
CSS-in-JS 라이브러리의 장점과 단점은 무엇인가요?
CSS-in-JS는 JavaScript 파일 안에서 CSS를 작성하는 방법론입니다. 대표적인 라이브러리로는 styled-components, emotion, JSS, stitches 등이 있습니다.
CSS-in-JS란?
CSS-in-JS는 컴포넌트의 스타일을 JavaScript로 정의하는 방식입니다. 스타일이 컴포넌트와 함께 관리되어 더 모듈화된 개발이 가능합니다.
CSS-in-JS 기본 예시
// styled-components 예시
import styled from "styled-components";
// 스타일이 적용된 컴포넌트 생성
const Button = styled.button`
background-color: ${(props) => (props.primary ? "#007bff" : "#6c757d")};
color: white;
padding: 0.5rem 1rem;
border: none;
border-radius: 0.25rem;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s ease;
&:hover {
background-color: ${(props) => (props.primary ? "#0056b3" : "#545b62")};
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
`;
// 사용
function App() {
return (
<div>
<Button primary>Primary 버튼</Button>
<Button>Secondary 버튼</Button>
<Button disabled>비활성 버튼</Button>
</div>
);
}CSS-in-JS의 장점
1. 컴포넌트 스코프 스타일링
// ❌ 기존 CSS - 전역 스코프 문제
/* styles.css */
.button {
background-color: blue;
}
.card .button { /* 덮어쓰기 위해 더 구체적인 셀렉터 필요 */
background-color: green;
}
// ✅ CSS-in-JS - 자동으로 고유한 클래스명 생성
const Button = styled.button`
background-color: blue;
`;
const Card = styled.div`
${Button} { /* 중첩 선택자도 쉽게 */
background-color: green;
}
`;생성되는 HTML:
<!-- 자동으로 고유한 클래스명 생성 -->
<button class="sc-bdvvaa jIzXuG">버튼</button>
<div class="sc-bdfBQB kFGWyT">
<button class="sc-bdvvaa bQWxyz">카드 안의 버튼</button>
</div>2. 동적 스타일링
// props에 따라 동적으로 스타일 변경
const ProgressBar = styled.div`
width: 100%;
height: 20px;
background-color: #f0f0f0;
border-radius: 10px;
overflow: hidden;
`;
const ProgressFill = styled.div`
height: 100%;
background-color: ${(props) => {
if (props.percentage < 30) return "#dc3545"; // 빨강
if (props.percentage < 70) return "#ffc107"; // 노랑
return "#28a745"; // 초록
}};
width: ${(props) => props.percentage}%;
transition: all 0.3s ease;
/* 애니메이션도 동적으로 */
animation: ${(props) => (props.animated ? "pulse 1s infinite" : "none")};
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.7;
}
100% {
opacity: 1;
}
}
`;
// 사용
function ProgressIndicator({ value, animated = false }) {
return (
<ProgressBar>
<ProgressFill percentage={value} animated={animated}>
{value}%
</ProgressFill>
</ProgressBar>
);
}3. 테마 시스템
// 테마 정의
const lightTheme = {
colors: {
primary: "#007bff",
background: "#ffffff",
text: "#000000",
},
spacing: {
small: "0.5rem",
medium: "1rem",
large: "2rem",
},
};
const darkTheme = {
colors: {
primary: "#0056b3",
background: "#1a1a1a",
text: "#ffffff",
},
spacing: {
small: "0.5rem",
medium: "1rem",
large: "2rem",
},
};
// 테마를 사용하는 컴포넌트
const ThemedButton = styled.button`
background-color: ${(props) => props.theme.colors.primary};
color: ${(props) => props.theme.colors.text};
padding: ${(props) => props.theme.spacing.medium};
border: none;
border-radius: 4px;
`;
const Container = styled.div`
background-color: ${(props) => props.theme.colors.background};
color: ${(props) => props.theme.colors.text};
min-height: 100vh;
padding: ${(props) => props.theme.spacing.large};
`;
// ThemeProvider로 전역 테마 제공
import { ThemeProvider } from "styled-components";
function App() {
const [isDark, setIsDark] = useState(false);
return (
<ThemeProvider theme={isDark ? darkTheme : lightTheme}>
<Container>
<ThemedButton onClick={() => setIsDark(!isDark)}>
테마 변경
</ThemedButton>
</Container>
</ThemeProvider>
);
}4. 자동 벤더 프리픽스
const FlexBox = styled.div`
display: flex; /* 자동으로 -webkit-flex 등 추가 */
user-select: none; /* 자동으로 -webkit-user-select 등 추가 */
backdrop-filter: blur(10px); /* 자동으로 -webkit-backdrop-filter 추가 */
`;5. 죽은 코드 제거
// 컴포넌트가 삭제되면 스타일도 함께 삭제됨
const UnusedComponent = styled.div`
/* 이 컴포넌트를 삭제하면 이 스타일도 자동으로 사라짐 */
`;CSS-in-JS의 단점
1. 런타임 성능 비용
// 매 렌더마다 스타일이 재계산됨
const DynamicComponent = styled.div`
/* 이 계산이 매번 실행됨 */
color: ${(props) => expensiveColorCalculation(props.data)};
/* 복잡한 조건문도 매번 평가 */
background: ${(props) => {
if (props.type === "primary") return "#007bff";
if (props.type === "secondary") return "#6c757d";
if (props.type === "success") return "#28a745";
// ... 복잡한 로직
}};
`;해결책:
// 1. 메모이제이션 사용
const memoizedStyle = useMemo(
() => ({
color: expensiveColorCalculation(data),
}),
[data],
);
// 2. CSS 변수 활용
const OptimizedComponent = styled.div`
color: var(--dynamic-color);
`;
// 3. 정적 스타일과 동적 스타일 분리
const BaseComponent = styled.div`
/* 정적 스타일 */
padding: 1rem;
border-radius: 4px;
`;
const DynamicComponent = styled(BaseComponent)`
/* 동적 스타일만 여기에 */
color: ${(props) => props.color};
`;2. 번들 크기 증가
// styled-components 라이브러리 자체의 크기
import styled from "styled-components"; // ~13KB gzipped
// 더 가벼운 대안들
import { styled } from "@stitches/react"; // ~6KB
import { css } from "@emotion/css"; // ~5KB3. 서버사이드 렌더링(SSR) 복잡성
// Next.js에서 styled-components 설정
// _document.js
import Document from 'next/document';
import { ServerStyleSheet } from 'styled-components';
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
sheet.collectStyles(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
};
} finally {
sheet.seal();
}
}
}
// babel 설정도 필요
// .babelrc
{
"plugins": [["styled-components", { "ssr": true }]]
}4. 디버깅의 어려움
// 생성된 클래스명으로는 어떤 컴포넌트인지 알기 어려움
<div class="sc-bdvvaa jIzXuG"></div> <!-- 이게 뭔지 모르겠음 -->
// 해결책: displayName 설정
const Button = styled.button`
/* 스타일 */
`;
Button.displayName = 'CustomButton'; // 디버깅 시 도움됨5. 학습 곡선
// 기존 CSS 지식에 더해 라이브러리별 문법 학습 필요
// styled-components
const Button = styled.button``;
// emotion
const Button = styled.button``;
/** @jsxImportSource @emotion/react */
const Button = () => (
<button
css={css`
color: red;
`}
>
버튼
</button>
);
// stitches
const Button = styled("button", {});각 라이브러리별 특징
styled-components
// 장점: 가장 널리 사용됨, 풍부한 생태계
const Button = styled.button.attrs((props) => ({
type: props.type || "button",
}))`
padding: 1rem;
background: ${(props) => (props.primary ? "blue" : "gray")};
`;
// 상속도 쉬움
const PrimaryButton = styled(Button)`
background: blue;
`;emotion
/** @jsxImportSource @emotion/react */
import { css } from "@emotion/react";
// CSS prop 방식 (더 직관적)
const App = () => (
<div
css={css`
color: red;
&:hover {
color: blue;
}
`}
>
Hello World
</div>
);
// styled 방식도 지원
import styled from "@emotion/styled";
const Button = styled.button`
color: red;
`;stitches
import { styled } from "@stitches/react";
// TypeScript 친화적, 높은 성능
const Button = styled("button", {
// 기본 스타일
padding: "$2",
variants: {
color: {
primary: { backgroundColor: "$blue9" },
secondary: { backgroundColor: "$gray9" },
},
size: {
small: { fontSize: "$1" },
large: { fontSize: "$3" },
},
},
});
// 사용
<Button color="primary" size="large">
버튼
</Button>;CSS-in-JS 선택 가이드
styled-components 선택 시기:
- React 생태계에 깊이 통합하고 싶을 때
- 풍부한 커뮤니티와 플러그인이 필요할 때
- 기존 프로젝트에서 널리 사용되고 있을 때
emotion 선택 시기:
- css prop으로 더 유연하게 작업하고 싶을 때
- 성능이 중요한 프로젝트일 때
- styled-components보다 작은 번들 크기를 원할 때
stitches 선택 시기:
- TypeScript 프로젝트일 때
- 최고의 런타임 성능이 필요할 때
- 디자인 시스템 구축이 주 목적일 때
사용하지 않는 것이 좋은 경우:
- 정적인 스타일이 대부분인 프로젝트
- SSR이 복잡하게 느껴지는 팀
- 번들 크기와 성능이 매우 중요한 프로젝트
성능 최적화 팁
// 1. 정적 스타일과 동적 스타일 분리
const StaticStyles = styled.div`
/* 변하지 않는 스타일 */
display: flex;
align-items: center;
padding: 1rem;
`;
const DynamicStyles = styled(StaticStyles)`
/* 변하는 스타일만 */
color: ${(props) => props.color};
`;
// 2. shouldForwardProp 사용으로 불필요한 props 전달 방지
const Button = styled.button.withConfig({
shouldForwardProp: (prop, defaultValidatorFn) =>
!["color", "size"].includes(prop) && defaultValidatorFn(prop),
})`
color: ${(props) => props.color};
`;
// 3. 컴파일 타임 최적화 (babel 플러그인)
// babel.config.js
module.exports = {
plugins: [
[
"styled-components",
{
displayName: true, // 개발 시 디버깅 도움
ssr: true, // SSR 최적화
preprocess: false, // 런타임 최적화
},
],
],
};Edit on GitHub
Last updated on