Emotion 톺아보기

해당 글은 Emotion 이라는 라이브러리의 문서를 읽고 간략히 정리한 글입니다. 더 많고 자세한 정보는 공식 문서를 참고해주세요.

About

emotion 라이브러리는 glam 및 styled-components 에 영감을 받았다.

Install

React 환경이라면 @emotion/react 패키지를 사용하는 것을 권장한다.
  1. css 프로퍼티를 지원한다
  2. 추가적인 설정 없이 SSR을 지원한다
  3. ESLint 설정 및 적용 가능

CSS Prop

emotion을 이용해 스타일링을 하는 최고의 방법은 css 프로퍼티를 활용하는 것이다.
className 을 받을 수 있는 모든 요소와 컴포넌트는 css 프로퍼티를 사용할 수 있다. 이 프로퍼티는 (1) css 함수를 이용한 스타일 객체와 (2)리터럴 스타일 객체 둘 다 받을 수 있다.
@emotion/react 패키지에서 제공하는 css 함수는 tagged templates 와 유사하게 생겼다.
<div
    css={css`
      background-color: hotpink;
      &:hover {
        color: ${color};
      }
    `}
>
<div
    css={css({
      backgroundColor: "hotpink",
      "&:hover": {
        color: color,
      },
    })}
>
@emotion/react 기준 css 함수의 결과물은 name 과 평탄화된 스타일 문자열인 styles 속성을 가지는 객체다.
{
  // computed name
  "name": "v5tptf",
  "styles": "\\n        background-color: antiquewhite;\\n        color: red;\\n      "
}

Styled Components

styled-components 와 glamorous에 크게 영감을 받은 styled API는 스타일이 적용된 컴포넌트를 만드는데 사용할 수 있다.
이 API 또한 css 와 마찬가지로 className 속성을 받을 수 있는 컴포넌트라면 모두 적용이 가능하다.

Object Styles

string style 대신 object style을 사용할 수 있다. 이 방법은 css 함수 호출 없이 스타일링을 할 수 있다. 단, 이 경우에는 카멜 케이스로 작성해야 한다.
<div
  css={{
    backgroundColor: 'hotpink',
    '&:hover': {
      color: 'lightgreen'
    }
  }}
 />
객체 스타일의 소소한 장점이라고 한다면, fallback 기능을 지원한다. (fallback 을 앞쪽에 선언해야 한다.)
<div
  css={{
    background: ['red', 'linear-gradient(#e66465, #9198e5)'],
    height: 100
  }}
>

Global Styles

Global 컴포넌트를 이용해 전역 스타일을 적용할 수 있다.
<Global
  styles={css`
    * {
      color: hotpink;
    }
  `}
/>

Best Practice

TypeScript + Object style

만약 css 문법 검사 기능이 없다면 타입스크립트와 함께 오브젝트 스타일 방식을 추천.
const myCss = css({
  color: 'blue',
  grid: 1 // Error: Type 'number' is not assignable to type 'Grid | Grid[] | undefined'
})

컴포넌트와 스타일을 같은 곳에 두기 (응집도)

이모션의 강력한 장점 중 하나는 스타일을 컴포넌트와 함께 둘 수 있다는 것이다. 이로 인해 컴포넌트 수정 시, 스타일 수정을 깜빡하는 실수를 줄일 수 있다.
이모션을 활용하되, 스타일은 컴포넌트와 같은 파일에 두는 습관을 들이자.

스타일 공유 방법을 고려해보기

이모션에서 스타일을 공유하는 두가지 방법이 있다.

1. 스타일 객체를 내보내기

export const errorCss = css({
  color: 'red',
  fontWeight: 'bold'
})

// Use arrays to compose styles
export const largeErrorCss = css([errorCss, { fontSize: '1.5rem' }])
기본 스타일을 정해둔 뒤, 이를 활용하는 곳에서 가져와 오버라이딩을 한다.
이 방법은 단순히 “스타일”만을 공유할 때 사용하면 좋다. 다만, 이 방법은 응집도 방면에서 좋지는 못한 것을 잘 고려하자.

2. 컴포넌트 재사용하기

export function ErrorMessage({ className, children }) {
  return (
    <p css={{ color: 'red', fontWeight: 'bold' }} className={className}>
      {children}
    </p>
  )
}

// `fontSize: '1.5rem'` is passed down to the ErrorMessage component via the
// className prop, so ErrorMessage must accept a className prop for this to
// work!
export function LargeErrorMessage({ className, children }) {
  return (
    <ErrorMessage css={{ fontSize: '1.5rem' }} className={className}>
      {children}
    </ErrorMessage>
  )
}
이 방법은 조금 더 복잡하지만, 더 강력한 방법이다. 스타일과 컴포넌트가 응집도 있게 뭉쳐있기 때문이다.
다만 코드에 적힌 대로, css 프로퍼티는 내부적으로 className 을 이용해 스타일이 적용되기 때문에, 스타일을 상속받을 컴포넌트는 className 을 구현해야 한다.

동적 스타일에는 style prop 사용하기

css 프로퍼티와 styled API는 정적 스타일에 사용되어야 한다. 동적 스타일링에는 style 프로퍼티를 사용해야 한다.
css 프로퍼티는 입력값에 따라 고유한 스타일을 만들어낸다. 예를 들어, 프사 이미지를 생각해보자. 모든 프사는 width와 height 가 40px 이며 border-radius 가 50%이다. 그런데 각 이미지는 사람마다 다를 것이다.
  .css-1udhswa {
    border-radius: 50%;
    width: 40px;
    height: 40px;
    background-image: url(<https://i.pravatar.cc/150?u=0>);
  }

  .css-1cpwmbr {
    border-radius: 50%;
    width: 40px;
    height: 40px;
    background-image: url(<https://i.pravatar.cc/150?u=1>);
  }

  .css-am987o {
    border-radius: 50%;
    width: 40px;
    height: 40px;
    background-image: url(<https://i.pravatar.cc/150?u=2>);
  }
3개의 프사가 있으면 3개의 스타일이 생성된다. 만약 100명이라면? 100개의 스타일이 생성된다.
만약 스타일이 자주 변경되거나 동적이라면 css 프로퍼티 대신 style 프로퍼티를 사용하자.
<div style={{
  borderRadius: "50%",
  width: "40px",
  height: "40px",
  backgroundImage: "url(...)",
}}/>
또 다른 방법으로는 css 변수를 활용하는 방법이 있다.
<div
  css={{
    backgroundColor: 'var(--background-image)',
  }}
  style={{ ['--background-image' as any]: imgUrl }}
/>

React 환경이라면 @emotion/react 혹은 @emotion/styled 사용하기

위 두 패키지는 @emotion/css 보다 더 나은 개발자 경험을 제공한다.

CSS 프로퍼티와 @emotion/styled 둘 중 하나만 쓰자

두 방식은 공존할 수 있긴 하지만, 이왕이면 둘 중 하나로 통일해서 사용하는 것이 좋다.

CSS 정의는 컴포넌트 밖에 정의하자

컴포넌트 내부에 정의하면 매 렌더링마다 새롭게 스타일 객체가 할당된다.
실수로 css 프로퍼티에 동적 스타일을 할당하는 것을 방지할 수 있다.
CSS 정의가 추상화되어 있기 때문에, JSX 를 읽을 때 가독성이 좋다.

자주 사용하는 스타일 값은 상수로 정의하자

export const colors = {
  primary: '#0d6efd',
  success: '#198754',
  danger: '#dc3545'
}
자주 사용하는 스타일 값은 상수로 정의해서 활용하자.

멀티 테마가 아니라면 테마 기능 대신 스타일 상수를 사용하자

이모션은 라이트 모드 및 다크 모드와 같은 기능을 지원하기 위해 테마 기능을 지원하지만, 하나의 테마만 사용할 예정이라면 스타일 상수가 훨씬 간편하니 이 방법을 사용하자.

Keyframes

애니메이션 효과는 keyframes API를 활용해 구현할 수 있다.
const bounce = keyframes`
  from, 20%, 53%, 80%, to {
    transform: translate3d(0,0,0);
  }
  40%, 43% {
    transform: translate3d(0, -30px, 0);
  }
  70% {
    transform: translate3d(0, -15px, 0);
  }
  90% {
    transform: translate3d(0,-4px,0);
  }
`;

render(
  <div
    css={css`
      animation: ${bounce} 1s ease infinite;
    `}
  >
    some bouncing text!
  </div>
)

Server Side Rendering

이모션 v10에서 SSR은 두가지 방법을 지원한다. (각각은 트레이드 오프가 있다.)
첫번째 방법은 스트리밍을 이용한 방법이고, 별다른 설정이 필요없다. 하지만 nth 자식이나 비슷한 셀렉터가 동작하지 않는다. 이러한 기능이 필요한 게 아니라면, 첫번째 방법을 사용할 것을 권장한다.

기본 접근방식

@emotion/react 혹은 @emotion/styled 만 사용하는 환경이라면, SSR을 지원한다. 이 말은, renderToString 혹은 renderToNodeStream 같은 함수를 직접 호출해도 된다는 뜻이다.
// App.ts
import { renderToString } from 'react-dom/server'
import App from './App'

let html = renderToString(<App />)

// MyDiv.js
const MyDiv = styled('div')({ fontSize: 12 })
<MyDiv>Text</MyDiv>
렌더링 결과는 해당 컴포넌트 바로 위에 <style/> 태그로 삽입이된다.
<!-- MyDiv.js 가 렌더링 된 후 -->
<style data-emotion-css="21cs4">.css-21cs4 { font-size: 12 }</style>
<div class="css-21cs4">Text</div>
이렇게 특정 요소 바로 위에 새로운 요소가 삽입되는 구조로 인해, nth 자식 셀렉터 같은 동작들이 방해받을 수 있다.

더 나은 접근방식

렌더링 서버에서 이것저것 해줘야 할 것들이 많다.
코드를 HTML 코드와 CSS 코드로 분리한 뒤, <head> 요소 안에 CSS 코드를 넣고 body > div#root 요소에다가 HTML 코드를 삽입한 문서를 내려줘야 한다.

Attatching Props

수많은 css-in-js 라이브러리가 제공하듯이, 이모션도 props를 컴포넌트에 붙이는 기능을 제공한다.
const PasswordInput = props => (
  <input
	type="password"
    css={css`...`}
    {...props}
  />
);

render(
  <PasswordInput placeholder="pink"/>
);

Theming

테마 기능은 @emotion/react 에 포함되어있다.
기본적은 동작원리는 ThemeProvider 를 앱 최상단에 배치하여 스타일을 전역적으로 공유하는 것이다.
import { ThemeProvider } from '@emotion/react'

const theme = {
  colors: {
    primary: 'hotpink'
  }
}

render(
  <ThemeProvider theme={theme}>
    <div css={theme => ({ color: theme.colors.primary })}>some other text</div>
  </ThemeProvider>
)
기본적으로 css 프로퍼티에 함수를 넣어서 theme 에 접근할 수 있고, useTheme 라는 커스텀 훅도 제공한다.

withTheme API

import * as PropTypes from 'prop-types'
import * as React from 'react'
import { withTheme } from '@emotion/react'

class TellMeTheColor extends React.Component {
  render() {
    return <div>The color is {this.props.theme.color}.</div>
  }
}

TellMeTheColor.propTypes = {
  theme: PropTypes.shape({
    color: PropTypes.string
  })
}

const TellMeTheColorWithTheme = withTheme(TellMeTheColor)
HOC 방식을 이용한 withTheme 를 이용해서 테마를 주입해줄 수 있다.

Labels

이모션의 기본 동작 원리가 css 프로퍼티로 받은 스타일 값을 이리저리 잘 조물조물해서 label 이라고 부르는 고유한 class name을 만들어 className 에 붙여주는 것이다.