Storybook에서 Emotion 설정하기

2021-10-12

해당 글에서 사용된 packages 버전은 아래와 같습니다.

package.json

{
  // ...생략
  "dependencies": {
    "@emotion/babel-preset-css-prop": "^11.2.0",
    "@emotion/react": "^11.4.0",
    "@emotion/styled": "^11.3.0",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-router-dom": "^5.2.0",
    "react-scripts": "4.0.3",
    "typescript": "^4.1.2"
  },
  "devDependencies": {
    "@babel/preset-react": "^7.13.13",
    "@emotion/babel-plugin": "^11.3.0",
    "@storybook/addon-actions": "^6.3.8",
    "@storybook/addon-essentials": "^6.3.8",
    "@storybook/addon-links": "^6.3.8",
    "@storybook/node-logger": "^6.3.8",
    "@storybook/preset-create-react-app": "^3.2.0",
    "@storybook/react": "^6.3.8",
    "babel-loader": "8.1.0"
  }
}

만약 react와 storybook의 babel-loader가 상충되어 에러가 뜬다면, package-lock.json 혹은 yarn.lock 그리고 node_modules를 삭제 후 재설치한다.

그 다음에도 에러가 뜬다면 npm ls babel-loader로 가장 낮은 babel-loader 버전을 프로젝트에 설치한다. 해당 프로젝트는 8.1.0 버전으로 설치했다.

Cannot read properties of undefined 에러 해결하기

스토리북을 설치하고, yarn storybook으로 스토리북 서버까지 잘 실행되지만, 막상 작성한 Button.stories.index를 열려니 emotion으로 설정한 값을 제대로 불러오지 못했다.

error

Cannot read properties of undefined (reading 'mobile')

src/styles/theme.ts

const theme: Theme = {
  // ...생략
  mq: {
    laptop: `only screen and (min-width: ${size.largest})`,
    tablet: `only screen and (max-width: ${size.large})`,
    mobile: `only screen and (max-width: ${size.small})`,
  },
}

이는 로컬서버에서는 emotion으로 정의한 theme이 잘 적용되었지만, 스토리북에서는 해당 설정을 찾지 못해 뜨는 에러였다.


스토리북 설정은 .storybook/main.js.storybook/preview.js에서 가능하며, 해결 방법은 아래와 같다.

.storybook/main.js

const path = require('path')

const toPath = _path => path.join(process.cwd(), _path)

module.exports = {
  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/preset-create-react-app',
  ],
  webpackFinal: async config => ({
    ...config,
    resolve: {
      ...config.resolve,
      alias: {
        ...config.resolve.alias,
        '@emotion/core': toPath('node_modules/@emotion/react'),
      },
    },
  }),
}

따로 설정한 emotion 전역 스타일이 있을 경우, 프로젝트의 App.tsx에 설정하는 것처럼, 스토리북에서는 preview.js에서 설정해준다.

.storybook/preview.js

import { ThemeProvider } from '@emotion/react'

import theme from '../src/styles/theme'
import GlobalStyle from '../src/styles/global'

export const decorators = [
  Story => (
    <ThemeProvider theme={theme}>
      <GlobalStyle />
      <Story /> // Canvas 영역을 뜻함
    </ThemeProvider>
  ),
]

위처럼 decorators를 통해서 stories 컴포넌트에 공통으로 사용할 스타일이나 요소를 설정할 수 있다. preview.js는 모든 stories에 전역으로 설정하는 것이고, 개별 stories내에서도 설정이 가능하다.

// 예시: YourComponent.stories.js | YourComponent.stories.jsx
export default {
  component: YourComponent,
  decorators: [
    Story => (
      <div style={{ margin: '3em' }}>
        <Story />
      </div>
    ),
  ],
}

What & Why

@emotion/core

해당 프로젝트에서는 @emotion/core 패키지가 없는데 왜 넣었는지 궁금했다.

그래서 해당 줄을 지우고 다시 켜봤더니, 작동은 하지만, Docs를 들어가보면 에러가 뜬다.

docs error

Uncaught TypeError: Cannot read properties of undefined (reading 'content')

해당 버전의 Storybook에서는 @storybook/addon-essentials 안에 @storybook/addon-docs가 포함되어 있다. 그리고 addon-docs의 package.json을 보니 emotion을 보니 @emotion/core 10.1.1 버전이었다. (참조)

따라서 스토리북이 구동할 때 필요한 패키지가 무엇인지 알려줘야 한다. 그리고 그 일을 도와주는 것이 resolve의 역할이다.

webpackFinal

Storybook은 webpack을 통해서 프로젝트에서 만든 컴포넌트들을 우리가 만든 웹 어플리케이션에 보여준다. 그리고 관련 webpack 설정은 .storybook/main.js 파일에서 가능하다. (참조)

위처럼 Storybook 설정을 변경하기 위해서는 webpack config를 따로 설정해줘야 한다.

resolve.alias는 모듈 내에서 import를 쉽게 생성해주는 것이다. 사용법은 아래와 같다.

const path = require('path')

module.exports = {
  //...
  resolve: {
    alias: {
      Utilities: path.resolve(__dirname, 'src/utilities/'),
      Templates: path.resolve(__dirname, 'src/templates/'),
    },
  },
}

해당 글에서는 toPath라는 함수를 만들어 사용했다.

const toPath = _path => path.join(process.cwd(), _path)

process.cws()는 현재 프로젝트의 최상단 폴더를 뜻하고, __dirname는 현재 연 파일이 위치한 폴더를 뜻하는 것이다. 만약 여기서도 __dirname을 썼더라면, 상대경로로 해당 패키지가 어디 있는지 설정해줘야 한다.


참고