본문 바로가기
웹개발/개발환경 설정

next.js 에 typescript, storybook 설정하기

by 현댕5697 2021. 7. 25.
반응형

🎠 내 tmi...


쿠키파킹의 웹사이트는 원래 react.js로 개발했었다.

쿠키파팅 서비스에는 디렉토리 공유 기능이 있다.

우리는 사용자들이 카톡으로 공유 링크를 전달하는 경우가 많을 것이라고 예측했다.

카톡으로 웹 사이트 링크를 전달하는 경우 카톡에서 자체적으로 메타태그들을 인식해서 사이트 이름, 썸네일 등을 띄워준다.

그런데 spa 특성상 공유 링크를 전달하게 되면, 디렉토리에 해당하는 메타태그들 값이 뜨는 것이 아니라 모든 페이지가 동일한 값이 뜬다는 불편함이 있었다.

 

그래서 이 문제를 해결하기 위해 웹 사이트를 next.js로 바꾸기로 결정했다.

앞으로 추가하게 될 기능들이나, SEO를 위해서도 next.js로 바꾸는 것이 맞는 선택이라고 생각했다.

이왕 옮기는 김에 typescript로 전환하고, 컴포넌트들도 atomic design 구조로 다시 개발하기로 결정했다.

storybook 라이브러리를 사용하면 CDD(component-driven development) 방식으로 개발할 때 유용하게 쓸 수 있을 것 같았고, 특히 atomic design 구조로 개발할 때 매우 편리한 라이브러리라고 생각되어 도입하기로 하였다.

또한 협업할 때 코드 스타일로 인해 충돌이 생기는 것을 방지하기 위해 eslint, prettier, stylelint를 적용하기로 하였다.

 

1. next.js 생성


next.js 어플리케이션을 쉽게 생성하려면 이 명령어를 터미널에 입력해주자.

npx create-next-app [프로젝트 폴더명]
# 또는
yarn create next-app [프로젝트 폴더명]

만약 typescript로 next.js 어플리케이션을 설정하고 싶다면

npx create-next-app --typescript [프로젝트 폴더명]
# 또는
yarn create next-app --typescript [프로젝트 폴더명]

이 명령어를 입력해주면 된다.

 

2. typescript 설정


next.js 어플리케이션을 생성할 때 명령어 뒤에 --typescript를 붙이는 것을 까먹어서 이미 생성된 next.js를 typescript로 설정해야 했다.

먼저 명령어를 입력하여 필요한 패키지를 설치하자.

npm i -D typescript @types/react @types/node 
# or
yarn add --dev typescript @types/react @types/node

그 후 아래 명령어를 입력하자.

npm i
npm run build
npm run dev
# or
yarn install
yarn build
yarn dev

 

그러면 프로젝트 root 폴더에 tsconfig, next-env.d.ts 파일이 자동으로 생성된다.

그 다음 원하는데로 tsconfig.json 설정을 해주면 된다.

// tsconfig.json
{
  "compilerOptions": {
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "jsx": "preserve",
    "lib": ["dom", "es2017"],
    "module": "esnext",
    "moduleResolution": "node",
    "noEmit": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "preserveConstEnums": true,
    "removeComments": false,
    "skipLibCheck": true,
    "sourceMap": true,
    "strict": true,
    "target": "esnext",
    "forceConsistentCasingInFileNames": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
  },
  "include": ["src", "pages", "styles", "next.config.js", "next-env.d.ts"],
  "exclude": ["node_modules"]
}

 

3. eslint, prettier 설정


해당 명령어로 eslint, prettier 그외 사용하고 싶은 여러 플러그인들을 설치해주자.

npm i -D eslint eslint-config-airbnb eslint-config-next eslint-config-prettier eslint-plugin-babel eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-hooks prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser
# or
yarn add --dev eslint eslint-config-airbnb eslint-config-next eslint-config-prettier eslint-plugin-babel eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-hooks prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser

 

그 후 .eslintrc.json, .prettierrc.json 파일을 프로젝트 root 폴더에 생성한 후 eslint, prettier 설정을 해주자.

// .eslintrc.json
{
  "parser": "@typescript-eslint/parser", // typescript 전용 parser로 지정
  "parserOptions": {
    "project": "./tsconfig.json",
    "sourceType": "module"
  },
  "env": {
    "node": true, // node 사용 OK
    "browser": true, // browser 인식 Ok
    "es6": true // es6 버전사용 OK
  },
  "plugins": ["@typescript-eslint", "prettier", "react"],
  "extends": [
    "eslint:recommended",
    "airbnb",
    "plugin:react/recommended",
    "prettier", // 8.0.0 버젼이상은 모두 prettier로 통합됨.
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:prettier/recommended",
    "plugin:jsx-a11y/recommended",
    "plugin:import/errors",
    "plugin:import/warnings"
  ], //plugin과 extends로 적용된 규칙에 덮어씌워져서 강제 설정할 수 있는 부분
  "rules": {
    "prettier/prettier": 0,
    "arrow-body-style": "off",
    "prefer-arrow-callback": "off",
    "react/react-in-jsx-scope": "off",
    "react/prefer-stateless-function": 0,
    "react/jsx-one-expression-per-line": 0,
    "react/jsx-filename-extension": "off", // <> </>쓰는 것을 방지
    "no-unused-expressions": 0,
    "import/extensions": ["off"],
    "import/no-unresolved": "off",
    "import/prefer-default-export": "off",
    "@typescript-eslint/no-var-requires": "off", // require 사용을 방지
    "@typescript-eslint/explicit-module-boundary-types": "off", //values returned from a module are of the expected type.
    "no-nested-ternary": "off", // 삼항연산안에 또 삼항연산 하는 것을 방지
    "spaced-comment": "off", // 주석 쓰는 것 방지
    "no-unused-variable": "off", // 사용되지 않는 변수가 있는 것을 방지
    "@typescript-eslint/no-non-null-assertion": "off",
    "react/prop-types": "off",
    "react/jsx-props-no-spreading": "off", // spread 연산자 사용을 방지
    "camelcase": "off", // camelcase만 써야함
    // "@typescript-eslint/ban-types": "off", // function, object를 types로 명시하는 것을 방지
    "no-use-before-define": "off", // 정의 되기 전에 사용하는 것을 방지
    "@typescript-eslint/no-inferrable-types": "off", // 초기값 할당하는 경우 type 명시를 방지
    "react/require-default-props": "off", // props에 undefined 들어가는 것을 방지
    "jsx-a11y/accessible-emoji": "off", // emoji 대신 img 태그 사용
    "jsx-a11y/no-static-element-interactions": "off", // html tag에서 event handler있을 때 role props도 있어야 함
    "jsx-a11y/click-events-have-key-events": "off" // non-interactive한 tag의 경우 클릭 이벤트가 있을 때 keyboard 이벤트도 함께 있어야 함
  },
  "settings": {
    "react": {
      "version": "detect"
    },
    "import/parsers": {
      "@typescript-eslint/parser": [".ts", ".tsx", ".js"]
    }
  }
}
// .prettierrc.json
{
	"singleQuote": false,
	"semi": true,
	"useTabs": false,
	"tabWidth": 2,
	"trailingComma": "all",
	"printWidth": 80
}

.eslintrc.json에서 코드를 짜면서 불필요한 규칙들을 그때 그때 off 해주었더니 rules 부분이 매우 길어졌다...

 

4. 절대경로 설정


디렉토리 구조가 복잡해질 것을 대비하여 절대경로 설정을 해놔야 겠다고 생각했다.

typesciprt에서 절대경로를 설정하기 위해서는 tsconfig.json 파일을 수정해야 한다.

compilerOptions에 baseUrl과 paths 설정을 추가하면 절대경로를 사용할 수 있다.

"baseUrl": ".",
"paths": {
  "@components/*": ["src/components/*"],
  "@pages/*": ["pages/*"],
  "@api/*": ["src/lib/api/*"],
  "@assets/*": ["src/assets/*"]
}

 

5. styled-components 설정


ssr에서 styled-components를 사용하는 경우 서버단에서 미리 만들어진 페이지에 css를 적용시키는 방식이기 때문에, 사용자에게 css가 적용되지 않은 페이지가 먼저 보이게 된다.

이를 방지하기 위해서 미리 css를 수집해서 처리하는 과정이 필요하다.

pages 폴더에 _document.tsx 파일을 생성하고 아래 코드를 입력해주자.

// _document.tsx
import Document, { DocumentContext } from "next/document";
import { ServerStyleSheet } from "styled-components";

export default class MyDocument extends Document {
  static async getInitialProps(ctx: DocumentContext) {
    const sheet = new ServerStyleSheet();
    const originalRenderPage = ctx.renderPage;

    try {
      // sheet을 사용해 정의된 모든 스타일을 수집
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: (App) => (props) =>
            sheet.collectStyles(<App {...props} />),
        });

      // Documents의 initial props
      const initialProps = await Document.getInitialProps(ctx);

      // props와 styles를 반환
      return {
        ...initialProps,
        styles: (
          <>
            {initialProps.styles}
            {sheet.getStyleElement()}
          </>
        ),
      };
    } finally {
      sheet.seal();
    }
  }
}

이를 통해 ssr 방식에서도 css를 페이지에 미리 적용할 수 있다.

typescript에서 theme을 적용하기 위해서는 먼저 theme의 타입을 정의하는 부분이 필요하다.

이를 위해 styled.d.ts 파일을 styles 폴더안에 만들어주었다.

// styled.d.ts
import "styled-components";

declare module "styled-components" {
  export interface DefaultTheme {
    media: any;
  }
}

그리고 theme 파일 안에 미디어쿼리를 위한 코드를 작성해주었다.

import { DefaultTheme, css, CSSObject } from "styled-components";

const sizes = {
  desktop_1: 1919,
  desktop_2: 1599,
  desktop_3: 1365,
  desktop_4: 1023,
  tablet: 600,
  mobile: 374,
};

const theme: DefaultTheme = {
  media: Object.keys(sizes).reduce((acc: any, label: string) => {
    if (
      label === "desktop_1" ||
      label === "desktop_2" ||
      label === "desktop_3" ||
      label === "desktop_4" ||
      label === "tablet" ||
      label === "mobile"
    ) {
      acc[label] = (args: CSSObject | TemplateStringsArray) => css`
        @media (max-width: ${sizes[label]}px) {
          ${css(args)};
        }
      `;
    }
    return acc;
  }, {}),
};

export { theme };

원래는 color system도 theme에서 관리하려고 했는데, 팀원이 :root 를 사용하면 더 간편하게 색상을 관리할 수 있다는 것을 알려주어서 이것으로 관리하기로 결정했다.

https://developer.mozilla.org/ko/docs/Web/CSS/Using_CSS_custom_properties

 

사용자 지정 CSS 속성 사용하기 (변수) - CSS: Cascading Style Sheets | MDN

사용자 지정 속성(CSS 변수, 종속 변수)은 CSS 저작자가 정의하는 개체로, 문서 전반적으로 재사용할 임의의 값을 담습니다. 사용자 지정 속성은 전용 표기법을 사용해 정의하고, (--main-color: black;)

developer.mozilla.org

:root, var을 사용해서 colors system을 관리할 경우 개발도구에서 색상 목록도 볼 수 있다는 장점이 있었다.

그리고 GlobalStyle을 통해 font 설정과 기본 css를 normalize 해주었다.

 

 

6. @svgr/webpack, url-loader 설정


cra와 다르게 next.js에서는 svg 파일을 사용하기 위해 웹팩설정을 직접 해주어야 했다.

웹팩은 next.config.js 파일에서 하면 된다.

먼저 svgr 웹팩과 url-loader를 설치해주자.

npm i @svgr/webpack url-loader
# or
yarn add @svgr/webpack url-loader

 그 후 next.config.js 파일에 다음과 같은 코드를 작성해주면 svg 파일을 사용할 수 있다.

svg 웹팩을 설정하는 김에 개발환경에 따라 다른 환경변수를 사용하기 위해 define 플러그인도 설정해주었다.

const Dotenv = require("dotenv-webpack");

module.exports = {
  reactStrictMode: true,
  webpack(config, { dev, webpack }) {
    config.module.rules.push({
      test: /\.svg$/,
      use: ["@svgr/webpack", "url-loader"],
    });

    config.plugins.push(
      new webpack.DefinePlugin({
        API_DOMAIN:
          process.env.NODE_ENV === "production"
            ? JSON.stringify(process.env.API_URL)
            : JSON.stringify(process.env.DEV_API_URL),
      }),
      new webpack.EnvironmentPlugin(["NODE_ENV"]),
      new Dotenv({ silent: true }),
    );

    return config;
  },
};

 

7. storybook 관련 설정


storybook을 사용하기 위해서는 먼저 프로젝트 root 폴더에서 터미널에 해당 명령어를 입력하여 패키지를 다운받아야 한다.

npx sb init

 

그러면 프로젝트 root 폴더 안에 .storybook이라는 폴더가 생성되었을 것이다.

그리고 src 폴더 안에 storybook이라는 폴더가 생성되었을텐데 이 친구는 필요없으니 삭제해도 된다.

그리고 해당 명령어를 입력해주자.

npm i -D react-docgen-typescript-loader
# or
yarn add --dev react-docgen-typescript-loader

이 라이브러리는 storybook을 실행했을 때 컴포넌트에 대한 테이블을 만들기 위함이다.

이 테이블에는 컴포넌트의 props, type, 주석 등이 적혀 있어서 개발할 때 아주 유용하다.

.storybook 폴더 안에는 main.js와 preview.js가 있는데 이 파일들로 storybook 설정을 하면 된다!

 

1) 절대경로 설정

storybook에서 절대경로를 사용하기 위해서는 절대경로 설정을 따로 해주어야 한다.

main.js 파일을 열어서 다음과 같이 코드를 수정해주자.

// .storybook/main.js
const path = require("path");

module.exports = {
  stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "storybook-addon-styled-component-theme/dist/preset",
  ],
  webpackFinal: async (config) => {
    config.resolve.modules = [
      path.resolve(__dirname, ".."),
      "node_modules",
      "styles",
    ];

    config.resolve.alias = {
      ...config.resolve.alias,
      "@components": path.resolve(__dirname, "../src/components"),
      "@assets": path.resolve(__dirname, "../src/assets"),
    };

    return config;
  },
};

config.resolve.modules 부분은 node_modules 폴더와 styles 폴더 안의 모듈을 인식할 수 있게 하는 부분이고, config.resolve.alias 부분이 절대경로를 설정하는 부분이다.

 

2) GlobalStyle과 theme 적용

위의 설정을 모두 끝낸 후에 yarn storybook 명령어를 입력하여 storybook이 잘 되는지 봤더니 GlobalStyle과 theme 적용이 되지 않았다.

이를 해결하기 위해 먼저

npm i -D storybook-addon-styled-component-theme
# or
yarn add --dev storybook-addon-styled-component-theme

를 통해 storybook-addon-styled-component-theme를 설치해주었다.

그 후 preview.js 파일을 수정해주었다.

// .storybook/preview.js
import { addDecorator } from "@storybook/react";
import { withThemesProvider } from "storybook-addon-styled-component-theme";
import { ThemeProvider } from "styled-components";
import { theme } from "../styles/theme";
import { GlobalStyle } from "../styles/GlobalStyles";

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
};

export const decorators = [
  (Story) => (
    <>
      <GlobalStyle />
      <Story />
    </>
  ),
];

const Themes = [theme];

addDecorator(withThemesProvider(Themes), ThemeProvider);

 

3) svg 설정

storybook에서 svg를 사용하려면 역시 관련 설정을 해주어야 한다.

그래서 main.js 파일을 다음과 같이 수정해주었다.

// .storybook/main.js
const path = require("path");

module.exports = {
  stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "storybook-addon-styled-component-theme/dist/preset",
  ],
  webpackFinal: async (config) => {
    config.module.rules.unshift({
      test: /\.svg$/,
      use: ["@svgr/webpack"],
    });

    config.resolve.modules = [
      path.resolve(__dirname, ".."),
      "node_modules",
      "styles",
    ];

    config.resolve.alias = {
      ...config.resolve.alias,
      "@components": path.resolve(__dirname, "../src/components"),
      "@assets": path.resolve(__dirname, "../src/assets"),
    };

    return config;
  },
};

그리고 svg를 ReactComponent를 통해 컴포넌트처럼 사용하고 싶었는데, 참고한 사이트에서는 이를 위해 storybook이 ReactComponent를 인식할 수 있도록 해야하는 과정이 필요하다고 하였다.

그래서 src 폴더안에 typing.d.ts 파일을 만들어 주었다.

// src/typing.d.ts
declare module '*.mdx';

declare module '*.svg' {
  import * as React from 'react';

  export const ReactComponent: React.FunctionComponent<React.SVGProps<
    SVGSVGElement
  >>;

  const src: string;
  export default src;
}

 

이 모든 과정을 끝냈다면 이제 next.js에서 typescript와 styled-components, storybook을 사용할 수 있다~~~🎉

 

 

 

참고 사이트


- typescript로 styled-components 적용

https://flowkater.io/frontend/setup-styled-components/

 

2. Styled Components 세팅하기

이번에는 React Typescript 환경에서 Styled Components를 세팅해보자.

flowkater.io

- storybook 적용

https://velog.io/@velopert/create-your-own-design-system-with-storybook

 

Storybook을 활용하여 본격적으로 디자인 시스템 구축하기

스토리북을 쓰는 방법을 어느정도 배웠으니, 이제 Hello 컴포넌트 말고 정말 디자인 시스템에 있어서 유의미한 컴포넌트들을 만들어봅시다. 그런데, 어떤 컴포넌트를 만들어야 할까요? 사실 가장

velog.io

 

 

 

반응형

댓글