2023년 01월 02일
8

React 프로젝트 표준

Frontend
KKingmo

Changmo Oh

@KKingmo

전체 글 보기

서론

React 애플리케이션에서 코드 품질, 일관성 및 확장성을 유지하기 위해 프로젝트 표준을 강제하는 것은 매우 중요하다. 최고의 표준을 설정하고 준수함으로써 개발자는 코드베이스를 깨끗하고, 조직적이며 유지보수하기 쉽게 유지할 수 있다.

ESLint

ESLint는 코드 품질을 유지하고 코딩 표준을 준수하는 데 매우 유용한 도구이다. .eslintrc.js 파일에서 규칙을 설정함으로써, ESLint는 일반적인 오류를 식별하고 방지하는 데 도움을 준다. 이는 초기 단계에서 실수를 발견하는 데 도움을 주며, 전체 코드베이스에 걸쳐 일관성을 증진시키고 코드의 품질과 가독성을 높인다.

ESLint 구성 예시 코드

module.exports = {
  root: true,
  env: {
    node: true,
    es6: true,
  },
  parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
  ignorePatterns: [
    'node_modules/*',
    'public/mockServiceWorker.js',
    'generators/*',
  ],
  extends: ['eslint:recommended'],
  plugins: ['check-file'],
  overrides: [
    {
      files: ['**/*.ts', '**/*.tsx'],
      parser: '@typescript-eslint/parser',
      settings: {
        react: { version: 'detect' },
        'import/resolver': {
          typescript: {},
        },
      },
      env: {
        browser: true,
        node: true,
        es6: true,
      },
      extends: [
        'eslint:recommended',
        'plugin:import/errors',
        'plugin:import/warnings',
        'plugin:import/typescript',
        'plugin:@typescript-eslint/recommended',
        'plugin:react/recommended',
        'plugin:react-hooks/recommended',
        'plugin:jsx-a11y/recommended',
        'plugin:prettier/recommended',
        'plugin:testing-library/react',
        'plugin:jest-dom/recommended',
        'plugin:tailwindcss/recommended',
        'plugin:vitest/legacy-recommended',
      ],
      rules: {
        'import/no-restricted-paths': [
          'error',
          {
            zones: [
              // disables cross-feature imports:
              // eg. src/features/discussions should not import from src/features/comments, etc.
              {
                target: './src/features/auth',
                from: './src/features',
                except: ['./auth'],
              },
              {
                target: './src/features/comments',
                from: './src/features',
                except: ['./comments'],
              },
              {
                target: './src/features/discussions',
                from: './src/features',
                except: ['./discussions'],
              },
              {
                target: './src/features/teams',
                from: './src/features',
                except: ['./teams'],
              },
              {
                target: './src/features/users',
                from: './src/features',
                except: ['./users'],
              },
              // enforce unidirectional codebase:
 
              // e.g. src/app can import from src/features but not the other way around
              {
                target: './src/features',
                from: './src/app',
              },
 
              // e.g src/features and src/app can import from these shared modules but not the other way around
              {
                target: [
                  './src/components',
                  './src/hooks',
                  './src/lib',
                  './src/types',
                  './src/utils',
                ],
                from: ['./src/features', './src/app'],
              },
            ],
          },
        ],
        'import/no-cycle': 'error',
        'linebreak-style': ['error', 'unix'],
        'react/prop-types': 'off',
        'import/order': [
          'error',
          {
            groups: [
              'builtin',
              'external',
              'internal',
              'parent',
              'sibling',
              'index',
              'object',
            ],
            'newlines-between': 'always',
            alphabetize: { order: 'asc', caseInsensitive: true },
          },
        ],
        'import/default': 'off',
        'import/no-named-as-default-member': 'off',
        'import/no-named-as-default': 'off',
        'react/react-in-jsx-scope': 'off',
        'jsx-a11y/anchor-is-valid': 'off',
        '@typescript-eslint/no-unused-vars': ['error'],
        '@typescript-eslint/explicit-function-return-type': ['off'],
        '@typescript-eslint/explicit-module-boundary-types': ['off'],
        '@typescript-eslint/no-empty-function': ['off'],
        '@typescript-eslint/no-explicit-any': ['off'],
        'prettier/prettier': ['error', {}, { usePrettierrc: true }],
        'check-file/filename-naming-convention': [
          'error',
          {
            '**/*.{ts,tsx}': 'KEBAB_CASE',
          },
          {
            ignoreMiddleExtensions: true,
          },
        ],
      },
    },
    {
      plugins: ['check-file'],
      files: ['src/**/!(__tests__)/*'],
      rules: {
        'check-file/folder-naming-convention': [
          'error',
          {
            '**/*': 'KEBAB_CASE',
          },
        ],
      },
    },
  ],
};

Prettier

Prettier는 프로젝트에서 코드의 일관된 포맷을 유지하는 데 유용한 도구다. IDE에서 "저장 시 자동 포맷" 기능을 활성화하면, 미리 설정된 .prettierrc 파일에 따라 코드가 자동으로 정리된다. 이 기능은 전체 코드베이스에 걸쳐 일관된 스타일을 보장하고 코드 문제에 대한 유용한 피드백을 제공한다. 만약 자동 포맷팅이 실패한다면, 이는 잠재적인 구문 오류가 있을 수 있음을 알려주는 신호가 될 수 있다. 더욱이, Prettier는 ESLint와 통합되어 코딩 규칙을 효과적으로 적용하며 동시에 코드 포맷팅 작업을 처리한다.

Prettier 구성 예시 코드

{
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 80,
  "tabWidth": 2,
  "useTabs": false
}

TypeScript

ESLint는 JavaScript에서 언어 관련 버그를 감지하는 데 효과적이다. 그러나 JavaScript의 동적 특성 때문에, 특히 복잡한 프로젝트에서는 ESLint가 모든 런타임 데이터 문제를 잡아내지 못할 수 있다. 이를 해결하기 위해 TypeScript를 권장한다. TypeScript는 대규모 리팩토링 과정에서 눈에 띄지 않을 수 있는 문제들을 식별하는 데 유용하다. 리팩토링할 때는 먼저 타입 선언을 업데이트하고, 그 후 프로젝트 전반에 걸쳐 TypeScript 오류를 해결하는 것이 중요하다. TypeScript는 빌드 시 타입 검사를 수행하여 개발자의 자신감을 높이지만, 런타임 오류를 방지하지는 않는다.

Husky

Husky는 워크플로에서 git 훅을 구현하고 실행하는 데 유용한 도구다. 각 커밋 전에 코드 검증을 실행하면, 코드가 높은 표준을 유지하고 결함 있는 커밋이 저장소에 푸시되지 않도록 할 수 있다. 또한 린팅, 코드 포맷팅, 타입 검사 등 다양한 작업을 코드 푸시 전에 수행할 수 있다.

절대 경로

절대 경로 임포트는 항상 구성하고 사용해야 한다. 이렇게 하면 파일을 이동하기 쉬워지고 ../../../component와 같은 복잡한 임포트 경로를 피할 수 있다. 파일을 어디로 이동하든 모든 임포트가 그대로 유지된다.
설정 방법은 다음과 같다.

JavaScript (jsconfig.json) 프로젝트의 경우

"compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }

TypeScript (tsconfig.json) 프로젝트의 경우:

"compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }

여러 폴더(@components, @hooks 등)에 대해 여러 경로를 정의할 수도 있지만, @/*를 사용하는 것이 매우 효과적이다. 경로가 짧아서 여러 경로를 구성할 필요가 없고, 다른 종속성 모듈과 구별되므로 node_modules에서 오는 것과 소스 폴더에서 오는 것을 혼동하지 않게 한다. 즉, src 폴더에 있는 모든 파일은 @를 통해 접근할 수 있다. 예를 들어 src/components/my-component에 있는 파일은 ../../../components/my-component 대신 @/components/my-component를 사용해 접근할 수 있다.

파일 명명 규칙

프로젝트에서 파일 명명 규칙과 폴더 명명 규칙도 강제할 수 있다. 예를 들어, 모든 파일을 케밥 케이스(kebab-case)로 명명하도록 강제할 수 있다. 이렇게 하면 코드베이스를 일관되게 유지하고 탐색하기 쉽게 만든다.

Eslint 설정

  'check-file/filename-naming-convention': [
  'error',
  {
    '**/*.{ts,tsx}': 'KEBAB_CASE',
  },
  {
    // ignore the middle extensions of the filename to support filename like bable.config.js or smoke.spec.ts
    ignoreMiddleExtensions: true,
  },
  ],
  'check-file/folder-naming-convention': [
  'error',
  {
    // all folders within src (except __tests__)should be named in kebab-case
    'src/**/!(__tests__)': 'KEBAB_CASE',
  },
  ],