storybook
은 UI 컴포넌트를 보여주고 문서화하는 오픈소스 툴이다.
npm create vite
명령어로 react-typescript 패키지를 설치해준 뒤
npx sb init --builder @storybook/builder-vite
를 입력하면 아래 이미지처럼 루트 경로에 vite
에 맞게 설정된 .storybook
폴더와 src/stories
경로에 테스트 해볼 수 있는 몇개의 컴포넌트들이 생성된다.
stories
폴더는 사용하지 않고 css
도 emotion
을 사용할 것이기 때문에 stories
폴더와 css
파일들을 삭제 해주고
npm i @emotion/styled @emotion/react emotion-reset
로 emotion
을 설정해준다.
프로젝트를 진행하다보면 import
경로가 ../../../~
이런 식으로 더러워지는 경우가 많다. 이를 해결하기 위해 루트 경로로 부터 접근할 수 있는 키워드를 설정하여 ../
지옥을 벗어날 수 있다.
vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import * as path from "path";
export default defineConfig({
plugins: [react()],
resolve: {
alias: [{ find: "@", replacement: path.resolve(__dirname, "src") }],
},
});
tsconfig.json
{
"compilerOptions": {
// ...rest of the template
"types": ["node"],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
위와 같이 각각 파일을 설정해 주면 ../../styles/colors
와 같은 경로를 @/styles/colors
로 바꿔 사용할 수 있다.
참고 링크
그런데 storybook
은 아직 @/~
와 같은 경로를 읽어내지 못해 이 상태에선 오류를 뱉어낸다.
이를 해결하는 방법은 .storybook/main.cjs
파일에 아래와 같이 설정해주면 된다.
const { loadConfigFromFile, mergeConfig } = require("vite");
module.exports = {
...
async viteFinal(config, { configType }) {
const { config: userConfig } = await loadConfigFromFile(
path.resolve(__dirname, "../vite.config.ts")
);
return mergeConfig(config, {
...userConfig,
// manually specify plugins to avoid conflict
plugins: [],
});
},
};
이 방식은 vite.config.ts
에 작성된 설정을 재사용하는 방법이다.
참고 링크
|-- components
| `-- Button
| |-- Button.stories.tsx
| |-- Button.styles.ts
| |-- Button.tsx
| `-- Button.types.ts
|-- styles
| |-- GlobalStyle.tsx
| |-- colors.ts
| `-- shared
| `-- flex.ts
|-- App.tsx
|-- main.tsx
`-- vite-env.d.ts
Button.tsx
import * as S from "./Button.styles";
import { ButtonProps } from "./Button.types";
/**
* @param {Pick<ButtonProps,"size">} size - 버튼 크기
* @param {Pick<ButtonProps,"color">} color - 버튼 색상
* @todo 기능 더 만들기
*/
const Button = ({
size = "md",
color = "primary",
children,
...rest
}: ButtonProps) => {
return (
<S.Container {...rest} size={size} color={color}>
{children}
</S.Container>
);
};
export default Button;
Button.types.ts
import { ButtonHTMLAttributes, ReactNode } from "react";
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
size?: "sm" | "md" | "lg";
children: ReactNode;
color?: "primary" | "secondary";
}
Button.styles.ts
import colors from "@/styles/colors";
import { flexCenter } from "@/styles/shared/flex";
import { css } from "@emotion/react";
import styled from "@emotion/styled";
import { ButtonProps } from "./Button.types";
export const Container = styled.button<Omit<ButtonProps, "children">>`
transition: all 0.5s ease-in-out;
cursor: pointer;
border: none;
${flexCenter}
border-radius: 0.2rem;
background-color: ${({ color }) => color && colors[color]};
color: ${colors.white};
${({ size }) =>
size === "sm"
? css`
padding: 0.5rem 1.5rem;
`
: size === "md"
? css`
padding: 0.7rem 2rem;
`
: css`
padding: 1rem 2.5rem;
`}
`;
Button.stories.tsx
import Button from "./Button";
import { ComponentStory, ComponentMeta } from "@storybook/react";
export default {
title: "Button",
component: Button,
} as ComponentMeta<typeof Button>;
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;
export const Default = Template.bind({});
Default.args = {
children: "버튼",
};
export const Secondary = Template.bind({});
Secondary.args = {
...Default.args,
color: "secondary",
};
export const Small = Template.bind({});
Small.args = {
...Default.args,
size: "sm",
};
export const Large = Template.bind({});
Large.args = {
...Default.args,
size: "lg",
};
App.tsx
import Button from "@/components/Button/Button";
function App() {
return (
<>
<div style={{ padding: "1rem 2rem" }}>
<Button size="sm" color="primary">
sm primary
</Button>
<br />
<Button>md primary</Button>
<br />
<Button color="secondary" size="lg">
lg secondary
</Button>
</div>
</>
);
}
export default App;
npm run dev
npm storybook