μ λͺ©μ μ΄λ°λ°, μ¬μ€ μ° styled-componentsλ₯Ό μ¬μ©νλ λ΄μ©μ μλλ€.
dream-css-tool
dream-css-toolλΌλ μλλ₯Ό νκ³ κ³μ λΆμ λ°κ²¬νλ€. styled κ°μ λ°©μμΌλ‘ μλ²μ»΄ν¬λνΈμμλ λμνκ²λ λ§λλ μλλ₯Ό νκ³ μμλ€.
μ°μ μ¬μ©λ² λΆν° 보μ. μλμ κ°μ΄ μ¬μ©ν μ μλ€.
- μ°μ StyleRegistryλ₯Ό 루νΈμ κ°μΈμ€λ€. (μ¬κΈ°μ next app dirμ΄λΌ layoutμ κ°μΈμ€)
import type { Metadata } from 'next';
import StyleRegistry from '@/components/StyleRegistry';
export const metadata: Metadata = {
title: 'Dream CSS tool',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<StyleRegistry>
<html lang="en">
<body>{children}</body>
</html>
</StyleRegistry>
);
}
- styledλ₯Ό μ¬μ©νλ€. κΈ°μ‘΄ μ°λ¦¬κ° styledλ₯Ό μ°λ λ°©μμ²λΌ μ»΄ν¬λνΈλ₯Ό μμ±νλ€.
import React from 'react';
import styled from '../styled.js';
export default function StaticButton() {
return <Button>Static Button</Button>;
}
const Button = styled('button')`
display: block;
padding: 1rem 2rem;
border: none;
border-radius: 4px;
background: hsl(270deg 100% 30%);
color: white;
font-size: 1rem;
cursor: pointer;
`;
μ 체 μ½λκ° μμ§ λͺ μ€ λμ§ μμ, κ°λ¨ν μ½λλ₯Ό μ΄ν΄λ³΄μ. μλλ μ£Όμ λΆλΆμ μ½λ μλ¬Έμ΄λ€. (μΌλ¨ μ£ΌμκΉμ§ νΌμ΄)
// styled.js
import React from 'react';
import { cache } from './components/StyleRegistry';
// TODO: Ideally, this API would use dot notation (styled.div) in
// addition to function calls (styled('div')). We should be able to
// use Proxies for this, like Framer Motion does.
export default function styled(Tag) {
return (css) => {
return function StyledComponent(props) {
let collectedStyles = cache();
// Instead of using the filename, I'm using the `useId` hook to
// generate a unique ID for each styled-component.
const id = React.useId().replace(/:/g, '');
const generatedClassName = `styled-${id}`;
const styleContent = `.${generatedClassName} { ${css} }`;
collectedStyles.push(styleContent);
return <Tag className={generatedClassName} {...props} />;
};
};
}
// StyleRegistry.js
import React from 'react';
import StyleInserter from './StyleInserter';
export const cache = React.cache(() => {
return [];
});
function StyleRegistry({ children }) {
const collectedStyles = cache();
return (
<>
<StyleInserter styles={collectedStyles} />
{children}
</>
);
}
export default StyleRegistry;
// StyleInserter.js
'use client';
import React from 'react';
import { useServerInsertedHTML } from 'next/navigation';
function StyleInserter({ styles }) {
useServerInsertedHTML(() => {
return <style>{styles.join('\n')}</style>;
});
return null;
}
export default StyleInserter;
λΆλΆ λΆλΆ λ―μ΄λ³΄μ
// StyledRegistry.js
export const cache = React.cache(() => {
return [];
});
// styled.js
// ...
let collectedStyles = cache();
// ...
styledμ 첫 λΆλΆμ 보면 StyleRegistryμμ React.cacheλ₯Ό μ¬μ©ν΄μ λ§λ€μ΄μ§ cacheλ₯Ό κ°μ Έμ¨λ€.
// styled.js
// ...
const id = React.useId().replace(/:/g, '');
const generatedClassName = `styled-${id}`;
const styleContent = `.${generatedClassName} { ${css} }`;
collectedStyles.push(styleContent);
return <Tag className={generatedClassName} {...props} />;
μ΄λ, κ³ μ ν idλ₯Ό λ§λ€μ΄μ, classNameμ λ§λ€κ³ , cssλ₯Ό λ§λ€μ΄ cacheμ λ£λλ€. κ·Έλ¦¬κ³ ν΄λΉ classNameμ κ°μ§ μ»΄ν¬λνΈλ₯Ό 리ν΄νλ€.
// StyleRegistry.js (server)
function StyleRegistry({ children }) {
const collectedStyles = cache();
return (
<>
<StyleInserter styles={collectedStyles} />
{children}
</>
);
}
StyleRegistryμμλ cacheλ₯Ό κ°μ Έμμ, λͺ¨μΈ cssλ₯Ό StyleInserterμ λ겨μ€λ€.
// StyleInserter.js
'use client';
import React from 'react';
import { useServerInsertedHTML } from 'next/navigation';
function StyleInserter({ styles }) {
useServerInsertedHTML(() => {
return <style>{styles.join('\n')}</style>;
});
return null;
}
export default StyleInserter;
λ°νμμμλ useServerInsertedHTMLμ μ¬μ©ν΄μ, λ°μ cssλ₯Ό style νκ·Έλ‘ λ§λ€μ΄μ λ£μ΄μ€λ€. μ΄λ κ² λλ©΄, μλ² μ»΄ν¬λνΈμμλ λμνλ styled μ»΄ν¬λνΈλ₯Ό λ§λ€ μ μλ€.
νμ§λ§ ν° λ¬Έμ κ° μλ€.
// src/components/CountButton.js
'use client';
import React from 'react';
import styled from '../styled.js';
export default function CountButton() {
const [count, setCount] = React.useState(0);
return (
<Button onClick={() => setCount(count + 1)}>
Clicks: {count}
</Button>
);
}
// Currently, this doesn't work, because `cache()` can't be used in
// Client Components. It throws an error, and none of the styles get
// created.
const Button = styled('button', 'client')`
padding: 1rem 2rem;
color: red;
font-size: 1rem;
`;
ν΄λΌμ΄μΈνΈ μ»΄ν¬λνΈμμλ cacheλ₯Ό μ¬μ©ν μ μλ€. κ·Όλ° cacheλ§ μ¬μ©ν μ μμ λΏ cacheμ μν μ μλ΅νκ±°λ λ체 νλ©΄ λ κ² κ°λ€.
ν΄λΌμ΄μΈνΈ μ»΄ν¬λνΈμμλ λμνκ²λ μμ νκΈ°
μ±λ₯μ μΌλ‘ λ¬Έμ κ° μμ μ μμΌλ, μ€μ λ‘ μ¬μ©ν λλ μ£Όμν΄μ μ¬μ©ν΄μΌ νλ€.
cacheλ₯Ό μ¬μ©νμ§ μκ³ κ·Έλ₯ λ°λ‘ cssλ₯Ό λ£μ΄μ£Όλ λ°©μμΌλ‘ μμ ν΄λ³΄μ.
// clientStyled.js
import React from 'react';
import { useServerInsertedHTML } from 'next/navigation';
export default function styled(Tag) {
return (css) => {
return function StyledComponent(props) {
const id = React.useId().replace(/:/g, '');
const generatedClassName = `styled-${id}`;
const styleContent = `.${generatedClassName} { ${css} }`;
useServerInsertedHTML(() => {
return <style>{styleContent}</style>;
});
return <Tag className={generatedClassName} {...props} />;
};
};
}
κ·Έ λ€ κ·Έλ₯ μΈμλ₯Ό νλλ λ°μμ λΆκΈ°λ₯Ό μ³μ€¬λ€. λ°μμ§λ₯Ό 체ν¬ν μ μλ λ°©λ²μ΄ μμ κ² κ°κΈ΄ νλ°β¦ μΌλ¨μ μ΄λ κ² νμ.
// styled.js
import React from 'react';
import serverStyled from './serverStyled';
import clientStyled from './clientStyled';
export default function styled(Tag, from = 'server') {
if (from === 'client'){
return clientStyled(Tag);
}
return serverStyled(Tag);
}
μ΄λ κ² λλ©΄ ν΄λΌμ΄μΈνΈμμ μ€νμ βclientβλ₯Ό λ겨주면 λμνκΈ΄ νλ€. λ§λ€κ³ 보λ κ²°κ΅ βuse clientβ κΌ΄μ΄ λκ² μλκ° νλ μμ¬μμ΄ λ¨λλ€.
React.cache
https://react.dev/reference/react/cache
https://github.com/facebook/react/blob/main/packages/react/src/ReactCacheServer.js
κ°λ¨ν cacheμ λμ λ°©μμ μ΄ν΄λ³΄μ. ν¨μλ₯Ό λκΈ°λ©΄ cache λ μ΄μ΄κ° λνλ ν¨μλ₯Ό λ°ννλ€. κ·Έλ€ λ°νλ ν¨μλ₯Ό apply νκΈ°μ μ μΊμ λ μ΄μ΄λ₯Ό λκ³ μΊμ λ μ΄μ΄μμ μΊμ±λ κ²°κ³Όλ₯Ό λ°ννλ λ°©μμ΄λΌκ³ 보면λλ€.
import ReactCurrentCache from './ReactCurrentCache';
const UNTERMINATED = 0; // λ―Έμλ£ μνλ₯Ό λνλ΄λ μμ
const TERMINATED = 1; // μλ£ μνλ₯Ό λνλ΄λ μμ
const ERRORED = 2; // μλ¬ μνλ₯Ό λνλ΄λ μμ
type UnterminatedCacheNode<T> = {
s: 0, // μν (λ―Έμλ£)
v: void, // κ° (λ―Έμλ£ μνμμλ κ°μ΄ μμ)
o: null | WeakMap<Function | Object, CacheNode<T>>, // κ°μ²΄ μΊμ (WeakMap μ¬μ©)
p: null | Map<string | number | null | void | symbol | boolean, CacheNode<T>>, // μμ νμ
μΊμ (Map μ¬μ©)
};
type TerminatedCacheNode<T> = {
s: 1, // μν (μλ£)
v: T, // κ° (μΊμλ κ²°κ³Ό)
o: null | WeakMap<Function | Object, CacheNode<T>>, // κ°μ²΄ μΊμ
p: null | Map<string | number | null | void | symbol | boolean, CacheNode<T>>, // μμ νμ
μΊμ
};
type ErroredCacheNode<T> = {
s: 2, // μν (μλ¬)
v: mixed, // κ° (μλ¬ κ°μ²΄)
o: null | WeakMap<Function | Object, CacheNode<T>>, // κ°μ²΄ μΊμ
p: null | Map<string | number | null | void | symbol | boolean, CacheNode<T>>, // μμ νμ
μΊμ
};
type CacheNode<T> =
| TerminatedCacheNode<T>
| UnterminatedCacheNode<T>
| ErroredCacheNode<T>;
function createCacheRoot<T>(): WeakMap<Function | Object, CacheNode<T>> {
return new WeakMap(); // μλ‘μ΄ WeakMapμΌλ‘ μΊμ λ£¨νΈ μμ±
}
function createCacheNode<T>(): CacheNode<T> {
return {
s: UNTERMINATED, // κΈ°λ³Έ μνλ λ―Έμλ£
v: undefined, // μ΄κΈ° κ°μ undefined
o: null, // κ°μ²΄ μΊμ μ΄κΈ°ν
p: null, // μμ νμ
μΊμ μ΄κΈ°ν
};
}
/*
* μΊμ λ
Έλλ₯Ό μμ±νκ³ , μΊμ 루νΈμ μΊμ λ
Έλλ₯Ό μ μ₯νλ ν¨μ
*
* @param fn μΊμ λ
Έλλ₯Ό μμ±ν ν¨μ
* @returns μΊμ λ
Έλ
* @example
* const styleCache = cache(() => []);
*/
export function cache<A: Iterable<mixed>, T>(fn: (...A) => T): (...A) => T {
return function () {
const dispatcher = ReactCurrentCache.current; // νμ¬ μΊμ λμ€ν¨μ² κ°μ Έμ€κΈ°
if (!dispatcher) {
// λμ€ν¨μ²κ° μμΌλ©΄ μΊμ± μμ΄ ν¨μ μ€ν (ν΄λΌμ΄μΈνΈ μ»΄ν¬λνΈμμ μ€νλ κ²½μ°)
return fn.apply(null, arguments);
}
// μΊμ λ£¨νΈ κ°μ Έμ€κΈ°
const fnMap: WeakMap<any, CacheNode<T>> = dispatcher.getCacheForType(
createCacheRoot,
); // μΊμ 루νΈκ° μμΌλ©΄ μλ‘ μμ±
const fnNode = fnMap.get(fn); // ν¨μμ λν μΊμ λ
Έλ κ°μ Έμ€κΈ°
let cacheNode: CacheNode<T>; // μΊμ λ
Έλ
if (fnNode === undefined) {
cacheNode = createCacheNode(); // μΊμ λ
Έλκ° μμΌλ©΄ μλ‘ μμ±
fnMap.set(fn, cacheNode); // ν¨μμ μΊμ λ
Έλ μ€μ
} else {
cacheNode = fnNode; // μ‘΄μ¬νλ μΊμ λ
Έλ μ¬μ©
}
for (let i = 0, l = arguments.length; i < l; i++) { // ν¨μκ° λ°μ λͺ¨λ μΈμλ€μ λν΄ μν
const arg = arguments[i]; // νμ¬ μ²λ¦¬μ€μΈ μΈμ
if (
typeof arg === 'function' ||
(typeof arg === 'object' && arg !== null)
// νμ¬ μΈμκ° κ°μ²΄ λλ ν¨μμΈ κ²½μ°
let objectCache = cacheNode.o; // κ°μ²΄λ₯Ό μν μΊμ λ§΅ (WeakMap)
if (objectCache === null) {
cacheNode.o = objectCache = new WeakMap(); // κ°μ²΄ μΊμ λ§΅μ΄ μμΌλ©΄ μλ‘ μμ±
}
const objectNode = objectCache.get(arg); // νμ¬ κ°μ²΄ μΈμμ λν μΊμ λ
Έλ μ‘°ν
if (objectNode === undefined) {
cacheNode = createCacheNode(); // μΊμ λ
Έλκ° μμΌλ©΄ μλ‘ μμ±
objectCache.set(arg, cacheNode); // μ μΊμ λ
Έλλ₯Ό κ°μ²΄ μΊμ λ§΅μ μΆκ°
} else {
cacheNode = objectNode; // μ‘΄μ¬νλ μΊμ λ
Έλ μ¬μ©
}
} else {
// νμ¬ μΈμκ° μμ νμ
μΈ κ²½μ°
let primitiveCache = cacheNode.p; // μμ νμ
μ μν μΊμ λ§΅ (Map)
if (primitiveCache === null) {
cacheNode.p = primitiveCache = new Map(); // μμ νμ
μΊμ λ§΅μ΄ μμΌλ©΄ μλ‘ μμ±
}
const primitiveNode = primitiveCache.get(arg); // νμ¬ μμ νμ
μΈμμ λν μΊμ λ
Έλ μ‘°ν
if (primitiveNode === undefined) {
cacheNode = createCacheNode(); // μΊμ λ
Έλκ° μμΌλ©΄ μλ‘ μμ±
primitiveCache.set(arg, cacheNode); // μ μΊμ λ
Έλλ₯Ό μμ νμ
μΊμ λ§΅μ μΆκ°
} else {
cacheNode = primitiveNode; // μ‘΄μ¬νλ μΊμ λ
Έλ μ¬μ©
}
}
}
if (cacheNode.s === TERMINATED) { // μΊμλ κ²°κ³Όκ° μμΌλ©΄
return cacheNode.v; // μΊμλ κ²°κ³Ό λ°ν
}
if (cacheNode.s === ERRORED) { // μΊμλ μλ¬κ° μμΌλ©΄
throw cacheNode.v; // μΊμλ μλ¬ μ¬λ°μ
}
try {
// μΊμλ κ²°κ³Όκ° μμΌλ©΄ ν¨μ μ€ν λ° κ²°κ³Ό μΊμ±
const result = fn.apply(null, arguments); // ν¨μ μ€ν
const terminatedNode: TerminatedCacheNode<T> = (cacheNode: any); // μΊμ λ
Έλλ₯Ό μλ£ μνλ‘ λ³κ²½
terminatedNode.s = TERMINATED; // μλ£ μνλ‘ λ³κ²½
terminatedNode.v = result; // μΊμλ κ²°κ³Ό μ€μ
return result; // κ²°κ³Ό λ°ν
} catch (error) {
// μλ¬ λ°μ μ μλ¬ μΊμ±
const erroredNode: ErroredCacheNode<T> = (cacheNode: any);
erroredNode.s = ERRORED;
erroredNode.v = error;
throw error;
}
};
}
κ°λ¨ ν μμλ₯Ό λ€μ΄λ³΄λ©΄ μλμ κ°λ€.
import { cache } from 'react';
// μμλ‘ μ¬μ©ν κ³μ° ν¨μ
function expensiveCalculation(x, y) {
console.log('Calculating result...');
return x + y;
}
// cache ν¨μλ₯Ό μ¬μ©νμ¬ κ³μ° ν¨μλ₯Ό λν
const cachedCalculation = cache(expensiveCalculation);
// 첫 λ²μ§Έ νΈμΆ - κ³μ° ν¨μκ° μ€νλ©λλ€.
const result1 = cachedCalculation(2, 3); // λ‘κ·Έ: "Calculating result..."
// λ λ²μ§Έ νΈμΆ - κ°μ μΈμλ‘ νΈμΆλλ―λ‘ μΊμλ κ²°κ³Ό λ°ν
const result2 = cachedCalculation(2, 3); // κ²°κ³Όλ 5μ΄λ©° λ‘κ·Έλ λνλμ§ μμ
// λ€λ₯Έ μΈμλ‘ νΈμΆ - λ€μ κ³μ° ν¨μκ° μ€νλ©λλ€.
const result3 = cachedCalculation(4, 5); // λ‘κ·Έ: "Calculating result..."
console.log(result1); // 5
console.log(result2); // 5
console.log(result3); // 9
μ΄λ° μμλ₯Ό ν΅ν΄μλ μΊμμ μν μ΄ μ΄ν΄κ° λ°λ‘ λλλ°, dream-css-toolμμμ μ©λλ μ΄μ§ λ€λ₯΄λ€. λ§€λ² μ°μ°μ μ μ½νλ€λ λλ 보λ€λ, cacheλ₯Ό μ΄μ©ν΄μ μ°Έμ‘°κ°μ μ μ§νλ λ°©μμΌλ‘ μ¬μ©νκ³ μλ€.
// StyledRegistry.js
export const cache = React.cache(() => {
return [];
});
// ...
function StyleRegistry({ children }) {
const collectedStyles = cache();
...
// styled.js
import { cache } from './components/StyleRegistry';
...
let collectedStyles = cache();
collectedStyles.push(styleContent);
...
κ°λ¨ν μλ κ°μ λ°©μμΌλ‘ λμνλ€κ³ 보면 λλ€.
const arr1 = [1, 2, 3];
const arr2 = arr1;
// arr1κ³Ό arr2κ° λμΌν λ°°μ΄μ μ°Έμ‘°νκ³ μλμ§ νμΈ
console.log(arr1 === arr2); // true
// arr1 λ°°μ΄μ μμλ₯Ό μΆκ°νλ©΄ arr2μμλ λμΌν λ³κ²½μ΄ λ°μλ©λλ€.
arr1.push(4);
console.log(arr2); // [1, 2, 3, 4]
κ²°λ‘
μμ±μ€μ΄λ€.