🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Emotion 최신 기능 완벽 가이드 - 슬라이드 1/13
A

AI Generated

2025. 11. 3. · 13 Views

Emotion 스타일링 완벽 가이드

CSS-in-JS 라이브러리 Emotion의 최신 기능과 실전 활용법을 다룹니다. styled components, css prop, 테마 관리부터 성능 최적화까지 실무에서 바로 적용할 수 있는 모든 것을 담았습니다.


목차

  1. styled 컴포넌트 - 재사용 가능한 스타일 컴포넌트 생성
  2. css prop - 인라인 스타일처럼 빠르게 스타일 적용
  3. ThemeProvider - 일관된 디자인 시스템 구축
  4. Global Styles - 전역 스타일 정의하기
  5. keyframes - 커스텀 애니메이션 만들기
  6. 컴포넌트 합성 - 기존 컴포넌트 확장하기
  7. 객체 스타일 - JavaScript 객체로 스타일 작성
  8. SSR과 hydration - 서버 사이드 렌더링 대응
  9. 미디어 쿼리와 반응형 디자인 - 모바일부터 데스크톱까지
  10. 성능 최적화 - 프로덕션 환경 튜닝

1. styled 컴포넌트 - 재사용 가능한 스타일 컴포넌트 생성

시작하며

여러분이 React 프로젝트에서 버튼을 만들 때마다 className을 일일이 작성하고, CSS 파일을 왔다 갔다 하면서 스타일을 찾느라 시간을 낭비한 적 있나요? 특히 프로젝트가 커질수록 CSS 클래스명 충돌을 피하기 위해 BEM 같은 복잡한 네이밍 규칙을 따라야 했던 경험이 있을 겁니다.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 컴포넌트와 스타일이 분리되어 있으면 유지보수가 어렵고, 사용하지 않는 CSS가 계속 쌓이게 됩니다.

또한 동적 스타일링을 위해 인라인 스타일을 남발하면 성능 문제가 생길 수 있죠. 바로 이럴 때 필요한 것이 Emotion의 styled 컴포넌트입니다.

스타일과 로직을 한 곳에서 관리하면서도 TypeScript 지원과 자동 벤더 프리픽스까지 제공하여 개발 생산성을 크게 높여줍니다.

개요

간단히 말해서, styled 컴포넌트는 JavaScript 안에서 CSS를 작성하여 스타일이 적용된 React 컴포넌트를 만드는 방식입니다. 전통적인 CSS 파일 방식과 달리, styled 컴포넌트는 스타일을 컴포넌트 자체에 캡슐화합니다.

이를 통해 props 기반 동적 스타일링, 자동 critical CSS 추출, 그리고 사용되지 않는 스타일 자동 제거가 가능해집니다. 예를 들어, 버튼의 색상을 props로 받아 다양한 변형을 손쉽게 만들 수 있습니다.

기존에는 CSS 클래스를 만들고 조건부로 className을 토글했다면, 이제는 props를 전달하는 것만으로 스타일을 완전히 제어할 수 있습니다. styled 컴포넌트의 핵심 특징은 세 가지입니다.

첫째, 자동으로 고유한 클래스명을 생성하여 스타일 충돌을 방지합니다. 둘째, props를 활용한 강력한 동적 스타일링이 가능합니다.

셋째, 컴포넌트 기반 사고방식과 완벽하게 일치하여 코드의 응집도를 높입니다. 이러한 특징들이 대규모 프로젝트에서 스타일 관리를 훨씬 쉽게 만들어줍니다.

코드 예제

import styled from '@emotion/styled';

// props를 받아 동적으로 스타일을 변경하는 버튼
const Button = styled.button`
  padding: ${props => props.size === 'large' ? '16px 32px' : '8px 16px'};
  background-color: ${props => props.primary ? '#007bff' : '#6c757d'};
  color: white;
  border: none;
  border-radius: 4px;
  font-size: ${props => props.size === 'large' ? '18px' : '14px'};
  cursor: pointer;
  transition: all 0.3s ease;

  &:hover {
    background-color: ${props => props.primary ? '#0056b3' : '#5a6268'};
    transform: translateY(-2px);
  }

  &:disabled {
    opacity: 0.6;
    cursor: not-allowed;
  }
`;

// 사용 예시
// <Button primary size="large">저장하기</Button>

설명

이것이 하는 일: styled 함수는 HTML 태그나 React 컴포넌트를 받아서, 템플릿 리터럴로 작성된 CSS를 적용한 새로운 컴포넌트를 생성합니다. 첫 번째로, styled.button 부분은 기본 button 엘리먼트를 스타일링할 대상으로 지정합니다.

템플릿 리터럴 안에서는 일반 CSS 문법을 그대로 사용할 수 있으며, 중첩 선택자(&)를 활용해 hover나 disabled 같은 의사 클래스도 자연스럽게 처리할 수 있습니다. 그 다음으로, props를 활용한 동적 스타일링이 핵심입니다.

${props => ...} 형태의 함수를 사용하면 컴포넌트에 전달된 props 값에 따라 스타일을 실시간으로 변경할 수 있습니다. 예를 들어 primary props가 true일 때와 false일 때 서로 다른 배경색을 적용하는 것이 단 한 줄로 가능합니다.

마지막으로, Emotion은 이 스타일들을 런타임에 처리하여 고유한 클래스명을 생성하고 실제 DOM에 주입합니다. transition과 transform 같은 애니메이션 속성도 자동으로 벤더 프리픽스가 추가되어 크로스 브라우저 호환성을 보장합니다.

여러분이 이 코드를 사용하면 하나의 Button 컴포넌트로 primary/secondary 버튼, 크기가 다른 버튼 등 수십 가지 변형을 손쉽게 만들 수 있습니다. 더 이상 버튼마다 별도의 CSS 클래스를 만들 필요가 없으며, props만 바꾸면 즉시 디자인이 변경됩니다.

실무에서는 디자인 시스템을 구축할 때 이 패턴을 활용하면 일관된 UI 컴포넌트를 빠르게 생산할 수 있습니다. 또한 TypeScript와 함께 사용하면 props의 타입 안정성까지 확보할 수 있어 런타임 오류를 사전에 방지할 수 있습니다.

실전 팁

💡 props의 타입을 TypeScript로 정의하면 자동완성과 타입 체크를 받을 수 있습니다. styled.button<{primary?: boolean}> 형태로 제네릭을 활용하세요.

💡 자주 사용하는 색상이나 크기 값은 별도의 theme 객체로 분리하여 일관성을 유지하세요. 나중에 다룰 ThemeProvider와 함께 사용하면 더욱 강력합니다.

💡 &:hover 같은 의사 클래스 대신 @media 쿼리도 동일하게 중첩해서 사용할 수 있어 반응형 디자인이 매우 간편합니다.

💡 성능 최적화를 위해 props로 전달하는 값이 자주 바뀌지 않도록 주의하세요. 매 렌더마다 다른 객체를 전달하면 불필요한 리렌더링이 발생합니다.

💡 기존 컴포넌트를 확장할 때는 styled(ExistingComponent) 형태로 감싸면 기존 스타일에 추가 스타일을 덧붙일 수 있습니다.


2. css prop - 인라인 스타일처럼 빠르게 스타일 적용

시작하며

여러분이 간단한 스타일 하나를 추가하려고 할 때, 굳이 새로운 styled 컴포넌트를 만들기에는 과하다고 느낀 적 있나요? 특히 일회성 스타일이나 레이아웃 조정을 위해 매번 컴포넌트를 만들면 코드가 지나치게 복잡해집니다.

이런 문제는 프로토타이핑이나 빠른 개발이 필요한 상황에서 자주 발생합니다. styled 컴포넌트는 재사용성이 뛰어나지만, 때로는 그냥 해당 위치에서 바로 스타일을 적용하고 싶을 때가 있죠.

인라인 스타일을 쓰자니 의사 클래스나 미디어 쿼리를 사용할 수 없어 제약이 큽니다. 바로 이럴 때 필요한 것이 Emotion의 css prop입니다.

인라인 스타일의 편리함과 CSS의 모든 기능을 결합하여, 필요한 곳에 즉시 스타일을 적용할 수 있게 해줍니다.

개요

간단히 말해서, css prop은 JSX 엘리먼트에 직접 스타일을 작성할 수 있게 해주는 기능으로, 별도의 styled 컴포넌트 생성 없이도 CSS-in-JS의 모든 이점을 누릴 수 있습니다. css prop을 사용하면 컴포넌트 파일 안에서 스타일을 바로 확인하고 수정할 수 있어 개발 속도가 빨라집니다.

template literal이나 객체 문법 모두 지원하며, props 접근도 가능합니다. 예를 들어, 특정 섹션의 레이아웃만 빠르게 조정하거나 조건부 스타일을 간단히 적용할 때 매우 유용합니다.

기존에는 인라인 스타일로 style={{color: 'red'}}처럼 작성했다면, 이제는 css prop으로 hover, media query, 중첩 선택자까지 모두 사용할 수 있습니다. css prop의 핵심 특징은 세 가지입니다.

첫째, 완전한 CSS 문법을 지원하여 제약이 없습니다. 둘째, Babel 플러그인을 통해 자동으로 최적화됩니다.

셋째, 스타일 우선순위가 명확하여 예측 가능한 결과를 보장합니다. 이러한 특징들이 빠른 프로토타이핑과 유연한 스타일링을 가능하게 합니다.

코드 예제

/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';

function ProductCard({ isOnSale, discount }) {
  return (
    <div css={css`
      padding: 20px;
      border-radius: 8px;
      background: white;
      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
      transition: transform 0.2s;

      &:hover {
        transform: scale(1.05);
        box-shadow: 0 4px 16px rgba(0,0,0,0.2);
      }

      ${isOnSale && css`
        border: 2px solid #ff4444;
        background: linear-gradient(135deg, #fff 0%, #ffe6e6 100%);
      `}
    `}>
      <h3>상품명</h3>
      {isOnSale && (
        <span css={css`
          color: #ff4444;
          font-weight: bold;
          font-size: 18px;
        `}>
          {discount}% 할인!
        </span>
      )}
    </div>
  );
}

설명

이것이 하는 일: css prop은 JSX 엘리먼트의 css 속성에 스타일 객체나 템플릿 리터럴을 전달하여, 해당 엘리먼트에만 스타일을 적용합니다. 첫 번째로, 파일 최상단의 /** @jsxImportSource @emotion/react */ 주석은 Babel에게 JSX를 변환할 때 Emotion의 jsx 함수를 사용하라고 알려줍니다.

이 설정 없이는 css prop이 작동하지 않으므로 반드시 필요합니다. 또는 Babel 설정 파일에서 전역으로 설정할 수도 있습니다.

그 다음으로, css 함수로 감싼 템플릿 리터럴 안에 일반 CSS를 작성합니다. 여기서 중요한 점은 조건부 스타일링입니다.

${isOnSale && css...} 패턴을 사용하면 props나 state에 따라 스타일을 동적으로 추가하거나 제거할 수 있습니다. 이는 클래스명을 조건부로 토글하는 것보다 훨씬 직관적입니다.

마지막으로, 중첩된 엘리먼트에도 각각 독립적인 css prop을 적용할 수 있습니다. span 엘리먼트의 css prop은 해당 span에만 영향을 미치며, 부모의 스타일과 충돌하지 않습니다.

Emotion은 각 css prop마다 고유한 클래스명을 생성하여 격리된 스타일을 보장합니다. 여러분이 이 코드를 사용하면 컴포넌트 로직과 스타일을 한눈에 파악할 수 있어 유지보수가 쉬워집니다.

특히 조건부 UI에서 조건부 스타일까지 한 곳에서 관리할 수 있어 코드의 응집도가 높아집니다. 실무에서는 페이지별로 독특한 레이아웃이 필요할 때, 또는 A/B 테스트로 스타일을 빠르게 변경해야 할 때 css prop이 큰 힘을 발휘합니다.

재사용이 필요 없는 일회성 스타일에 적합하며, 프로토타이핑 단계에서 특히 생산성이 높습니다.

실전 팁

💡 css prop과 styled 컴포넌트를 혼용할 때는 일관된 규칙을 정하세요. 예를 들어 "3번 이상 재사용되면 styled 컴포넌트로 분리"같은 기준이 좋습니다.

💡 객체 문법도 지원하므로 css={{color: 'red', '&:hover': {color: 'blue'}}} 형태로도 작성 가능합니다. 동적 값이 많으면 객체가 더 편할 수 있습니다.

💡 성능을 위해 css 함수 호출을 컴포넌트 외부로 빼거나 useMemo로 감싸세요. 매 렌더마다 새로운 css 객체가 생성되는 것을 방지합니다.

💡 css prop에 배열을 전달하면 여러 스타일을 합성할 수 있습니다. css={[baseStyle, conditionalStyle]} 형태로 스타일을 조합하세요.

💡 TypeScript 사용 시 tsconfig.json에서 "jsxImportSource": "@emotion/react"를 설정하면 매번 주석을 달지 않아도 됩니다.


3. ThemeProvider - 일관된 디자인 시스템 구축

시작하며

여러분이 프로젝트 전체에서 색상, 폰트, 간격 같은 디자인 토큰을 관리할 때, 하드코딩된 값들이 여기저기 흩어져 있어서 디자인 변경이 악몽처럼 느껴진 적 있나요? 예를 들어 주요 브랜드 색상을 바꾸려면 수백 개 파일을 일일이 수정해야 하는 상황 말이죠.

이런 문제는 중대형 프로젝트에서 반드시 발생합니다. 디자이너가 스타일 가이드를 업데이트해도 개발자는 어디를 어떻게 고쳐야 할지 파악하기 어렵습니다.

또한 다크 모드 같은 테마 전환 기능을 구현하려면 모든 컴포넌트를 수정해야 하는 고통이 따릅니다. 바로 이럴 때 필요한 것이 Emotion의 ThemeProvider입니다.

전역 디자인 변수를 한 곳에서 관리하고, 모든 컴포넌트에서 일관되게 사용할 수 있게 해줍니다.

개요

간단히 말해서, ThemeProvider는 React Context API를 활용하여 디자인 토큰(색상, 타이포그래피, 간격 등)을 앱 전체에 주입하는 컴포넌트입니다. ThemeProvider를 사용하면 하드코딩 대신 의미 있는 이름으로 스타일 값을 참조할 수 있습니다.

예를 들어 #007bff 대신 theme.colors.primary를 사용하면, 나중에 브랜드 색상이 바뀌어도 theme 객체만 수정하면 됩니다. 다크 모드 구현, A/B 테스트, 화이트라벨 제품 개발 등에서 필수적입니다.

기존에는 CSS 변수나 SCSS 변수를 사용했다면, 이제는 JavaScript 객체로 타입 안전하게 관리하면서 런타임에도 동적으로 변경할 수 있습니다. ThemeProvider의 핵심 특징은 세 가지입니다.

첫째, 중첩 가능하여 특정 영역에만 다른 테마를 적용할 수 있습니다. 둘째, TypeScript와 완벽하게 통합되어 자동완성을 제공합니다.

셋째, 테마 전환이 즉각 반영되어 사용자 경험이 매끄럽습니다. 이러한 특징들이 확장 가능한 디자인 시스템의 기반이 됩니다.

코드 예제

import { ThemeProvider } from '@emotion/react';
import styled from '@emotion/styled';

// 테마 객체 정의
const lightTheme = {
  colors: {
    primary: '#007bff',
    background: '#ffffff',
    text: '#333333',
    error: '#dc3545'
  },
  spacing: {
    small: '8px',
    medium: '16px',
    large: '24px'
  },
  typography: {
    fontSize: {
      small: '12px',
      medium: '16px',
      large: '20px'
    }
  }
};

// 테마를 사용하는 컴포넌트
const ThemedButton = styled.button`
  background-color: ${props => props.theme.colors.primary};
  color: ${props => props.theme.colors.background};
  padding: ${props => props.theme.spacing.medium};
  font-size: ${props => props.theme.typography.fontSize.medium};
  border: none;
  border-radius: 4px;
  cursor: pointer;

  &:hover {
    opacity: 0.8;
  }
`;

function App() {
  return (
    <ThemeProvider theme={lightTheme}>
      <ThemedButton>클릭하세요</ThemedButton>
    </ThemeProvider>
  );
}

설명

이것이 하는 일: ThemeProvider는 theme prop으로 받은 객체를 React Context에 저장하고, 하위 모든 컴포넌트에서 props.theme으로 접근할 수 있게 만듭니다. 첫 번째로, theme 객체는 여러분의 디자인 시스템을 표현하는 단일 진실 공급원(Single Source of Truth)입니다.

colors, spacing, typography 같은 카테고리로 체계적으로 구조화하면 나중에 찾기 쉽고 확장하기도 편합니다. 이 객체는 일반 JavaScript 객체이므로 조건문이나 함수로 동적으로 생성할 수도 있습니다.

그 다음으로, styled 컴포넌트 안에서 props.theme으로 테마 값에 접근합니다. Emotion이 자동으로 ThemeProvider의 theme을 모든 styled 컴포넌트의 props에 주입하기 때문에 별도의 import나 설정이 필요 없습니다.

이를 통해 theme.colors.primary 같은 명확한 이름으로 값을 참조할 수 있어 코드 가독성이 크게 향상됩니다. 마지막으로, 테마를 전환하려면 ThemeProvider의 theme prop만 바꾸면 됩니다.

예를 들어 다크 모드 버튼을 클릭하면 setTheme(darkTheme)처럼 state를 업데이트하고, 이 state를 ThemeProvider에 전달하면 앱 전체가 즉시 다크 모드로 전환됩니다. 리렌더링이 자동으로 일어나기 때문에 별도의 새로고침이 필요 없습니다.

여러분이 이 코드를 사용하면 디자인 일관성을 쉽게 유지할 수 있습니다. 새로운 개발자가 팀에 합류해도 theme 객체만 보면 어떤 색상과 간격을 사용해야 하는지 바로 알 수 있습니다.

실무에서는 theme 객체를 별도 파일로 분리하고, TypeScript 인터페이스로 타입을 정의하여 자동완성을 활용하는 것이 일반적입니다. 또한 localStorage에 사용자의 테마 선택을 저장하여 다음 방문 시에도 유지되게 하는 패턴이 많이 쓰입니다.

실전 팁

💡 TypeScript 사용 시 declare module '@emotion/react' { export interface Theme { ... } }로 theme 타입을 확장하면 자동완성과 타입 체크를 받을 수 있습니다.

💡 ThemeProvider는 중첩 가능하므로, 특정 섹션에만 다른 테마를 적용하려면 해당 영역을 별도의 ThemeProvider로 감싸세요.

💡 useTheme 훅을 사용하면 함수형 컴포넌트에서도 theme 객체에 직접 접근할 수 있습니다. const theme = useTheme(); 형태로 사용하세요.

💡 theme 객체에 함수를 넣어서 계산된 값을 반환할 수도 있습니다. 예: spacing: (multiplier) => \${8 * multiplier}px``

💡 환경변수나 feature flag와 연동하여 실험적 테마를 특정 사용자에게만 보여주는 패턴도 유용합니다.


4. Global Styles - 전역 스타일 정의하기

시작하며

여러분이 앱을 만들 때 모든 페이지에 공통으로 적용되어야 하는 기본 스타일이 필요한데, 어디에 어떻게 작성해야 할지 고민한 적 있나요? 예를 들어 body의 margin 제거, 폰트 설정, CSS reset 같은 것들은 컴포넌트별로 작성하기에는 어색합니다.

이런 문제는 프로젝트 초기 세팅에서 반드시 마주치게 됩니다. 기존에는 별도의 global.css 파일을 만들어서 import 했지만, CSS-in-JS 환경에서는 이것이 일관성을 해칩니다.

또한 조건부로 전역 스타일을 변경해야 할 때(예: 다크 모드) 별도 CSS 파일로는 한계가 있습니다. 바로 이럴 때 필요한 것이 Emotion의 Global 컴포넌트입니다.

JavaScript 안에서 전역 스타일을 선언하고, 테마나 props에 따라 동적으로 제어할 수 있게 해줍니다.

개요

간단히 말해서, Global 컴포넌트는 특정 엘리먼트에 바인딩되지 않고 문서 전체에 적용되는 스타일을 정의하는 방법입니다. Global 스타일을 사용하면 CSS reset, 기본 폰트, 전역 변수 등을 CSS-in-JS 방식으로 관리할 수 있습니다.

ThemeProvider와 함께 사용하면 테마에 따라 전역 스타일도 자동으로 변경되어 매우 강력합니다. 예를 들어, 다크 모드일 때 body의 배경색을 검은색으로 바꾸는 것이 단 몇 줄로 가능합니다.

기존에는 별도의 CSS 파일을 import하거나 index.html에 직접 style 태그를 넣었다면, 이제는 React 컴포넌트처럼 Global을 선언하여 조건부로 렌더링할 수 있습니다. Global 컴포넌트의 핵심 특징은 세 가지입니다.

첫째, React 컴포넌트 트리 어디에나 배치할 수 있어 유연합니다. 둘째, props와 theme에 접근 가능하여 동적 전역 스타일이 가능합니다.

셋째, 여러 Global 컴포넌트를 조합하여 스타일을 모듈화할 수 있습니다. 이러한 특징들이 복잡한 전역 스타일 관리를 단순화시킵니다.

코드 예제

import { Global, css, useTheme } from '@emotion/react';

function GlobalStyles() {
  const theme = useTheme();

  return (
    <Global
      styles={css`
        /* CSS Reset */
        * {
          margin: 0;
          padding: 0;
          box-sizing: border-box;
        }

        /* 기본 폰트  배경 설정 */
        body {
          font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
          background-color: ${theme.colors.background};
          color: ${theme.colors.text};
          line-height: 1.6;
        }

        /* 링크 기본 스타일 */
        a {
          color: ${theme.colors.primary};
          text-decoration: none;

          &:hover {
            text-decoration: underline;
          }
        }

        /* 스크롤바 커스터마이징 (webkit) */
        ::-webkit-scrollbar {
          width: 8px;
        }

        ::-webkit-scrollbar-thumb {
          background: ${theme.colors.primary};
          border-radius: 4px;
        }
      `}
    />
  );
}

// App.tsx에서 사용
// <ThemeProvider theme={theme}>
//   <GlobalStyles />
//   <YourApp />
// </ThemeProvider>

설명

이것이 하는 일: Global 컴포넌트는 styles prop으로 받은 CSS를 문서의 head에 style 태그로 주입하여 전역에 적용합니다. 첫 번째로, Global 컴포넌트는 실제 DOM 엘리먼트를 렌더링하지 않습니다.

대신 React의 라이프사이클을 활용하여 마운트될 때 전역 스타일을 주입하고, 언마운트될 때 제거합니다. 따라서 조건부로 렌더링하면 해당 전역 스타일도 조건부로 적용되는 강력한 패턴을 만들 수 있습니다.

그 다음으로, useTheme 훅으로 현재 테마를 가져와서 전역 스타일에서도 테마 값을 사용합니다. 예를 들어 body의 background-color를 theme.colors.background로 설정하면, 라이트/다크 테마 전환 시 배경색이 자동으로 바뀝니다.

이는 별도의 CSS 파일로는 불가능한 동적 기능입니다. 마지막으로, 여러 선택자를 자유롭게 사용할 수 있습니다.

*, body, a 같은 태그 선택자뿐만 아니라 ::-webkit-scrollbar 같은 의사 요소도 스타일링할 수 있습니다. Emotion이 자동으로 벤더 프리픽스를 처리하므로 브라우저 호환성 걱정 없이 최신 CSS를 사용할 수 있습니다.

여러분이 이 코드를 사용하면 프로젝트의 기본 스타일을 한눈에 파악하고 관리할 수 있습니다. 새로운 팀원이 합류해도 GlobalStyles 컴포넌트만 보면 어떤 전역 설정이 적용되어 있는지 즉시 알 수 있습니다.

실무에서는 normalize.css나 reset.css의 내용을 Global 컴포넌트로 옮겨서 일관된 CSS-in-JS 환경을 유지하는 것이 좋습니다. 또한 프린트 스타일 같은 특수한 용도의 전역 스타일은 별도의 Global 컴포넌트로 분리하여 필요할 때만 렌더링하는 패턴도 유용합니다.

실전 팁

💡 여러 Global 컴포넌트를 사용할 때는 나중에 선언된 것이 우선순위가 높습니다. 베이스 스타일과 테마별 스타일을 분리하여 순서대로 배치하세요.

💡 개발 모드에서만 적용할 스타일(디버깅용 아웃라인 등)은 조건부 렌더링으로 처리하세요. {process.env.NODE_ENV === 'development' && <DebugGlobalStyles />}

💡 폰트 로딩을 최적화하려면 font-display: swap을 사용하고, @font-face도 Global 스타일에서 정의할 수 있습니다.

💡 CSS 변수도 Global 스타일에서 정의 가능합니다. --custom-property: ${theme.value} 형태로 작성하면 외부 라이브러리와의 통합이 쉬워집니다.

💡 SSR(Server-Side Rendering) 환경에서는 Global 스타일이 서버에서 먼저 주입되므로, FOUC(Flash of Unstyled Content)를 방지할 수 있습니다.


5. keyframes - 커스텀 애니메이션 만들기

시작하며

여러분이 웹 페이지에 생동감을 더하기 위해 애니메이션을 추가하려고 할 때, 별도의 CSS 파일에 @keyframes를 정의하고 클래스명으로 연결하는 과정이 번거롭게 느껴진 적 있나요? 특히 애니메이션 파라미터를 동적으로 조절하고 싶을 때는 인라인 스타일로도 해결이 안 됩니다.

이런 문제는 인터랙티브한 UI를 만들 때 자주 발생합니다. 로딩 스피너, 페이드 인/아웃, 슬라이드 효과 등을 구현하려면 CSS 애니메이션이 필수인데, 컴포넌트와 애니메이션이 분리되어 있으면 재사용성이 떨어지고 관리가 어렵습니다.

바로 이럴 때 필요한 것이 Emotion의 keyframes 함수입니다. JavaScript 안에서 애니메이션을 정의하고, props나 theme 값으로 동적 애니메이션을 만들 수 있게 해줍니다.

개요

간단히 말해서, keyframes는 CSS @keyframes 규칙을 JavaScript로 정의할 수 있게 해주는 함수로, 생성된 애니메이션 이름을 styled 컴포넌트나 css prop에서 사용할 수 있습니다. keyframes를 사용하면 애니메이션을 컴포넌트 파일 안에서 선언하고 즉시 적용할 수 있어 개발 흐름이 매끄럽습니다.

또한 고유한 애니메이션 이름이 자동 생성되어 전역 네임스페이스 오염을 방지합니다. 예를 들어, 여러 컴포넌트에서 각각 "fadeIn"이라는 애니메이션을 정의해도 충돌하지 않습니다.

기존에는 CSS 파일에 @keyframes fadeIn { ... }을 작성하고 animation: fadeIn 1s로 참조했다면, 이제는 JavaScript 상수로 애니메이션을 정의하고 템플릿 리터럴에 삽입할 수 있습니다.

keyframes의 핵심 특징은 세 가지입니다. 첫째, 동적 값을 사용하여 파라미터화된 애니메이션을 만들 수 있습니다.

둘째, TypeScript와 함께 사용하면 애니메이션 이름도 타입 안전합니다. 셋째, 코드 스플리팅 시 해당 컴포넌트와 함께 번들되어 불필요한 애니메이션이 로드되지 않습니다.

이러한 특징들이 성능과 유지보수성을 동시에 향상시킵니다.

코드 예제

import styled from '@emotion/styled';
import { keyframes } from '@emotion/react';

// 페이드인 + 슬라이드업 애니메이션 정의
const fadeInUp = keyframes`
  from {
    opacity: 0;
    transform: translateY(30px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
`;

// 회전 애니메이션 (무한 반복)
const spin = keyframes`
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
`;

// 애니메이션을 사용하는 컴포넌트
const AnimatedCard = styled.div`
  animation: ${fadeInUp} 0.6s ease-out;
  padding: 20px;
  background: white;
  border-radius: 8px;
`;

const LoadingSpinner = styled.div`
  width: 40px;
  height: 40px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #3498db;
  border-radius: 50%;
  animation: ${spin} 1s linear infinite;
`;

// 사용 예시
// <AnimatedCard>내용이 부드럽게 나타납니다</AnimatedCard>
// <LoadingSpinner />

설명

이것이 하는 일: keyframes 함수는 템플릿 리터럴로 작성된 애니메이션 단계를 받아서 고유한 애니메이션 이름을 생성하고, CSS @keyframes 규칙을 스타일시트에 주입합니다. 첫 번째로, keyframes 함수는 실행될 때마다 고유한 애니메이션 이름을 반환합니다.

이 이름은 해시값으로 생성되므로 서로 다른 파일에서 같은 변수명을 사용해도 실제 CSS 애니메이션 이름은 다릅니다. 따라서 모듈화된 컴포넌트 개발에 매우 적합합니다.

그 다음으로, 정의된 keyframes를 styled 컴포넌트의 animation 속성에 템플릿 리터럴로 삽입합니다. animation: ${fadeInUp} 0.6s처럼 사용하면 Emotion이 자동으로 올바른 애니메이션 이름으로 변환합니다.

duration, timing-function, iteration-count 같은 다른 애니메이션 속성들은 평범한 CSS 문법으로 추가하면 됩니다. 마지막으로, 여러 애니메이션을 조합할 수도 있습니다.

animation: ${fadeIn} 0.3s, ${slideUp} 0.3s 형태로 쉼표로 구분하여 동시에 여러 애니메이션을 적용할 수 있습니다. 또한 keyframes 내부에서도 JavaScript 표현식을 사용할 수 있어 props나 theme 값으로 동적 애니메이션을 만드는 것이 가능합니다.

여러분이 이 코드를 사용하면 페이지 전환, 모달 등장, 알림 표시 같은 다양한 인터랙션을 부드럽게 구현할 수 있습니다. 특히 로딩 스피너는 거의 모든 프로젝트에서 필요하므로, 한 번 정의해두면 재사용성이 매우 높습니다.

실무에서는 공통 애니메이션들을 별도의 animations.js 파일로 분리하여 프로젝트 전체에서 import하여 사용하는 패턴이 일반적입니다. 또한 애니메이션 duration이나 easing을 theme 객체에 정의하여 일관성을 유지하는 것도 좋은 방법입니다.

실전 팁

💡 애니메이션 성능을 위해 transform과 opacity 속성만 사용하세요. width, height, left, top 같은 속성은 레이아웃을 다시 계산하므로 느립니다.

💡 will-change 속성을 추가하면 브라우저가 미리 최적화할 수 있습니다. will-change: transform, opacity; 단, 너무 많이 사용하면 역효과가 날 수 있으니 주의하세요.

💡 접근성을 고려하여 prefers-reduced-motion 미디어 쿼리를 체크하세요. 사용자가 애니메이션 감소를 설정했다면 애니메이션을 비활성화하는 것이 좋습니다.

💡 복잡한 애니메이션은 여러 keyframes로 나누고 animation-delay로 순차 실행하면 더 정교하게 제어할 수 있습니다.

💡 개발자 도구의 Performance 탭에서 애니메이션이 60fps로 실행되는지 확인하세요. 프레임 드롭이 발생하면 GPU 가속을 활용할 수 있도록 최적화가 필요합니다.


6. 컴포넌트 합성 - 기존 컴포넌트 확장하기

시작하며

여러분이 이미 만들어진 버튼 컴포넌트를 재사용하고 싶은데, 특정 페이지에서만 약간 다른 스타일이 필요한 상황을 겪어본 적 있나요? 완전히 새로운 컴포넌트를 만들자니 중복 코드가 생기고, 기존 컴포넌트를 수정하자니 다른 곳에 영향을 줄까 걱정됩니다.

이런 문제는 디자인 시스템을 구축할 때 필연적으로 발생합니다. 기본 버튼, 아이콘 버튼, 텍스트 버튼 등 여러 변형이 필요한데, 각각을 독립적으로 만들면 공통 로직을 공유할 수 없습니다.

상속보다는 합성이 권장되는 React에서 스타일도 합성할 방법이 필요합니다. 바로 이럴 때 필요한 것이 Emotion의 컴포넌트 합성 기능입니다.

기존 styled 컴포넌트를 감싸서 스타일을 추가하거나 덮어쓸 수 있게 해줍니다.

개요

간단히 말해서, 컴포넌트 합성은 styled 함수에 기존 컴포넌트를 전달하여 새로운 스타일 레이어를 추가하는 방식입니다. 이 기능을 사용하면 기존 컴포넌트의 모든 props와 로직은 유지하면서 스타일만 확장할 수 있습니다.

DRY(Don't Repeat Yourself) 원칙을 지키면서도 유연한 변형을 만들 수 있어 디자인 시스템에서 특히 유용합니다. 예를 들어, 기본 Button을 만들고 PrimaryButton, DangerButton 등으로 확장하는 패턴이 가능합니다.

기존에는 className prop을 추가로 받아서 병합하는 복잡한 로직을 작성했다면, 이제는 단순히 styled(ExistingComponent) 형태로 감싸기만 하면 됩니다. 컴포넌트 합성의 핵심 특징은 세 가지입니다.

첫째, 원본 컴포넌트를 수정하지 않아 사이드 이펙트가 없습니다. 둘째, 기존 스타일을 선택적으로 덮어쓸 수 있어 CSS 우선순위 관리가 쉽습니다.

셋째, 여러 단계로 중첩하여 스타일 계층을 만들 수 있습니다. 이러한 특징들이 확장 가능하고 유지보수하기 쉬운 컴포넌트 라이브러리를 만들 수 있게 합니다.

코드 예제

import styled from '@emotion/styled';

// 기본 버튼 컴포넌트
const Button = styled.button`
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  font-size: 14px;
  cursor: pointer;
  transition: all 0.2s;

  &:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
`;

// 기본 버튼을 확장한 Primary 버튼
const PrimaryButton = styled(Button)`
  background-color: #007bff;
  color: white;

  &:hover:not(:disabled) {
    background-color: #0056b3;
    box-shadow: 0 4px 8px rgba(0,123,255,0.3);
  }
`;

// Primary 버튼을 한 번 더 확장한 Large Primary 버튼
const LargePrimaryButton = styled(PrimaryButton)`
  padding: 16px 32px;
  font-size: 18px;
  font-weight: bold;
`;

// 기존 컴포넌트에 스타일 추가
const StyledLink = styled.a`
  color: #007bff;
  text-decoration: none;

  &:hover {
    text-decoration: underline;
  }
`;

// 외부 라이브러리 컴포넌트도 확장 가능
// const StyledReactRouterLink = styled(Link)`
//   color: ${props => props.theme.colors.primary};
// `;

설명

이것이 하는 일: styled 함수는 HTML 태그뿐만 아니라 React 컴포넌트도 받을 수 있으며, 해당 컴포넌트에 새로운 스타일 레이어를 추가한 새 컴포넌트를 반환합니다. 첫 번째로, 기본 Button 컴포넌트는 모든 버튼에 공통으로 적용될 최소한의 스타일만 정의합니다.

padding, border-radius, transition 같은 기본 속성들은 여기서 한 번만 정의하면 모든 확장 컴포넌트가 자동으로 상속받습니다. 이를 통해 일관된 기본 동작을 보장하면서도 코드 중복을 제거할 수 있습니다.

그 다음으로, styled(Button)처럼 기존 컴포넌트를 감싸면 원본의 모든 스타일이 유지된 채로 새로운 스타일이 추가됩니다. CSS 특성상 나중에 정의된 스타일이 우선순위가 높으므로, PrimaryButton에서 정의한 background-color는 Button의 기본 배경색을 덮어씁니다.

하지만 padding 같이 PrimaryButton에서 재정의하지 않은 속성은 Button의 값을 그대로 사용합니다. 마지막으로, 합성은 여러 단계로 중첩 가능합니다.

LargePrimaryButton은 PrimaryButton을 확장하고, PrimaryButton은 Button을 확장하는 3단계 구조를 가집니다. 이렇게 하면 각 레벨에서 필요한 스타일만 추가하여 계층적인 컴포넌트 시스템을 만들 수 있습니다.

여러분이 이 코드를 사용하면 버튼 하나를 수정할 때 관련된 모든 변형이 자동으로 업데이트됩니다. 예를 들어 Button의 border-radius를 8px로 바꾸면 PrimaryButton, LargePrimaryButton도 모두 자동으로 반영됩니다.

실무에서는 atomic design 패턴과 결합하여 atoms(Button) → molecules(PrimaryButton) → organisms(SubmitButton) 형태로 점진적으로 확장하는 것이 일반적입니다. 또한 React Router의 Link나 외부 UI 라이브러리 컴포넌트에 스타일을 입힐 때도 이 패턴이 유용합니다.

실전 팁

💡 합성된 컴포넌트는 원본의 모든 props를 받으므로, TypeScript 사용 시 타입이 자동으로 전달됩니다. 별도의 타입 정의가 필요 없습니다.

💡 외부 라이브러리 컴포넌트를 감쌀 때는 해당 컴포넌트가 className prop을 지원하는지 확인하세요. 지원하지 않으면 스타일이 적용되지 않습니다.

💡 너무 깊은 중첩은 피하세요. 3단계 이상 확장하면 스타일 우선순위를 추적하기 어려워집니다. 대신 합성보다는 props로 변형을 제어하는 것을 고려하세요.

💡 기존 스타일을 완전히 덮어쓰려면 속성 이름을 동일하게 사용하세요. 새로운 속성을 추가하려면 다른 이름을 사용하면 됩니다.

💡 as prop을 사용하면 컴포넌트의 렌더링 태그를 바꿀 수 있습니다. <PrimaryButton as="a" href="..."> 형태로 버튼 스타일의 링크를 만들 수 있습니다.


7. 객체 스타일 - JavaScript 객체로 스타일 작성

시작하며

여러분이 템플릿 리터럴보다 JavaScript 객체 문법이 더 익숙하거나, 동적 스타일 값을 계산할 때 객체가 더 편하다고 느낀 적 있나요? 특히 조건부 스타일을 여러 개 합성해야 할 때, 객체 스프레드 문법을 활용하면 더 깔끔한 코드를 작성할 수 있습니다.

이런 문제는 복잡한 조건부 스타일링이 필요한 상황에서 자주 발생합니다. 템플릿 리터럴에서 조건문을 중첩하면 가독성이 떨어지고, 여러 스타일 객체를 병합하는 로직이 복잡해질 수 있습니다.

또한 일부 개발자는 CSS 문법보다 JavaScript 객체가 더 직관적이라고 느낍니다. 바로 이럴 때 필요한 것이 Emotion의 객체 스타일 문법입니다.

CSS 속성을 camelCase JavaScript 객체로 표현하여, JavaScript의 모든 기능을 자연스럽게 활용할 수 있게 해줍니다.

개요

간단히 말해서, 객체 스타일은 CSS를 JavaScript 객체 형태로 작성하는 방식으로, styled 컴포넌트와 css prop 모두에서 템플릿 리터럴 대신 사용할 수 있습니다. 객체 스타일을 사용하면 조건부 스타일 병합, 계산된 값, 스타일 함수 재사용 등이 매우 편리해집니다.

CSS 속성명은 camelCase로 변환되고(background-color → backgroundColor), 값은 문자열 또는 숫자로 지정합니다. 예를 들어, 여러 조건에 따라 다른 스타일 객체를 스프레드하여 합성하는 패턴이 가능합니다.

기존에는 템플릿 리터럴에서 ${condition ? 'color: red;' : 'color: blue;'} 형태로 작성했다면, 이제는 ...condition && { color: 'red' } 같은 JavaScript 관용구를 그대로 사용할 수 있습니다.

객체 스타일의 핵심 특징은 세 가지입니다. 첫째, JavaScript의 스프레드, 구조 분해, 조건 연산자 등을 자유롭게 활용할 수 있습니다.

둘째, 타입 체크와 자동완성이 더 정확합니다. 셋째, 런타임 성능이 약간 더 좋을 수 있습니다(문자열 파싱이 불필요).

이러한 특징들이 JavaScript 중심 개발 경험을 제공합니다.

코드 예제

import styled from '@emotion/styled';
import { css } from '@emotion/react';

// 재사용 가능한 스타일 객체 정의
const baseButtonStyle = {
  padding: '10px 20px',
  border: 'none',
  borderRadius: '4px',
  cursor: 'pointer',
  fontSize: 14,  // 단위 생략 시 px로 자동 변환
  transition: 'all 0.2s',
};

const primaryStyle = {
  backgroundColor: '#007bff',
  color: 'white',
  '&:hover': {
    backgroundColor: '#0056b3',
  },
};

// 객체 스타일을 사용하는 styled 컴포넌트
const Button = styled.button(props => ({
  ...baseButtonStyle,
  ...(props.primary && primaryStyle),
  ...(props.large && {
    padding: '16px 32px',
    fontSize: 18,
  }),
  ...(props.disabled && {
    opacity: 0.5,
    cursor: 'not-allowed',
  }),
}));

// css prop에서 객체 스타일 사용
function Card({ highlighted }) {
  return (
    <div css={{
      padding: 20,
      borderRadius: 8,
      backgroundColor: 'white',
      boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
      ...(highlighted && {
        borderLeft: '4px solid #007bff',
        backgroundColor: '#f0f8ff',
      }),
    }}>
      카드 내용
    </div>
  );
}

설명

이것이 하는 일: 객체 스타일은 CSS 속성을 JavaScript 객체의 키-값 쌍으로 표현하며, Emotion이 이를 실제 CSS로 변환하여 적용합니다. 첫 번째로, CSS 속성명을 camelCase로 변환하는 것이 핵심입니다.

background-colorbackgroundColor, font-sizefontSize가 됩니다. 값은 대부분 문자열로 작성하지만, 숫자만 지정하면 px 단위가 자동으로 붙습니다.

따라서 fontSize: 14font-size: 14px로 변환됩니다. 그 다음으로, 스프레드 연산자를 활용한 조건부 스타일 병합이 매우 강력합니다.

...(props.primary && primaryStyle) 패턴은 props.primary가 true일 때만 primaryStyle 객체를 펼쳐서 병합합니다. 이는 템플릿 리터럴의 조건문보다 훨씬 간결하고 읽기 쉽습니다.

마지막으로, 의사 클래스와 의사 요소도 문자열 키로 표현합니다. '&:hover', '&::before' 형태로 작성하며, 값은 중첩된 객체가 됩니다.

미디어 쿼리도 동일하게 '@media (max-width: 768px)' 키를 사용하여 반응형 스타일을 정의할 수 있습니다. 여러분이 이 코드를 사용하면 여러 스타일 조건을 명확하게 분리하고 조합할 수 있습니다.

특히 디자인 토큰을 객체로 관리하는 프로젝트에서는 객체 스타일이 자연스럽게 어울립니다. 실무에서는 공통 스타일을 별도 객체로 추출하여 여러 컴포넌트에서 재사용하는 패턴이 일반적입니다.

또한 theme 객체를 구조 분해하여 const { colors, spacing } = props.theme 형태로 사용하면 코드가 더 간결해집니다.

실전 팁

💡 배열을 사용하여 여러 스타일 객체를 합성할 수 있습니다. css={[baseStyle, conditionalStyle, anotherStyle]} 형태로 순서대로 병합됩니다.

💡 함수를 반환하는 패턴으로 재사용 가능한 스타일 유틸리티를 만들 수 있습니다. const flexCenter = () => ({ display: 'flex', justifyContent: 'center', alignItems: 'center' })

💡 TypeScript를 사용하면 CSSObject 타입을 import하여 타입 안정성을 확보할 수 있습니다. import { CSSObject } from '@emotion/react'

💡 중첩 선택자에서 부모를 참조할 때는 & 기호를 사용하세요. '& > li' 형태로 자식 선택자를 표현할 수 있습니다.

💡 객체 스타일은 번들 크기가 약간 작을 수 있습니다. 템플릿 리터럴은 문자열로 저장되지만, 객체는 더 효율적으로 압축되기 때문입니다.


8. SSR과 hydration - 서버 사이드 렌더링 대응

시작하며

여러분이 Next.js 같은 SSR 프레임워크를 사용할 때, 페이지가 로드되는 순간 스타일이 깜빡이거나 잠깐 사라졌다가 다시 나타나는 FOUC(Flash of Unstyled Content)를 경험한 적 있나요? 이는 서버에서 생성된 HTML과 클라이언트에서 적용되는 스타일이 동기화되지 않아 발생합니다.

이런 문제는 CSS-in-JS 라이브러리를 SSR 환경에서 사용할 때 자주 발생합니다. 서버에서 렌더링된 HTML에는 스타일이 포함되어야 하는데, Emotion이 생성하는 동적 스타일을 올바르게 추출하지 못하면 초기 렌더링이 깨집니다.

또한 클라이언트에서 hydration할 때 스타일 충돌이 일어날 수도 있습니다. 바로 이럴 때 필요한 것이 Emotion의 SSR 유틸리티입니다.

서버에서 생성된 스타일을 추출하고, 클라이언트에서 올바르게 hydration하여 완벽한 SSR 경험을 제공합니다.

개요

간단히 말해서, Emotion의 SSR 지원은 서버에서 렌더링 시 생성된 모든 스타일을 수집하여 HTML에 주입하고, 클라이언트에서는 이를 재사용하여 불필요한 재계산을 방지하는 메커니즘입니다. SSR 설정을 올바르게 하면 초기 페이지 로드 속도가 빨라지고, SEO가 향상되며, FOUC가 완전히 사라집니다.

Next.js 같은 프레임워크는 Emotion과의 통합이 이미 준비되어 있어 간단한 설정만으로 작동합니다. 예를 들어, _document.js에서 스타일을 추출하는 코드 몇 줄이면 충분합니다.

기존에는 별도의 CSS 파일을 link 태그로 로드하여 SSR을 처리했다면, 이제는 JavaScript 번들에 포함된 스타일을 서버에서 미리 렌더링하여 HTML에 인라인으로 삽입할 수 있습니다. Emotion SSR의 핵심 특징은 세 가지입니다.

첫째, createEmotionCache와 extractCritical을 통해 서버 스타일을 추출합니다. 둘째, 클라이언트에서 동일한 캐시를 사용하여 중복 계산을 방지합니다.

셋째, Next.js 같은 프레임워크와의 공식 통합이 제공됩니다. 이러한 특징들이 프로덕션급 SSR 애플리케이션을 가능하게 합니다.

코드 예제

// Next.js _document.tsx 예시
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { cache } from '@emotion/css';
import { extractCritical } from '@emotion/server';

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const originalRenderPage = ctx.renderPage;

    // 페이지 렌더링 중 생성된 스타일 수집
    ctx.renderPage = () =>
      originalRenderPage({
        enhanceApp: (App) => (props) => <App {...props} />,
      });

    const initialProps = await Document.getInitialProps(ctx);
    const { html, css, ids } = extractCritical(initialProps.html);

    return {
      ...initialProps,
      html,
      styles: (
        <>
          {initialProps.styles}
          <style
            data-emotion={`css ${ids.join(' ')}`}
            dangerouslySetInnerHTML={{ __html: css }}
          />
        </>
      ),
    };
  }

  render() {
    return (
      <Html>
        <Head>{this.props.styles}</Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

// _app.tsx에서 캐시 설정
import { CacheProvider } from '@emotion/react';
import { cache } from '@emotion/css';

function MyApp({ Component, pageProps }) {
  return (
    <CacheProvider value={cache}>
      <Component {...pageProps} />
    </CacheProvider>
  );
}

설명

이것이 하는 일: extractCritical 함수는 서버에서 렌더링된 HTML을 분석하여 필요한 모든 Emotion 스타일을 CSS 문자열로 추출하고, 이를 HTML head에 주입합니다. 첫 번째로, Next.js의 getInitialProps에서 페이지를 렌더링하면 Emotion이 사용된 모든 스타일이 메모리에 수집됩니다.

extractCritical 함수는 이 수집된 스타일을 분석하여 실제로 사용된 것만 골라냅니다. 이를 "critical CSS"라고 부르며, 초기 렌더링에 필수적인 스타일만 포함하여 번들 크기를 최소화합니다.

그 다음으로, 추출된 CSS를 style 태그로 만들어 Head에 삽입합니다. 중요한 것은 data-emotion 속성입니다.

이 속성에는 스타일 ID들이 포함되어 있어, 클라이언트에서 hydration할 때 Emotion이 "이 스타일은 이미 서버에서 렌더링되었으니 다시 생성할 필요 없다"고 인식합니다. 마지막으로, 클라이언트 측에서는 CacheProvider로 동일한 캐시를 사용하여 서버와 클라이언트의 스타일 생성 로직을 일치시킵니다.

이렇게 하면 hydration 시 React의 "서버와 클라이언트 HTML이 다릅니다" 경고가 발생하지 않으며, 불필요한 스타일 재계산도 방지됩니다. 여러분이 이 코드를 사용하면 사용자가 페이지를 열었을 때 즉시 완전히 스타일링된 콘텐츠를 볼 수 있습니다.

JavaScript가 로드되기 전에도 디자인이 완벽하게 표시되므로 사용자 경험이 크게 향상됩니다. 실무에서는 이 설정을 프로젝트 초기에 한 번 구성해두면 이후 추가 작업 없이 모든 컴포넌트가 자동으로 SSR 혜택을 받습니다.

Next.js 13의 App Router를 사용한다면 설정 방식이 약간 다르므로 공식 문서를 참고하세요.

실전 팁

💡 Next.js 13+ App Router에서는 별도의 emotion-cache.ts 파일을 만들고 'use client' 지시어와 함께 사용해야 합니다.

💡 개발 모드에서는 SSR 설정이 제대로 작동하는지 확인하기 위해 "네트워크 탭 → Disable cache → 새로고침"으로 테스트하세요.

💡 styled-components에서 마이그레이션하는 경우, babel-plugin-emotion을 설정하면 자동 최적화를 받을 수 있습니다.

💡 성능 모니터링을 위해 extractCritical이 반환하는 css 문자열의 크기를 로깅하여 critical CSS가 과도하게 크지 않은지 확인하세요.

💡 서버리스 환경(Vercel, AWS Lambda)에서는 캐시를 요청마다 새로 생성해야 합니다. 전역 캐시를 재사용하면 메모리 누수가 발생할 수 있습니다.


9. 미디어 쿼리와 반응형 디자인 - 모바일부터 데스크톱까지

시작하며

여러분이 모바일, 태블릿, 데스크톱에서 각각 다른 레이아웃을 보여줘야 할 때, 미디어 쿼리를 여러 번 반복해서 작성하느라 지친 적 있나요? 특히 브레이크포인트가 프로젝트 전체에 하드코딩되어 있으면, 디자인 가이드가 바뀔 때 수백 곳을 수정해야 합니다.

이런 문제는 반응형 웹을 만들 때 필연적으로 발생합니다. CSS 파일에서는 @media 쿼리를 매번 전체 문법으로 작성해야 하고, 브레이크포인트를 일관되게 관리하기 어렵습니다.

또한 props에 따라 미디어 쿼리를 동적으로 변경하는 것은 거의 불가능합니다. 바로 이럴 때 필요한 것이 Emotion의 미디어 쿼리 헬퍼입니다.

재사용 가능한 브레이크포인트를 정의하고, 템플릿 리터럴이나 객체 문법으로 간편하게 반응형 스타일을 작성할 수 있게 해줍니다.

개요

간단히 말해서, Emotion에서 미디어 쿼리는 일반 CSS처럼 @media 문법을 사용하되, JavaScript 변수와 함수로 추상화하여 재사용성과 유지보수성을 높일 수 있습니다. 미디어 쿼리를 theme 객체나 별도 유틸리티 함수로 관리하면 브레이크포인트가 변경되어도 한 곳만 수정하면 됩니다.

또한 props를 활용하여 조건부로 미디어 쿼리를 적용하거나, 특정 화면 크기에서만 컴포넌트를 표시하는 패턴도 쉽게 구현할 수 있습니다. 기존에는 SCSS의 mixin을 사용하거나 CSS 변수로 관리했다면, 이제는 JavaScript 함수로 더 강력하고 유연한 미디어 쿼리 시스템을 구축할 수 있습니다.

미디어 쿼리 헬퍼의 핵심 특징은 세 가지입니다. 첫째, 브레이크포인트를 theme 객체에 정의하여 일관성을 유지합니다.

둘째, 함수형 프로그래밍 기법으로 재사용 가능한 미디어 쿼리를 만듭니다. 셋째, 모바일 퍼스트 또는 데스크톱 퍼스트 전략을 코드로 명확하게 표현할 수 있습니다.

이러한 특징들이 복잡한 반응형 디자인을 체계적으로 관리할 수 있게 합니다.

코드 예제

import styled from '@emotion/styled';
import { css } from '@emotion/react';

// 브레이크포인트 정의 (theme 객체에 넣는 것을 권장)
const breakpoints = {
  mobile: '480px',
  tablet: '768px',
  desktop: '1024px',
  wide: '1280px',
};

// 재사용 가능한 미디어 쿼리 헬퍼 함수
const mq = {
  mobile: `@media (min-width: ${breakpoints.mobile})`,
  tablet: `@media (min-width: ${breakpoints.tablet})`,
  desktop: `@media (min-width: ${breakpoints.desktop})`,
  wide: `@media (min-width: ${breakpoints.wide})`,
};

// 반응형 그리드 컴포넌트
const ResponsiveGrid = styled.div`
  display: grid;
  gap: 16px;
  padding: 16px;

  /* 모바일: 1열 */
  grid-template-columns: 1fr;

  /* 태블릿: 2열 */
  ${mq.tablet} {
    grid-template-columns: repeat(2, 1fr);
    gap: 24px;
    padding: 24px;
  }

  /* 데스크톱: 3열 */
  ${mq.desktop} {
    grid-template-columns: repeat(3, 1fr);
    gap: 32px;
    padding: 32px;
  }

  /* 와이드 스크린: 4열 */
  ${mq.wide} {
    grid-template-columns: repeat(4, 1fr);
  }
`;

// 특정 크기에서만 표시/숨김
const MobileOnly = styled.div`
  display: block;

  ${mq.tablet} {
    display: none;
  }
`;

const DesktopOnly = styled.div`
  display: none;

  ${mq.desktop} {
    display: block;
  }
`;

설명

이것이 하는 일: 미디어 쿼리 헬퍼는 브레이크포인트 값을 중앙에서 관리하고, 템플릿 리터럴에 삽입 가능한 문자열로 반환하여 반복적인 @media 문법 작성을 줄여줍니다. 첫 번째로, breakpoints 객체에 프로젝트의 모든 화면 크기 기준점을 정의합니다.

이 값들은 디자인 시스템에서 정한 표준 크기여야 하며, 팀 전체가 동일한 값을 사용하도록 theme 객체에 포함시키는 것이 좋습니다. 픽셀 값 대신 em이나 rem을 사용하면 사용자의 폰트 크기 설정을 존중할 수 있습니다.

그 다음으로, mq 헬퍼 객체는 각 브레이크포인트에 대한 미디어 쿼리 문자열을 생성합니다. min-width를 사용하면 모바일 퍼스트 전략이 되고, max-width를 사용하면 데스크톱 퍼스트가 됩니다.

모바일 퍼스트가 일반적으로 권장되는데, 기본 스타일을 모바일용으로 작성하고 큰 화면에서 점진적으로 개선하는 방식이기 때문입니다. 마지막으로, 이 헬퍼를 styled 컴포넌트에서 템플릿 리터럴 삽입 구문으로 사용합니다.

${mq.tablet} { ... } 형태로 작성하면 Emotion이 이를 실제 CSS 미디어 쿼리로 변환합니다.

중첩된 구조도 자동으로 올바르게 처리되므로 가독성이 매우 높습니다. 여러분이 이 코드를 사용하면 디자이너가 브레이크포인트를 변경해도 breakpoints 객체만 수정하면 전체 앱이 자동으로 업데이트됩니다.

또한 MobileOnly, DesktopOnly 같은 유틸리티 컴포넌트를 만들어 특정 화면에서만 렌더링하는 패턴도 쉽게 구현할 수 있습니다. 실무에서는 더 정교한 헬퍼를 만들 수 있습니다.

예를 들어 between(minWidth, maxWidth) 함수로 특정 범위에만 적용되는 미디어 쿼리를 만들거나, retina() 함수로 고해상도 디스플레이용 스타일을 쉽게 작성하는 등의 확장이 가능합니다.

실전 팁

💡 em 단위를 사용하면 사용자가 브라우저 폰트 크기를 변경해도 레이아웃이 적절히 조정됩니다. 768px 대신 48em (768/16) 사용을 고려하세요.

💡 orientation 미디어 쿼리를 추가하여 가로/세로 모드를 구분할 수 있습니다. @media (orientation: landscape) 형태로 사용하세요.

💡 prefers-reduced-motion, prefers-color-scheme 같은 사용자 환경 설정 미디어 쿼리도 헬퍼로 만들어 접근성을 향상시키세요.

💡 개발자 도구의 responsive mode로 모든 브레이크포인트를 테스트하세요. Chrome DevTools에서 디바이스 프리셋을 활용하면 편리합니다.

💡 container queries가 필요하다면 @container 쿼리를 사용하세요. Emotion에서도 동일하게 작동하며, 컴포넌트 크기 기반 스타일링이 가능합니다.


10. 성능 최적화 - 프로덕션 환경 튜닝

시작하며

여러분이 Emotion을 사용하는 앱이 점점 커지면서, 초기 로딩 속도가 느려지거나 스타일 재계산으로 인한 렌더링 지연을 경험한 적 있나요? CSS-in-JS는 편리하지만 잘못 사용하면 성능 오버헤드가 발생할 수 있습니다.

이런 문제는 프로덕션 배포 전에 반드시 점검해야 합니다. 매 렌더마다 새로운 스타일 객체를 생성하거나, 불필요한 리렌더링으로 스타일이 재주입되면 사용자 경험이 나빠집니다.

또한 번들 크기에 불필요한 코드가 포함되면 초기 로딩이 느려집니다. 바로 이럴 때 필요한 것이 Emotion의 성능 최적화 기법들입니다.

컴파일 타임 최적화, 런타임 캐싱, 코드 스플리팅 등을 활용하여 프로덕션급 성능을 달성할 수 있습니다.

개요

간단히 말해서, Emotion 성능 최적화는 Babel 플러그인 설정, 스타일 캐싱 전략, 불필요한 재계산 방지, 번들 크기 최소화 등 여러 기법을 종합적으로 적용하는 과정입니다. 올바른 최적화를 적용하면 초기 로딩 시간이 단축되고, 런타임 성능이 향상되며, 메모리 사용량이 줄어듭니다.

Babel 플러그인은 개발 시 작성한 코드를 빌드 타임에 최적화된 형태로 변환하여 런타임 오버헤드를 최소화합니다. 예를 들어, 정적 스타일을 미리 추출하고 동적 부분만 런타임에 계산하는 식입니다.

기존에는 성능 문제가 생긴 후에 프로파일링하여 수정했다면, 이제는 처음부터 베스트 프랙티스를 따라 성능 문제를 예방할 수 있습니다. 성능 최적화의 핵심 특징은 세 가지입니다.

첫째, @emotion/babel-plugin으로 컴파일 타임 최적화를 적용합니다. 둘째, useMemo와 함께 사용하여 불필요한 스타일 재계산을 방지합니다.

셋째, 동적 import로 스타일 코드를 분할하여 초기 번들 크기를 줄입니다. 이러한 특징들이 사용자에게 빠른 앱 경험을 제공합니다.

코드 예제

// .babelrc 설정 - 컴파일 타임 최적화
{
  "plugins": [
    [
      "@emotion/babel-plugin",
      {
        "sourceMap": true,
        "autoLabel": "dev-only",
        "labelFormat": "[local]",
        "cssPropOptimization": true
      }
    ]
  ]
}

// 스타일 객체를 컴포넌트 외부로 분리 (재생성 방지)
import styled from '@emotion/styled';
import { css } from '@emotion/react';
import { useMemo } from 'react';

// ❌ 나쁜 예: 매 렌더마다 새로운 객체 생성
function BadButton() {
  return (
    <button css={{
      padding: '10px 20px',  // 매번  객체 생성
      backgroundColor: '#007bff',
    }}>
      클릭
    </button>
  );
}

// ✅ 좋은 예: 정적 스타일은 외부로 분리
const buttonStyle = css`
  padding: 10px 20px;
  background-color: #007bff;
`;

function GoodButton() {
  return <button css={buttonStyle}>클릭</button>;
}

// ✅ 동적 스타일은 useMemo로 메모이제이션
function OptimizedButton({ color, size }) {
  const dynamicStyle = useMemo(
    () => css`
      background-color: ${color};
      padding: ${size === 'large' ? '16px 32px' : '8px 16px'};
    `,
    [color, size]  // 의존성이 바뀔 때만 재계산
  );

  return <button css={[buttonStyle, dynamicStyle]}>클릭</button>;
}

// ✅ styled 컴포넌트는 외부에 선언 (재생성 방지)
const StyledButton = styled.button`
  padding: ${props => props.size === 'large' ? '16px' : '8px'};
`;

// ❌ 나쁜 예: 컴포넌트 내부에서 styled 사용
function BadComponent() {
  const Button = styled.button`...`;  // 매 렌더마다 새 컴포넌트 생성
  return <Button>클릭</Button>;
}

설명

이것이 하는 일: @emotion/babel-plugin은 빌드 타임에 Emotion 코드를 분석하여 최적화된 JavaScript로 변환하고, useMemo는 런타임에 불필요한 스타일 재계산을 방지합니다. 첫 번째로, Babel 플러그인의 cssPropOptimization 옵션은 css prop을 최대한 정적으로 추출합니다.

예를 들어 css={{ color: 'red' }}처럼 정적 값만 있으면 빌드 타임에 미리 CSS를 생성하고, 런타임에는 이미 생성된 클래스명만 적용합니다. autoLabel 옵션은 개발 환경에서만 컴포넌트 이름을 클래스명에 포함시켜 디버깅을 쉽게 만듭니다.

그 다음으로, 런타임 최적화의 핵심은 불필요한 객체 생성을 피하는 것입니다. 컴포넌트 함수 내부에서 css 함수나 styled를 호출하면 매 렌더마다 새로운 스타일 객체가 생성되어 React가 "이전 스타일과 다르다"고 판단하여 DOM을 업데이트합니다.

정적 스타일은 모듈 레벨(컴포넌트 외부)에서 한 번만 정의하여 재사용해야 합니다. 마지막으로, 동적 스타일이 정말 필요한 경우에만 useMemo를 사용합니다.

useMemo는 의존성 배열의 값이 바뀔 때만 스타일을 재계산하고, 그렇지 않으면 이전 결과를 재사용합니다. 하지만 props가 자주 바뀌는 경우 useMemo의 오버헤드가 더 클 수 있으므로, 실제 성능을 프로파일링하여 판단해야 합니다.

여러분이 이 코드를 사용하면 React DevTools Profiler에서 컴포넌트 렌더링 시간이 크게 줄어든 것을 확인할 수 있습니다. 특히 리스트 아이템이 수백 개 이상인 경우 최적화 효과가 극적으로 나타납니다.

실무에서는 번들 분석 도구(webpack-bundle-analyzer)로 Emotion 관련 코드 크기를 모니터링하고, Lighthouse로 성능 점수를 정기적으로 체크하는 것이 좋습니다. 또한 프로덕션 빌드에서는 소스맵을 제거하여 번들 크기를 더 줄일 수 있습니다.

실전 팁

💡 React.memo()와 함께 사용하면 props가 바뀌지 않을 때 컴포넌트 전체가 리렌더링되지 않아 더 큰 성능 향상을 얻을 수 있습니다.

💡 Next.js에서는 swcMinify 옵션과 함께 사용할 때 Babel 플러그인 설정이 달라질 수 있으니 공식 문서를 확인하세요.

💡 동적 import로 큰 스타일 파일을 분리하면 초기 번들 크기를 줄일 수 있습니다. const styles = await import('./heavy-styles.js')

💡 크롬 개발자 도구의 Coverage 탭으로 사용되지 않는 CSS를 찾아내세요. critical CSS만 초기 로딩에 포함하는 것이 이상적입니다.

💡 styled 컴포넌트의 shouldForwardProp 옵션으로 불필요한 DOM 속성 전달을 막으면 DOM 크기와 렌더링 성능이 개선됩니다.


#React#Emotion#CSS-in-JS#Styling#Theme

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.
이전3/3
다음