Emotion 제대로 사용하기

Kingmo·2022년 3월 19일
post-thumbnail

React 생태계의 대표적인 CSS-in-JS 라이브러리로 styled-componentsEmotion이 자주 비교된다.

Emotion은 상대적으로 가벼운 번들 사이즈와 유연한 css prop 지원 덕분에 실무에서 많이 채택된다.

번들 사이즈 비교

1. Emotion 초기 설정 이슈와 해결

Next.js 환경에서 ${ChildDiv} { background-color: red; }처럼 컴포넌트 셀렉터를 사용할 때 @emotion/styled가 컴포넌트 클래스명을 제대로 인식하지 못하는 문제가 발생할 수 있다.

이 현상은 Emotion 전용 바벨 플러그인이 컴포넌트를 고유 클래스 셀렉터로 변환해 주지 못해 일어난다. 아래 절차로 설정을 추가하면 해결된다.

플러그인 설치

yarn add --dev @emotion/babel-plugin

.babelrc 설정

프로젝트 루트 경로에 .babelrc 파일을 생성하고 Next.js 프리셋과 Emotion 플러그인을 연결한다.

{
  "presets": [
    [
      "next/babel",
      {
        "preset-react": {
          "runtime": "automatic",
          "importSource": "@emotion/react"
        }
      }
    ]
  ],
  "plugins": ["@emotion/babel-plugin"]
}

JSX Pragma 선언

스타일을 적용할 파일 상단에 pragma 주석과 필요한 모듈을 불러온다.

/** @jsxImportSource @emotion/react */
import { jsx } from "@emotion/react";
import styled from "@emotion/styled";

2. Emotion vs Styled-components

npm trends 지표를 보면 다운로드 수치 면에서 Emotion의 선호도가 높은 편이다.

번들 사이즈 관점에서는 Emotion이 상대적으로 더 가볍다. 다만 두 라이브러리 간 런타임 렌더링 성능 차이는 일반적인 서비스 환경에서 유의미하게 크지 않다.

문법 비교

두 라이브러리 모두 태그드 템플릿 리터럴과 객체 문법을 지원하지만, Emotion은 인라인 스타일에 가까운 css prop 문법을 더욱 매끄럽게 지원한다.

// styled-components
const Title = styled.h1`
  font-size: 1.5em;
  text-align: center;
  color: palevioletred;
`;

// Emotion (styled 방식 및 css prop 방식 모두 지원)
const titleStyles = css`
  font-size: 1.5em;
  text-align: center;
  color: palevioletred;
`;

<h1 css={titleStyles}>Hiya!</h1>;

3. Emotion 핵심 실전 문법

기본 사용법과 가상 클래스 (&)

& 기호는 자기 자신(현재 컴포넌트의 셀렉터)을 가리킨다.

import styled from "@emotion/styled";

const MyDiv = styled.div`
  width: 200px;
  height: 200px;
  background-color: red;

  // MyDiv:hover 와 동일
  &:hover {
    background-color: blue;
  }
`;

부모에서 하위 컴포넌트 타겟팅

부모 컴포넌트의 스타일 블록 안에서 ${자식컴포넌트} 형태로 특정 하위 컴포넌트의 스타일을 오버라이딩할 수 있다.

/** @jsxImportSource @emotion/react */
import { jsx } from "@emotion/react";
import styled from "@emotion/styled";

const ChildDiv = styled.div`
  width: 100px;
  height: 100px;
  background-color: blue;
`;

const ParentDiv = styled.div`
  width: 500px;
  height: 400px;

  // ParentDiv 내부의 ChildDiv만 배경색을 red로 변경
  ${ChildDiv} {
    background-color: red;
  }
`;

export default function App() {
  return (
    <ParentDiv>
      <ChildDiv />
    </ParentDiv>
  );
}

전역 스타일 (Global Styles)

@emotion/reactGlobal 컴포넌트를 활용해 Reset CSS나 전체 공통 스타일을 주입한다.

// styles/reset.js
import { css, Global } from "@emotion/react";

export const globalStyles = (
  <Global
    styles={css`
      * {
        box-sizing: border-box;
        padding: 0;
        margin: 0;
      }
      html, body {
        width: 100vw;
        height: 100vh;
        font-size: 16px;
      }
    `}
  />
);
// pages/_app.js
import { globalStyles } from "../styles/reset";

export default function MyApp({ Component, pageProps }) {
  return (
    <>
      {globalStyles}
      <Component {...pageProps} />
    </>
  );
}

스타일 상속 (styled(BaseComponent))

이미 정의된 컴포넌트의 기본 속성을 유지한 채 새로운 스타일을 덮어씌울 때 유용하다.

const MyButton = styled.button`
  font-size: 23px;
  border: none;
`;

const BlackButton = styled(MyButton)`
  background-color: black;
  color: white;
`;

const RedButton = styled(MyButton)`
  background-color: red;
  color: blue;
`;

태그 변환: withComponent vs as prop

스타일은 그대로 유지하되 렌더링되는 HTML 태그만 변경해야 할 때 두 가지 방식이 있다.

const MyFont = styled.p`
  font-size: 24px;
  font-weight: bold;
`;

// 방법 1: withComponent 사용 (새로운 컴포넌트 정의)
const MyItalicFont = styled(MyFont.withComponent("span"))`
  font-style: italic;
  font-weight: normal;
`;
// 방법 2: as prop 전달 (컴포넌트 선언 없이 JSX 레벨에서 태그 치환)
<MyFont as="span">span으로 렌더링된 텍스트</MyFont>

불필요한 변수 선언을 줄이고 코드 가독성을 높이기 위해서는 as prop을 활용하는 편이 훨씬 직관적이다.

믹스인(Mixin) 패턴: 공통 스타일 조각 주입

자주 사용하는 스타일 조각을 css 함수로 선언해 두고 필요한 컴포넌트 내부에 삽입한다.

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

const hoverEffect = css`
  &:hover {
    background-color: red;
  }
`;

const BaseButton = styled.button`
  border: none;
  padding: 10px;
  font-weight: bold;
`;

const BlueButton = styled(BaseButton)`
  background-color: blue;
  ${hoverEffect} // 믹스인 스타일 반영
`;

테마 적용 (ThemeProvider)

공통 테마 객체를 정의하고 최상단에서 ThemeProvider로 감싸면 하위 컴포넌트 어디서든 props.theme로 변수를 읽어올 수 있다.

// color.js
const MyColor = {
  default: "black",
  myAppColor: "red",
};
export default MyColor;
import { ThemeProvider } from "@emotion/react";
import styled from "@emotion/styled";
import MyColor from "./color";

const MyButton = styled.button`
  border: none;
  padding: 10px;
  // props 조건에 따라 테마 색상 분기
  background-color: ${(props) => (props.myAppColor ? MyColor.myAppColor : MyColor.default)};
`;

export default function App() {
  return (
    <ThemeProvider theme={MyColor}>
      <MyButton>기본버튼</MyButton>
      <MyButton myAppColor>테마버튼</MyButton>
    </ThemeProvider>
  );
}

Keyframes 애니메이션


keyframes 헬퍼 함수로 정의한 애니메이션 키프레임을 컴포넌트 내부에서 변수처럼 사용한다.

import { css, keyframes } from "@emotion/react";
import styled from "@emotion/styled";

const rotation = keyframes`
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
`;

const WarningButton = styled.button`
  border: none;
  padding: 10px;
  ${(props) =>
    props.warning &&
    css`
      animation: ${rotation} 1s linear infinite;
      background-color: red;
    `}
`;

외부 UI 라이브러리(Material-UI 등) 스타일 확장 시 주의점

styled(MUIComponent) 형태로 래핑하여 스타일을 재정의할 때, HTML 고유 속성이나 라이브러리 전용 props는 styled 내부에서 주입하려 하지 말고 렌더링되는 JSX 태그에 직접 prop으로 전달해야 스타일 누락이나 타입 에러를 피할 수 있다.

profile
Developer

0개의 댓글