
해당 글은 Emotion 이라는 라이브러리의 문서를 읽고 간략히 정리한 글입니다. 더 많고 자세한 정보는 공식 문서를 참고해주세요.
About
emotion 라이브러리는 glam 및 styled-components 에 영감을 받았다.
Install
React 환경이라면 @emotion/react 패키지를 사용하는 것을 권장한다.
css
프로퍼티를 지원한다style
프로퍼티와 유사하지만 자동 접두사, 중첩된 선택자, 미디어 쿼리를 지원한다.styled
API를 사용할 필요 없이, 요소에 바로 스타일 적용이 가능하다.- 함수도 입력받을 수 있어, (함수의 인자로 제공되는) 테마에 맞게 스타일링할때 유용하다.
- 추가적인 설정 없이 SSR을 지원한다
- ESLint 설정 및 적용 가능
CSS Prop
emotion을 이용해 스타일링을 하는 최고의 방법은
css
프로퍼티를 활용하는 것이다.className
을 받을 수 있는 모든 요소와 컴포넌트는 css
프로퍼티를 사용할 수 있다. 이 프로퍼티는 (1) css 함수를 이용한 스타일 객체와 (2)리터럴 스타일 객체 둘 다 받을 수 있다.@emotion/react 패키지에서 제공하는
css
함수는 tagged templates 와 유사하게 생겼다.css 함수는 스타일 문자열 혹은 스타일 객체 둘 다 인자로 받을 수 있다.
<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 " }
단, 여기서 name 값은 class name으로 사용되는 것은 아니다!!
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 변수를 활용하는 방법이 있다.
TS 환경이라면 타입 어설션이 필요하다.
<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
같은 함수를 직접 호출해도 된다는 뜻이다.다만 React 18 기준 renderToNodeStream 은 deprecated 되었다.
// 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
에 붙여주는 것이다.@emotion/babel-plugin 패키지는 위 동작과 더불어 css 프로퍼티에 대해 컴파일 타임 최적화를 수행해준다고 하니, 성능 문제가 생긴다면 도입을 고려해보자.
해당 패키지는 필수는 아니다. 다만 각종 스타일을 최적화 하거나 압축하는 방식으로 최적화를 해주고, 소스맵과 레이블을 만들어 DX를 향상시켜준다.
import { css } from '@emotion/react' let style = css` color: hotpink; label: some-name; ` let anotherStyle = css({ color: 'lightgreen', label: 'another-name' }) let ShowClassName = ({ className }) => ( <div className={className}>{className}</div> ) render( <div> <ShowClassName css={style} /> {/* css-11z9jyx-some-name */} <ShowClassName css={anotherStyle} /> {/* css-970ucr-another-name */} </div> )
Performance
이모션은 매우 성능이 좋은 라이브러리다. (라고 주장한다…)
우선, 앱이 느리다고 느껴진다면 React DevTool 같은 분석 툴을 이용해 이모션때문인지 아닌지 분석해봐라. 만약 이모션 관련 코드가 원인으로 파악되었다면, 다음과 같은 수정을 시도해봐라.
- 잦은 리렌더링이 일어나는 컴포넌트를
memo
같은 기술을 이용해 최적화해라.
- 이모션을 사용하는 컴포넌트의 수를 줄여라. 예를 들어, 스타일을 공유하는 10만개의 목록 컴포넌트가 있다고 한다면, 각각에 스타일을 설정하기 보다 컨테이너에 넣어주자.
- 정적 스타일엔
css
프로퍼티를, 동적 스타일엔style
프로퍼티를 사용해라.
- 스타일 정의를 컴포넌트 외부로 분리해서, 리렌더링에 영향을 받지 않도록 해라.
css
프로퍼티에 대한 컴파일 타임 최적화를 위해 @emotion/babel-plugin 을 사용해라.