styled-components의 내부 구현을 살펴봅니다.
원문 링크 : https://www.joshwcomeau.com/react/demystifying-styled-components/
핵심 아이디어
공식 문서의 예제를 살펴봅시다.
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`;
const Title = styled.h1(`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`);
- h1은 스타일드 컴포넌트의 헬퍼 메서드이며 단일 인수인 스트링으로 호출합니다.
- 헬퍼 메서드는 작은 컴포넌트 팩토리입니다.
- 호출할 때마다 완전히 새로운 React 컴포넌트를 생성합니다.
// When I call this function…
function h1(styles) {
// …it generates a brand-new React component…
return function NewComponent(props) {
// …which will render the associated HTML element:
return <h1 {...props} />
}
}
- 스타일을 dKamQW 또는 iOacVe와 같이 무작위로 보이는 문자열로 해싱하여 고유한 클래스 이름을 만듭니다.
- 경량 CSS 전처리기*인 Stylis를 통해 CSS를 실행합니다.
- less, sass와 같이 벤더 프리픽스를 처리하고, 몇가지 웹의 다루기 어려운 문제를 자동으로 해결해 줍니다.
- 해시된 문자열을 이름으로 사용하고 스타일 문자열의 모든 CSS 선언을 포함하는 새 CSS 클래스를 페이지에 삽입합니다.
- 반환된 HTML 엘리먼트에 해당 클래스 명을 적용합니다.
코드로 보면 다음과 같습니다.
function h1(styles) {
return function NewComponent(props) {
const uniqueClassName = comeUpWithUniqueName(styles);
const processedStyles = runStylesThroughStylis(styles);
createAndInjectCSSClass(uniqueClassName, processedStyles);
return <h1 className={uniqueClassName} {...props} />
}
}
<style>
.dKamQW {
font-size: 1.5em;
text-align: center;
color: palevioletred;
}
</style>
<h1 class="dKamQW">Hello World</h1>
실제 styled-components 코드베이스는 이보다 훨씬 더 복잡합니다.
우리는 많은 최적화와 개발자의 삶의 질을 개선해주는 수정을 건너뛰고 있습니다.
예를 들어 모든 렌더링에서 새 CSS 클래스가 생성될 수 있습니다.
현실 세계에서 우리는 필요할 때만 발생하도록 그 작업을 useMemo 또는 useEffect 후크 뒤에 두면 좋겠죠.
느긋한 CSS 주입
function ItemList({ items }) {
if (items.length === 0) {
return "No items";
}
return (
<Wrapper>
{/* Stuff omitted */}
</Wrapper>
)
}
const Wrapper = styled.ul`
background: goldenrod;
`;
styled-components가 이 경우에 우리가 제공한 CSS를 적용하지 않는다는 사실에 놀랄 것입니다.
background 선언은 DOM에 추가되지 않습니다.
styled-components가 정의될 때마다 CSS 클래스를 열성적으로 생성하는 대신
해당 스타일을 페이지에 삽입하기 전에 컴포넌트가 렌더링될 때까지 기다립니다.
이것은 좋은 일입니다!
더 큰 웹사이트에서 수백 킬로바이트의 사용되지 않은 CSS가 브라우저로 전송되는 것은 드문 일이 아닙니다.
styled-components를 사용하면 작성한 CSS가 아닌 렌더링한 CSS에 대해서만 비용을 지불합니다.
(물론 기술적으로 css는 js 내부에 이미 포함되어 있긴 합니다.
이럴 경우 SSR에서 장점이 있는데, CSS가 크리티컬 렌더링 패스를 막지 않고 화면을 먼저 그릴 수 있도록 해줍니다.)
CSS 삽입을 연기하는 또 다른 이유가 있습니다.
보간된 스타일 때문입니다.
우리는 이 기사의 끝부분에서 그것들을 다룰 것입니다.
동적으로 CSS 추가하기
const styleTag = document.createElement('style');
document.head.appendChild(styleTag);
const newRule = document.createTextNode(`
.dKamQW {
font-size: 1.5em;
text-align: center;
color: palevioletred;
}
`);
styleTag.appendChild(newRule);
const styleSheet = document.styleSheets[0];
styleSheet.insertRule(`
.dKamQW {
font-size: 1.5em;
text-align: center;
color: palevioletred;
}
`);
함수형 헬퍼 메서드
function h1(styles) {
return function NewComponent(props) {
const uniqueClassName = comeUpWithUniqueName(styles);
const processedStyles = runStylesThroughStylis(styles);
createAndInjectCSSClass(uniqueClassName, processedStyles);
return <h1 className={uniqueClassName} {...props} />
}
}
function magic() {
console.log('✨');
}
magic.hands = function() {
console.log('👋')
}
magic(); // logs '✨'
magic.hands(); // logs '👋'
const styled = (Tag) => (styles) => {
return function NewComponent(props) {
const uniqueClassName = comeUpWithUniqueName(styles);
createAndInjectCSSClass(uniqueClassName, styles);
return <Tag className={uniqueClassName} {...props} />
}
}
styled.h1 = styled('h1');
styled.button = styled('button');
// ...And so on, for all DOM nodes!
// This:
styled.h1(`
color: peachpuff;
`);
// …is equivalent to this:
styled('h1')(`
color: peachpuff;
`);
변수를 렌더링한다고?
function App() {
const Tag = 'button';
return <Tag>Hello</Tag>
}
render(<App />)
const Tag = 'button';
React.createElement(Tag, {}, "Hello");
React.createElement('button', {}, "Hello");
<Button> // React.createElement(Button);
<button> // React.createElement('button');
const Tag = 'button';
<Tag> // React.createElement(Tag);
<tag> // React.createElement('tag');
커스텀 컴포넌트 감싸기
function Message({ children, ...delegated }) {
return (
<p {...delegated}>
You've received a message: {children}
</p>
);
}
const UrgentMessage = styled(Message)`
background-color: pink;
`;
render(
<UrgentMessage>
We're having a fire sale!
</UrgentMessage>
);
function Message({ children, ...delegated }) {
console.log(delegated);
// { className: 'OkjqvF' }
return (
<p {...delegated}>
You've received a message: {children}
</p>
);
}
const styled = (Tag) => (styles) => {
return function NewComponent(props) {
const uniqueClassName = comeUpWithUniqueName(styles);
createAndInjectCSSClass(uniqueClassName, styles);
return <Tag className={uniqueClassName} {...props} /> // here
}
}
- Message 컴포넌트를 합성하는 스타일드 컴포넌트 UrgentMessage를 렌더링합니다.
- 고유한 클래스 이름(OkjqvF)을 만들고 className prop으로 Tag 변수(Message 구성 요소)를 렌더링합니다.
- 메시지가 렌더링되고 className prop을 <p> 요소에 전달합니다.
function Message({ children }) {
/*
Because we're ignoring the `className` prop,
the styles will never be set.
*/
return (
<p>
You've received a message: {children}
</p>
);
}
Message가 받는 모든 props를 렌더링하는 <p> 요소에 위임하여 이 패턴을 잠금 해제합니다.
고맙게도 많은 서드파티 컴포넌트(예: react-router의 Link 컴포넌트)가 이 규칙을 따릅니다.
styled-components 합성하기
const Button = styled.button`
background-color: transparent;
font-size: 2rem;
`
const PinkButton = styled(Button)`
background-color: pink;
`;
render(
<PinkButton>Hello World</PinkButton>
);
<style>
.abc123 {
background-color: transparent;
font-size: 2rem;
}
.def456 {
background-color: pink;
}
</style>
<button class="abc123 def456">Hello World</button>
- ID 선택자 #btn은 클래스 선택자 .btn을 이기게 됩니다.
- 이는 tag 선택자인 button을 이긴다는 것을 의미합니다.
하지만 이 경우에는 두 개의 클래스가 있습니다! 두 선택자 .abc123 및 .def456은 동일하게 일치합니다.
<style>
.red {
color: red;
}
.blue {
color: blue;
}
</style>
<p class="blue red">Hello</p>
<p class="red blue">Hello</p>
const styled = (Tag) => (styles) => {
return function NewComponent(props) {
const uniqueClassName = comeUpWithUniqueName(styles);
const processedStyles = runStylesThroughStylis(styles);
createAndInjectCSSClass(uniqueClassName, processedStyles);
const combinedClasses =
[uniqueClassName, props.className].join(' ');
return <Tag {...props} className={combinedClasses} />
}
}
- PinkButton을 렌더링할 때 Button도 렌더링합니다.
- styled-components는 abc123 또는 def456과 같은 고유한 클래스를 생성합니다.
- 모든 클래스는 놓여야 하는 DOM 노드에 적용됩니다.
- styled-components는 이러한 규칙을 올바른 순서로 삽입하여 PinkButton의 스타일이 Button의 충돌을 덮어쓰도록 합니다.
Interpolated styles (보간된 스타일)
styled-components 클론을 거의 완료했지만 목록에 보간된 스타일이라는 작업이 하나 더 있습니다.
때때로 CSS는 React prop에 의존합니다. 예를 들어 image는 maxWidth prop을 사용할 수 있습니다.
const ContentImage = styled.img`
display: block;
margin-bottom: 8px;
width: 100%;
max-width: ${p => p.maxWidth};
`;
render(
<>
<ContentImage
alt="A running shoe with pink laces and a rainbow decal"
src="/images/shoe.png"
maxWidth="200px"
/>
<ContentImage
alt="A close-up shot of the same running shoe"
src="/images/shoe-closeup.png"
/>
</>
)
<style>
.JDSLg {
display: block;
margin-bottom: 8px;
width: 100%;
max-width: 200px;
}
.eXyedY {
display: block;
margin-bottom: 8px;
width: 100%;
}
</style>
<img
alt="A running shoe with pink laces and a rainbow decal"
src="/images/shoe.png"
class="sc-bdnxRM JDSLg"
/>
<img
alt="A close-up shot of the same running shoe"
src="/images/shoe-closeup.png"
class="sc-bdnxRM eXyedY"
/>
const ContentImage = styled.img`
display: block;
margin-bottom: 8px;
width: 100%;
max-width: var(--max-width);
`;
render(
<>
<ContentImage
alt="A running shoe with pink laces and a rainbow decal"
src="/images/shoe.png"
style={{
'--max-width': '200px',
}}
/>
<ContentImage
alt="A close-up shot of the same running shoe"
src="/images/shoe-closeup.png"
/>
</>
)
<style>
.JDSLg {
display: block;
margin-bottom: 8px;
width: 100%;
max-width: var(--max-width);
}
</style>
<img
alt="A running shoe with pink laces and a rainbow decal"
src="/images/shoe.png"
class="sc-bdnxRM JDSLg"
style="--max-width: 200px"
/>
<img
alt="A close-up shot of the same running shoe"
src="/images/shoe-closeup.png"
class="sc-bdnxRM JDSLg"
/>
기록 수정하기
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`;
const Title = styled.h1(`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`);
const styled = (Tag) => (rawStyles, ...interpolations) => {
return function NewComponent(props) {
/*
Compute the styles from the template string, the
interpolation functions, and the provided React props.
*/
const styles = reconcileStyles(
rawStyles,
interpolations,
props
)
/* The rest is unchanged: */
const uniqueClassName = comeUpWithUniqueName(styles);
const processedStyles = runStylesThroughStylis(styles);
createAndInjectCSSClass(uniqueClassName, processedStyles);
const combinedClasses =
[uniqueClassName, props.className].join(' ');
return <Tag {...props} className={combinedClasses} />
}
}
'FrontEnd' 카테고리의 다른 글
리액트 성능 최적화 : children props를 이용하여 리렌더링 방지 (0) | 2022.07.09 |
---|---|
개인적으로 생각해본 컴포넌트 설계론 + 카카오 FE 기술블로그 염탐 (0) | 2022.07.08 |
Tailwind CSS와 CSS-in-JS 무엇을 사용할까? (0) | 2022.07.08 |
새로운 리액트 공식문서로 배우는 Context API 2편 : 리듀서와 컨텍스트로 애플리케이션 확장하기 (0) | 2022.07.06 |
새로운 리액트 공식문서로 배우는 Context API 1편 : 프롭 드릴링 해결하기 (0) | 2022.07.05 |