주로 컴포넌트를 만들 때 emotion의 css prop을 자주 사용했다. css prop을 사용하는 경우는 크게 두가지다.
이미 생성된 컴포넌트의 경우에는 본래의 컴포넌트 이름을 가질수 있도록 css prop을 사용
특히나 공통 컴포넌트라면 더욱 기존의 이름을 가지는 것이 좋다고 생각했다. 만약 styled component로 덮어서 만들게 된다면, 공통 컴포넌트 본래의 이름이 아닌 다른 이름으로 설정해야하는데, 그렇게되면 헷갈릴 수 있을거라 생각했다. 그래서 컴포넌트를 정의하는 파일의 라인이 길어지더라도 css prop을 설정했다.
시간이 없을 때(?)
styled component로 덮어서 만드는 게 css prop으로 만드는 것보다 더 오래걸리고 번거로운 일이라고 생각했다. 그래서 빨리 컴포넌트를 만들어야할 때 css prop을 자주 사용했다. 따로 css를 만들지 않고 inline css를 사용했다.
굉장히 잘 사용했던 css prop에는 잘 못 사용하는 경우 성능을 저하시키는 요인이 될 수 있다는 걸 나중이 되어서야 알게 되었다.
성능을 저하시키지 않으면서 효과적으로 사용하는 방법에 대해 알아보려 한다.
emotion에서 제공하는 css prop은 사용하는 방법이 크게 두가지가 있다.
css를 따로 정의하는 방법
const myCss1 = css`
font-size: 20px;
color: red;
`;
// 또는
const myCss2 = css({
fontSize: 20,
color: 'red',
});
// component
<div css={myCss1}>hello</div>
<div css={myCss2}>world!</div>
inline css를 사용하는 방법
<div
css={css`
font-size: 20px;
color: red;
`}
>
hello world!
</div>
위의 방법 중 2번인 inline css를 사용하면 일반적으로 1번과 동일하게 동작 하더라도 성능은 다를 수 있다. 그 이유에 대해서 알아보려 한다.
컴포넌트가 렌더링을 시작하면 css 함수도 실행이 된다. 말 그대로 inline css이기 때문에 렌더가 될 때마다 css함수를 실행하는데, 이는 inline style처럼 성능에 악영향을 줄 수 있다.
아래의 코드는 리스트 렌더링을 통해 요소들을 리턴하는 컴포넌트이다. 배열이 길이만큼 돌면서 엘리먼트를 만들고 동시에 css함수도 실행하고있다.
export const SomeComponent = () => {
return (
<React.Fragment>
{['green', 'blue', 'red'].map((color) => (
<div
key={color}
css={css`
font-size: 14px;
color: ${color};
`}
>
{color}
</div>
))}
</React.Fragment>
);
};
위의 코드는 바벨에 의해 대강 아래처럼 변환된다.
import React from "react";
import { jsx } from "@emotion/core";
var _ref = ['green', 'blue', 'red'];
var SomeComponent = function SomeComponent() {
return React.createElement(
React.Fragment,
null,
_ref.map(function (i) {
return jsx('div', {
// 아래의 css는 emotion의 css함수를 통해 리턴받은 값이다.
css: {
name: 'css-hashed-Component',
styles: `font-size: 14px; color: ${i}`,
},
key: i,
});
}),
);
};
export default SomeComponent;
그리고 실제 화면에 보일 때 엘리먼트 구조는 아래와 같다.
// html
<body>
<div class="css-hashed1-SomeComponent">green</div>
<div class="css-hashed2-SomeComponent">blue</div>
<div class="css-hashed3-SomeComponent">red</div>
</body>
// css
<style>
.css-hashed1-SomeComponent {
font-size: 14px;
color: green;
}
.css-hashed2-SomeComponent {
font-size: 14px;
color: blue;
}
.css-hashed3-SomeComponent {
font-size: 14px;
color: red;
}
</style>
위는 각 선택자마다 다른 폰트 색상이 다르지만, font-size는 모두 동일하다. 중복되는 속성이 있어도 inline css를 사용하면 중복된 css 속성이 있어도 포함시키고, 모두 고유한 이름을 가진 css를 생성한다.
여기서 리스트 렌더링을 통해 inline css를 하면 두가지 성능 상의 손해를 본다.
만약 배열의 길이가 매우 길어진다면 그만큼의 css 함수를 실행시켜야 한다. 생각보다 css 함수는 아무 때나 실행시켜도 될만큼 값싼 연산을 하고있지 않다. 깃허브에서 css함수를 어떻게 실행하는 지를 보면 알 수있다.
아주 친절하게도 emotion 공식문서에 설명이 되어있다. 정적 스타일에는 css prop을 사용하고, 동적 스타일에는 style prop을 사용하라고 안내하고있다. 위의 코드 예제에서는 폰트 컬러가 동적으로 변하니까 color 속성만 inline style로 주면 될 것 같다.
const SomeComponent = () => {
return (
<React.Fragment>
{['green', 'blue', 'red'].map((color) => (
<div
key={color}
css={css`
font-size: 14px;
`}
style={{ color }}
>
{color}
</div>
))}
</React.Fragment>
);
};
화면상에 보이는 엘리먼트 구조는 아래와 같다. 각 div 태그에 동일한 classname이 들어간 것을 보면 드디어 재사용을 하는걸로 보인다.
<body>
<div class="css-cached-SomeComponent" style="color: green;">green</div>
<div class="css-cached-SomeComponent" style="color: blue;">blue</div>
<div class="css-cached-SomeComponent" style="color: red;">red</div>
</body>
<style>
.css-c7jwbi-SomeComponent {
font-size: 14px;
}
</style>
그럼 바벨에 의해 트랜스파일된 코드는 어떻게 보여질까? (실제로는 아래처럼 트랜스파일되지 않는다. 쉽게 비교할 수 있게 조금 변경하였다.)
import { css, jsx } from '@emotion/react';
import React from 'react';
const SomeComponent = () => {
return (
React.createElement(React.Fragment, null, [
'green',
'blue',
'red'
].map(color =>
jsx("div", {
key: color,
// 이미 직렬화 과정을 거친 객체 중 스타일이 같은 것을 찾고, 있으면 기존에 생성된 객체를 가져온다.
// 캐싱되어있는 객체를 찾는 건데, 이 또한 결국 비용이 든다.
css: cachedCss,
style: { color },
}, color)
))
);
};
export default SomeComponent;
결국 같은 이름을 가진 선택자를 사용한다고 해도, 결국 속성이 같은 css를 찾기 위한 연산이 들어가게 된다. 객체 css의 키(key) 중 styles의 타입은 문자열이라, 속성이 같아도 순서가 다르면 서로 다른 문자열이 되다보니 완벽환 최적화도 아니고 그렇다고 캐싱이라고 보기엔 내부의 동작원리를 모르면 아리송하다고 느낄 수 있다.
아래는 css객체의 인터페이스다.
export interface SerializedStyles {
name: string
styles: string
map?: string
next?: SerializedStyles
}
다시 돌아와서, 더 최적화 할 수 있는 방법을 찾아보자.
// 따로 css 객체를 만들어두면 inline css보다 높은 성능을 기대할 수 있다.
const fontSize = css`
font-size: 14px;
`;
const SomeComponent = () => {
return (
<React.Fragment>
{['green', 'blue', 'red'].map((color) => (
<div key={color} css={fontSize} style={{ color }}>
{color}
</div>
))}
</React.Fragment>
);
};
export default SomeComponent;
위 코드는 변환된 html 코드를 보더라도, 바벨에 의해 트랜스파일된 자바스크립트 코드도 동일하다. 하지만 중간에 동작이 조금 달라졌다. 내부적으로 캐싱된 css가 있는지 찾아볼 필요도 없게 되었다!
일반적으로 컴포넌트가 리렌더링 될때마다 컴포넌트 안에있는 요소들을 다시 실행하는데, 이때 inline style을 설정하면 매 번 스타일이 새롭게 생성되기 때문이다. 이는 불필요한 연산을 낳게된다. 이는 곧 퍼포먼스 저하의 원인과도 연관이 있다.
이는 리액트가 컴포넌트를 만드는 과정을 살펴보면, 리액트에서 생성한 컴포넌트는 곧바로 html로 변하는 게 아니라 중간에 과정들이 있다. 리액트의 컴포넌트는 자바스크립트 코드이다. 결국 자바스크립트에서 html로 변환하는 과정을 가지고있다.
리액트 컴포넌트 내의 style prop은 object 형식이다. 문자열이 아니다. 자바스크립트에서 비어있는 object 두개는 보기에 똑같아보여도 자바스크립트는 다르다고 판단한다. 자바스크립트의 동작을 살펴보면 컴포넌트가 리렌더링 됐을 때 style prop이 보기에 동일하게 보이더라도 매 번 새롭게 생성한다.
블로그 글을 위해 자료를 조사하던 도중 간혹 React.useMemo를 사용하여 css객체를 만드는 경우를 본 적이 있었다. 아래의 코드를 보자.
const SomeComponent = () => {
const someCss = React.useMemo(
() =>
css`
font-size: 14px;
`,
[],
);
return (
// ...
);
};
export default SomeComponent;
useMemo를 사용하면 리렌더링을 최소화하여 성능을 향상시킨다는 글을 본 적 있는데, 이는 잘 생각해봐야할 문제라고 생각한다.
결국 useMemo도 비용이 드는 작업이다. 두번 째 인자의 deps를 통해 논리를 따지고 나서야 리렌더링 여부를 결정하는 것이다. useMemo(또는 useCallback)은 값비싼 연산을 효율적으로 처리하기 위한 훅으로 사용되는것이 적합하다고 생각한다.
과연 css객체를 만드는 것이 훅을 사용할 정도인지는 고민을 해봐야 하지 않을까 조심스레 생각해본다. 만약 useMemo 또는 useCallback에 대해 더 자세하게 알고싶다면 Kent C. dodds의 블로그에 자세하게 다룬 글이 있으니 확인해보면 좋을것 같다.
그렇다면 훅을 사용하지않고 조금 더 성능을 높이는 방법은 없을까?
useMemo를 사용하지 않고 최적화하는 방법은 간단하다. 컴포넌트 바깥에 정의하면 된다.
https://ko.reactjs.org/docs/introducing-jsx.html
https://kentcdodds.com/blog/usememo-and-usecallback
주로 styled를 쓰고 자주 쓰이는 스타일을 css 따로 atomic style로 만들어서 사용하고 있습니다. 당근마켓 공개 레포에 vanilla extract 사용 예제 있는데 비슷하게 쓰는중..
주로 styled를 쓰고 자주 쓰이는 스타일을 css 따로 atomic style로 만들어서 사용하고 있습니다. 당근마켓 공개 레포에 vanilla extract 사용 예제 있는데 비슷하게 쓰는중..
이 큰 기여에 감사드립니다. 매우 흥미롭고 잘 생각하고 정리한 내용입니다. 앞으로 당신의 작품을 볼 수 있기를 바랍니다. https://watermelon-game.io
마무리 정리까지 완벽한 글이었습니다 👍
개인적으로 컴포넌트가 가지고 있는 스타일이 아닌 경우 인라인 css를 사용하여 스타일을 작성했었는데 해주신 고민 덕분에 좋지 않은 방법을 사용하고 있었음을 깨닫게 되었습니다.
명심하고 갑니다!!
잘 읽었습니다 :)