eslint-plugin-compat으로 개발단계에서 호환성 검사하기
예제 링크
https://github.com/dkpark10/playground/tree/main/examples/lint-compat
사내 서비스에서 레거시 브라우저의 원활한 지원을 위해 개발단계에서 호환성을 검사 아래 린트 도구를 이용하였다.
eslint-plugin-compat은 https://github.com/mdn/browser-compat-data 을 기반으로 브라우저 호환성을 검사해주는 린트 플러그인이다.
packagejson 설정#
1{ 2 "name": "lint-compat", 3 "version": "0.0.0", 4 "private": true, 5 "scripts": { 6 "dev": "next dev", 7 "build": "next build", 8 "build:bundle": "cross-env ANALYZE=true next build", 9 "start": "next start -p 8080", 10 "lint": "next lint", 11 "lint:fix": "eslint --fix './src/**/*.{ts,tsx,js,jsx}'", 12 "lint:compat": "next lint -f ./formatter.js", 13 "lint:compat:output": "pnpm run lint:compat -o ./compat.txt", 14 "test": "vitest --run", 15 "test:e2e": "npx playwright test", 16 "test:e2e:ui": "npx playwright test --ui" 17 }, 18 "dependencies": { 19 "@next/font": "13.1.6", 20 "next": "^14", 21 "react": "18.2.0", 22 "react-dom": "18.2.0" 23 }, 24 "devDependencies": { 25 "@types/node": "^20", 26 "@types/react": "^18.2.0", 27 "@types/react-dom": "^18.2.0", 28 "@typescript-eslint/eslint-plugin": "^7.13.1", 29 "@typescript-eslint/parser": "^7.13.1", 30 "dependency-tree": "^11.1.1", 31 "eslint": "^8.5.0", 32 "eslint-config-next": "15.0.3", 33 "eslint-config-prettier": "^9.1.0", 34 "eslint-plugin-compat": "^6.0.2", 35 "eslint-plugin-import": "^2.27.5", 36 "eslint-plugin-react-hooks": "^4.6.0", 37 "eslint-plugin-unused-imports": "^4.2.0", 38 "glob": "^11.0.2", 39 "prettier": "^3.3.3" 40 } 41}
.browserslistrc 설정#
우선 next 공식문서에서 나와있는 브라우저 버전을 설정한다. https://nextjs.org/docs/architecture/supported-browsers
1# https://nextjs.org/docs/architecture/supported-browsers 2defaults 3chrome >= 64 4edge >= 79 5firefox >= 67 6opera >= 51 7safari >= 12 8 9not OperaMini all 10not KaiOS > 0
린트 설정#
8버전 기준 overrides 속성으로 특정 파일만을 린트 검사하도록 한다. 여기서 특정 파일은 브라우저에서 실행되는 클라이언트 파일이다.
오버라이드 파일 규칙은 상대경로로 들어가야 하기에 __dirname 변수로 경로를 재수정하여 루트 경로를 설정하도록 한다.
.eslintrc.js
1const getClientFileList = require('./get-client-files'); 2 3/** 4 * @description lint overrides 파일 목록에는 상대 경로가 들어가야 한다 5 * 'Users/mac/Desktop/playground/examples/lint-compat/src/components/non-use-client.tsx', (x) 6 * '/src/components/non-use-client.tsx', (o) 7 */ 8const clientFileList = getClientFileList().map( 9 /** @param {string} clientFile @returns {string} */ 10 (clientFile) => { 11 return clientFile.replace(__dirname, ''); 12 }).map( 13 /** @param {string} clientFile @returns {string} */ 14 (clientFile) => clientFile[0] === '/' ? clientFile.slice(1) : clientFile 15 ) 16 17module.exports = 18{ 19 root: true, 20 // lint는 js만 해석 가능하기에 ts를 트랜스파일할 Parser가 필요 21 parser: "@typescript-eslint/parser", 22 23 "env": { 24 "browser": true, 25 "node": true, 26 "es6": true 27 }, 28 29 "parserOptions": { 30 "ecmaFeatures": { 31 "jsx": true 32 }, 33 "ecmaVersion": "latest", 34 "sourceType": "module", 35 "project": "./tsconfig.json", 36 "tsconfigRootDir": __dirname, // tsconfig 파일을 현재 경로에서 찾도록 함 37 }, 38 39 extends: [ 40 "plugin:react/recommended", 41 "next/core-web-vitals", 42 "plugin:import/recommended", 43 "plugin:import/typescript", 44 "prettier" 45 ], 46 47 plugins: ["react", "import", "unused-imports"], 48 49 ignorePatterns: [".eslintrc.js", "get-client-files.js", "formatter.js"], 50 51 "rules": { 52 "import/no-unresolved": "off", 53 54 /** @description react 17 이상부터 react import 불필요 */ 55 "react/react-in-jsx-scope": "off", 56 }, 57 58 "overrides": [ 59 { 60 "files": [...clientFileList, 'src/hooks/**'], 61 "plugins": ["compat"], 62 "extends": ["plugin:compat/recommended"], 63 "settings": { 64 // polyfills: ['IntersectionObserver'], // 폴리필 65 }, 66 } 67 ], 68}
클라이언트 파일 추출#
app router 에서는 서버컴포넌트에서 브라우저 호환성을 검사하기가 불필요 하기에 클라이언트 파일을 추출하여야 한다.
진입점은 app/layout.tsx 로 시작한다. 클라이언트 파일은 아래와 같이 구별할 수 있겠다.
11. 'use client' 디렉티브가 명시되어 있는 파일 22. 'use client' 디렉티브가 명시되어 있는 파일에서 불러오는 모든 모듈 33. 클라이언트에서 사용되는 것이 확실하다고 생각하는 파일 (ex: 리액트 커스텀 훅 파일)
glob과 dependency-tree를 이용하여 파일을 추출할 수 있다. get-client-files.js
1const { globSync } = require('glob'); 2const fs = require('fs'); 3const path = require('path'); 4const dependencyTree = require('dependency-tree'); 5 6function getClientFileList() { 7 try { 8 const srcPath = path.resolve(__dirname, `./src/app`); 9 /** @description root layout을 엔트리로 모든 tsx 파일리스트를 얻음 */ 10 const tsxFilePaths = globSync(`${srcPath}/**/*.tsx`, { 11 ignore: 'node_modules/**' 12 }); 13 14 /** 15 * @param {string[]} fileList 16 * @return {string[]} 17 */ 18 function getDependency(fileList) { 19 return fileList.reduce((acc, fileItem) => { 20 const d = dependencyTree.toList({ 21 filename: fileItem, 22 directory: 'path/to/all/files', 23 filter: (path) => 24 !( 25 /node_modules/.test(path) || 26 /s?.css/.test(path) || 27 /constants/.test(path) 28 ), 29 /** @description path alias를 사용할 경우 경로를 매칭하지 못하므로 tsconfig 경로 추가 */ 30 tsConfig: require('./tsconfig.json'), 31 }); 32 33 return [...acc, ...d]; 34 }, []); 35 } 36 37 /** @description tsx로부터 불러오는 모든 의존성을 파악 */ 38 const dependency = getDependency(tsxFilePaths); 39 40 const clientFiles = dependency.filter((tsxFile) => { 41 const content = fs.readFileSync(tsxFile, 'utf-8'); 42 return /['"]use client['"]\;?/.test(content); 43 }); 44 45 /** @description use client가 명시되어었는 파일로부터 다시 모든 의존성을 파악 */ 46 const clintDependency = getDependency(clientFiles); 47 48 const result = [...new Set(clintDependency)]; 49 return result; 50 } catch (err) { 51 console.error('get client file list error: ', err); 52 return ['./src/**/*.{tsx,ts}']; 53 } 54} 55 56module.exports = getClientFileList; 57 58/** @desc 린트 파일 검사 경로를 위한 log 출력 */ 59console.log(getClientFileList().join(' '));
추출된 클라이언트 파일 목록을 린트 overrides에 명시한다면 아래와 같이 개발단계에서 검증할 수 있다.
결과#
서버 컴포넌트
서버 컴포넌트는 브라우저 호환성을 신경쓰지 않아도 되므로 린트 에러를 검출하지 아니한다.

클라이언트 컴포넌트
클라이언트 컴포넌트는 브라우저에서 동작하므로 린트 에러가 검출되는 것을 확인할 수 있다.

한계
아래 파일은 'use client'가 명시된 클라이언트 파일에서 모듈을 호출하고 있으므로 오버라이드 파일속성에 명시되어 있지만
개발단계에서 린트가 에러를 검출하지 못한다. 오버라이드 파일 속성은 하드코딩으로 경로를 명시할 시 개발단계에서
린트 에러를 검출할 수 있지만 아무래도 동적으로 경로를 명시해서 생기는 이유로 추측된다.
