Storybook์ด๋?
Storybook์ UI ๊ตฌ์ฑ ์์(์ปดํฌ๋ํธ)๋ฅผ ๊ฐ๋ฐํ๊ธฐ์ํ ์คํ ์์ค ๋๊ตฌ์ด๋ค.
React๋ Vue๋ฅผ ๋น๋กฏํ ์ปดํฌ๋ํธ ๊ธฐ๋ฐ UI ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ด ๋ง์์ง ํ๋์ ์น Front๊ฐ๋ฐ ์ํ๊ณ์์๋ ์ปดํฌ๋ํธ ํ๋ ํ๋์ ์ญํ ์ด ๋งค์ฐ ์ค์ํด์ก๋ค.
์ฌ๊ธฐ์ ์ปดํฌ๋ํธ๋ ์ธ๋ถ์ ์ํฅ์ ๋ฐ์ง ์๊ฒ Isolated ๋์ด์ผ ํ๋ค.
๊ทธ๋ฌ๊ธฐ ์ํด์ Atomic Design Pattern์ด๋ผ๋ ๊ฒ์ด ๋ฑ์ฅํ์๊ณ ๊ทธ์ ๋ฐ๋ผ์ UI ์์ฒด์ ๋ ๋๋ง์๋ ํ ์คํธ๋ฅผ ์ ๊ฒฝ์ธ ์ ์๊ฒ ๋์๋ค.
Storybook์ ์ฌ์ค ํ ์คํธ์๋ง ๊ตญํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ์๋๊ณ ๊ฐ๋ฐ๊ณผ ์ดํด๊ด๊ณ์๋ค ์ฌ์ด์์์ ์ํต๊ณผ Docs ๋ฐ Props Test๋ฅผ ์งํํด์ฃผ๋ ์ปดํฌ๋ํธ ๊ธฐ๋ฐ ์คํ ์์ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค.
์ฐ๋ฆฌ๋ ์ด Storybook์ ํตํด์ TDD๋ฅผ ์งํํ๋ ค ํ๋ค.
TDD๋?
TDD๋ Test Driven Development์ ์ค์๋ง๋ก TDD๋ ๋ค์๊ณผ ๊ฐ์ ์์๋ฅผ ๊ฐ๊ฒ ๋๊ณ ํด๋น 3๊ฐ์ง ์ฌ์ดํด์ ๋๋ฉฐ ๊ฐ๋ฐ์ด ์งํ๋๋ค.
- Red
- Green
- Refactor
๊ทธ๋ฆฌ๊ณ ๊ฐ ์ฌ์ดํด์์ ์ฐ๋ฆฌ๋ storybook์ ํ์ฉํ์ฌ ๋ ๋๋ง์ ํ ์คํธํ ์์ ์ด๋ค.
Red
Red Stage ์์๋ ์คํจํ๋ ์ฝ๋๋ฅผ ์์ฑํ๋ค.
์ฒซ ๋ฒ์งธ Red ์ฌ์ดํด์์ ์ฐ๋ฆฌ๋ Storybook์ ์๋ฌด๊ฒ๋ ์๋ ๋น ์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋ง ํ ๊ฒ์ด๋ค.
์๋๋ผ๋ฉด ํด๋น story์์ Rendering ์ด ๋์ด์ผ ํ ๊ฒ์ด ์ ๋๋ก ๋์ค์ง ์์ ๊ฒ์ด๋ค.
Green
Green Stage ์์๋ ์คํจํ ์ฝ๋๋ฅผ ํ ๋๋ก ์ค์ ์๋ํ๋ ์ฝ๋๋ฅผ ์์ฑํ๋ค.
๋น ์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋งํ๋ Red Stage์์ ๋ ํ์ฅํด ์ค์ ์ฐ๋ฆฌ๊ฐ ์์ํ๋ ๊ธฐ๋ฅ์ด ๋์ํ๋ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค ๊ฒ์ด๋ค.
Refactor
Refactor Stage ์์๋ Green ์์ ๋์ํ๋ ์ฝ๋๋ฅผ ๋์ฑ ํจ์จ์ ์ด๊ฒ ๋์ํ๊ฒ ํ๋ค.
Green Stage์์ ๋์ด์จ ์ฝ๋๋ฅผ ์งง๊ฒ ๋ฆฌํฉํ ๋ง์ ์งํํ๋ค.
ํด๋น Stage์์๋ ๋งํด ํ์ธ๋ฌ๊ฐ ๋งํ๋ ์ฐ๋๊ธฐ ์ค๊ธฐ ๋ฆฌํฉํ ๋ง๊ณผ ๋น์ทํ ๋๋์ผ๋ก ๋ฆฌํฉํ ๋ง์ ์งํํ๋ค.
์?
์ TDD๋ฅผ ์์ฑํ๋ฉด์ ๊ฐ๋ฐ์ ํ ๊น?
์ฐ์ ์ฝ๋๋ฅผ ์์ฑ ํ ๋ ์ฝ๋๊ฐ ๋๋ฌด ๋ฐฉ๋ํด์ง์ง ์๋๋ค.
TDD ๋ฅผ ์งํํ๋ฉด์ ํ
์คํธ ์ผ์ด์ค๋ฅผ ์์ฑํ ๋ ์ฃผ๋ก ์์ ๋จ์๋ก ๋ง๋ค๊ธฐ ๋๋ฌธ์ ์ฝ๋์ ๋ชจ๋ํ๊ฐ ์ด๋ฃจ์ด์ง๋ค.
๊ทธ๋ผ ๊ทธ์ ๋ฐ๋ผ ๋น์ฐํ๊ฒ Test Coverage๊ฐ ๋์์ง๋ฉฐ ์์ ์ ์ธ ํ๋ก์ ํธ๊ฐ ๊ตฌ์ฑ๋๋ค.
ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง๊ฐ ๋์์ง๋ฉด ๊ฒฐ๊ตญ ๋ฆฌํฉํ ๋ง๋ ์ฌ์์ง๊ณ ์ ์ง๋ณด์๋ ์ฌ์์ง๋ค.
์ด๋ฅผ ๋์ฑ ์์ธํ๊ธฐ ์ดํดํ๊ณ ์ถ๋ค๋ฉด ๋งํด ๋ง์ธ๋ฌ๊ฐ ๋งํ๋ ๋ฆฌํฉํ ๋ง์ ํด์ผํ๋ ์ด์ ์ ๋ํด์ ํ์ธํด๋ณด๋๋ก ํ์.
TDD์์ SDD๋ก ํ์ฅ
TDD๋ Test Driven Development์ ์ค์๋ง์ด๋ผ๊ณ ์์ ์ด์ผ๊ธฐ๋ฅผ ํ์๋ค.
์ฌ๊ธฐ์ ๋์ฑ ํ์ฅ์์ผ Storybook Driven Development๋ก ํ์ฉํ ์ ์๋ค.
์ฐ๋ฆฌ๋ UI ํ ์คํธ๋ฅผ Storybook์ ํตํด์ ์งํํ ๊ฒ์ด๋ Red, Green Cycle์ ๋ชจ๋ Storybook์ ํตํด์ ์งํํ๋ค.
์ค์ ๋ก ์ ์ฉํด๋ณด์.
์ด๋ฒ ์์ ์์๋ Atomic Design Pattern์ ์ ์ฉํ์ฌ Label๊ณผ Title๋ฅผ TDD๋ก ์งํํด๋ณผ ๊ฒ์ด๋ค.
- Title Component : ์ ๋ชฉ๊ณผ ๋ถ์ ๋ชฉ์ ์ํ ์ปดํฌ๋ํธ๋ก font-size๋ 2rem๊ณผ 1.5rem์ผ๋ก ๊ตฌ์ฑ๋๋ค.
- Label Component : ์ค๋ช
๊ณผ Name Tag, ๋ง๊ผฌ๋ฆฌ ๋ฑ๊ณผ ๊ฐ์ Text Field๋ฅผ ์ํ ์ปดํฌ๋ํธ๋ก
span
ํ๊ทธ์p
ํ๊ทธ๋ก ๊ตฌ์ฑ๋๋ค.
ํ๋ก์ ํธ Set up
- next.js ์ค์น
- npm modules install
- storybook init
.storybook
ํด๋ ์ ๋ฆฌ
next.js ์ค์น
// npm install
$ npx create-next-app client
// yarn install
$ yarn create-next-app client
npm modules install ์ค์น
์ฐ๋ฆฌ๋ SDD๋ฅผ ์ํด Css-In-Js ๋ชจ๋์ ์ฌ์ฉํ๋ คํ๋ค.
styled-components๋ฅผ ์ค์นํ์.
// npm ์ฌ์ฉ์
$ npm install styled-components styled-reset styled-tools
// yarn ์ฌ์ฉ์
$ yarn add styled-components styled-reset styled-tools
storybook ์ค์น
// npm ์ฌ์ฉ์
$ npx sb init
// yarn ์ฌ์ฉ์
$ yarn sb init
storybook ์คํ
// npm ์ฌ์ฉ์
$ npm run storybook
// yarn ์ฌ์ฉ์
$ yarn storybook
.storybook
ํด๋ ์ ๋ฆฌ
.storybook์ด storybook์ ๊ด๋ฆฌํ๋ ํจํค์ง์ด๋ค.
ํด๋น ํจํค์ง๋ ํด๋ ๊ตฌ์กฐ๋ง ์ฐ๋ฆฌ์ next app ๋ด๋ถ์ ์กด์ฌํ๋ฉฐ ์ค์ ๋ก๋ ์์ ํ ๋ณ๊ฐ๋ก ๋์๊ฐ๋ค.
๊ทธ๋์ theme.js๋ reset.js ์ ๊ฐ์ ์ค์ ํ์ผ๋ storybook์ ๋ฐ๋ก ์ ์ฉ์์ผ์ผ ํ๋ค.
์ด๋ฅผ ์ํด์ ๋๋ถ๋ถ webpack.config.js
๋ฑ๊ณผ ๊ฐ์ด ์ค์ ์ ํ์ง๋ง ์ฐ์ ํฌ์ปค์ค๋ SDD ์ด๋ฏ๋ก .storybook
ํด๋ ๋ด๋ถ์์ js ํ์ผ์ ๋ถ๋ฌ์ค๋ ๋ฐฉ์์ผ๋ก ์งํํ ๊ฒ์ด๋ค.
main.js
module.exports = {
"stories": [
"../components/atoms/**/*.stories.js",
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials"
]
}
preview.js
import React from "react";
import { ThemeProvider } from 'styled-components';
import { MINIMAL_VIEWPORTS } from "@storybook/addon-viewport";
import theme from "./theme";
import Reset from "./reset";
export const decorators = [
(Story) => (
<>
<Reset />
<ThemeProvider theme={theme}>
<Story />
</ThemeProvider>
</>
),
];
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
viewport: {
viewports: MINIMAL_VIEWPORTS,
},
}
reset.js
import { createGlobalStyle } from "styled-components";
import reset from "styled-reset";
const GlobalStyles = createGlobalStyle`
${reset}
a{
text-decoration: none;
color: inherit;
}
*{
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-size: 14px;
background-color: #000;
color: white;
}
`
export default GlobalStyles;
theme.js
const theme = {
fontSize: {
MainTitle: "2rem;",
SubTitle: "1.5rem;",
PrimaryLabel: "0.9rem;",
SecondaryLabel: "0.8rem;",
PrimaryDescription: "0.7rem;",
SecondaryDescription: "0.6rem;"
}
}
export default theme;
TDD ์๋ SDD ์งํํ๊ธฐ
์ฐ๋ฆฐ 2๊ฐ์ atoms๋ฅผ TDDํ๋ ค ํ๋ค.
- Title
- Label
์ฐ์ components/atoms
ํด๋๋ฅผ ๋ง๋ ๋ค.
๊ทธ๋ฆฌ๊ณ ๋ค์๊ณผ ๊ฐ์ด 2๊ฐ์ ํด๋๋ฅผ ๋ง๋ ๋ค.
components/atoms/Title
- index.js
- index.stories.js
- styles.js
components/atoms/Label
- index.js
- index.stories.js
- styles.js
index.js
ํด๋น ํ์ผ์์ ์ปดํฌ๋ํธ๊ฐ ์ ์๋ ๊ฒ์ด๋ค.
์ผ๋ฐ์ ์ธ React ์ปดํฌ๋ํธ์ธ๋ฐ, ์กฐ๊ฑด๋ถ ๋ ๋๋ง์ ํตํด์ Atomic ํ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค์.
index.stories.js
์ฐ๋ฆฌ์ ์ปดํฌ๋ํธ๊ฐ ์ ๋ ๋๋ง ๋๋์ง, props๋ฅผ ์ ๋ฐ๋์ง ํ
์คํธํ story๊ฐ ๋ค์ด์๋ ํด๋์ด๋ค.
styles.js
์ฐ๋ฆฌ๊ฐ ๋ง๋ ์ปดํฌ๋ํธ์ ์คํ์ผ๋ง์ ์ ์ฉํ Css-In-Js ํ์ผ์ด๋ค.
Title Components ์์ .
Red ์ฌ์ดํด
์ฐ์ Title ์ปดํฌ๋ํธ์์ Red ์ฌ์ดํด์ ๋๋ ค๋ณด์.
index.js
import React from 'react'
const Title = ({styleType, children}) => {
return (
<div>
this is title;
</div>
)
}
export default Title;
index.stories.js
import Title from ".";
export default {
title: "atoms / Title",
component: Title
}
const Template = (args) => <Title {...args} />
styles.js
import styled, { css } from 'styled-components';
import { theme } from "styled-tools";
export const MainTitle = styled.h1`
`;
export const SubTitle = styled.h3`
`;
Green ์ฌ์ดํด
์ค์ง์ ์ผ๋ก ๋์ํ๋ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค๊ฒ์ด๋ค.
Green ์ฌ์ดํด์์์ ๋ชฉ์ ์ ๋์์ด๋ค.
์ด๋ป๊ฒ ํจ์จ์ ์ผ๋ก ๋์ํ๊ฒ ํ ๊น?๋ ์์ง ์๊ฐํ์ง ๋ง์.
index.js
import React from 'react'
import * as S from "./styles";
const Title = ({ styleType, children }) => {
if (styleType === "MainTitle") return <S.MainTitle>{children}</S.MainTitle>
else (styleType === "SubTitle") return <S.SubTitle>{children}</S.SubTitle>
}
export default Title;
index.stories.js
import Title from ".";
export default {
title: "atoms / Title",
component: Title
}
export const MainTitleRendering = <Title styleType="MainTitle">This is MainTitle</Title>
export const SubTitleRendering = <Title styleType="SubTitle">This is SubTitle</Title>
styles.js
import styled, { css } from 'styled-components';
import { theme } from "styled-tools";
export const MainTitle = styled.h1`
font-weight: bold;
font-size: 2rem;
`;
export const SubTitle = styled.h3`
font-weight: bold;
font-size: 1.5rem;
`;
Blue (Refactor) ์ฌ์ดํด
์ด์ ์ค๋ณต๋ ์ฝ๋๋ ์ง์ฐ๊ณ ์ข ๋ ์ด์๊ฒ ๋ฆฌํฉํ ๋ง์ ์งํํด๋ณด์.
์ด์ ํจ์จ์ ์๊ฐํ ๋ ์ด๋ค.
index.js
import React from 'react'
import * as S from "./styles";
const Title = ({ styleType, children }) => {
if (styleType === "MainTitle") return <S.MainTitle>{children}</S.MainTitle>
else if (styleType === "SubTitle") return <S.SubTitle>{children}</S.SubTitle>
else retur <></>
}
export default Title;
index.stories.js
import Title from ".";
export default {
title: "atoms / Title",
component: Title
}
const Template = (args) => <Title {...args} />
export const MainTitleRendering = Template.bind({});
MainTitleRendering.args = {
styleType: "MainTitle",
children: "This is Main Ttitle",
}
export const SubTitleRendering = Template.bind({});
SubTitleRendering.args = {
styleType: "SubTitle",
children: "This is Sub Title",
}
styles.js
import styled, { css } from 'styled-components';
import { theme } from "styled-tools";
const defaultStyle = css`
font-weight: bold;
`;
export const MainTitle = styled.h1`
${defaultStyle}
font-size: ${theme("fontSize.MainTitle")};
`;
export const SubTitle = styled.h3`
${defaultStyle}
font-size: ${theme("fontSize.SubTitle")};
`;
Refactoring ํ์๋ ๋์ผํ๊ฒ ์ ๋์ํ๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
์ด์ ๋๊ฐ์ด Label ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค ์ฐจ๋ก์ด๋ค.
Label์ ์ฌ๋ฌ๋ถ์ด ํ ๋ฒ ์์๊ฐ์ ๊ณผ์ ์ ๊ฑฐ์ณ์ ๋ง๋ค์ด๋ณด๊ธธ ๋ฐ๋๋ค.
๋๊ธ