해당 글은 Emotion 이라는 라이브러리의 문서를 읽고 간략히 정리한 글입니다. 더 많고 자세한 정보는 공식 문서를 참고해주세요.
emotion 라이브러리는 glam 및 styled-components 에 영감을 받았다.
React 환경이라면 @emotion/react 패키지를 사용하는 것을 권장한다.
css 프로퍼티를 지원한다style 프로퍼티와 유사하지만 자동 접두사, 중첩된 선택자, 미디어 쿼리를 지원한다.styled API를 사용할 필요 없이, 요소에 바로 스타일 적용이 가능하다.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 와 glamorous에 크게 영감을 받은 styled API는 스타일이 적용된 컴포넌트를 만드는데 사용할 수 있다.
이 API 또한 css 와 마찬가지로 className 속성을 받을 수 있는 컴포넌트라면 모두 적용이 가능하다.
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 컴포넌트를 이용해 전역 스타일을 적용할 수 있다.
<Global
styles={css`
* {
color: hotpink;
}
`}
/>
만약 css 문법 검사 기능이 없다면 타입스크립트와 함께 오브젝트 스타일 방식을 추천.
const myCss = css({
color: 'blue',
grid: 1 // Error: Type 'number' is not assignable to type 'Grid | Grid[] | undefined'
})
이모션의 강력한 장점 중 하나는 스타일을 컴포넌트와 함께 둘 수 있다는 것이다. 이로 인해 컴포넌트 수정 시, 스타일 수정을 깜빡하는 실수를 줄일 수 있다.
이모션을 활용하되, 스타일은 컴포넌트와 같은 파일에 두는 습관을 들이자.
이모션에서 스타일을 공유하는 두가지 방법이 있다.
export const errorCss = css({
color: 'red',
fontWeight: 'bold'
})
// Use arrays to compose styles
export const largeErrorCss = css([errorCss, { fontSize: '1.5rem' }])
기본 스타일을 정해둔 뒤, 이를 활용하는 곳에서 가져와 오버라이딩을 한다.
이 방법은 단순히 “스타일”만을 공유할 때 사용하면 좋다. 다만, 이 방법은 응집도 방면에서 좋지는 못한 것을 잘 고려하자.
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 }}
/>
위 두 패키지는 @emotion/css 보다 더 나은 개발자 경험을 제공한다.
@emotion/styled 둘 중 하나만 쓰자두 방식은 공존할 수 있긴 하지만, 이왕이면 둘 중 하나로 통일해서 사용하는 것이 좋다.
💡 공식 문서에서는 두 방식의 차이는 단순히 취향이라고 한다.
컴포넌트 내부에 정의하면 매 렌더링마다 새롭게 스타일 객체가 할당된다.
실수로 css 프로퍼티에 동적 스타일을 할당하는 것을 방지할 수 있다.
CSS 정의가 추상화되어 있기 때문에, JSX 를 읽을 때 가독성이 좋다.
export const colors = {
primary: '#0d6efd',
success: '#198754',
danger: '#dc3545'
}
자주 사용하는 스타일 값은 상수로 정의해서 활용하자.
이모션은 라이트 모드 및 다크 모드와 같은 기능을 지원하기 위해 테마 기능을 지원하지만, 하나의 테마만 사용할 예정이라면 스타일 상수가 훨씬 간편하니 이 방법을 사용하자.
애니메이션 효과는 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>
)
이모션 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 코드를 삽입한 문서를 내려줘야 한다.
수많은 css-in-js 라이브러리가 제공하듯이, 이모션도 props를 컴포넌트에 붙이는 기능을 제공한다.
const PasswordInput = props => (
<input
type="password"
css={css`...`}
{...props}
/>
);
render(
<PasswordInput placeholder="pink"/>
);
테마 기능은 @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 라는 커스텀 훅도 제공한다.
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 를 이용해서 테마를 주입해줄 수 있다.
🤔 근데 왜 예제에는 클래스 컴포넌트로 썼을까? 함수 컴포넌트는 미지원인가..?
이모션의 기본 동작 원리가 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>
)
이모션은 매우 성능이 좋은 라이브러리다. (라고 주장한다…)
우선, 앱이 느리다고 느껴진다면 React DevTool 같은 분석 툴을 이용해 이모션때문인지 아닌지 분석해봐라. 만약 이모션 관련 코드가 원인으로 파악되었다면, 다음과 같은 수정을 시도해봐라.
memo 같은 기술을 이용해 최적화해라.css 프로퍼티를, 동적 스타일엔 style 프로퍼티를 사용해라.css 프로퍼티에 대한 컴파일 타임 최적화를 위해 @emotion/babel-plugin 을 사용해라.