나의 전용 템플릿 엔진(?) 제작기 - ESLint
개요
앞선 템플릿엔진용 모노레포를 구성하였다.
이번 내용에서는 템플릿 엔진용 ESLinst 구성을 해볼까한다. 사실 너무나 간단한 구성이라서 저번 내용에 넣을까 고민했지만, 분리시키는게 가독성이나 내용 측면에서 좋을 것 같아서 따로 분리시켰다.
환경구성
my-monorepo-template/
├── packages/
│ ├── **eslint-config/**
│ ├── prettier-config/
│ ├── tsconfig/
│ ├── react-template/
│ └── nextjs-template/
├── package.json
└── pnpm-workspace.yaml
기존 구조에서 packages/eslint-confing
부분에 넣을예정이다
초기설정
패키지내부에 폴더랑 파일을 추가해 주도록한다
🚨Typescript 및 Eslint 등의 설치는 되어있는 상태로 설명
mkdir -p packages/eslint-config cd packages/eslint-config # 패키지 초기화 pnpm init
내부 파일을 만들어서 넣는다
# 다음 파일들 생성 touch base.js index.js react.js next.js typeCheck.js prettier.js README.md
설치
패키지 내부에 의존성을 설치해준다 (현재 필자는 아래 패키지들만 설치)
패키지 이름부분은 패키지내부 package.json - name
부분을 따라간다 참고
pnpm --filter **${본인 패키지 이름}**/eslint-config add @next/eslint-plugin-next @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-airbnb eslint-config-next eslint-config-prettier eslint-config-standard eslint-config-standard-jsx eslint-import-resolver-typescript eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-n eslint-plugin-prettier eslint-plugin-promise eslint-plugin-react eslint-plugin-react-hooks
각 파일별 설정은 아래와 같다
base.js
: 기본 ESLint 설정 (프론트엔드/백엔드 공통)index.js
: 기본 JavaScript/TypeScript 규칙react.js
: React 관련 규칙next.js
: Next.js 확장 규칙typeCheck.js
: 강화된 TypeScript 타입 체크prettier.js
: ESLint와 Prettier 통합
package.json
homepage, repository는 각각 본인들의 깃 링크를 삽입, files또한 배포파일 경로를 넣는다
{ "name": "@rendardev/eslint-config", "homepage": "https://github.com/RendarCP/rendar-mono-template", "repository": { "type": "git", "url": "git+https://github.com/RendarCP/rendar-mono-template.git" }, "version": "1.0.1", "description": "ESLint configurations for Rendar Mono Template", "main": "index.js", "files": [ "*.js", "README.md" ], "publishConfig": { "access": "public" }, "license": "MIT", "dependencies": { "@next/eslint-plugin-next": "^15.1.4", "@typescript-eslint/eslint-plugin": "^8.20.0", "@typescript-eslint/parser": "^8.20.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-next": "latest", "eslint-config-prettier": "^10.0.1", "eslint-config-standard": "^17.1.0", "eslint-config-standard-jsx": "^11.0.0", "eslint-import-resolver-typescript": "^3.8.7", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-n": "^16.6.2", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-promise": "^6.6.0", "eslint-plugin-react": "^7.37.4", "eslint-plugin-react-hooks": "^5.1.0" }, "peerDependencies": { "eslint": ">=8.0.0" } }
각 파일들
base.js
/** * base.js * * 기본 ESLint 설정 파일 * 프론트엔드와 백엔드 모두에서 사용할 수 있는 공통 규칙 정의 * * @type {import('eslint').Linter.Config} */ module.exports = { // 실행 환경 설정 env: { browser: true, // 브라우저 환경 지원 es6: true, // ES6 문법 지원 node: true, // Node.js 환경 지원 }, // TypeScript 파서 사용 parser: "@typescript-eslint/parser", // 확장할 기본 규칙 설정 extends: [ "eslint:recommended", // ESLint 기본 권장 규칙 "plugin:react/recommended", // React 권장 규칙 "plugin:prettier/recommended", // Prettier 통합 지원 ], // React 설정 settings: { react: { version: "detect", // React 버전 자동 감지 }, }, // 파서 옵션 설정 parserOptions: { ecmaFeatures: { jsx: true, // JSX 문법 지원 }, ecmaVersion: "latest", // 최신 ECMAScript 버전 사용 sourceType: "module", // ES 모듈 시스템 사용 }, // 사용할 플러그인 plugins: [ "react", // React 린트 규칙 "react-hooks", // React Hooks 린트 규칙 "@typescript-eslint", // TypeScript 규칙 "prettier", // Prettier 통합 "import", // import/export 문법 규칙 ], // 세부 규칙 설정 rules: { // React 관련 규칙 "react-hooks/rules-of-hooks": "error", // Hooks 규칙 강제 "react/jsx-no-useless-fragment": 0, // 불필요한 Fragment 검사 비활성화 "react/prop-types": "off", // PropTypes 검사 비활성화 (TypeScript 사용 시) "react/function-component-definition": [ 2, // error { namedComponents: ["arrow-function", "function-declaration"], // 화살표 함수와 함수 선언식 모두 허용 }, ], "react/jsx-props-no-spreading": "off", // props 전개 연산자 사용 허용 "react/react-in-jsx-scope": 0, // React import 필수 규칙 비활성화 (React 17+) "react/prefer-stateless-function": 0, // 상태 없는 컴포넌트 강제 비활성화 "react/jsx-filename-extension": 0, // JSX 파일 확장자 제한 비활성화 "react/jsx-one-expression-per-line": 0, // JSX 내 한 줄 한 표현식 규칙 비활성화 // Import 관련 규칙 "import/order": 0, // import 순서 규칙 비활성화 "import/prefer-default-export": "off", // 기본 내보내기 선호 규칙 비활성화 "import/extensions": 0, // 확장자 명시 규칙 비활성화 "import/no-unresolved": 0, // 미해결 import 경로 검사 비활성화 // JavaScript/TypeScript 관련 규칙 "prefer-arrow-callback": "off", // 화살표 함수 콜백 선호 규칙 비활성화 "no-var": "error", // var 대신 let/const 사용 강제 "no-dupe-keys": "error", // 객체 내 중복 키 금지 "no-nested-ternary": 0, // 중첩 삼항 연산자 허용 // TypeScript 관련 규칙 "@typescript-eslint/no-explicit-any": "error", // any 타입 사용 금지 }, // 전역 변수 설정 globals: { React: "writable", // React를 전역 변수로 설정 }, };
index.js
/** * index.js * * 기본 ESLint 설정 - 주로 Node.js 환경에서 사용되는 설정 * JavaScript/TypeScript 코드를 위한 핵심 규칙 정의 * * @type {import('eslint').Linter.Config} */ module.exports = { // Node.js 환경 설정 env: { node: true, }, // 확장할 규칙 설정 extends: [ "eslint:recommended", // ESLint 기본 권장 규칙 "plugin:import/recommended", // import 관련 권장 규칙 "plugin:promise/recommended", // Promise 관련 권장 규칙 "standard", // JavaScript Standard 스타일 ], // 기본 규칙 rules: { // 함수 스타일 규칙 "func-style": ["error", "declaration", { allowArrowFunctions: true }], // 함수 선언식 강제, 화살표 함수 허용 // 코드 품질 규칙 "no-console": "error", // console.log() 등 사용 금지 "no-empty-function": "off", // 빈 함수 허용 "no-implicit-coercion": ["error", { allow: ["!!"] }], // 암시적 형변환 제한, !! 연산자는 허용 "no-return-await": "error", // 불필요한 return await 금지 "no-unused-expressions": [ // 미사용 표현식 제한 "error", { allowShortCircuit: true, // 단축 평가 허용 allowTernary: true, // 삼항 연산자 허용 allowTaggedTemplates: true, // 태그된 템플릿 허용 }, ], "no-unused-vars": [ // 미사용 변수 제한 "error", { ignoreRestSiblings: true, argsIgnorePattern: "^_+$" }, // rest 연산자 형제 무시, _ 시작 매개변수 무시 ], "no-use-before-define": ["error", { functions: false }], // 정의 전 사용 금지, 함수는 예외 "no-void": ["error", { allowAsStatement: true }], // void 연산자 제한, 문장으로는 허용 "object-shorthand": ["error", "properties"], // 객체 속성 단축 문법 사용 "require-atomic-updates": "off", // 원자적 업데이트 요구 비활성화 // Import 관련 규칙 "import/order": [ // import 순서 규칙 "error", { "newlines-between": "always", // 그룹 간 항상 새 줄 추가 pathGroups: [ { pattern: "@/**", // @ 경로 패턴 group: "parent", // 부모 그룹에 포함 position: "before", // 부모 그룹 앞에 배치 }, ], pathGroupsExcludedImportTypes: ["builtin"], // 내장 모듈은 제외 }, ], "import/newline-after-import": "error", // import 문 후 새 줄 강제 // Promise 관련 규칙 "promise/catch-or-return": ["error", { allowFinally: true }], // Promise는 catch 또는 return 필수, finally 허용 "promise/prefer-await-to-callbacks": "error", // 콜백보다 await 선호 "promise/prefer-await-to-then": "error", // then보다 await 선호 }, // TypeScript 파일을 위한 추가 설정 overrides: [ { files: ["*.ts", "*.tsx"], // TypeScript 파일 대상 extends: [ "plugin:@typescript-eslint/recommended", // TypeScript 권장 규칙 "plugin:import/typescript", // TypeScript import 지원 ], parser: "@typescript-eslint/parser", // TypeScript 파서 사용 settings: { "import/resolver": { typescript: { alwaysTryTypes: true, // 항상 타입 정의 파일 시도 }, }, }, rules: { // TypeScript 관련 규칙 "@typescript-eslint/consistent-type-assertions": "error", // 일관된 타입 단언 형식 사용 "@typescript-eslint/consistent-type-definitions": "error", // 일관된 타입 정의 형식 사용 "@typescript-eslint/explicit-member-accessibility": "error", // 명시적 접근 제한자 요구 "@typescript-eslint/no-var-requires": "off", // require() 사용 허용 "@typescript-eslint/no-empty-function": "off", // 빈 함수 허용 "@typescript-eslint/no-empty-object-type": [ // 빈 객체 타입 제한 "error", { allowInterfaces: "always" }, // 인터페이스는 항상 허용 ], // TypeScript로 재정의된 규칙 "no-use-before-define": "off", // JS 버전 비활성화 "@typescript-eslint/no-use-before-define": [ // TS 버전 활성화 "error", { functions: false }, // 함수는 정의 전 사용 허용 ], "no-unused-expressions": "off", // JS 버전 비활성화 "@typescript-eslint/no-unused-expressions": [ // TS 버전 활성화 "error", { allowShortCircuit: true, // 단축 평가 허용 allowTernary: true, // 삼항 연산자 허용 allowTaggedTemplates: true, // 태그된 템플릿 허용 }, ], "no-unused-vars": "off", // JS 버전 비활성화 "@typescript-eslint/no-unused-vars": [ // TS 버전 활성화 "error", { ignoreRestSiblings: true, argsIgnorePattern: "^_+$" }, // rest 연산자 형제 무시, _ 시작 매개변수 무시 ], // '@typescript-eslint/naming-convention': createNamingConventionConfig(), // 명명 규칙 (주석 처리됨) // TypeScript에서 불필요한 import 규칙 비활성화 "import/named": "off", // TS가 이미 확인함 "import/namespace": "off", // TS가 이미 확인함 "import/default": "off", // TS가 이미 확인함 "import/no-named-as-default-member": "off", // TS가 이미 확인함 }, }, ], };
react.js
/** * react.js * * React 애플리케이션을 위한 ESLint 설정 * React, JSX, 접근성(a11y) 관련 규칙을 정의 * * @type {import('eslint').Linter.Config} */ module.exports = { // 브라우저 환경 설정 env: { browser: true, // 브라우저 환경에서 실행 }, // 확장할 규칙 설정 extends: [ "plugin:react/recommended", // React 권장 규칙 "plugin:react/jsx-runtime", // React 17+ JSX 변환 지원 (import React 불필요) "plugin:react-hooks/recommended", // React Hooks 권장 규칙 "plugin:jsx-a11y/recommended", // JSX 접근성(a11y) 권장 규칙 "standard-jsx", // JSX 표준 스타일 ], // React 설정 settings: { react: { version: "detect", // React 버전 자동 감지 }, }, // React 관련 규칙 rules: { // JSX 문법 관련 규칙 "react/jsx-no-useless-fragment": ["error", { allowExpressions: true }], // 불필요한 Fragment 금지, 표현식은 허용 "react/jsx-pascal-case": "error", // 컴포넌트 이름 파스칼 케이스 강제 // 속성 관련 규칙 "react/no-unknown-property": ["error", { ignore: ["css"] }], // 알 수 없는 속성 금지, css 속성은 허용 (CSS-in-JS) "react/prop-types": "off", // PropTypes 검사 비활성화 (TypeScript 사용 시) // 컴포넌트 스타일 규칙 "react/self-closing-comp": [ "error", { component: true, // 내용 없는 컴포넌트는 자체 닫는 태그 사용 html: true, // HTML 요소도 적용 }, ], // 일반 코드 스타일 규칙 curly: ["error", "all"], // 모든 조건문에 중괄호 사용 강제 }, };
next.js
/** * next.js * * Next.js 프레임워크 프로젝트를 위한 ESLint 설정 * React 설정을 확장하고 Next.js 특화 규칙 추가 */ module.exports = { // 확장할 설정 extends: [ "./react.js", // React 기본 설정 확장 "next/core-web-vitals", // Next.js 코어 웹 바이탈 규칙 (성능 측정 지표) ], // Next.js 설정 settings: { next: { rootDir: ["apps/*/", "packages/*/"], // 모노레포 구조의 루트 디렉토리 설정 }, }, };
prettier.js
/** * prettier.js * * ESLint와 Prettier 통합을 위한 설정 * 코드 포맷팅 일관성 유지 * * @type {import('eslint').Linter.Config} */ module.exports = { // Prettier 관련 설정 확장 extends: ["prettier"], // ESLint와 Prettier 충돌 규칙 비활성화 // Prettier와 함께 사용할 ESLint 규칙 rules: { /** * prettier에서 기본적으로 비활성화하지만 * 필요에 의해 다시 활성화하는 규칙 */ curly: ["error", "all"], // 모든 조건문에 중괄호 사용 강제 }, };
배포 및 테스트
전 글에서 root - package.json은 변동될 예정이다했는데 아래와같아진다
아직은 eslint 설정 부분이지만 아래 과정으로 넣을 예정이다 (모두 배포예정)
- prettier
- nextjs-template
- react-template
{
"name": "root",
"version": "1.0.0",
"description": "",
"main": "index.js",
"private": true,
"scripts": {
"build": "pnpm -r build",
"test": "pnpm -r test",
"lint": "pnpm -r lint",
"publish-all": "pnpm -r publish",
"publish-eslint": "pnpm --filter @rendardev/eslint-config publish",
"publish-prettier": "pnpm --filter @rendardev/prettier-config publish",
"publish-react": "pnpm --filter @rendardev/react-template publish",
"publish-nextjs": "pnpm --filter @rendardev/nextjs-template publish"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"eslint": "^9.0.0",
"typescript": "^5.8.2"
},
"workspaces": [
"packages/*"
],
"packageManager": "pnpm@8.9.0"
}
배포
pnpm publish-eslint
테스트는 어렵지 않다
아래 링크에서 README.md를 참고하면된다
https://www.npmjs.com/package/@rendardev/eslint-config?activeTab=readme
물론 모두 사용할리는 없겠지만 만약 모두 사용한다면 순서가 중요하다
위 순서대로 오버라이드 시키는 편이 좋다.
module.exports = {
extends: [
"@rendar-mono-template/eslint-config",
"@rendar-mono-template/eslint-config/next",
"@rendar-mono-template/eslint-config/typeCheck",
"@rendar-mono-template/eslint-config/prettier",
],
};
마무리
전편에 이어 전용 템플릿 시리즈 - ESLint 편을 보았다
사실 Lint 설정이랄게 크게 없어서 어려움은 없었지만 관련된 패키지 부분에서 많은 부분 시간을 쏟은 것 같다.
GPT랑 클로드 활용해서 코드 분석 및 주석을 통해서 어떤 패키지인지 파악하는게 굉장히 쉬워져서
패키지 만드는데 큰 어려움이 없었던 것 같다
다음은 가장 쉬운 prettier 부분으로 넘어갈려고한다