๐จ Vanilla Extract๋ก Next.js ์คํ์ผ๋ง ๋ง์คํฐํ๊ธฐ
โข2026๋ 2์ 8์ผ ์ผ์์ผ
CSS-in-JS์ ๋๋ ๋ง์ ์๋ก์ด ํด๊ฒฐ์ฑ
Vanilla Extract๋ ์ ๋ก ๋ฐํ์ CSS-in-TypeScript ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค. ๋น๋ ํ์์ ์ ์ CSS ํ์ผ์ ์์ฑํ๋ฉด์๋ TypeScript์ ํ์ ์์ ์ฑ์ ์๋ฒฝํ๊ฒ ์ ๊ณตํฉ๋๋ค. Styled-components๋ Emotion๊ณผ๋ ๋ฌ๋ฆฌ ๋ฐํ์ ์ค๋ฒํค๋๊ฐ ์ ํ ์์ต๋๋ค.
ํ๋ ์น ๊ฐ๋ฐ์์ CSS-in-JS๋ ์ปดํฌ๋ํธ ๊ธฐ๋ฐ ์คํ์ผ๋ง์ ํ์ค์ด ๋์์ง๋ง, ๋ฐํ์ ์ฑ๋ฅ ๋ฌธ์ ๋ผ๋ ๊ทผ๋ณธ์ ์ธ ํ๊ณ๋ฅผ ์๊ณ ์์์ต๋๋ค. Vanilla Extract๋ ์ด๋ฌํ ๋๋ ๋ง๋ฅผ ํด๊ฒฐํ๋ฉฐ, ํนํ Next.js์ ๊ฐ์ SSR ํ๊ฒฝ์์ ํ์ํ ์ฑ๋ฅ์ ๋ฐํํฉ๋๋ค.
Next.js ํ๊ฒฝ์์์ Vanilla Extract ๋์
ํจํค์ง ์ค์น ๋ฐ ์์กด์ฑ ๊ตฌ์ฑ
Vanilla Extract๋ฅผ Next.js ํ๋ก์ ํธ์ ํตํฉํ๊ธฐ ์ํด์๋ ๋ค์ ํจํค์ง๋ค์ด ํ์ํฉ๋๋ค.
# Next.js ํ๋ฌ๊ทธ์ธ (๊ฐ๋ฐ ์์กด์ฑ)
npm install --save-dev @vanilla-extract/next-plugin
# ํต์ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค
npm install @vanilla-extract/css @vanilla-extract/sprinkles @vanilla-extract/recipesNext.js ๋น๋ ์์คํ ํตํฉ
Next.js์์ Vanilla Extract๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด ๋ฐ๋์ ์ ์ฉ ํ๋ฌ๊ทธ์ธ์ ์ค์ ํด์ผ
ํฉ๋๋ค. ์ด ํ๋ฌ๊ทธ์ธ์ ๋น๋ ํ์์ .css.ts ํ์ผ์ ์ ์ CSS๋ก ๋ณํํ๋ ํต์ฌ
์ญํ ์ ๋ด๋นํฉ๋๋ค.
// next.config.js
const { createVanillaExtractPlugin } = require("@vanilla-extract/next-plugin");
const withVanillaExtract = createVanillaExtractPlugin();
/** @type {import('next').NextConfig} */
const nextConfig = {};
module.exports = withVanillaExtract(nextConfig);๋ณตํฉ ํ๋ฌ๊ทธ์ธ ํ๊ฒฝ์์์ ์ค์
MDX๋ ๋ค๋ฅธ Next.js ํ๋ฌ๊ทธ์ธ๊ณผ ํจ๊ป ์ฌ์ฉํ ๋๋ ํ๋ฌ๊ทธ์ธ ์กฐํฉ ์์๊ฐ ์ค์ํฉ๋๋ค.
// next.config.js
const { createVanillaExtractPlugin } = require("@vanilla-extract/next-plugin");
const withVanillaExtract = createVanillaExtractPlugin();
const withMDX = require("@next/mdx")({
extension: /\.mdx$/,
});
/** @type {import('next').NextConfig} */
const nextConfig = {};
// ํ๋ฌ๊ทธ์ธ ์์๊ฐ ์ค์ํฉ๋๋ค
module.exports = withVanillaExtract(withMDX(nextConfig));๊ธฐ๋ณธ ์คํ์ผ๋ง ์์คํ ๊ตฌ์ถ
TypeScript ๊ธฐ๋ฐ ์คํ์ผ ์ ์
Vanilla Extract์ ํ์ฌ์ .css.ts ํ์ผ์์ TypeScript๋ก ์คํ์ผ์ ์ ์ํ๋ ๊ฒ์
๋๋ค. ์ด๋ฅผ ํตํด ์ปดํ์ผ ํ์์ ํ์
์์ ์ฑ์ ๋ณด์ฅ๋ฐ์ ์ ์์ต๋๋ค.
// styles.css.ts
import { style } from "@vanilla-extract/css";
export const container = style({
padding: 10,
backgroundColor: "blue",
borderRadius: 4,
color: "white",
});React ์ปดํฌ๋ํธ ํตํฉ
Vanilla Extract์์ ์์ฑ๋ ์คํ์ผ์ ์ผ๋ฐ์ ์ธ CSS ํด๋์ค๋ช
์ผ๋ก ์ฌ์ฉํ ์
์์ต๋๋ค. React ์ปดํฌ๋ํธ์์๋ className prop์ ์ง์ ํ ๋นํ์ฌ ์ฌ์ฉํฉ๋๋ค.
// app.tsx
import { container } from "./styles.css.ts";
export default function App() {
return <div className={container}>Hello Vanilla Extract!</div>;
}๊ธฐ์ ๊ธ ํ ๋ง ์์คํ ์ค๊ณ
CSS ๋ณ์ ๊ธฐ๋ฐ ํ ๋ง ๊ตฌ์กฐ
Vanilla Extract์ ํต์ฌ ๊ฐ์ ์ CSS ๋ณ์๋ฅผ ํ์ฉํ ์ฒด๊ณ์ ์ธ ํ ๋ง ์์คํ ์ ๋๋ค. ์ด๋ฅผ ํตํด ๋คํฌ ๋ชจ๋, ๋ธ๋๋ ์ปค์คํ ๋ง์ด์ง ๋ฑ์ ์ํํ๊ฒ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
// theme.css.ts
import { createTheme } from "@vanilla-extract/css";
export const [themeClass, vars] = createTheme({
color: {
brand: "blue",
accent: "green",
background: "white",
text: "black",
},
font: {
body: "Arial, sans-serif",
heading: "Georgia, serif",
},
});
export const darkTheme = createTheme(vars, {
color: {
brand: "lightblue",
accent: "lightgreen",
background: "black",
text: "white",
},
font: {
body: "Arial, sans-serif",
heading: "Georgia, serif",
},
});ํ ๋ง ๊ณ์ฝ(Contract) ๊ธฐ๋ฐ ์ํคํ ์ฒ
ํ ๋ง ๊ณ์ฝ์ CSS ์์ด ํ ๋ง ๊ตฌ์กฐ๋ง ์ ์ํ๋ ๋ฐฉ์์ ๋๋ค. ์ด ์ ๊ทผ๋ฒ์ ๋ฒ๋ค ๋ถํ ์ต์ ํ์ ์ฌ๋ฌ ํ ๋ง์ ๋ ๋ฆฝ์ ๊ด๋ฆฌ๋ฅผ ๊ฐ๋ฅํ๊ฒ ํฉ๋๋ค.
// contract.css.ts - ํ
๋ง ๊ตฌ์กฐ๋ง ์ ์
import { createThemeContract } from "@vanilla-extract/css";
export const vars = createThemeContract({
color: {
brand: null,
accent: null,
background: null,
text: null,
},
font: {
body: null,
heading: null,
},
});// light-theme.css.ts
import { createTheme } from "@vanilla-extract/css";
import { vars } from "./contract.css.ts";
export const lightTheme = createTheme(vars, {
color: {
brand: "blue",
accent: "green",
background: "white",
text: "black",
},
font: {
body: "Arial, sans-serif",
heading: "Georgia, serif",
},
});์ ํธ๋ฆฌํฐ ํผ์คํธ CSS ์์คํ
Sprinkles์ ๊ธฐ์ ์ ์ฐ์์ฑ
Sprinkles๋ Tailwind CSS์ ๋น์ทํ ์ ํธ๋ฆฌํฐ ํผ์คํธ ์ ๊ทผ๋ฒ์ ์ ๊ณตํ์ง๋ง, ๋น๋ ํ์ ์ต์ ํ์ ์์ ํ ํ์ ์์ ์ฑ์ ๋์์ ์ ๊ณตํฉ๋๋ค.
์์์ CSS ํด๋์ค ์์คํ ๊ตฌ์ฑ
// sprinkles.css.ts
import { defineProperties, createSprinkles } from "@vanilla-extract/sprinkles";
// ์คํ์ด์ฑ ๊ฐ๋ค ์ ์
const space = {
none: 0,
small: "4px",
medium: "8px",
large: "16px",
xlarge: "32px",
};
// ์์ ํ๋ ํธ ์ ์
const colors = {
"blue-50": "#eff6ff",
"blue-100": "#dbeafe",
"blue-200": "#bfdbfe",
"gray-100": "#f3f4f6",
"gray-700": "#374151",
"gray-800": "#1f2937",
"gray-900": "#111827",
};
// ๋ฐ์ํ ์์ฑ๋ค ์ ์
const responsiveProperties = defineProperties({
conditions: {
mobile: {},
tablet: { "@media": "screen and (min-width: 768px)" },
desktop: { "@media": "screen and (min-width: 1024px)" },
},
defaultCondition: "mobile",
properties: {
display: ["none", "flex", "block", "inline"],
flexDirection: ["row", "column"],
justifyContent: [
"stretch",
"flex-start",
"center",
"flex-end",
"space-around",
"space-between",
],
alignItems: ["stretch", "flex-start", "center", "flex-end"],
paddingTop: space,
paddingBottom: space,
paddingLeft: space,
paddingRight: space,
},
shorthands: {
padding: ["paddingTop", "paddingBottom", "paddingLeft", "paddingRight"],
paddingX: ["paddingLeft", "paddingRight"],
paddingY: ["paddingTop", "paddingBottom"],
placeItems: ["justifyContent", "alignItems"],
},
});
// ์์ ์์ฑ๋ค ์ ์ (๋คํฌ๋ชจ๋ ์ง์)
const colorProperties = defineProperties({
conditions: {
lightMode: {},
darkMode: { "@media": "(prefers-color-scheme: dark)" },
},
defaultCondition: "lightMode",
properties: {
color: colors,
background: colors,
},
});
// ์ต์ข
sprinkles ํจ์ ์์ฑ
export const sprinkles = createSprinkles(responsiveProperties, colorProperties);
export type Sprinkles = Parameters<typeof sprinkles>[0];์ค์ ํ์ฉ ์์
// styles.css.ts
import { sprinkles } from "./sprinkles.css.ts";
export const responsiveContainer = sprinkles({
display: "flex",
paddingX: "small",
// ๋ฐ์ํ ์คํ์ผ๋ง
flexDirection: {
mobile: "column",
desktop: "row",
},
// ๋คํฌ๋ชจ๋ ๋์
background: {
lightMode: "blue-50",
darkMode: "gray-700",
},
});Sprinkles์ ์ฅ์
- ํ์ ์์ ์ฑ: ์๋ชป๋ ์์ฑ๊ฐ ์ฌ์ฉ ์ ์ปดํ์ผ ์๋ฌ
- ๋ฒ๋ค ์ต์ ํ: ์ฌ์ฉ๋์ง ์๋ ์คํ์ผ์ ์๋์ผ๋ก ์ ๊ฑฐ
- ๋ฐ์ํ ๋์์ธ: ๋ฏธ๋์ด ์ฟผ๋ฆฌ๋ฅผ ๊ฐ๋จํ๊ฒ ์ฒ๋ฆฌ
์ปดํฌ๋ํธ ๋ณํ ๊ด๋ฆฌ ์์คํ
Recipe ํจํด์ ํ์์ฑ
Recipe ์์คํ ์ ์ปดํฌ๋ํธ์ ๋ค์ํ ๋ณํ(variant)์ ์ฒด๊ณ์ ์ผ๋ก ๊ด๋ฆฌํ๋ ํจํด์ ๋๋ค. ๋๊ท๋ชจ ๋์์ธ ์์คํ ์์ ์ปดํฌ๋ํธ์ ์ผ๊ด์ฑ๊ณผ ์ ์ฐ์ฑ์ ๋์์ ํ๋ณดํ ์ ์์ต๋๋ค.
๊ธฐ๋ณธ Recipe ๊ตฌ์กฐ ์ค๊ณ
// button.css.ts
import { recipe } from "@vanilla-extract/recipes";
export const button = recipe({
base: {
borderRadius: 6,
border: "none",
cursor: "pointer",
fontWeight: "bold",
},
variants: {
color: {
neutral: { background: "whitesmoke", color: "black" },
brand: { background: "blueviolet", color: "white" },
accent: { background: "slateblue", color: "white" },
},
size: {
small: { padding: "8px 12px", fontSize: "14px" },
medium: { padding: "12px 16px", fontSize: "16px" },
large: { padding: "16px 24px", fontSize: "18px" },
},
rounded: {
true: { borderRadius: 999 },
},
},
// ์ฌ๋ฌ variant ์กฐํฉ์ ๋ํ ํน๋ณํ ์คํ์ผ
compoundVariants: [
{
variants: {
color: "neutral",
size: "large",
},
style: {
background: "ghostwhite",
},
},
],
defaultVariants: {
color: "accent",
size: "medium",
},
});Sprinkles์ Recipe ํตํฉ ํจํด
๊ณ ๊ธ ๋์์ธ ์์คํ ์์ ๊ฐ์ฅ ํจ๊ณผ์ ์ธ ์ ๊ทผ๋ฒ์ Sprinkles ์ ํธ๋ฆฌํฐ์ Recipe ์์คํ ์ ์ฌ๋ฆฌ์ ๋ง๊ฒ ๊ฒฐํฉํ๋ ๊ฒ์ ๋๋ค.
// advanced-button.css.ts
import { recipe } from "@vanilla-extract/recipes";
import { reset } from "./reset.css.ts";
import { sprinkles } from "./sprinkles.css.ts";
export const advancedButton = recipe({
base: [
reset, // CSS ๋ฆฌ์
sprinkles({
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
}),
],
variants: {
color: {
neutral: sprinkles({ background: "gray-100" }),
brand: sprinkles({ background: "blue-100" }),
accent: sprinkles({ background: "blue-200" }),
},
size: {
small: sprinkles({ paddingX: "small", paddingY: "small" }),
medium: sprinkles({ paddingX: "medium", paddingY: "medium" }),
large: sprinkles({ paddingX: "large", paddingY: "large" }),
},
},
defaultVariants: {
color: "accent",
size: "medium",
},
});๋ฐํ์ ์คํ์ผ ์ ์ด
๋์ ๋์์ธ ์์คํ ๊ตฌ์ถ
์ฌ์ฉ์ ์ธํฐ๋์
์ด๋ API ์๋ต์ ๋ฐ๋ผ ๋ฐํ์ ์คํ์ผ ์
๋ฐ์ดํธ๊ฐ ํ์ํ ๊ฒฝ์ฐ๊ฐ
์์ต๋๋ค. vanilla-extract๋ @vanilla-extract/dynamic ํจํค์ง๋ฅผ ํตํด ํ์
์์ ํ ๋์ ์คํ์ผ๋ง์ ์ง์ํฉ๋๋ค.
// app.tsx
import { assignInlineVars } from "@vanilla-extract/dynamic";
import { container, themeVars } from "./theme.css.ts";
interface ContainerProps {
brandColor: string;
fontFamily: string;
}
const Container = ({ brandColor, fontFamily }: ContainerProps) => (
<section
className={container}
style={assignInlineVars(themeVars, {
color: { brand: brandColor },
font: { body: fontFamily },
})}
>
๋์ ์คํ์ผ๋ง์ด ์ ์ฉ๋ ์ปจํ
์ด๋
</section>
);๋ช ๋ นํ ์คํ์ผ ์ ์ด
DOM API๋ฅผ ํ์ฉํ์ฌ CSS ๋ณ์๋ฅผ ์ง์ ์ ์ดํ๋ ๋ฐฉ๋ฒ์ ๋๋ค.
// app.ts
import { setElementVars } from "@vanilla-extract/dynamic";
import { brandColor, textColor } from "./styles.css.ts";
const el = document.getElementById("myElement");
setElementVars(el, {
[brandColor]: "pink",
[textColor]: null, // null ๊ฐ์ ํ ๋น๋์ง ์์
});ํธ๋ฌ๋ธ์ํ ๊ฐ์ด๋
์ฃผ์ ์ด์ ๋ฐ ํด๊ฒฐ ๋ฐฉ๋ฒ
์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํตํฉ ์ด์
์ฆ์
์ธ๋ถ ๋์์ธ ์์คํ ์ปดํฌ๋ํธ์ ์คํ์ผ์ด ๋๋ฝ๋๊ฑฐ๋ ๋ฐํ์ ์๋ฌ๊ฐ ๋ฐ์ํ๋ ๊ฒฝ์ฐ์ ๋๋ค.
ํด๊ฒฐ ๋ฐฉ๋ฒ: Next.js ์ค์ ์์ transpilePackages ์ต์
์ ํ์ฉํ์ฌ ํด๋น ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํธ๋์คํ์ผ ๋์์ ํฌํจ์ํต๋๋ค.
// next.config.js
const nextConfig = {
transpilePackages: ["@company/design-system"],
};
module.exports = withVanillaExtract(nextConfig);Jest ํ ์คํธ ํ๊ฒฝ ์ถฉ๋
Jest์ ๊ธฐ๋ณธ CSS ๋ชจํน ์ค์ ์ด vanilla-extract์ .css.ts ํ์ผ๊ณผ ์ถฉ๋ํ์ฌ ํ
์คํธ
์คํ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.
ํด๊ฒฐ ๋ฐฉ๋ฒ 1: vanilla-extract ์ ์ฉ Jest transformer ์ ์ฉ
// jest.config.js
{
"transform": {
"\\.css\\.ts$": "@vanilla-extract/jest-transform"
}
}ํด๊ฒฐ ๋ฐฉ๋ฒ 2: ์ ๊ท ํํ์์ ํตํด ๋ชจํน ๋์์ ์ ๋ฐํ๊ฒ ์ ์ด
{
"jest": {
"moduleNameMapper": {
"legacy-styles/.*\\.css$": "<rootDir>/styleMock.js"
}
}
}์ฑ๋ฅ ์ต์ ํ ์ ๋ต
CSS ํด๋์ค๋ช ์ต์ ํ
๋น๋ ํ๊ฒฝ์ ๋ฐ๋ผ CSS ํด๋์ค๋ช ์์ฑ ์ ๋ต์ ์กฐ์ ํ์ฌ ๋ฒ๋ค ํฌ๊ธฐ๋ฅผ ํจ๊ณผ์ ์ผ๋ก ์ต์ ํํ ์ ์์ต๋๋ค.
// next.config.js
const withVanillaExtract = createVanillaExtractPlugin({
identifiers: "short", // 'hnw5tz3' ๊ฐ์ ์งง์ ํด์
});
// ๊ฐ๋ฐ ํ๊ฒฝ์์๋ ๋๋ฒ๊น
์ ์ํด
const withVanillaExtract = createVanillaExtractPlugin({
identifiers: "debug", // 'myfile_mystyle_hnw5tz3' ๊ฐ์ ์ค๋ช
์ ์ด๋ฆ
});๋ฒ๋ค ํฌ๊ธฐ ์ต์ ํ
๋ฐ์ํ ๋ฐฐ์ด ํ๊ธฐ๋ฒ์ ํ์ฉํ์ฌ ์ฝ๋ ๊ฐ๋ ์ฑ๊ณผ ๋ฒ๋ค ํจ์จ์ฑ์ ๋์์ ํฅ์์ํฌ ์ ์์ต๋๋ค.
// sprinkles.css.ts
const responsiveProperties = defineProperties({
conditions: {
mobile: {},
tablet: { "@media": "screen and (min-width: 768px)" },
desktop: { "@media": "screen and (min-width: 1024px)" },
},
defaultCondition: "mobile",
responsiveArray: ["mobile", "tablet", "desktop"], // ์์ ์ ์
});
// ์ฌ์ฉ๋ฒ
sprinkles({
flexDirection: ["column", "row", "row"], // mobile, tablet, desktop ์์
});์ค๋ฌด ์ ์ฉ ๊ฐ์ด๋
์ปดํฌ๋ํธ Props ๋ถ๋ฆฌ ํจํด
Sprinkles ์คํ์ผ ์์ฑ๊ณผ ์ปดํฌ๋ํธ ๊ณ ์ ์์ฑ์ ์๋์ผ๋ก ๋ถ๋ฆฌํ์ฌ ๊ด๋ฆฌํ๋ ๊ณ ๊ธ ํจํด์ ๋๋ค.
// ์คํ๋งํด์ค ์์ฑ์ธ์ง ํ์ธ
sprinkles.properties.has('paddingX'); // boolean ๋ฐํ
// ์ปดํฌ๋ํธ์์ ํ์ฉ
function MyComponent(props) {
const sprinkleProps = {};
const otherProps = {};
Object.keys(props).forEach(key => {
if (sprinkles.properties.has(key)) {
sprinkleProps[key] = props[key];
} else {
otherProps[key] = props[key];
}
});
return (
<div
className={sprinkles(sprinkleProps)}
{...otherProps}
>
{props.children}
</div>
);
}ํคํ๋ ์ ์ ๋๋ฉ์ด์ ๊ณ ๊ธ ๊ธฐ๋ฒ
import { createVar, keyframes } from "@vanilla-extract/css";
const angle = createVar({
syntax: "<angle>",
inherits: false,
initialValue: "0deg",
});
export const spin = keyframes({
"0%": {
vars: { [angle]: "0deg" },
},
"100%": {
vars: { [angle]: "360deg" },
},
});CSS ๊ณ์ฐ ํจ์ ์ต์ ํ
vanilla-extract์ @vanilla-extract/css-utils ํจํค์ง๋ CSS calc() ํจ์๋ฅผ ํ์
์์ ํ๊ฒ ์์ฑํ ์ ์๋ ๊ฐ๋ ฅํ ์ ํธ๋ฆฌํฐ๋ฅผ ์ ๊ณตํฉ๋๋ค.
๊ธฐ๋ณธ calc ์ฐ์ฐ
import { calc } from "@vanilla-extract/css-utils";
// ์ง์ ๋ฉ์๋ ์ฌ์ฉ
const styles = {
height: calc.multiply("var(--grid-unit)", 2),
width: calc.divide("100vw", 3),
margin: calc.negate("var(--space-large)"),
};์ฒด์ด๋ API ํ์ฉ
๋ณต์กํ ๊ณ์ฐ์์ ์ฒด์ธ์ผ๋ก ์ฐ๊ฒฐํ์ฌ ๊ฐ๋ ์ฑ ์๊ฒ ์์ฑํ ์ ์์ต๋๋ค.
import { calc } from "@vanilla-extract/css-utils";
const complexCalculation = style({
// ์ฌ๋ฌ ์ฐ์ฐ์ ์ฒด์ด๋
marginTop: calc("var(--space-large)").divide(2).negate().toString(),
// CSS Grid ๊ณ์ฐ
gridTemplateColumns: calc("100%")
.subtract("var(--sidebar-width)")
.subtract("var(--padding)")
.toString(),
});์ค์ฒฉ ์ฐ์ฐ ๊ฐ์ (v1.8.0+)
์ต์ ๋ฒ์ ์์๋ ์ค์ฒฉ๋ calc ์ฐ์ฐ์ด ํฌ๊ฒ ๊ฐ์ ๋์ด ๋ ์ง๊ด์ ์ธ ์ฝ๋ ์์ฑ์ด ๊ฐ๋ฅํฉ๋๋ค.
// ์ด์ ๋ฒ์ : toString() ๋ช
์์ ํธ์ถ ํ์
const oldStyle = {
width: calc("10px").add(calc("20px").subtract("4px").toString()),
};
// ๊ฐ์ ๋ ๋ฒ์ : ์๋ ์ฒ๋ฆฌ
const newStyle = {
width: calc("10px").add(calc("20px").subtract("4px")),
};ํ ๋ง ๋ณ์์์ ๊ฒฐํฉ
ํ ๋ง ์์คํ ๊ณผ ๊ฒฐํฉํ์ฌ ๋์ ๊ณ์ฐ์ ์ํํ ์ ์์ต๋๋ค.
import { calc } from "@vanilla-extract/css-utils";
import { vars } from "./theme.css.ts";
const responsiveContainer = style({
// ํ
๋ง ๋ณ์๋ฅผ ํ์ฉํ ๋ฐ์ํ ๊ณ์ฐ
padding: calc(vars.space.medium).multiply(2),
// ๋ทฐํฌํธ ๊ธฐ๋ฐ ๊ณ์ฐ
maxWidth: calc("100vw").subtract(vars.space.large),
// ๋ณตํฉ ๊ณ์ฐ์
fontSize: calc(vars.fontSize.base)
.multiply("var(--scale-factor, 1)")
.add("2px"),
});์ ์ฉํ calc ํจํด
์ค๋ฌด์์ ์์ฃผ ์ฌ์ฉ๋๋ ์ ์ฉํ ํจํด๋ค์ ๋๋ค.
import { calc } from "@vanilla-extract/css-utils";
// ์ผํฐ๋ง ๊ณ์ฐ
const centeredElement = style({
left: "50%",
transform: `translateX(${calc("50%").negate()})`,
});
// ๋น์จ ๊ธฐ๋ฐ ๊ณ์ฐ
const aspectRatioContainer = style({
width: "100%",
height: calc("100%").multiply(9).divide(16), // 16:9 ๋น์จ
});
// ๊ทธ๋ฆฌ๋ ๊ฐญ ๊ณ์ฐ
const gridItem = style({
width: calc("100%").subtract(calc(vars.grid.gap).multiply(2)).divide(3),
});
// ๋ค์ค ๋จ์ ๊ณ์ฐ
const fluidTypography = style({
fontSize: calc("1rem").add(calc("2vw")).add(calc("0.5em")),
});์ด๋ฌํ calc ์ ํธ๋ฆฌํฐ๋ฅผ ํ์ฉํ๋ฉด ๋ณต์กํ CSS ๊ณ์ฐ์ ํ์
์์ ํ๊ฒ ๊ด๋ฆฌํ๋ฉด์๋ ์ฝ๋์ ๊ฐ๋
์ฑ๊ณผ ์ ์ง๋ณด์์ฑ์ ํฌ๊ฒ ํฅ์์ํฌ ์ ์์ต๋๋ค.
๊ฒฐ๋ก
vanilla-extract๋ Next.js 14+ ํ๊ฒฝ์์ ์ ๋ก ๋ฐํ์ CSS-in-TypeScript ์๋ฃจ์ ์ ์๋ก์ด ํ์ค์ ์ ์ํฉ๋๋ค. ์ปดํ์ผ ํ์ ์ต์ ํ์ TypeScript์ ํ์ ์์ ์ฑ์ ๊ฒฐํฉํ์ฌ, ๋๊ท๋ชจ ํ๋ก๋์ ์ ํ๋ฆฌ์ผ์ด์ ์์ ์๊ตฌ๋๋ ์ฑ๋ฅ๊ณผ ๊ฐ๋ฐ์ ๊ฒฝํ์ ๋ชจ๋ ๋ง์กฑ์ํฌ ์ ์์ต๋๋ค.
Afaik ยฉ 2025
์ธ๋ถ ๋งํฌ