wkd2ev

eslint-plugin-compat으로 개발단계에서 호환성 검사하기

예제 링크

https://github.com/dkpark10/playground/tree/main/examples/lint-compat

사내 서비스에서 레거시 브라우저의 원활한 지원을 위해 개발단계에서 호환성을 검사 아래 린트 도구를 이용하였다.

eslint-plugin-compathttps://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}

클라이언트 파일 추출#

rsc dom tree app router 에서는 서버컴포넌트에서 브라우저 호환성을 검사하기가 불필요 하기에 클라이언트 파일을 추출하여야 한다. 진입점은 app/layout.tsx 로 시작한다. 클라이언트 파일은 아래와 같이 구별할 수 있겠다.

11. 'use client' 디렉티브가 명시되어 있는 파일
22. 'use client' 디렉티브가 명시되어 있는 파일에서 불러오는 모든 모듈
33. 클라이언트에서 사용되는 것이 확실하다고 생각하는 파일 (ex: 리액트 커스텀 훅 파일)

globdependency-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'가 명시된 클라이언트 파일에서 모듈을 호출하고 있으므로 오버라이드 파일속성에 명시되어 있지만

개발단계에서 린트가 에러를 검출하지 못한다. 오버라이드 파일 속성은 하드코딩으로 경로를 명시할 시 개발단계에서 린트 에러를 검출할 수 있지만 아무래도 동적으로 경로를 명시해서 생기는 이유로 추측된다. 린트 경고문구가 나타나지 않은 클라이언트 컴포넌트 코드 이미지