Logo

Emotion으로 React 컴포넌트 스타일하기

React에서는 컴포넌트 스타일할 때는 CSS-in-JS 라이브러리를 사용하는 것이 거의 표준이 되어가는 추세인데요.

이번 포스팅에서는 Styled Components와 함께 대표적인 CSS-in-JS 라이브러리로 손꼽히는 이모션(Emotion)을 사용해서 React 컴포넌트를 스타일링하는 방법에 대해서 살펴보겠습니다.

Styled Components에 대해서도 별도로 다루고 있으니 관련 포스팅을 참고 바랍니다.

패키지 설치

자바스크립트 패키지 매니저인 npm을 이용하면 React 프로젝트에 Emotion을 간편하게 설치할 수 있습니다.

$ npm i @emotion/react

참고로 Emotion 반드시 React 프로젝트에서만 사용해야하는 것은 아닙니다. 일반 자바스크립트 프로젝트에서는 @emotion/css 패키지를 설치하시면 됩니다.

기본 문법

Styled Components의 꽃은 styled() 함수였다면 Emotion의 꽃은 css() 함수일 것입니다. 사실 오랜 시간동안 두 라이브러리가 서로 경쟁하면서 현재는 Styled Components도 css() 함수를 사용할 수 있고, Emotion도 뒤질세라 styled() 함수를 제공하고 있죠.

Emotion이 css() 함수는 위에서 설치한 @emotion/react 패키지에서 불러올 수 있는데요. css() 함수는 CSS 스타일 선언 내용을 인자로 받는데 객체형으로 넘겨도 되고, 문자형으로 넘겨도 됩니다. 그리고 css() 함수가 반환 결과를 해당 스타일을 적용하고 싶은 HTML 요소나 React 컴포넌트의 css라는 prop에 넘겨주면 됩니다.

예를 들어, 어떤 <div> 요소의 배경색을 노란색으로 스타일하고 싶다고 가정해봅시다.

객체형 스타일을 css() 함수의 인자로 넘기면 아래와 같은 모습이 되고,

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

function MyComponent() {
  return (
    <div
      css={css({
        backgroundColor: "yellow",
      })}
    >
      노란색 영역
    </div>
  );
}

문자형 스타일을 css() 함수의 인자로 넘기면 아래와 같은 모습이 될 것입니다.

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

function MyComponent() {
  return (
    <div
      css={css`
        background-color: yellow;
      `}
    >
      노란색 영역
    </div>
  );
}

Emotion 문서에서는 가급적 스타일을 객체로 선언해서 css() 함수에 넘기라고 권장하고 있는데요. 이 방법을 사용하면 css() 함수를 호출을 생략하고 css prop에 바로 객체를 넘길 수 있으며, 특히 타입스크립트(TypeScript)를 사용하면 타입 체킹(type checking)을 통해 오타를 줄일 수 있기 때문입니다.

JSX pragma

눈썰미가 좋으신 분들은 위 예제 코드에서 /** @jsxImportSource @emotion/react */ 부분이 눈에 띄셨을 거 같은데요. 이 것을 소위 JSX pragma라고 하는데 쉽게 설명하면 Babel 트랜스파일러한테 JSX 코드를 변환할 때, React의 jsx() 함수를 사용하지 말고, Emotion의 jsx() 함수를 대신 사용하라고 알려주기 위함입니다.

이렇게 해주지 않으면 css prop에 넘어간 스타일이 제대로 반영이 되지 않을테니 주의바라겠습니다.

만약에 React v16 이하의 오래된 버전을 사용하는 프로젝트에서는 다음과 같이 약간 다른 형태의 JSX pragma를 사용하셔야 하고, jsx() 함수도 불러와야 합니다.

/** @jsx jsx */
import { css, jsx } from "@emotion/react";

function MyComponent() {
  return (
    <div
      css={css({
        backgroundColor: "yellow",
      })}
    >
      노란색 영역
    </div>
  );
}

이 것은 React v17부터 JSX 코드를 작성할 때 굳이 react 패키지에서 React를 불러오지 않더라도 Babel이 트랜스파일을 할 수 있도록 사용성이 개선되었기 때문인데요. 이 부분은 본 포스팅에서 다루고자 하는 범위에서 크게 벚어나기 때문에 관심이 있으신 분들은 따로 검색을 해보시면 좋을 것 같습니다.

참고로 이렇게 매번 JSX pragma를 사용하기가 번거로우시다면 Emotion에서 제공하는 Babel 플러그인이나 프리셋을 사용하시는 것을 고려 바랍니다.

고정 스타일링

지금까지 배운 Emotion의 기본 문법을 이용해서 React로 버튼 컴포넌트를 한 번 작성해볼까요? GitHub에서 볼 수 있는 버튼을 흉내내보면 어떨까요?

<button> 요소의 css prop을 통해서 다양한 CSS 속성 정의를 객체로 넘기겠습니다.

/** @jsxImportSource @emotion/react */

function Button({ children }) {
  return (
    <button
      css={{
        borderRadius: "6px",
        border: "1px solid rgba(27, 31, 36, 0.15)",
        backgroundColor: "rgb(246, 248, 250)",
        color: "rgb(36, 41, 47)",
        fontFamily: "-apple-system, BlinkMacSystemFont, sans-serif",
        fontWeight: "600",
        lineHeight: "20px",
        fontSize: "14px",
        padding: "5px 16px",
        textAlign: "center",
        cursor: "pointer",
        appearance: "none",
        userSelect: "none",
      }}
    >
      {children}
    </button>
  );
}

export default Button;

벌써 끝났습니다. 참, 간단하죠? 😁 이제 이 버튼 컴포넌트를 다른 React 컴포넌트에서 사용하면 됩니다.

import { Button } from "./Button";

function App() {
  return <Button>Button</Button>;
}

브라우저에서 소스 보기를 해보면 아래와 같이 <button> 요소에 Emotion이 자동으로 생성해준 클래스 이름이 붙어있는 것을 확인하실 수 있으실 거에요. (클래스 이름은 랜덤으로 만들어지기 때문에 저랑 다를 수 있습니다.)

<button class="css-3dgxq9-Button">Button</button>

한편, 내부 스타일시트를 확인해보면 클래스 선택자(class selector)로 적용된 스타일이 위에서 위에서 Emotion으로 삽입한 CSS 선언 내용과 동일함을 알 수 있습니다.

.beQCgz {
  border-radius: 6px;
  border: 1px solid rgba(27, 31, 36, 0.15);
  background-color: rgb(246, 248, 250);
  color: rgb(36, 41, 47);
  font-family: -apple-system, BlinkMacSystemFont, sans-serif;
  font-weight: 600;
  line-height: 20px;
  font-size: 14px;
  padding: 5px 16px;
  text-align: center;
  cursor: pointer;
  -webkit-appearance: none;
  -moz-appearance: none;
  -ms-appearance: none;
  appearance: none;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

여기서 주의깊게 볼 부분은 바로 Emotion이 자동으로 브라우저 별로 필요한 CSS 속성을 추가해준다는 것인데요. 소위 이러한 작업을 vendor prefixing이라고 하죠? Emotion을 사용할 때 공짜로 얻을 수 있는 또 다른 이점이 되겠습니다.

가변 스타일링 1

Emotion을 사용하면 React 컴포넌트에 넘어온 props에 따라 다른 스타일을 쉽게 적용할 수 있는데요. 바꾸고 싶은 CSS 속성 값에 상수 대신에 prop에 따라 변하는 변수를 할당해주기만 하면 됩니다.

예를 들어, <Button/> 컴포넌트에 variant라는 prop을 추가한 후에, 이 prop에 어떤 값이 넘어오느냐에 따라 글자색이 바뀌도록 해볼까요?

/** @jsxImportSource @emotion/react */

const colors = {  default: "rgb(36, 41, 47)",  danger: "rgb(207, 34, 46)",  outline: "rgb(9, 105, 218)",};
function Button({ children, variant = "default" }) {
  return (
    <button
      css={{
        borderRadius: "6px",
        border: "1px solid rgba(27, 31, 36, 0.15)",
        backgroundColor: "rgb(246, 248, 250)",
        color: colors[variant],        fontFamily: "-apple-system, BlinkMacSystemFont, sans-serif",
        fontWeight: "600",
        lineHeight: "20px",
        fontSize: "14px",
        padding: "5px 16px",
        textAlign: "center",
        cursor: "pointer",
        appearance: "none",
        userSelect: "none",
      }}
    >
      {children}
    </button>
  );
}

export default Button;

자, 이제 3가지 색상의 버튼을 모두 사용해보겠습니다.

import Button from "./Button";

function App() {
  return (
    <>
      <Button variant="default">Default</Button>
      <Button variant="danger">Danger</Button>
      <Button variant="outline">Outline</Button>
    </>
  );
}

가변 스타일링 2

prop에 따라 바꾸고 싶은 CSS 속성이 위와 같이 하나가 아니라 여러 개일 경우가 있습니다. 이럴 경우에는 CSS 속성의 값뿐만 아니라 CSS 속성 이름과 값을 동시에 객체로 만들어주면 되는데요.

예를 들어, size라는 prop에 넘어온 값에 따라서 버튼의 크기가 바뀌도록 해보겠습니다. fontSize 속성과 padding 속성을 다르게 해보죠.

/** @jsxImportSource @emotion/react */

const colors = {
  default: "rgb(36, 41, 47)",
  danger: "rgb(207, 34, 46)",
  outline: "rgb(9, 105, 218)",
};

const sizeStyles = {  sm: {    fontSize: "12px",    padding: "3px 12px",  },  md: {    fontSize: "14px",    padding: "5px 16px",  },  lg: {    fontSize: "16px",    padding: "9px 20px",  },};
function Button({ children, size = "md", variant = "default" }) {
  return (
    <button
      css={{
        borderRadius: "6px",
        border: "1px solid rgba(27, 31, 36, 0.15)",
        backgroundColor: "rgb(246, 248, 250)",
        color: colors[variant],
        fontFamily: "-apple-system, BlinkMacSystemFont, sans-serif",
        fontWeight: "600",
        lineHeight: "20px",
        ...sizeStyles[size],        textAlign: "center",
        cursor: "pointer",
        appearance: "none",
        userSelect: "none",
      }}
    >
      {children}
    </button>
  );
}

export default Button;

자, 이제 3가지 크기의 3가지 색상, 총 9가지 종류의 버튼을 만들어 낼 수 있게 되었습니다.

import Button from "./Button";

function App() {
  return (
    <>
      <Button size="sm" variant="default">
        Default
      </Button>
      <Button size="md" variant="default">
        Default
      </Button>
      <Button size="lg" variant="default">
        Default
      </Button>
      <hr />
      <Button size="sm" variant="danger">
        Danger
      </Button>
      <Button size="md" variant="danger">
        Danger
      </Button>
      <Button size="lg" variant="danger">
        Danger
      </Button>
      <hr />
      <Button size="sm" variant="outline">
        Outline
      </Button>
      <Button size="md" variant="outline">
        Outline
      </Button>
      <Button size="lg" variant="outline">
        Outline
      </Button>
    </>
  );
}

전체 코드

본 포스팅에서 작성한 코드는 아래에서 직접 확인하고 실행해보실 수 있습니다.

마치면서

이상으로 React 컴포넌트를 Emotion을 이용해서 어떻게 스타일하는지 알아보았습니다.

재작년까지만 해도 Styled Components라는 라이브러리가 가장 돋보였는데 최근에는 MUI(Material UI)가 스타일링 엔진을 Styled Components 대신에 Emotion을 채택하면서 Emotion 쪽으로 살짝 기우는 게 아닌가 하는 느낌이 드는데요.

저는 두 개의 CSS-in-JS 라이브러리는 모두 많이 써봤는데 개인적으로 Emotion의 css() 함수가 좀 더 사용하기가 편한 것 같습니다.