프론트엔드 현재 환경 설정 - woowacourse-teams/2023-shook GitHub Wiki

23.09.01 - webpack 및 stylelint 설정 최신화, 설명 추가

목차

  1. Webpack
  2. Babel
  3. tsconfig
  4. prettier
  5. eslint
  6. jest
  7. storybook
  8. stylelint

Webpack

webpack.common.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/index.tsx',
  output: {
    assetModuleFilename: 'assets/[hash][ext]',
    publicPath: '/',
  },
  module: {
    rules: [
      {
        test: /\.(png|svg|jpe?g)$/,
        type: 'asset/resource',
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/i,
        type: 'asset/resource',
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
  resolve: {
    extensions: ['.tsx', '.ts', '.jsx', '.js'],
    alias: {
      '@': path.resolve(__dirname, 'src/'),
    },
  },
};
  • output.assetModuleFilename - asset의 정적 리소스 빌드될 파일명.
  • output.publicPath - asset에 대한 기본 경로 설정. 설정하지 않는다면 새로 고침시 해당 경로에서 파일을 불러오려고 함. (ex - http://localhost:3000/songs/main.js)
  • resolve.alias - 모듈 경로의 별칭 지정. (ex - import GlobalStyles from '@/shared/styles/GlobalStyles';)

webpack.dev.js

const { merge } = require('webpack-merge');
const common = require('./webpack.common');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const webpack = require('webpack');
const dotenv = require('dotenv');

dotenv.config({ path: '.env/.env.development' });

module.exports = merge(common, {
  mode: 'development',
  devServer: {
    historyApiFallback: true,
    open: true,
    port: 3000,
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              plugins: ['react-refresh/babel'],
            },
          },
          'ts-loader',
        ],
      },
    ],
  },
  plugins: [
    new ReactRefreshWebpackPlugin({ overlay: false }),
    new webpack.DefinePlugin({
      'process.env': JSON.stringify(process.env),
    }),
  ],
});
  • devServer.historyApiFallback - 404 응답에 index.html을 제공.
  • devServer.port - 현재 개발서버 8080 포트로 api 요청시 CORS 에러가 발생하여서 개발환경시 3000 포트 사용중.
  • devServer.static - 기본적으로 활성화되어 있으며, 개발서버 빌드시 public 폴더를 정적 제공함.

webpack.prod.js

const path = require('path');
const { merge } = require('webpack-merge');
const common = require('./webpack.common');
const webpack = require('webpack');
const dotenv = require('dotenv');

dotenv.config({ path: '.env/.env.production' });

module.exports = merge(common, {
  mode: 'production',
  output: {
    filename: 'static/[contenthash].js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        exclude: /node_modules/,
        use: ['babel-loader', 'ts-loader'],
      },
    ],
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': JSON.stringify(process.env),
    }),
  ],
});

Babel

babel.config.js

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          // TODO: 브라우저 지원 범위
          chrome: '51',
        },
        useBuiltIns: 'entry',
        corejs: '3.31.1',
      },
    ],
    ['@babel/preset-react'],
    ['@babel/preset-typescript'],
  ],
};

tsconfig

tsconfig.json

{
  "compilerOptions": {
    "target": "ES6",
    "lib": ["DOM", "DOM.Iterable", "ES2023"],
    "jsx": "react-jsx",
    "module": "ESNext",
    "moduleResolution": "Node",
    "resolveJsonModule": true,
    "allowJs": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noFallthroughCasesInSwitch": true,
    "skipLibCheck": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src", ".storybook/*"]
}
  • "target": "ES6" : 컴파일러가 ES6버전의 자바스크립트로 변환합니다.
  • "lib": ["DOM", "DOM.Iterable", "ES2023"] : 컴파일 과정에서 사용하는 라이브러리 목록입니다. DOM.Iterable의 경우 DOM요소를 루프를 돌릴 수 있도록 합니다.
  • "jsx": "react-jsx" : _jsx()을 사용하여 React 가상 dom 객체를 만들도록 합니다.
  • "module": "ESNext" : 컴파일을 완료한 파일이 어떤 모듈 시스템으로 적용되는 지 정합니다.
  • "moduleResolution": "Node" : 컴파일 과정에서 어떤 모듈 시스템으로 해결할 지 정합니다.
  • "resolveJsonModule": true : json 파일도 허용해줍니다.
  • "allowJs": true : javascript 파일도 허용해줍니다.
  • "isolatedModules": true : 모듈을 export 하도록 강제합니다.
  • "esModuleInterop": true : 각각의 소스파일을 모듈로 만들도록 강제합니다. 타입스크립트에선 import/export가 없으면 전역으로 접근이 가능합니다. 해당 속성을 통해 해당 문제를 방지할 수 있습니다.
  • "forceConsistentCasingInFileNames": true : 파일 이름의 대소문자를 구분합니다.
  • "strict": true : 타입스크립트 타입 체크를 엄격하게 합니다.
  • "noFallthroughCasesInSwitch": true : switch fall through를 막습니다.
  • "skipLibCheck": true : TypeScript가 라이브러리 파일을 체크하지 않도록 설정합니다.
  • "baseUrl": "." : baseUrl을 root로 지정합니다.
  • "paths": { "@/*": ["src/*"] } : path @/*를 src/*로 읽습니다.
  • "include": ["src", ".storybook/*"] : src와 .storybook에 있는 파일을 컴파일합니다.

prettier

.prettierrc

{
  "printWidth": 100,
  "singleQuote": true,
  "trailingComma": "es5",
  "endOfLine": "auto"
}

eslint

.eslintrc.js

module.exports = {
  env: {
    browser: true,
    es2023: true,
  },
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 2023,
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true,
    },
  },
  plugins: ['import', 'react', '@typescript-eslint'],
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:react/jsx-runtime',
    'plugin:react-hooks/recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:import/recommended',
    'plugin:import/typescript',
    'plugin:prettier/recommended',
    'plugin:storybook/recommended',
  ],
  settings: {
    'import/resolver': {
      typescript: {},
      node: {},
    },
    'import/parsers': { '@typescript-eslint/parser': ['.ts', '.tsx'] },
    react: {
      version: 'detect',
    },
  },
  rules: {
    'prettier/prettier': 'warn',
    'prefer-const': 'warn',
    '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],
    'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
    'react/self-closing-comp': [
      'error',
      {
        component: true,
        html: true,
      },
    ],
    'import/no-named-as-default': 'off',
    'import/order': [
      'error',
      {
        groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
        alphabetize: {
          order: 'asc',
          caseInsensitive: true,
        },
        'newlines-between': 'never',
      },
    ],
  },
  ignorePatterns: ['/*', '!/src'],
};

jest

jest.config.js

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
};

storybook

.storybook/main.ts

import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
import type { StorybookConfig } from '@storybook/react-webpack5';

const config: StorybookConfig = {
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/addon-styling',
  ],
  framework: {
    name: '@storybook/react-webpack5',
    options: {},
  },
  docs: {
    autodocs: true,
  },
  webpackFinal: async (config) => {
    if (config.resolve) {
      config.resolve.plugins = [
        ...(config.resolve?.plugins || []),
        new TsconfigPathsPlugin({
          extensions: config.resolve?.extensions,
        }),
      ];
      return config;
    }
    return config;
  },
};

export default config;

.storybook/preview.ts

import type { Preview } from '@storybook/react';
import { withThemeFromJSXProvider } from '@storybook/addon-styling';
import GlobalStyles from '../src/shared/styles/GlobalStyles';
import { ThemeProvider } from 'styled-components';
import theme from '../src/shared/styles/theme';

const preview: Preview = {
  parameters: {
    actions: { argTypesRegex: '^on[A-Z].*' },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/,
      },
    },
  },
  decorators: [
    (Story) => (
      <ThemeProvider theme={theme}>
        <Story />
      </ThemeProvider>
    ),
    withThemeFromJSXProvider({ GlobalStyles }),
  ],
};

export default preview;

stylelint

.stylelintrc.json

{
  "extends": ["stylelint-config-clean-order"],
  "customSyntax": "postcss-styled-syntax"
}
⚠️ **GitHub.com Fallback** ⚠️