๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

react/๋ฏธ๋‹ˆํ”„๋กœ์ ํŠธ 1 - cyber ์‚ฌ์ดํŠธ

React / Eslint(airbnb ...?) prettier ์ ์šฉํ•˜๊ธฐ

+ 250416 ์ˆ˜์ • ๋‚ด์šฉโ–ผ

๋”๋ณด๊ธฐ

- eslint-plugin-import, eslint-plugin-jsx-a11y ๋ถ€๋ถ„ import, plugin, rules์— ๋นผ๋จน์€๊ฑฐ ์ˆ˜์ •

- ๊ทธ ์™ธ ์—์–ด๋น„์—”๋น„ ๊ทœ์น™ ์ผ๋ถ€ ์ถ”๊ฐ€


์›๋ž˜ ์ œ๋ชฉ์—๋Š” airbnb ํ˜•์‹์ด๋ผ๊ณ  ์ผ์—ˆ๋Š”๋ฐ

์‚ฌ์‹ค airbnb ํŒจํ‚ค์ง€๋Š” ๋‚ด๊ฐ€ ์ด ํฌ์ŠคํŠธ์—์„œ ์ž‘์„ฑํ•œ Flat Config ํ˜•์‹์—์„œ๋Š” ์•„์ง ์ง€์›ํ•˜์ง€ ์•Š๊ณ  ์žˆ๋‹ค...

 

์ˆ˜์ • ์ „์—๋„ eslint-config-airbnb๋Š” flat config์—์„œ ์•„์ง์€ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— rules์—์„œ ์ˆ˜๋™์œผ๋กœ ๋ถˆ๋Ÿฌ์™€์ค˜์•ผ ํ•œ๋‹ค๊ณ  ์ค‘๊ฐ„์— ์ž‘์„ฑํ•ด๋‘๊ธด ํ–ˆ์œผ๋‚˜ ์•„์˜ˆ ๋Œ์˜ฌํ•ด๋‘”๋‹ค...

 

์™œ๋ƒํ•˜๋ฉด ๊ธฐ์กด .eslintrc ํŒŒ์ผ ํ˜•์‹์—์„œ๋Š” "extends":["airbnb"] ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ๋Š” ์‹์œผ๋กœ ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•˜์ง€๋งŒ

Flat Config ํ˜•์‹์—์„œ๋Š” ๋ฃฐ์„ ์ง์ ‘ ํ’€์–ด์„œ ์จ์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ง์ ‘ ๋‹ค ๊ฐ€์ ธ์˜ค๋Š”๊ฒŒ ์•„๋‹ˆ๊ณ ์„œ์•ผ ์ด์ „์ฒ˜๋Ÿผ ํŒจํ‚ค์ง€ ์„ค์น˜, ํ•œ์ค„์ถ”๊ฐ€๋กœ ์‚ฌ์šฉํ•  ์ˆ˜๊ฐ€ ์—†๋‹ค๊ณ  ํ•œ๋‹ค.

 

* react + javascript ํ”„๋กœ์ ํŠธ ๊ธฐ์ค€์ž…๋‹ˆ๋‹ค.

 

 

 

1. ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ

npm create vite@latest my-app -- --template react
// ์™„๋ฃŒ๋˜๋ฉด ํ”„๋กœ์ ํŠธ ํด๋”๋กœ ์ด๋™
cd my-app
// ์„ค์น˜
npm install

 

  • my-app : ์ƒ์„ฑํ•  ํ”„๋กœ์ ํŠธ ์ด๋ฆ„
  • -- --template react : ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๊ธฐ๋ฐ˜ React ํ…œํ”Œ๋ฆฟ์œผ๋กœ ์ง€์ •
    • --template react : React + JavaScript
    • --template react-ts : React + TypeScript
    • --template vanilla : ์ผ๋ฐ˜ JS
    • --template vanilla-ts : ์ผ๋ฐ˜ TS

* -- --๋กœ ์จ์•ผํ•˜๋Š” ์ด์œ 

  • ์ฒซ๋ฒˆ์งธ -- : ๊ตฌ๋ถ„ ์—ญํ• (my-app๊นŒ์ง€๋งŒ create vite๋กœ ๋„˜๊ธฐ๊ณ  ๊ทธ ๋’ค ๋‚ด์šฉ์€ ์‹ค์ œ Vite CLI์— ์ „๋‹ฌํ•˜๋„๋ก ๊ตฌ๋ถ„, ๊ฒฝ๊ณ„ ํ‘œ์‹œ์ž
  • ๋‘๋ฒˆ์งธ -- : ์‹ค์ œ Vite CLI์— ์ „๋‹ฌ๋  ์˜ต์…˜

 

 

2. ํ•„์š” ํŒจํ‚ค์ง€ ์„ค์น˜

npm install -D eslint@8.57.1 prettier @eslint/js eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks@4.6.0 eslint-config-prettier eslint-plugin-prettier

 

์ด๋•Œ npm install -D์—์„œ -D๋Š” --save-dev์˜ ์ค„์ž„๋ง๋กœ, ๊ฐœ๋ฐœ ์˜์กด์„ฑ์œผ๋กœ ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•˜๊ฒ ๋‹ค๋Š” ๋œป์ด๋‹ค. (ํ”„๋กœ์ ํŠธ๋ฅผ ์‹คํ–‰ํ•  ๋•Œ๋Š” ํ•„์š” ์—†๊ธฐ ๋•Œ๋ฌธ)

- eslint-plugin-react-hooks : use๋กœ ์‹œ์ž‘ํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ํ›…์œผ๋กœ ๊ฐ„์ฃผํ•˜๊ณ  (์ปค์Šคํ…€ ํ›…๋„ ํฌํ•จ) ๊ฒ€์‚ฌ

- eslint-plugin-jsx-a11y : JSX ๋‚ด์˜ ์ ‘๊ทผ์„ฑ ๋ฌธ์ œ์— ๋Œ€ํ•ด ์ฆ‰๊ฐ์ ์ธ AST ๋ฆฐํŒ… ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณต

- eslint-plugin-import :์ฃผ๋กœ ๋ชจ๋“ˆ์ด๋‚˜ import๋ฌธ ๊ด€๋ จ ๊ทœ์น™์„ ๊ฒ€์‚ฌํ•˜๋Š” eslint ํ”Œ๋Ÿฌ๊ทธ์ธ

 

 

 

3. ESLint ์„ค์ • ํŒŒ์ผ ์ˆ˜์ •

 

eslint v9๋ถ€ํ„ฐ ๊ธฐ์กด์— ์‚ฌ์šฉํ•˜๋˜ .eslintrc.json|js|yaml ์˜ ์„ค์ •ํŒŒ์ผ์€ ์ด์ œ ๋”์ด์ƒ ์‚ฌ์šฉ๋˜์ง€ ์•Š์œผ๋ฉฐ, Flat config๋ฅผ ์“ด๋‹ค๊ณ  ํ–ˆ๋‹ค.

๋ฌผ๋ก  ๋‚ด ํ”„๋กœ์ ํŠธ๋Š” v8.57.1์„ ์‚ฌ์šฉํ•ด์„œ .eslintrc.json ๋“ฑ์„ ์จ๋„ ๋˜๊ธดํ•˜์ง€๋งŒ ์–ด์ฐจํ”ผ ์ด์ œ ์•ˆ์‚ฌ์šฉ๋œ๋‹ค๋Š”๋ฐ ๊ตณ์ด...?

์•„๋ฌดํŠผ 2๋ฒˆ๊นŒ์ง€ ์ง„ํ–‰๋๋‹ค๋ฉด eslint.config.js๊ฐ€ ์ž๋™ ์ƒ์„ฑ๋ผ์žˆ์„ ๊ฒƒ์ด๋‹ค.

 

eslint.config.js

import js from '@eslint/js';
import importPlugin from 'eslint-plugin-import';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import prettier from 'eslint-plugin-prettier';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import globals from 'globals';

export default [
  {
    ignores: ['dist', 'node_modules'],
  },
  {
    files: ['**/*.{js,jsx}'],
    languageOptions: {
      ecmaVersion: 'latest',
      sourceType: 'module',
      parserOptions: {
        ecmaFeatures: {
          jsx: true,
        },
      },
      globals: globals.browser,
    },
    plugins: {
      react,
      'react-hooks': reactHooks,
      'react-refresh': reactRefresh,
      prettier,
      'jsx-a11y': jsxA11y,
      import: importPlugin,
    },
    rules: {
      ...js.configs.recommended.rules,
      ...react.configs.recommended.rules,
      ...reactHooks.configs.recommended.rules,
      ...jsxA11y.configs.recommended.rules,
      ...importPlugin.configs.recommended.rules,

      'prettier/prettier': [
        'error',
        {
          endOfLine: 'auto',
          singleQuote: true,
          trailingComma: 'all',
          tabWidth: 2,
          useTabs: false,
        },
      ],

      'react/jsx-filename-extension': ['warn', { extensions: ['.js', '.jsx'] }],
      'react/react-in-jsx-scope': 'off',
      'no-unused-vars': ['warn', { varsIgnorePattern: '^[A-Z_]' }],
      'react-refresh/only-export-components': [
        'warn',
        { allowConstantExport: true },
      ],
      'no-var': 'warn', // ← var ์‚ฌ์šฉํ•˜๋ฉด ๊ฒฝ๊ณ 
      'prefer-const': 'warn', // ← const ์“ธ ์ˆ˜ ์žˆ์„ ๋•Œ let ์“ฐ๋ฉด ๊ฒฝ๊ณ 
      'import/order': [
        'warn',
        {
          // ← import ์ˆœ์„œ ๊ด€๋ฆฌ
          groups: ['builtin', 'external', 'internal'],
          pathGroups: [
            {
              pattern: 'react',
              group: 'external',
              position: 'before',
            },
          ],
          pathGroupsExcludedImportTypes: ['react'],
          'newlines-between': 'always',
          alphabetize: {
            order: 'asc',
            caseInsensitive: true,
          },
        },
      ],
    },
    settings: {
      react: {
        version: 'detect',
      },
    },
  },
];

 

 

eslint.config.js ์ƒ์„ธ ์„ค๋ช…

๋”๋ณด๊ธฐ

์ฑ—์ง€ํ”ผํ‹ฐ์˜ ํž˜์„ ๋นŒ๋ ธ์Šต๋‹ˆ๋‹ค. ๋Š˜ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค... 

 

import ๋ถ€๋ถ„

import js from '@eslint/js';
import globals from 'globals';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import prettier from 'eslint-plugin-prettier';
  • @eslint/js: ESLint ๊ณต์‹ ๊ธฐ๋ณธ ๊ทœ์น™ ๋ชจ์Œ. (ex. no-unused-vars, no-console ๋“ฑ)
  • globals: ๋ธŒ๋ผ์šฐ์ € ํ™˜๊ฒฝ์—์„œ์˜ ์ „์—ญ ๋ณ€์ˆ˜๋“ค์„ ์ •์˜ (window, document, console, ๋“ฑ).
  • eslint-plugin-react: ๋ฆฌ์•กํŠธ ๊ด€๋ จ ๊ทœ์น™์„ ์ œ๊ณตํ•˜๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ.
  • eslint-plugin-react-hooks: useEffect, useState ๋“ฑ ๋ฆฌ์•กํŠธ ํ›… ๊ทœ์น™์„ ๊ฒ€์‚ฌ.
  • eslint-plugin-react-refresh: Vite์˜ Fast Refresh ๊ด€๋ จ ๊ทœ์น™.
  • eslint-plugin-prettier: Prettier์™€ ESLint ํ†ตํ•ฉ์šฉ. Prettier ํฌ๋งทํŒ… ์˜ค๋ฅ˜๋ฅผ ESLint ์—๋Ÿฌ๋กœ ๋ณด์—ฌ์คŒ

 

ํŒŒ์ผ ๋ฌด์‹œ ์„ค์ •

  { // ESLint๊ฐ€ ๋ถ„์„ํ•˜์ง€ ์•Š์„ ๋””๋ ‰ํ† ๋ฆฌ ๋ชฉ๋ก
    ignores: ['dist', 'node_modules'],
  },

 

์‹ค์ œ Lint ์„ค์ •

  { // .js, .jsx ํ™•์žฅ์ž์˜ ๋ชจ๋“  ํŒŒ์ผ์— ์•„๋ž˜ ์„ค์ • ์ ์šฉ
    files: ['**/*.{js,jsx}'],
    languageOptions: {
      ecmaVersion: 'latest', // ๊ฐ€์žฅ ์ตœ์‹  ECMAScript ๋ฌธ๋ฒ• ์‚ฌ์šฉ
      sourceType: 'module', // ES ๋ชจ๋“ˆ ์‚ฌ์šฉ ๊ฐ€๋Šฅ (import, export)
      parserOptions: {
        ecmaFeatures: {
          jsx: true, // jsx ๋ฌธ๋ฒ• ์‚ฌ์šฉ ๊ฐ€๋Šฅ
        },
      },
      globals: globals.browser,// window, document ๋“ฑ ๋ธŒ๋ผ์šฐ์ € ์ „์—ญ๊ฐ์ฒด ํ—ˆ์šฉ
    },
    plugins: { 
    // ๊ฐ ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์„ค์ •์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ถˆ๋Ÿฌ์˜ด
    // ์ด ์ด๋ฆ„๋“ค์„ ์•„๋ž˜ rules์—์„œ ํ‚ค๋กœ ์‚ฌ์šฉ(reactHooks, reactRefresh...)
      react,
      'react-hooks': reactHooks,
      'react-refresh': reactRefresh,
      prettier,
      'jsx-a11y': jsxA11y,
      import: importPlugin,
    },
    rules: {
      // ๊ธฐ๋ณธ ESLint + React + React Hooks ์ถ”์ฒœ ๊ทœ์น™
      ...js.configs.recommended.rules,
      ...react.configs.recommended.rules,
      ...reactHooks.configs.recommended.rules,
      ...jsxA11y.configs.recommended.rules,
      ...importPlugin.configs.recommended.rules,
	  
      // .jsx ํ™•์žฅ์ž๊ฐ€ ์•„๋‹Œ ํŒŒ์ผ์—์„œ JSX ์‚ฌ์šฉํ•˜๋Š” ๊ฑธ ๊ฒฝ๊ณ ๋งŒ ํ•˜๋„๋ก ํ•จ
      'react/jsx-filename-extension': ['warn', { extensions: ['.js', '.jsx'] }],
      // React 17+์—์„œ๋Š” JSX ์“ธ ๋•Œ import React from 'react' ์•ˆ ํ•ด๋„ ๋˜๋ฏ€๋กœ ๋”
      'react/react-in-jsx-scope': 'off',
      // ์•ˆ ์“ฐ๋Š” ๋ณ€์ˆ˜๋ฅผ ๊ฒฝ๊ณ ๋กœ ํ‘œ์‹œํ•˜๋˜, ๋Œ€๋ฌธ์ž๋‚˜ _๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋ณ€์ˆ˜๋Š” ๋ฌด์‹œ (๋ณดํ†ต ์ƒ์ˆ˜๋“ค)
      'no-unused-vars': ['warn', { varsIgnorePattern: '^[A-Z_]' }],
      // Vite์—์„œ React Fast Refresh๊ฐ€ ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•œ ๊ทœ์น™
      'react-refresh/only-export-components': [
        'warn',
        { allowConstantExport: true },
      ],
      'no-var': 'warn', // ← var ์‚ฌ์šฉํ•˜๋ฉด ๊ฒฝ๊ณ 
      'prefer-const': 'warn', // ← const ์“ธ ์ˆ˜ ์žˆ์„ ๋•Œ let ์“ฐ๋ฉด ๊ฒฝ๊ณ 
      'import/order': [
        'warn',
        {
          // ← import ์ˆœ์„œ ๊ด€๋ฆฌ
          groups: ['builtin', 'external', 'internal'],
          pathGroups: [
            {
              pattern: 'react',
              group: 'external',
              position: 'before',
            },
          ],
          pathGroupsExcludedImportTypes: ['react'],
          'newlines-between': 'always',
          alphabetize: {
            order: 'asc',
            caseInsensitive: true,
          },
        },
      ],
    },
    settings: {
      react: {
      // ์‚ฌ์šฉ ์ค‘์ธ React ๋ฒ„์ „์„ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•ด์„œ ๋งž์ถค ๊ทœ์น™์„ ์ ์šฉ
        version: 'detect',
      },
    },

 

 

 

4. prettier ์„ค์ • ํŒŒ์ผ

.prettierrc 

{
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 80,
  "tabWidth": 2,
  "semi": true,
  "endOfLine": "auto"
}

 

 

5. vscode์— ์ ์šฉ

./vscode/settings.json ํŒŒ์ผ์— ๋‹ค์Œ ๋‚ด์šฉ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

ํŠนํžˆ ๋งˆ์ง€๋ง‰์—์„œ ๋‘๋ฒˆ์งธ ํ•ญ๋ชฉ์ธ "eslint.experimental.useFlatConfig": true์„ ์ถ”๊ฐ€ํ•ด์•ผ flat config๊ฐ€ ์ ์šฉ์ด ๋œ๋‹ค. ...

 

 

{
    "editor.formatOnSave": true,
    "editor.tabSize": 2, // ํƒญ ํฌ๊ธฐ๋ฅผ 2๋กœ ์„ค์ •
    "editor.insertSpaces": true, // ํƒญ ๋Œ€์‹  ๊ณต๋ฐฑ ์‚ฌ์šฉ
    "editor.detectIndentation": false, // ํŒŒ์ผ ๋‚ด์šฉ์— ๋”ฐ๋ผ ์ž๋™์œผ๋กœ ๋“ค์—ฌ์“ฐ๊ธฐ ๊ฐ์ง€ ๋น„ํ™œ์„ฑํ™”
    "editor.codeActionsOnSave": {
      "source.fixAll.eslint": "explicit"
    },
    "eslint.validate": [
      "javascript",
      "javascriptreact",
      "typescript",
      "typescriptreact"
    ],
    "eslint.experimental.useFlatConfig": true,
    "prettier.requireConfig": true
  }

 

 

 

 

์ฐธ๊ณ 
https://medium.com/@Dodo3/eslint-airbnb-%EC%84%A4%EC%A0%95-84b22d0cd31a
https://yangtinomad.com/entry/ReactTS-airbnb-eslint-prettier%EC%A0%81%EC%9A%A9