๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
  • ์žฅ์›์ต ๊ธฐ์ˆ ๋ธ”๋กœ๊ทธ
๐Ÿ“บ Front End/-- react & redux & nextjs

[Next.js] Next.js ํ”„๋กœ์ ํŠธ์—์„œ Storybook์œผ๋กœ TDD ํ•˜๊ธฐ (Feat. Atomic Design Pattern)

by Wonit 2021. 1. 18.

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๊ฐ€์ง€ ์‚ฌ์ดํด์„ ๋Œ๋ฉฐ ๊ฐœ๋ฐœ์ด ์ง„ํ–‰๋œ๋‹ค.

  1. Red
  2. Green
  3. 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

  1. next.js ์„ค์น˜
  2. npm modules install
  3. storybook init
  4. .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ํ•˜๋ ค ํ•œ๋‹ค.

  1. Title
  2. Label

์šฐ์„  components/atoms ํด๋”๋ฅผ ๋งŒ๋“ ๋‹ค.
๊ทธ๋ฆฌ๊ณ  ๋‹ค์Œ๊ณผ ๊ฐ™์ด 2๊ฐœ์˜ ํด๋”๋ฅผ ๋งŒ๋“ ๋‹ค.

  1. components/atoms/Title
    • index.js
    • index.stories.js
    • styles.js
  2. 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์€ ์—ฌ๋Ÿฌ๋ถ„์ด ํ•œ ๋ฒˆ ์œ„์™€๊ฐ™์€ ๊ณผ์ •์— ๊ฑฐ์ณ์„œ ๋งŒ๋“ค์–ด๋ณด๊ธธ ๋ฐ”๋ž€๋‹ค.

 

๋Œ“๊ธ€