React에서 스타일을 넣을 때 가장 익숙한 방법은 style prop이다.
<div style={{ color: 'red' }} />
Emotion을 쓰면 비슷한 위치에 css가 등장한다.
// @emotion/css
import { css } from '@emotion/css'
<div className={css({ color: 'red' })} />
<div className={css`
color: red;
`} />
// @emotion/react
import { css } from '@emotion/react'
<div css={css`color: red;`} />
Emotion을 사용하는 프로젝트에서 스타일 넣는 방법이 다양했다.
처음엔 모두 단순히 스타일을 지정하는 방법으로만 생각했다.
그래서 여러 방식이 섞여 있어도 크게 불편하게 느껴지지 않았다.
하지만 스타일 컨벤션을 정하려고 보니, 하나로 통일하기 전에 각 방식의 차이를 먼저 확인해야 했다.
그래서 style={}과 Emotion css가 각각 어떤 값을 다루고, DOM에 어떻게 적용되는지 살펴봤다.
React 공식 문서에서 style prop은 CSS 스타일 객체를 받는 DOM prop이다.
<div style={{ color: 'red', fontSize: 16 }} />
이 값은 CSS rule을 만들지 않는다. DOM element의 inline style로 들어간다.
<div style="color: red; font-size: 16px;" />
즉 style={}은 CSS를 작성하는 자리라기보다, 해당 element의 style attribute에 넣을 값을 작성하는 자리다.
React 공식 문서도 style을 JavaScript 변수에 의존하는 스타일에만 쓰는 것을 권장한다.
We recommend only using the style attribute when your styles depend on JavaScript variables.
스타일 값이 JavaScript 변수에 따라 달라질 때만 style 속성을 사용하는 것을 권장한다.
공식 문서: React - Applying CSS styles
JavaScript 값에 따라 달라지는 단순한 스타일은 style={}과 잘 맞는다. 계산된 값을 해당 element의 style attribute에 바로 넣을 수 있다.
<div style={{ width: `${progress}%` }} />
하지만 :hover, @media, 중첩 selector는 특정 조건이나 대상에 CSS를 적용하는 규칙이다.
.button:hover {
color: blue;
}
style={}은 하나의 element에 적용할 CSS 스타일 객체만 받는다.
<div style={{ color: 'red' }} />
style={}은 selector를 만들지 않아서, :hover나 @media처럼 조건을 가진 CSS rule을 그대로 표현하기 어렵다.
여기서 Emotion css와의 차이가 생긴다. Emotion은 스타일을 inline style로 넣지 않고, CSS rule을 만든 뒤 className으로 element에 연결한다.
css from @emotion/react does not return the computed class name string. The function returns an object containing the computed name and flattened styles.
@emotion/react의 css는 최종 className 문자열을 바로 반환하지 않는다. 대신 계산된 이름과 정리된 스타일을 담은 객체를 반환한다.
공식 문서: Emotion - css-prop#string-styles
소스에서도 같은 흐름을 볼 수 있다. serializeStyles()는 마지막에 name, styles, next를 반환한다.
return {
name,
styles,
next: cursor
}
Emotion github : serializeStyles의 반환값
styles는 정리된 CSS 문자열이다. name은 이 styles를 기반으로 만들어진다.
let name = hashString(styles) + identifierName
이 흐름은 @emotion/react의 css prop 처리 코드에서도 확인된다.
packages/react/src/emotion-element.tsx를 보면 props.css를 registeredStyles에 담고, 다시 serializeStyles()로 넘긴다.
let cssProp = props.css
let registeredStyles = [cssProp]
// return { name, styles, next }
let serialized = serializeStyles(
registeredStyles,
undefined,
useContext(ThemeContext)
)
이제 serialized.name으로 실제 element에 붙일 className을 만든다.
기존 className이 있으면 먼저 유지하거나 registeredStyles로 정리한다.
그 다음 cache.key와 serialized.name을 조합해 Emotion className을 붙인다.
최종적으로 실제 element에는 css prop이 아니라 정리된 className이 전달된다.
if (typeof props.className === 'string') {
// 기존 className을 유지하거나 registeredStyles로 분리
className = getRegisteredStyles(
cache.registered,
registeredStyles,
props.className
)
} else if (props.className != null) {
// 기존 값으로 유지
className = `${props.className} `
}
// cache.key와 serialized.name을 조합해 Emotion className 생성
// 예: 'css' + '1a2b3c' = 'css-1a2b3c'
className += `${cache.key}-${serialized.name}`
// 실제 element에는 css prop이 아닌 최종 className string 전달
newProps.className = className
Emotion - github : Emotion에서 className 처리
serialized는 Insertion 컴포넌트에도 전달된다. Insertion은 insertStyles()를 호출해 style 삽입 단계로 넘긴다.

여기까지 지나면 element에는 className이 붙고, 실제 CSS rule은 style tag에 들어간다.

여기까지 알아낸 내용을 중간 점검해보자.
| 구분 | style={} | Emotion css 계열 |
|---|---|---|
| 기대하는 값 | CSS 스타일 객체 | Emotion이 serialize할 스타일 |
| 적용 방식 | element의 inline style로 들어감 | className을 만들고 style tag에 CSS rule 삽입 |
| selector | 없음 | 사용 가능 |
| 적합한 경우 | JavaScript 값에 따라 달라지는 단순 스타일 | :hover, @media, 중첩 selector가 필요한 스타일 |
| DOM 결과 | style attribute | className + style tag |
다만, Emotion의 css()라고 해서 항상 같은 값을 반환하는 것은 아니다. @emotion/react와 @emotion/css는 같은 이름의 함수를 제공하지만, 반환값과 사용 위치가 다르다.
@emotion/react와 @emotion/css는 둘 다 css()라는 함수를 제공한다.
두 패키지의 css()는 모두 먼저 serializeStyles()를 호출한다.
// @emotion/react
function css(...args: CSSInterpolation[]) {
return serializeStyles(args)
}
// @emotion/css
let serialized = serializeStyles(args, cache.registered, undefined)
Emotion - github : react css에서 serializeStyles 호출, create-instance에서 serializeStyles 호출
이 단계는 React 전용 로직이 아니다. 객체 스타일이든 template literal이든, Emotion이 다양한 스타일 입력을 serializeStyles로 정리하는 공통 단계다.
style input
→ serializeStyles()
→ { name, styles, next }
두 css함수는 출발점이 같다.
차이는 serializeStyles() 이후에 어디까지 처리하느냐다.
@emotion/react의 css()는 className을 바로 만들지 않고, React의 css prop 처리 흐름에 넘길 중간 결과를 만든다.
반면 @emotion/css의 css()는 한 단계 더 간다.
serializeStyles()로 만든 값을 바로 삽입하고, className 문자열까지 반환한다.
insertStyles(cache, serialized, false)
return `${cache.key}-${serialized.name}`
Emotion - github : create-instance에서 className 문자열 반환
반환값이 다르기 때문에 넘기는 위치도 달라진다.
// @emotion/react
<div css={css`color: red;`} /> // css props
// @emotion/css
<div className={css`color: red;`} /> // className
Emotion을 뜯어보면서 알게 된 점은 크게 세 가지다.
style={}은 CSS rule을 만드는 방식이 아니라, element의 inline style을 직접 다루는 방식이다.css는 inline style이 아니라 className과 style tag로 연결된다.css()라도 @emotion/react는 React의 css prop 흐름에서 처리될 SerializedStyles를 만들고, @emotion/css는 className 문자열을 바로 반환한다.겉으로는 모두 스타일을 적용하는 코드처럼 보였지만, 내부에서는 style attribute에 바로 들어가는지, className과 style tag로 연결되는지가 달랐다.
생각보다 Emotion내부 로직 흐름도 단순해서 Emotion을 조금 더 편하게 이해할 수 있게 됐다.
Emotion 어때요 그냥 css ai 써서 작성하는게 용량도 적게들고 편하긴한거같기도하고....