스토리북 공통컴포넌트 배포 (emotion)
🌷 React + emotion
📢
Version
storybook : “^7.0.17”,
react : “^18.2.0”,
react-hook-form": "^7.43.9",
@emotion/react : "^11.10.5",
@emotion/styled : "^11.10.5",
@mui/material: "^5.13.2",
스토리북 설치
작업중이던 React 혹은 Next.js 로 프로젝트에 스토리북을 설치한다.
나같은 경우 빈 프로젝트에 스토리북을 바로 설치하여 추후 package.json 내부 script 에 리액트를 추가로 설정하였다.
main.js
stories : 스토리북이 보여질 경로를 설정한다. 나같은 경우 src 하위폴더에서 작업하여 src 를 추가했다.
addons : 설치한 addon 이 적용되기 위해 main.js 의 addons 배열안에 추가해야 한다.
framework : webpack 5 버전임을 명시하는 코드를 추가한다. storybook이 실행될때는 emotion을 찾을 수 없기때문에, webpack 설정을 반드시 같이 해줘야 한다.
preview.js
스토리북에서 볼 수 있는 폴더로, 보통 기본은 다음코드와 같이 이루어져 있으나,
이 부분에 GlobalStyle 을 적용해야 스토리북에 emotion 이 적용되어 보일 수 있다.
GlobalStyle 적용
전역으로 적용될 스타일을 적용하기 위해 preview.js 에 GlobalStyles 컴포넌트를 작성한 후
@storybook/addon-styling 에서 제공하는 withThemeFromJSXProvider
메소드로 감싸주어야 한다.
나같은 경우 코드가 길어지는것을 선호하지 않아 withTheme.decorator.js
파일을 생성하여 코드를 분리했다.
chromatic 배포
배포 순서
yarn add -D chromatic
설치
- git hub 레포지토리 연결
chromatic
사이트에서 토큰 발급
- 터미널에
npx chromatic --project-token=발급토큰
입력
사실 크로마틱 배포는 공식문서에 나온 그대로 하면 전혀 어렵지가 않다.
하지만 나는 앞서 말했듯이 storybook init 부터 했기때문에 chromatic 배포 시 script 가 존재하지 않다고 에러 문구가 나왔다.
현재까지 작업한것이 물거품이 되나 싶어 순간 당황했지만, package.json에 react 혹은 next.js 설정만 넣어주고 yarn install 하면 끝이었다.
나같은 경우 굳이 next.js 까진 필요없을 것 같아 리액트로 재구축 하였다.
(지금 생각해보니 검색엔진이 내 스토리를 찾을 확장성까지 생각했다면 next.js 로 구축해도 좋았을 것 같다.)
script 내부 안에 리액트로 빌드한다고 명시한 후
npx chromatic --project-token=발급토큰
을 터미널에 다시 재 입력하면 5분 내외로 배포가 되는것을 확인할 수 있다.
배포 사이트
참조
🦋 Next.js + Tailwindcss
💡
사용스택
Next.js
Typescript
Tailwindcss
+ Chromatic
배포
폴더구조
tailwind 설정
tailwind.config.js
src 디렉토리 생성하여 경로를 알맞게 설정해준다.
globals.css
src / styles / globals.css
👇🏻 테일윈드에 대한 자세한 설명은
스토리북 설정
스토리북 설치
스토리북은 cra, cna 와 같은 패키지모듈로 설치했더라도 웹팩 설정을 해줘야 한다.
package.json 에 다음과 같이 webpack 5 버전임을 명시하는 코드를 작성한다
웹팩5 를 이용하여 스토리북을 빌드하기 위해 다음과 같이 webpack5 빌더 설치하여 초기화
스토리북 설정
.storybook / main.js
.storybook / preview.js
꼭 스토리북에서 적용할 파일의 css 가 담긴 파일을 import 해주어야 한다.
스토리북 실행
스토리북 배포
//Add Storybook:
npx storybook@latest init
//스토리북에 적용할 addon 설치
yarn add -D @storybook/addon-styling @storybook/addon-actions @storybook/addon-knobs
/** @type { import('@storybook/react-webpack5').StorybookConfig } */
const config = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-styling',
'@storybook/addon-actions',
'@storybook/addon-knobs',
],
framework: {
name: '@storybook/react-webpack5',
options: {},
},
docs: {
autodocs: 'tag',
},
};
export default config;
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
expanded: true,
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};
//preview.js
import { withTheme } from './withTheme.decorator';
import { withThemeFromJSXProvider } from '@storybook/addon-styling';
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
expanded: true,
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};
export const globalTypes = {
theme: {
name: 'Theme',
description: 'Global theme for components',
toolbar: {
icon: 'paintbrush',
items: [
{ value: 'light', title: 'Light', left: '🌞' },
{ value: 'dark', title: 'Dark', left: '🌛' },
],
// Change title based on selected value
dynamicTitle: true,
},
},
};
export const decorators = [withThemeFromJSXProvider(withTheme)];
//withTheme.decorator.js
import { css, Global, ThemeProvider, useTheme } from '@emotion/react';
//light/dark 모드를 위한 테마 객체.
import { lightTheme, darkTheme } from '../src/stories/theme';
const THEMES = {
light: lightTheme,
dark: darkTheme,
};
// Sets the background based on theme
const GlobalStyles = () => {
const theme = useTheme();
return (
<Global
styles={css`
html,
body {
background-color: ${theme.colors.background};
color: ${theme.colors.text};
}
`}
/>
);
};
export const withTheme = (Story, context) => {
const { theme } = context.globals;
return (
<ThemeProvider theme={THEMES[theme] || THEMES['light']}>
<GlobalStyles />
<Story />
</ThemeProvider>
);
};
//package.json
"dependencies": {
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
...
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
},
// typescript, next 패키지 설치
npx create-next-app@latest --typescript
// tailwindcss 설치
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
npm i storybook -g
// 스토리북에 적용할 postcss를 위한 npm install
npm i -D @storybook/addon-postcss
// package.json
"resolutions": {
"@storybook/{app}/webpack": "^5"
}
npx sb init --builder webpack5
const path = require('path');
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
/** Expose public folder to storybook as static */
staticDir: ['../public'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
{
/**
* Fix Storybook issue with PostCSS@8
* @see https://github.com/storybookjs/storybook/issues/12668#issuecomment-773958085
*/
//postcss를 활용할 수 있도록 이 부분 추가.
name: '@storybook/addon-postcss',
options: {
postcssLoaderOptions: {
implementation: require('postcss'),
},
},
},
],
// webpack5를 사용할 수 있도록 이 부분 추가
core: {
builder: 'webpack5',
},
//여기서부터는 기타 경로, alias 설정
webpackFinal: (config) => {
/**
* Add support for alias-imports
* @see https://github.com/storybookjs/storybook/issues/11989#issuecomment-715524391
*/
config.resolve.alias = {
...config.resolve?.alias,
"@": [path.resolve(__dirname, "../src/"), path.resolve(__dirname, "../")],
};
/**
* Fixes font import with /
* @see https://github.com/storybookjs/storybook/issues/12844#issuecomment-867544160
*/
config.resolve.roots = [
path.resolve(__dirname, "../public"),
"node_modules",
];
return config;
},
}
import "../src/styles/globals.css";
import "../src/components/Buttons/Button.tsx"
/* next에서 제공하는 <NextImage/> 를 가져오려면 주석처리 된 코드 입력
import * as NextImage from "next/image";
const OriginalNextImage = NextImage.default;
Object.defineProperty(NextImage, "default", {
configurable: true,
value: (props) => <OriginalNextImage {...props} unoptimized />,
});
*/
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
previewTabs: {
"storybook/docs/panel": { index: -1 },
},
};
npm run storybook
//or
yarn storybook