- 브랜드와 제품의 일관성을 유지하고, 팀 간의 협업을 향상시켜 더 빠르고 효과적으로 UI를 디자인하고 구축하는데 도움이 됨
- 사용자 인터페이스를 구축하는데 필요한 모든 요소를 정의하는 포괄적인 문서와 가이드라인 집합
ex) 컴포넌트, 패턴, 디자인 원칙, 사용 가이드라인, 툴, 스타일 가이드
UI 라이브러리는 구체적인 코드를 포함하는, 재사용 가능한 UI컴포넌트들의 모음
코드의 재사용성을 향상시키고, 팀 간의 협업을 간소화하여, 제품의 UI를 빠르게 구축하는데 도움이 된다.
ex) 실제 개발에서 사용할 수 있는 버튼, 입력 폼, 카드 등의 구현체
(CSS Variable) -> CSS에서 사용하는 변수
:root {
--main-bg-color: brown;
}
element {
background-color: var(--main-bg-color);
}
// 대안 패턴
.three {
background-color : var(
--my-var, //첫번째 값,
--my-background, //첫번째 값이 없으면 두번째인 여기로
pink // 1,2 가 없으면 여기로
)
}
파일 하나하나, 특정 기능을 갖는 작은 코드 단위, 재사용성의 유지 및 관리, 네임스페이스 관리 등의 장점이 있음
ex ) CommonsJS, ES Module
여러개의 파일과 모듈을 하나, 몇 개의 파일로 결합하는 도구 (번들링, 트리쉐이킹, 트랜스파일링, 로더와 플러그인)
네트워크 통신으로 모듈을 불러와야 하는데 하나하나 불러오면 매우 느리고 도중에 인터넷이 멈추면 문제가 발생.
속도 개선, 효과적으로 모듈 합치기, 불필요한 코드 제거
ex) webpack, rollup, vite, esbuild
- 요새는 vite로 넘어가는 추세
브라우저
타겟이 명확하다.
브라우저에도 동작하는 것이니 JS로 만들어야 한다.
라이브러리
타겟이 상대적으로 불명확하다. (어떤 모듈번들러인지, 누가 사용할 것인지, 어느 환경인지)
어떤 언어로 쓸 것인지 ?(TS, JS)
라이브러리에서 고려해야 할것
- GO 언어로 작성되어서 빠름.
- 코드 파싱, 출력과 소스맵 생성을 모두 병렬로 처리함
- 불필요한 데이터 변환과 할당이
- Vite의 빠른 속도를 위해 개발 시 사전 번들링을 esbuild 기반에서 하고 있다. (프로덕트 빌드에서는 rollup 이유는 esbuild가 다 대응하지 못하기 때문에)
"build": "esbuild src/index.js --bundle --outfile=dist/index.js"
"build": "esbuild src/index.js --minify --format=cjs --bundle --outfile=dist/index.js"
import esbuild from "esbuild";
1. esm
esbuild.build({
entryPoints: ["src/index.js"],
bundle: true,
minify: true,
sourcemap: true,
outdir: "dist",
format: "esm",
});
2.cjs
esbuild.build({
entryPoints: ["src/index.js"],
bundle: true,
minify: true,
sourcemap: true,
outdir: "dist",
format: "cjs",
outExtension: {
".js": ".cjs",
},
});
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "npm run build:js",
"build:js": "node build.js"
},
import esbuild from "esbuild";
Promise.all([
esbuild.build({
entryPoints: ["src/index.js"],
bundle: true,
minify: true,
sourcemap: true,
outdir: "dist",
format: "esm",
}),
esbuild.build({
entryPoints: ["src/index.js"],
bundle: true,
minify: true,
sourcemap: true,
outdir: "dist",
format: "cjs",
outExtension: {
".js": ".cjs",
},
}),
]).catch(() => {
console.error("build faild");
process.exit(1);
});
import esbuild from "esbuild";
const baseConfig = {
bundle: true,
minify: true,
sourcemap: true,
outdir: "dist",
};
Promise.all([
esbuild.build({
...baseConfig,
format: "esm",
}),
esbuild.build({
...baseConfig,
format: "cjs",
outExtension: {
".js": ".cjs",
},
}),
]).catch(() => {
console.error("build faild");
process.exit(1);
});
import esbuild from "esbuild";
const dev = process.argv.includes("--dev");
const minify = !dev;
const watch = process.argv.includes("--watch");
const baseConfig = {
bundle: true,
minify,
sourcemap: true,
outdir: "dist",
target: "es2019",
watch,
};
Promise.all([
esbuild.build({
...baseConfig,
format: "esm",
}),
esbuild.build({
...baseConfig,
format: "cjs",
outExtension: {
".js": ".cjs",
},
}),
]).catch(() => {
console.error("build faild");
process.exit(1);
});
컬러 값들을 오브젝트로 빼준다.
export const color = {
black: "#000",
white: "#fff",
};
export const whiteAlpha = {
50: "rgba(255, 255, 255, 0.04)",
100: "rgba(255, 255, 255, 0.06)",
200: "rgba(255, 255, 255, 0.08)",
300: "rgba(255, 255, 255, 0.16)",
400: "rgba(255, 255, 255, 0.24)",
500: "rgba(255, 255, 255, 0.36)",
600: "rgba(255, 255, 255, 0.48)",
700: "rgba(255, 255, 255, 0.64)",
800: "rgba(255, 255, 255, 0.80)",
900: "rgba(255, 255, 255, 0.92)",
};
export const blackAlpha = {
50: "rgba(0, 0, 0, 0.04)",
100: "rgba(0, 0, 0, 0.06)",
200: "rgba(0, 0, 0, 0.08)",
300: "rgba(0, 0, 0, 0.16)",
400: "rgba(0, 0, 0, 0.24)",
500: "rgba(0, 0, 0, 0.36)",
600: "rgba(0, 0, 0, 0.48)",
700: "rgba(0, 0, 0, 0.64)",
800: "rgba(0, 0, 0, 0.80)",
900: "rgba(0, 0, 0, 0.92)",
};
export const gray = {
50: "#F7FAFC",
100: "#EDF2F7",
200: "#E2E8F0",
300: "#CBD5E0",
400: "#A0AEC0",
500: "#718096",
600: "#4A5568",
700: "#2D3748",
800: "#1A202C",
900: "#171923",
};
export const red = {
50: "#FFF5F5",
100: "#FED7D7",
200: "#FEB2B2",
300: "#FC8181",
400: "#F56565",
500: "#E53E3E",
600: "#C53030",
700: "#9B2C2C",
800: "#822727",
900: "#63171B",
};
export const orange = {
50: "#FFFAF0",
100: "#FEEBC8",
200: "#FBD38D",
300: "#F6AD55",
400: "#ED8936",
500: "#DD6B20",
600: "#C05621",
700: "#9C4221",
800: "#7B341E",
900: "#652B19",
};
export const yellow = {
50: "#FFFFF0",
100: "#FEFCBF",
200: "#FAF089",
300: "#F6E05E",
400: "#ECC94B",
500: "#D69E2E",
600: "#B7791F",
700: "#975A16",
800: "#744210",
900: "#5F370E",
};
export const green = {
50: "#F0FFF4",
100: "#C6F6D5",
200: "#9AE6B4",
300: "#68D391",
400: "#48BB78",
500: "#38A169",
600: "#2F855A",
700: "#276749",
800: "#22543D",
900: "#1C4532",
};
export const teal = {
50: "#E6FFFA",
100: "#B2F5EA",
200: "#81E6D9",
300: "#4FD1C5",
400: "#38B2AC",
500: "#319795",
600: "#2C7A7B",
700: "#285E61",
800: "#234E52",
900: "#1D4044",
};
export const blue = {
50: "#ebf8ff",
100: "#bee3f8",
200: "#90cdf4",
300: "#63b3ed",
400: "#4299e1",
500: "#3182ce",
600: "#2b6cb0",
700: "#2c5282",
800: "#2a4365",
900: "#1A365D",
};
export const cyan = {
50: "#EDFDFD",
100: "#C4F1F9",
200: "#9DECF9",
300: "#76E4F7",
400: "#0BC5EA",
500: "#00B5D8",
600: "#00A3C4",
700: "#0987A0",
800: "#086F83",
900: "#065666",
};
export const purple = {
50: "#FAF5FF",
100: "#E9D8FD",
200: "#D6BCFA",
300: "#B794F4",
400: "#9F7AEA",
500: "#805AD5",
600: "#6B46C1",
700: "#553C9A",
800: "#44337A",
900: "#322659",
};
export const pink = {
50: "#FFF5F7",
100: "#FED7E2",
200: "#FBB6CE",
300: "#F687B3",
400: "#ED64A6",
500: "#D53F8C",
600: "#B83280",
700: "#97266D",
800: "#702459",
900: "#521B41",
};
import * as theme from "../dist/index.js";
Object.entries(theme.vars).forEach(([key, value]) => {
console.log(key, value);
});
output : colors { '$static': [Getter] }
import * as theme from "../dist/index.js";
Object.entries(theme.vars).forEach(([key, value]) => {
console.log(key, value.$static);
});
output : colors { light: [Getter] }
import * as theme from "../dist/index.js";
Object.entries(theme.vars).forEach(([key, value]) => {
console.log(key, value.$static.light);
});
output :
colors {
blackAlpha: [Getter],
blue: [Getter],
color: [Getter],
cyan: [Getter],
gray: [Getter],
green: [Getter],
orange: [Getter],
pink: [Getter],
purple: [Getter],
red: [Getter],
teal: [Getter],
whiteAlpha: [Getter],
yellow: [Getter]
}
import * as theme from "../dist/index.js";
Object.entries(theme.vars).forEach(([key, value]) => {
console.log(key, value.$static.light.blue);
});
output : colors {
'50': '#ebf8ff',
'100': '#bee3f8',
'200': '#90cdf4',
'300': '#63b3ed',
'400': '#4299e1',
'500': '#3182ce',
'600': '#2b6cb0',
'700': '#2c5282',
'800': '#2a4365',
'900': '#1A365D'
}
fs.writeFileSync("dist/themes.css", "");
import * as theme from "../dist/index.js";
import fs from "fs";
// theme.css
// root: {
// --gray-900: #171923
// }
const toCssCasting = (str) => {
return str
.replace(/([a-z])(\d)/, "$1-$2")
.replace(/([A-Z])/g, "-$1")
.toLowerCase();
};
const generateThemeCssVariables = () => {
//여기다가 for 루프 돌린거를 푸쉬해준다.
const cssString = [];
Object.entries(theme.vars).forEach(([key, value]) => {
// key 가 color인 경우에
if (key === "colors") {
Object.entries(value.$static).forEach(([colorKey, colorValue]) => {
//ligth인 경우
if (colorKey === "light") {
const selector = ":root";
const cssVariables = Object.entries(colorValue)
// 각각 key들을 뽑아서
.map(([mainKey, mainValue]) =>
Object.entries(mainValue)
.map(
([subKey, subValue]) =>
`--${toCssCasting(mainKey)}-${toCssCasting(
subKey
)}: ${subValue};`
)
// 줄바꿈 해줘서 푸쉬해줌
.join("\n")
)
.join("\n");
return cssString.push(`${selector} {\n${cssVariables}\n}`);
}
if (colorKey === "dark") {
const selector = ":root .theme-dark";
const cssVariables = Object.entries(colorValue)
.map(([mainKey, mainValue]) =>
Object.entries(mainValue)
.map(
([subKey, subValue]) =>
`--${toCssCasting(mainKey)}-${toCssCasting(
subKey
)}: ${subValue};`
)
.join("\n")
)
.join("\n");
return cssString.push(`${selector} {\n${cssVariables}\n}`);
}
});
return;
}
const selector = ":root";
const cssVariables = Object.entries(value)
.map(([mainKey, mainValue]) =>
Object.entries(mainValue)
.map(
([subKey, subValue]) =>
`--${toCssCasting(mainKey)}-${toCssCasting(subKey)}: ${subValue};`
)
.join("\n")
)
.join("\n");
return cssString.push(`${selector} {\n${cssVariables}\n}`);
});
return cssString;
};
const generateThemeCss = () => {
const variables = generateThemeCssVariables();
fs.writeFileSync("dist/themes.css", [...variables].join("\n"));
};
generateThemeCss();
:root {
--black-alpha-50: rgba(0, 0, 0, 0.04);
--black-alpha-100: rgba(0, 0, 0, 0.06);
--black-alpha-200: rgba(0, 0, 0, 0.08);
--black-alpha-300: rgba(0, 0, 0, 0.16);
--black-alpha-400: rgba(0, 0, 0, 0.24);
--black-alpha-500: rgba(0, 0, 0, 0.36);
--black-alpha-600: rgba(0, 0, 0, 0.48);
--black-alpha-700: rgba(0, 0, 0, 0.64);
--black-alpha-800: rgba(0, 0, 0, 0.80);
--black-alpha-900: rgba(0, 0, 0, 0.92);
--blue-50: #ebf8ff;
--blue-100: #bee3f8;
--blue-200: #90cdf4;
--blue-300: #63b3ed;
--blue-400: #4299e1;
--blue-500: #3182ce;
--blue-600: #2b6cb0;
--blue-700: #2c5282;
--blue-800: #2a4365;
--blue-900: #1A365D;
--color-black: #000;
--color-white: #fff;
--cyan-50: #EDFDFD;
--cyan-100: #C4F1F9;
--cyan-200: #9DECF9;
--cyan-300: #76E4F7;
--cyan-400: #0BC5EA;
--cyan-500: #00B5D8;
--cyan-600: #00A3C4;
--cyan-700: #0987A0;
--cyan-800: #086F83;
--cyan-900: #065666;
--gray-50: #F7FAFC;
--gray-100: #EDF2F7;
--gray-200: #E2E8F0;
--gray-300: #CBD5E0;
--gray-400: #A0AEC0;
--gray-500: #718096;
--gray-600: #4A5568;
--gray-700: #2D3748;
--gray-800: #1A202C;
--gray-900: #171923;
--green-50: #F0FFF4;
--green-100: #C6F6D5;
--green-200: #9AE6B4;
--green-300: #68D391;
--green-400: #48BB78;
--green-500: #38A169;
--green-600: #2F855A;
--green-700: #276749;
--green-800: #22543D;
--green-900: #1C4532;
--orange-50: #FFFAF0;
--orange-100: #FEEBC8;
--orange-200: #FBD38D;
--orange-300: #F6AD55;
--orange-400: #ED8936;
--orange-500: #DD6B20;
--orange-600: #C05621;
--orange-700: #9C4221;
--orange-800: #7B341E;
--orange-900: #652B19;
--pink-50: #FFF5F7;
--pink-100: #FED7E2;
--pink-200: #FBB6CE;
--pink-300: #F687B3;
--pink-400: #ED64A6;
--pink-500: #D53F8C;
--pink-600: #B83280;
--pink-700: #97266D;
--pink-800: #702459;
--pink-900: #521B41;
--purple-50: #FAF5FF;
--purple-100: #E9D8FD;
--purple-200: #D6BCFA;
--purple-300: #B794F4;
--purple-400: #9F7AEA;
--purple-500: #805AD5;
--purple-600: #6B46C1;
--purple-700: #553C9A;
--purple-800: #44337A;
--purple-900: #322659;
--red-50: #FFF5F5;
--red-100: #FED7D7;
--red-200: #FEB2B2;
--red-300: #FC8181;
--red-400: #F56565;
--red-500: #E53E3E;
--red-600: #C53030;
--red-700: #9B2C2C;
--red-800: #822727;
--red-900: #63171B;
--teal-50: #E6FFFA;
--teal-100: #B2F5EA;
--teal-200: #81E6D9;
--teal-300: #4FD1C5;
--teal-400: #38B2AC;
--teal-500: #319795;
--teal-600: #2C7A7B;
--teal-700: #285E61;
--teal-800: #234E52;
--teal-900: #1D4044;
--white-alpha-50: rgba(255, 255, 255, 0.04);
--white-alpha-100: rgba(255, 255, 255, 0.06);
--white-alpha-200: rgba(255, 255, 255, 0.08);
--white-alpha-300: rgba(255, 255, 255, 0.16);
--white-alpha-400: rgba(255, 255, 255, 0.24);
--white-alpha-500: rgba(255, 255, 255, 0.36);
--white-alpha-600: rgba(255, 255, 255, 0.48);
--white-alpha-700: rgba(255, 255, 255, 0.64);
--white-alpha-800: rgba(255, 255, 255, 0.80);
--white-alpha-900: rgba(255, 255, 255, 0.92);
--yellow-50: #FFFFF0;
--yellow-100: #FEFCBF;
--yellow-200: #FAF089;
--yellow-300: #F6E05E;
--yellow-400: #ECC94B;
--yellow-500: #D69E2E;
--yellow-600: #B7791F;
--yellow-700: #975A16;
--yellow-800: #744210;
--yellow-900: #5F370E;
}
npx create-react-app test --template typescript
npm install file:../../packages/themes
index.tsx 에서
import "@hojoon/themes/themes.css";
6.바꿔보자
.App-header {
background-color: var(--gray-100);
색깔이 바꼈다.
import React from "react";
import logo from "./logo.svg";
import "./App.css";
import { ThemeProvider } from "@emotion/react";
import styled from "@emotion/styled";
import { vars } from "@hojoon/themes";
const View = () => {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<Text>
Edit <code>src/App.tsx</code> and save to reload.
</Text>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
};
function App() {
const theme = {
color: vars.colors.$static.light,
};
return (
<ThemeProvider theme={theme}>
<View />
</ThemeProvider>
);
}
export default App;
const Text = styled.p`
color: ${({ theme }) => {
// @ts-ignore
return theme.color.red[900];
}};
`;
이렇게도 사용 가능 하다.
const Text = styled.p`
color: ${vars.colors.$static.light.red[500]};
`;
객체 값이기 때문에 뽑아서 쓸 수도 있다.
<Text>font color ={vars.colors.$static.light.red[500]}</Text>
<body class="theme-dark"> 하면 다크 모드 설정됨
7.스크립트 파일 작성하기
<script>
const isDarkMode = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
if (isDarkMode) {
document.body.classList.add("theme-dark");
}
const mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
mediaQueryList.addEventListener("change", (e) => {
const isDarkMode = e.matches;
if (isDarkMode) {
document.body.classList.add("theme-dark");
} else {
document.body.classList.remove("theme-dark");
}
});
</script>
prefers-color-scheme CSS 미디어 특성은 사용자의 시스템이 라이트 테마나 다크 테마를 사용하는지 탐지하는 데에 사용됩니다.
- light
사용자가 시스템에 라이트 테마를 사용하는 것을 선호하거나 선호하는 테마를 알리지 않았음을 나타냅니다.- dark
사용자가 시스템에 다크 테마를 사용하는 것을 선호한다고 알렸음을 나타냅니다.- 보통의 서비스에서 버튼하나로 dark/light 모드를 바꾸고 저장하려면 이때 로컬스토리지를 사용하면 된다.
export const fontSize = {
72: "4.5rem",
60: "3.75rem",
48: "3rem",
36: "2.25rem",
30: "1.875rem",
24: "1.5rem",
20: "1.25rem",
18: "1.125rem",
16: "1rem",
14: "0.875rem",
12: "0.75rem",
};
export const fontWeight = {
700: "700",
600: "600",
500: "500",
400: "400",
};
export const lineHeight = {
150: "150%",
133: "133%",
120: "120%",
100: "100%",
};
console.log("!!", classes.typography);
const Text = styled.p`
${classes.typography.heading["4xl"]}
color: ${vars.colors.$static.light.red[500]};
`;
<Text className="heading3xl">
타이포그라피
다크모드
css variable 추가만 해줘도 토큰을 만들 수 있다. (shadoe, radius, spacing)
export const spacing = {
0: "0",
1: "0.25rem",
2: "0.5rem",
3: "0.75rem",
4: "1rem",
5: "1.25rem",
6: "1.5rem",
7: "1.75rem",
8: "2rem",
9: "2.25rem",
10: "2.5rem",
11: "2.75rem",
12: "3rem",
14: "3.5rem",
16: "4rem",
20: "5rem",
24: "6rem",
28: "7rem",
32: "8rem",
36: "9rem",
40: "10rem",
44: "11rem",
48: "12rem",
52: "13rem",
56: "14rem",
60: "15rem",
64: "16rem",
72: "18rem",
80: "20rem",
96: "24rem",
};
export const radii = {
none: "0",
sm: "0.125rem",
base: "0.25rem",
md: "0.375rem",
lg: "0.5rem",
xl: "0.75rem",
"2xl": "1rem",
"3xl": "1.5rem",
full: "9999px",
};
export const shadows = {
xs: "0 0 0 1px rgba(0, 0, 0, 0.05)",
sm: "0px 1px 2px 0px rgba(0, 0, 0, 0.05)",
base: "0px 1px 2px 0px rgba(0, 0, 0, 0.06), 0px 1px 3px 0px rgba(0, 0, 0, 0.10)",
md: "0px 2px 4px -1px rgba(0, 0, 0, 0.06), 0px 4px 6px -1px rgba(0, 0, 0, 0.10)",
lg: "0px 4px 6px -2px rgba(0, 0, 0, 0.05), 0px 10px 15px -3px rgba(0, 0, 0, 0.10)",
xl: "0px 10px 10px -5px rgba(0, 0, 0, 0.04), 0px 20px 25px -5px rgba(0, 0, 0, 0.10)",
"2xl": "0px 25px 50px -12px rgba(0, 0, 0, 0.25)",
inner: "0px 2px 4px 0px rgba(0, 0, 0, 0.06) inset",
darkLg:
"0px 15px 40px 0px rgba(0, 0, 0, 0.40), 0px 5px 10px 0px rgba(0, 0, 0, 0.20), 0px 0px 0px 1px rgba(0, 0, 0, 0.10)",
outline: "0 0 0 3px rgba(66, 153, 225, 0.6)",
};
<Text className="heading2xl">{vars.box.radii.base}</Text>
근데 이렇게 자동화된 스크립트 코드를 만드는것은 유지보수 측면에서 좋을 수 있으나 초기에는 좋지 않을수도 있다.
오히려 만드는데 많은 비용이 든다.