메이플 분배금 계산기 프로젝트 세팅

목차

요즘은 마음에 여유가 줄어들어서 거의 접속을 못 하고 있지만 메이플스토리를 열심히 했던 적이 잠깐 있었는데 그때 알게 된 분들이 메이플 보스 보상금 분배 시 쓸 수 있는 분배금 계산기를 만들자고 제안해 주셔서 프론트를 맡게 되었다.

react를 기본으로 하고 그리고 shadcn-ui와 같은 라이브러리를 팍팍 사용해서 일단 작동하도록 만들어 보고자 한다. shadcn-ui는 radix ui 기반으로 접근성까지 잘 챙기고 있으므로 내가 직접 접근성 등의 요소들을 조율해 주는 것보다 훨 나을 것이다.

1. 프로젝트 생성, 세팅

vite로 간단히 생성하자.

yarn create vite maple-share --template react-ts
yarn create vite maple-share --template react-ts
yarn create vite maple-share --template react-ts
yarn create vite maple-share --template react-ts

그리고 포매팅을 위한 툴들을 설치한다. 이번에는 다른 사람들도 코드를 볼 수도 있으니 더욱 강력한 포매팅을 할 것이다.

yarn add -D prettier eslint-config-prettier eslint-plugin-prettier
yarn add -D prettier eslint-config-prettier eslint-plugin-prettier
yarn add -D prettier eslint-config-prettier eslint-plugin-prettier
yarn add -D prettier eslint-config-prettier eslint-plugin-prettier

@typescript-eslint/parser@typescript-eslint/eslint-plugin는 기본적으로 설치되어 있었다.

그럼 이제 eslint 플러그인들을 설치하자. 이전에 다른 작은 프로젝트를 할 때 사용했던 플러그인들에 eslint-config-airbnb를 더한 것이다. eslint-config-airbnbairbnb의 JS 스타일 가이드를 자동으로 적용해 준다. 이 가이드에 대해서는 추후 더 글을 작성해볼 예정이다.

yarn add -D eslint-plugin-import eslint-plugin-react eslint-plugin-unused-imports eslint-config-airbnb eslint-plugin-jsx-a11y
yarn add -D eslint-plugin-import eslint-plugin-react eslint-plugin-unused-imports eslint-config-airbnb eslint-plugin-jsx-a11y
yarn add -D eslint-plugin-import eslint-plugin-react eslint-plugin-unused-imports eslint-config-airbnb eslint-plugin-jsx-a11y
yarn add -D eslint-plugin-import eslint-plugin-react eslint-plugin-unused-imports eslint-config-airbnb eslint-plugin-jsx-a11y

그리고 다음과 같이 철저한 포매팅을 위한 .eslintrc.cjs를 작성한다. 이 룰은 이창희님의 블로그에 있는 lint 파일과 내가 개인적으로 개발하면서 썼던 airbnb 룰 몇 가지를 적절히 섞은 것이다.

// .eslintrc.cjs
module.exports = {
  root: true,
  env: { browser: true, es2020: true },
  extends: [
    'airbnb',
    'airbnb/hooks',
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/recommended-requiring-type-checking',
    'plugin:react/jsx-runtime',
    'plugin:react-hooks/recommended',
    'plugin:import/recommended',
  ],
  ignorePatterns: ['dist', '.eslintrc.cjs', 'vite.config.ts'],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
    project: true,
    tsconfigRootDir: __dirname,
  },
  plugins: ['react-refresh', 'unused-imports'],
  rules: {
    'react/prop-types': 'off',
    'react/jsx-filename-extension': [
      'warn',
      {
        'extensions': [
          '.js',
          '.ts',
          '.jsx',
          '.tsx'
        ] // 확장자로 js와 jsx ts tsx 허용
      }
    ],
    'react/no-unescaped-entities': 'warn',
    'react/jsx-props-no-spreading': 'off',
    'react/jsx-boolean-value': 'off',
    'react/jsx-no-bind': 'off',
    'react/require-default-props': 'off',
    'react/self-closing-comp': 'warn', // 셀프 클로징 태그 가능하면 적용
    'react/no-array-index-key': 'off',
    'react-hooks/exhaustive-deps': ['warn'], // hooks의 의존성배열이 충분하지 않을때 강제로 의존성을 추가하는 규칙을 완화
    'react-refresh/only-export-components': [
      'warn',
      { allowConstantExport: true },
    ],
    'jsx-a11y/click-events-have-key-events': 'off', // onClick 사용하기 위해서 onKeyUp,onKeyDown,onKeyPress 하나 이상 사용
    'indent':[
      'error',
      2
    ],
    'import/no-named-as-default': 'off',
    'import/no-unresolved': 'off',
    'import/extensions': 'off',
    'import/prefer-default-export': 'off',
    'import/order': [
      'warn',
      {
        'alphabetize': {
          'order': 'asc',
          'caseInsensitive': true
        },
        'groups': [
          'builtin',
          'external',
          [
            'parent',
            'internal'
          ],
          'sibling',
          [
            'unknown',
            'index',
            'object'
          ]
        ],
        'pathGroups': [
          {
            'pattern': '~/**',
            'group': 'internal'
          }
        ],
        'newlines-between': 'always'
      }
    ],
    'arrow-parens': ['warn', 'as-needed'], // 화살표 함수의 파라미터가 하나일때 괄호 생략
    'no-console': ['off'], // 콘솔을 쓰면 에러가 나던 규칙 해제
    'no-alert': ['off'], // alert를 쓰면 에러가 나던 규칙 해제
    'jsx-quotes': [
      'error',
      'prefer-single'
    ],
    'keyword-spacing': 'error',
    'quotes': [
      'error',
      'single',
      {
        'avoidEscape': true
      }
    ],
    'no-console': [
      'warn',
      {
        'allow': [
          'warn',
          'error'
        ]
      }
    ],
    'no-extra-semi': 'error',
    'semi': 'off',
    'space-before-blocks': 'error',
    'no-shadow': 'off',
    'unused-imports/no-unused-imports': 'error',
    '@typescript-eslint/no-shadow': [
      'error'
    ],
    '@typescript-eslint/no-non-null-assertion': 'off',
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/no-explicit-any': 'warn',
    '@typescript-eslint/no-unused-vars': 'error',
    '@typescript-eslint/no-unsafe-assignment': 'warn',
    '@typescript-eslint/semi': [
      'error'
    ],
    '@typescript-eslint/no-misused-promises': [
      'error',
      {
        'checksVoidReturn': false
      }
    ],
    '@typescript-eslint/type-annotation-spacing': [
      'error',
      {
        'before': false,
        'after': true,
        'overrides': {
          'colon': {
            'before': false,
            'after': true
          },
          'arrow': {
            'before': true,
            'after': true
          }
        }
      }
    ]
  },
}
// .eslintrc.cjs
module.exports = {
  root: true,
  env: { browser: true, es2020: true },
  extends: [
    'airbnb',
    'airbnb/hooks',
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/recommended-requiring-type-checking',
    'plugin:react/jsx-runtime',
    'plugin:react-hooks/recommended',
    'plugin:import/recommended',
  ],
  ignorePatterns: ['dist', '.eslintrc.cjs', 'vite.config.ts'],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
    project: true,
    tsconfigRootDir: __dirname,
  },
  plugins: ['react-refresh', 'unused-imports'],
  rules: {
    'react/prop-types': 'off',
    'react/jsx-filename-extension': [
      'warn',
      {
        'extensions': [
          '.js',
          '.ts',
          '.jsx',
          '.tsx'
        ] // 확장자로 js와 jsx ts tsx 허용
      }
    ],
    'react/no-unescaped-entities': 'warn',
    'react/jsx-props-no-spreading': 'off',
    'react/jsx-boolean-value': 'off',
    'react/jsx-no-bind': 'off',
    'react/require-default-props': 'off',
    'react/self-closing-comp': 'warn', // 셀프 클로징 태그 가능하면 적용
    'react/no-array-index-key': 'off',
    'react-hooks/exhaustive-deps': ['warn'], // hooks의 의존성배열이 충분하지 않을때 강제로 의존성을 추가하는 규칙을 완화
    'react-refresh/only-export-components': [
      'warn',
      { allowConstantExport: true },
    ],
    'jsx-a11y/click-events-have-key-events': 'off', // onClick 사용하기 위해서 onKeyUp,onKeyDown,onKeyPress 하나 이상 사용
    'indent':[
      'error',
      2
    ],
    'import/no-named-as-default': 'off',
    'import/no-unresolved': 'off',
    'import/extensions': 'off',
    'import/prefer-default-export': 'off',
    'import/order': [
      'warn',
      {
        'alphabetize': {
          'order': 'asc',
          'caseInsensitive': true
        },
        'groups': [
          'builtin',
          'external',
          [
            'parent',
            'internal'
          ],
          'sibling',
          [
            'unknown',
            'index',
            'object'
          ]
        ],
        'pathGroups': [
          {
            'pattern': '~/**',
            'group': 'internal'
          }
        ],
        'newlines-between': 'always'
      }
    ],
    'arrow-parens': ['warn', 'as-needed'], // 화살표 함수의 파라미터가 하나일때 괄호 생략
    'no-console': ['off'], // 콘솔을 쓰면 에러가 나던 규칙 해제
    'no-alert': ['off'], // alert를 쓰면 에러가 나던 규칙 해제
    'jsx-quotes': [
      'error',
      'prefer-single'
    ],
    'keyword-spacing': 'error',
    'quotes': [
      'error',
      'single',
      {
        'avoidEscape': true
      }
    ],
    'no-console': [
      'warn',
      {
        'allow': [
          'warn',
          'error'
        ]
      }
    ],
    'no-extra-semi': 'error',
    'semi': 'off',
    'space-before-blocks': 'error',
    'no-shadow': 'off',
    'unused-imports/no-unused-imports': 'error',
    '@typescript-eslint/no-shadow': [
      'error'
    ],
    '@typescript-eslint/no-non-null-assertion': 'off',
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/no-explicit-any': 'warn',
    '@typescript-eslint/no-unused-vars': 'error',
    '@typescript-eslint/no-unsafe-assignment': 'warn',
    '@typescript-eslint/semi': [
      'error'
    ],
    '@typescript-eslint/no-misused-promises': [
      'error',
      {
        'checksVoidReturn': false
      }
    ],
    '@typescript-eslint/type-annotation-spacing': [
      'error',
      {
        'before': false,
        'after': true,
        'overrides': {
          'colon': {
            'before': false,
            'after': true
          },
          'arrow': {
            'before': true,
            'after': true
          }
        }
      }
    ]
  },
}
// .eslintrc.cjs
module.exports = {
  root: true,
  env: { browser: true, es2020: true },
  extends: [
    'airbnb',
    'airbnb/hooks',
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/recommended-requiring-type-checking',
    'plugin:react/jsx-runtime',
    'plugin:react-hooks/recommended',
    'plugin:import/recommended',
  ],
  ignorePatterns: ['dist', '.eslintrc.cjs', 'vite.config.ts'],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
    project: true,
    tsconfigRootDir: __dirname,
  },
  plugins: ['react-refresh', 'unused-imports'],
  rules: {
    'react/prop-types': 'off',
    'react/jsx-filename-extension': [
      'warn',
      {
        'extensions': [
          '.js',
          '.ts',
          '.jsx',
          '.tsx'
        ] // 확장자로 js와 jsx ts tsx 허용
      }
    ],
    'react/no-unescaped-entities': 'warn',
    'react/jsx-props-no-spreading': 'off',
    'react/jsx-boolean-value': 'off',
    'react/jsx-no-bind': 'off',
    'react/require-default-props': 'off',
    'react/self-closing-comp': 'warn', // 셀프 클로징 태그 가능하면 적용
    'react/no-array-index-key': 'off',
    'react-hooks/exhaustive-deps': ['warn'], // hooks의 의존성배열이 충분하지 않을때 강제로 의존성을 추가하는 규칙을 완화
    'react-refresh/only-export-components': [
      'warn',
      { allowConstantExport: true },
    ],
    'jsx-a11y/click-events-have-key-events': 'off', // onClick 사용하기 위해서 onKeyUp,onKeyDown,onKeyPress 하나 이상 사용
    'indent':[
      'error',
      2
    ],
    'import/no-named-as-default': 'off',
    'import/no-unresolved': 'off',
    'import/extensions': 'off',
    'import/prefer-default-export': 'off',
    'import/order': [
      'warn',
      {
        'alphabetize': {
          'order': 'asc',
          'caseInsensitive': true
        },
        'groups': [
          'builtin',
          'external',
          [
            'parent',
            'internal'
          ],
          'sibling',
          [
            'unknown',
            'index',
            'object'
          ]
        ],
        'pathGroups': [
          {
            'pattern': '~/**',
            'group': 'internal'
          }
        ],
        'newlines-between': 'always'
      }
    ],
    'arrow-parens': ['warn', 'as-needed'], // 화살표 함수의 파라미터가 하나일때 괄호 생략
    'no-console': ['off'], // 콘솔을 쓰면 에러가 나던 규칙 해제
    'no-alert': ['off'], // alert를 쓰면 에러가 나던 규칙 해제
    'jsx-quotes': [
      'error',
      'prefer-single'
    ],
    'keyword-spacing': 'error',
    'quotes': [
      'error',
      'single',
      {
        'avoidEscape': true
      }
    ],
    'no-console': [
      'warn',
      {
        'allow': [
          'warn',
          'error'
        ]
      }
    ],
    'no-extra-semi': 'error',
    'semi': 'off',
    'space-before-blocks': 'error',
    'no-shadow': 'off',
    'unused-imports/no-unused-imports': 'error',
    '@typescript-eslint/no-shadow': [
      'error'
    ],
    '@typescript-eslint/no-non-null-assertion': 'off',
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/no-explicit-any': 'warn',
    '@typescript-eslint/no-unused-vars': 'error',
    '@typescript-eslint/no-unsafe-assignment': 'warn',
    '@typescript-eslint/semi': [
      'error'
    ],
    '@typescript-eslint/no-misused-promises': [
      'error',
      {
        'checksVoidReturn': false
      }
    ],
    '@typescript-eslint/type-annotation-spacing': [
      'error',
      {
        'before': false,
        'after': true,
        'overrides': {
          'colon': {
            'before': false,
            'after': true
          },
          'arrow': {
            'before': true,
            'after': true
          }
        }
      }
    ]
  },
}
// .eslintrc.cjs
module.exports = {
  root: true,
  env: { browser: true, es2020: true },
  extends: [
    'airbnb',
    'airbnb/hooks',
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/recommended-requiring-type-checking',
    'plugin:react/jsx-runtime',
    'plugin:react-hooks/recommended',
    'plugin:import/recommended',
  ],
  ignorePatterns: ['dist', '.eslintrc.cjs', 'vite.config.ts'],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
    project: true,
    tsconfigRootDir: __dirname,
  },
  plugins: ['react-refresh', 'unused-imports'],
  rules: {
    'react/prop-types': 'off',
    'react/jsx-filename-extension': [
      'warn',
      {
        'extensions': [
          '.js',
          '.ts',
          '.jsx',
          '.tsx'
        ] // 확장자로 js와 jsx ts tsx 허용
      }
    ],
    'react/no-unescaped-entities': 'warn',
    'react/jsx-props-no-spreading': 'off',
    'react/jsx-boolean-value': 'off',
    'react/jsx-no-bind': 'off',
    'react/require-default-props': 'off',
    'react/self-closing-comp': 'warn', // 셀프 클로징 태그 가능하면 적용
    'react/no-array-index-key': 'off',
    'react-hooks/exhaustive-deps': ['warn'], // hooks의 의존성배열이 충분하지 않을때 강제로 의존성을 추가하는 규칙을 완화
    'react-refresh/only-export-components': [
      'warn',
      { allowConstantExport: true },
    ],
    'jsx-a11y/click-events-have-key-events': 'off', // onClick 사용하기 위해서 onKeyUp,onKeyDown,onKeyPress 하나 이상 사용
    'indent':[
      'error',
      2
    ],
    'import/no-named-as-default': 'off',
    'import/no-unresolved': 'off',
    'import/extensions': 'off',
    'import/prefer-default-export': 'off',
    'import/order': [
      'warn',
      {
        'alphabetize': {
          'order': 'asc',
          'caseInsensitive': true
        },
        'groups': [
          'builtin',
          'external',
          [
            'parent',
            'internal'
          ],
          'sibling',
          [
            'unknown',
            'index',
            'object'
          ]
        ],
        'pathGroups': [
          {
            'pattern': '~/**',
            'group': 'internal'
          }
        ],
        'newlines-between': 'always'
      }
    ],
    'arrow-parens': ['warn', 'as-needed'], // 화살표 함수의 파라미터가 하나일때 괄호 생략
    'no-console': ['off'], // 콘솔을 쓰면 에러가 나던 규칙 해제
    'no-alert': ['off'], // alert를 쓰면 에러가 나던 규칙 해제
    'jsx-quotes': [
      'error',
      'prefer-single'
    ],
    'keyword-spacing': 'error',
    'quotes': [
      'error',
      'single',
      {
        'avoidEscape': true
      }
    ],
    'no-console': [
      'warn',
      {
        'allow': [
          'warn',
          'error'
        ]
      }
    ],
    'no-extra-semi': 'error',
    'semi': 'off',
    'space-before-blocks': 'error',
    'no-shadow': 'off',
    'unused-imports/no-unused-imports': 'error',
    '@typescript-eslint/no-shadow': [
      'error'
    ],
    '@typescript-eslint/no-non-null-assertion': 'off',
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/no-explicit-any': 'warn',
    '@typescript-eslint/no-unused-vars': 'error',
    '@typescript-eslint/no-unsafe-assignment': 'warn',
    '@typescript-eslint/semi': [
      'error'
    ],
    '@typescript-eslint/no-misused-promises': [
      'error',
      {
        'checksVoidReturn': false
      }
    ],
    '@typescript-eslint/type-annotation-spacing': [
      'error',
      {
        'before': false,
        'after': true,
        'overrides': {
          'colon': {
            'before': false,
            'after': true
          },
          'arrow': {
            'before': true,
            'after': true
          }
        }
      }
    ]
  },
}

그런데 이 상태에서는 제대로 자동 수정이 되지 않는다. tsconfig.json에 해당 eslint 파일이 포함되어 있지 않기 때문이라고 한다. 이를 수정하기 위해서는 다음과 같이 tsconfig.json의 include항목을 수정해 주어야 한다.

{
  /* compilerOptions 생략 */
  "include": ["src", "vite.config.ts", ".eslintrc.cjs",],
  "references": [{ "path": "./tsconfig.node.json" }]
}
{
  /* compilerOptions 생략 */
  "include": ["src", "vite.config.ts", ".eslintrc.cjs",],
  "references": [{ "path": "./tsconfig.node.json" }]
}
{
  /* compilerOptions 생략 */
  "include": ["src", "vite.config.ts", ".eslintrc.cjs",],
  "references": [{ "path": "./tsconfig.node.json" }]
}
{
  /* compilerOptions 생략 */
  "include": ["src", "vite.config.ts", ".eslintrc.cjs",],
  "references": [{ "path": "./tsconfig.node.json" }]
}

그리고 vite.config.ts에서 이를 인식하도록 하기 위해 vite-tsconfig-paths를 설치한다.

yarn add -D vite-tsconfig-paths
yarn add -D vite-tsconfig-paths
yarn add -D vite-tsconfig-paths
yarn add -D vite-tsconfig-paths

그리고 vite.config.ts를 다음과 같이 수정한다.

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), tsconfigPaths()],
})
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), tsconfigPaths()],
})
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), tsconfigPaths()],
})
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), tsconfigPaths()],
})

이렇게 하면 자동 수정이 잘 되는 것을 볼 수 있다.

마지막으로 import alias를 통해 절대 경로 비슷하게 import해올 수 있도록 하자. tsconfig.json의 compilerOptions에 다음과 같이 paths를 추가한다.

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

2. shadcn ui

이 프로젝트에서는 shadcn ui를 사용하기로 했다. 원래 mantine ui를 사용하려 했으나 분배금 계산기 기획 특성상 커스텀을 많이 해야 하는 컴포넌트가 많고 따라서 커스텀이 더 쉬운 이쪽이 더 낫다고 생각했기 때문이다.

shadcn ui가 처음이라 사실 얼마나 장점이 많을지는 모르겠지만, 써본 몇 사람들의 말로는 굉장히 괜찮다고 하여 한번 부딪쳐본다. tailwind도 이전에 써본 적이 있어, 단점도 꽤 있지만 빠르게 무언가를 만들기에 굉장히 좋다는 걸 알고 있기에 이걸 이용하면 상당히 빠르게 프로토타입을 만들 수 있을 거라고 생각한다.

2.1. 설치

그럼 shadcn ui의 vite 설치 가이드를 따라해 보자. vite 프로젝트 생성하는 부분은 이미 했으므로 건너뛴다.

yarn add -D tailwindcss postcss autoprefixer
yarn tailwindcss init -p
yarn add -D tailwindcss postcss autoprefixer
yarn tailwindcss init -p
yarn add -D tailwindcss postcss autoprefixer
yarn tailwindcss init -p
yarn add -D tailwindcss postcss autoprefixer
yarn tailwindcss init -p

tsconfig.json에서 baseUrl도 설정해준다.

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

path를 사용하기 위해 @types/node 설치

yarn add -D @types/node
yarn add -D @types/node
yarn add -D @types/node
yarn add -D @types/node

vite.config.ts는 다음과 같이 작성

import path from "path"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
})
import path from "path"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
})
import path from "path"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
})
import path from "path"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
})

그리고 shadcn-ui를 받아온다. 패키지는 아니기 때문에 설치라고 하기는 좀 그렇고...

npx shadcn-ui@latest init
npx shadcn-ui@latest init
npx shadcn-ui@latest init
npx shadcn-ui@latest init

이렇게 하면 여러 문답이 나오는데 적당히 응답한다. 주의할 점은 Where is your global CSS file?이라는 질문이 있는데 여기서 기본 응답은 app/globals.css이다. 아마 nextJS를 기본으로 생각하고 만든 것 같다. 하지만 이 프로젝트는 vite를 사용하고 있으므로 src/index.css로 바꿔줘야 한다.

그렇게 하고 나면 src/index.css가 알아서 초기화된다.

2.2. 시험

이제 컴포넌트를 받아 와서 사용할 수 있다. 공식 문서에서는 버튼을 하나의 예시로 들고 있다.

npx shadcn-ui@latest add button
npx shadcn-ui@latest add button
npx shadcn-ui@latest add button
npx shadcn-ui@latest add button

이렇게 하면 src/components/button.tsx가 생성된다. 이제 이걸 사용해 보자. src/App.tsx로 간다. tailwind 클래스를 적용해서 색도 바꿔보자.

// src/App.tsx
import { Button } from './components/ui/button';

function App() {
  return (
    <div>
      <h1>메이플 분배금 계산기</h1>
      <Button>버튼</Button>
      <Button className='bg-indigo-500'>버튼</Button>
      <Button className='bg-pink-500'>버튼</Button>
    </div>
  );
}

export default App;
// src/App.tsx
import { Button } from './components/ui/button';

function App() {
  return (
    <div>
      <h1>메이플 분배금 계산기</h1>
      <Button>버튼</Button>
      <Button className='bg-indigo-500'>버튼</Button>
      <Button className='bg-pink-500'>버튼</Button>
    </div>
  );
}

export default App;
// src/App.tsx
import { Button } from './components/ui/button';

function App() {
  return (
    <div>
      <h1>메이플 분배금 계산기</h1>
      <Button>버튼</Button>
      <Button className='bg-indigo-500'>버튼</Button>
      <Button className='bg-pink-500'>버튼</Button>
    </div>
  );
}

export default App;
// src/App.tsx
import { Button } from './components/ui/button';

function App() {
  return (
    <div>
      <h1>메이플 분배금 계산기</h1>
      <Button>버튼</Button>
      <Button className='bg-indigo-500'>버튼</Button>
      <Button className='bg-pink-500'>버튼</Button>
    </div>
  );
}

export default App;

이렇게 하면 다음과 같이 각 색의 버튼 3개가 나온다.

shadcn 버튼들

3. react-router-dom

react-router-dom을 설치하자.

yarn add react-router-dom
yarn add react-router-dom
yarn add react-router-dom
yarn add react-router-dom

그리고 src/main.tsx에 다음과 같이 기본적인 라우터를 설정한다.

const router = createBrowserRouter([
  {
    path: '/',
    element: (
      <App />
    ),
  },
  {
    path: '/about',
    element: <div>about</div>,
  },
]);

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>,
);
const router = createBrowserRouter([
  {
    path: '/',
    element: (
      <App />
    ),
  },
  {
    path: '/about',
    element: <div>about</div>,
  },
]);

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>,
);
const router = createBrowserRouter([
  {
    path: '/',
    element: (
      <App />
    ),
  },
  {
    path: '/about',
    element: <div>about</div>,
  },
]);

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>,
);
const router = createBrowserRouter([
  {
    path: '/',
    element: (
      <App />
    ),
  },
  {
    path: '/about',
    element: <div>about</div>,
  },
]);

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>,
);

이제 개발 환경을 실행하고 /about라우터에 들어가면 작게 about이라는 글씨가 뜨는 페이지가 나오는 것을 볼 수 있다. 라우팅이 잘 설정된 것이다.

참고

eslint airbnb 사용하기 https://hayjo.tistory.com/111

shadcn ui 공식 문서 https://ui.shadcn.com/