ESLint에서 tsconfig paths를 인식하지 못하는 경우

이나리·2023년 11월 9일
1
post-thumbnail
post-custom-banner

요즘은 ../../../components/Button.tsx 와 같은 많이 중첩된 상대 경로 대신 @/components/Button.tsx 이런 절대 경로를 많이 사용하는 편입니다.

tsconfig.json paths 설정

타입스크립트로 코드를 작성하는 경우, tsconfig.json 파일에서 paths 옵션에 다음과 같이 값을 지정해줄 수 있습니다.

{
  	// typescript 4.1 버전부터는 baseUrl 옵션을 지정하지 않고,
  	// 직접 paths 옵션 안에서 설정하고 baseUrl을 생략할 수 있습니다.
	"paths": {
    	"@/*": ["./src/*"]
    },
  
  	// baseUrl 옵션을 사용하는 경우엔 다음과 같이 작성합니다.
  	"baseUrl": ".",
  	"paths": {
    	"@/*": ["src/*"]
    }
}

모듈 번들러 구성

이때 Webpack 이나 Vite 같은 모듈 번들러를 사용하는 경우엔, 설정한 paths에 해당하는 모듈을 resolve 할 수 있도록 똑같이 구성을 만들어줘야 합니다.

만약 설정한 경로가 많거나 또는 이를 나중에 변경하는 일이 발생한다면 tsconfig와 번들러의 구성 파일 모두 변경해야 하므로 번거로운 일이 아닐 수 없습니다.
이때는 Webpack과 Vite 모두 이를 간편하게 해주는 플러그인이 있으므로 그걸 사용하시면 됩니다.

각 번들러의 config 파일은 다음과 같이 구성될 수 있습니다.

webpack.config.ts

import path from 'node:path';
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
import webpack from 'webpack';

const config: webpack.Configuration = {
	// ...생략
  	resolve: {
      	// 플러그인을 사용하는 경우
      	plugins: [new TsconfigPathsPlugin()],
      
      	// 플러그인을 사용하지 않는 경우
    	alias: {
        	'@': path.join(__dirname, 'src')
        },
      	
      	// 타입스크립트 환경에서 사용할 모듈의 확장자를 추가로 지정해줍니다
      	// 플러그인 사용 여부와 관계없음
      	extensions: ['.ts', '.tsx', '...'],
    },
};

export default config;

웹팩의 경우, 타입스크립트 환경에서 사용할 모듈의 확장자를 추가로 지정해줘야 합니다. 기본적으로 해석하는 모듈 확장자는 .js, .json, .wasm 이기 때문에, 타입스크립트 환경에서 추가 지정을 해주지 않고, .ts, .tsx 파일 확장자를 사용하게 되면 번들링시 모듈을 resolve 할 수 없다는 에러가 발생합니다.
예를 들면, Module not found: Error: Can't resolve './src/index' in 'C:\Users\naril\dev\react-project' 이런 에러가 많이 발생할 겁니다.

... 은 기본값인 ['.js', '.json', '.wasm'] 을 추가로 사용할 수 있게 해줍니다.

참고: resolve.extensions

vite.config.ts

import path from 'node:path';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig({
  	// 플러그인을 사용하는 경우
	plugins: [tsconfigPaths()],
  
  	// 플러그인을 사용하지 않는 경우
	resolve: {
		alias: [{ find: '@', replacement: path.join(__dirname, 'src') }],
	},
});

반면 비트는 웹팩과 달리, 별도로 확장자를 지정할 필요가 없습니다.
기본값이 이미 ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json'] 으로 지정되어 있기 때문에 그렇습니다.

이렇게 해서 절대경로로 매핑한 모듈을 불러온 다음, 코드를 실행해보면 문제없이 잘 실행되는 것을 볼 수 있습니다.

ESLint 설정

현재 프로젝트의 디렉토리 구조는 다음과 같이, tsconfig.json 파일이 루트 디렉토리에 존재한다고 가정합니다. tsconfig.node.json 파일은 번들러 구성 파일에 대한 tsconfig 입니다.

┌─src
├─.eslintrc.cjs
├─tsconfig.json
├─tsconfig.node.json
└─webpack.config.ts 또는 vite.config.ts

여기서 더 나아가 ESLint 설정을 해주겠습니다.

타입스크립트 환경에서 ESLint 구성을 하기 위해서는 eslint 외에도 @typescript-eslint/eslint-plugin, @typescript-eslint/parser 패키지가 기본적으로 설치돼야 합니다.
또한, import, export 구문에 대한 린팅을 실행하도록 eslint-plugin-import 패키지 또한 설치합니다.

// in .eslintrc.cjs
module.exports = {
  root: true,
  env: { browser: true, es2022: true },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended-type-checked',
    'plugin:import/recommended',
  ],
  ignorePatterns: ['dist', '.eslintrc.cjs'],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 2022,
    sourceType: 'module',

    // 사용하는 tsconfig.json 파일의 경로를 전달합니다
    // 전달하는 방법은 여러가지 방식이 있으며, 공식문서를 참고하세요
    project: ['./tsconfig.json', './tsconfig.node.json'], 
    
    // 위의 project에서 지정된 tsconfig의 상대 경로에 대한 루트 디렉토리를 제공하는 옵션
    // 이 값이 설정되지 않으면 => 현재 디렉토리를 기준으로 해석함
    tsconfigRootDir: __dirname,
    
    ecmaFeatures: {
      jsx: true,
    },
  },
  // plugins, rules 옵션은 따로 설정되어 있지 않은 상황
};

하지만 이렇게 설정을 하고나면, 린팅 과정에서 절대경로로 매핑한 모듈 경로를 resolve 하지 못해 에러가 발생합니다.

eslint-import/no-unresolved

에러 메세지가 전부 다음과 같은 형태를 띄고 있죠?

Unable to resolve path to module '@/module'. 

이 문제를 해결하려면, 매핑한 모듈의 경로를 정확하게 resolve 해줄 수 있는 resolver가 필요합니다.
이용가능한 모듈 리졸버 목록은 eslint-plugin-import Resolvers 에서 확인할 수 있고, 이 안에서 필요한 것을 골라 사용하시면 됩니다.

웹팩은 별도의 리졸버가 구현되어 있기 때문에, 이 리졸버를 사용해도 되고 다른 방식의 리졸버를 사용해서 모듈 경로를 resolve 할 수도 있습니다.

(번외) 모듈에 .ts, .tsx 와 같은 확장자가 붙은 이유

타입스크립트 5버전 이상부터 타입스크립트 파일에 대한 확장자 사용이 가능한 allowImportingTsExtensions 옵션 설정을 켰기 때문입니다. 기본적으로는 타입스크립트 파일 확장자를 사용할 수 없습니다.
참고: Module resolution for bundlers, TypeScript runtimes, and Node.js loaders
또한 이 옵션을 켠 경우, ESLint Rules 설정을 통해 린팅시 이를 강제하도록 할 수도 있습니다.

// in .eslintrc.cjs
module.exports = {
	// ... 생략
	rules: {
		'import/extensions': ['error', 'ignorePackages']
	}	
};

그럼 이제 절대경로로 매핑한 모듈 경로가 resolve될 수 있도록 에러를 고쳐보도록 하겠습니다.

plugin:import/typescript

// in .eslintrc.cjs
module.exports = {
	extends: ['plugin:import/typescript'],
};

이 설정은 린팅 과정에서 tsconfig paths 매핑 문제를 직접적으로 해결하는 것은 아닙니다.

그럼 어떤 것을 위한 설정일까요? 이 설정은 일반적인 상대경로로서 타입스크립트 파일의 확장자를 가진 모듈을 불러올 때, 린팅 과정에서 모듈 경로가 resolve 되지 않는 문제를 해결합니다.

쉽게 말해, 타입스크립트 환경을 위한 추가 린팅 설정을 해주는 편의성 코드입니다.

relative-path-unresolved

위의 코드를 보시면, 상대 경로로 불러온 App 모듈에 대해 린팅 에러가 발생하죠?

이 코드가 무엇을 해주는지는 여기에서 직접 확인하실 수 있으며, 코드가 하는 일을 간략하게 요약하면 다음과 같습니다.

  1. resolve 할 모듈의 확장자를 '.ts', '.cts', '.mts', '.tsx', '.js', '.jsx' 로 설정해주고
  2. '.ts', '.tsx' 파일 확장자에 대해 @typescript-eslint/parser 가 파싱하도록 합니다.

해당 코드에 있는 import/extensions, import/parser 등과 같은 설정에 대해서는 공식문서를 읽어보시는 것을 더 추천드립니다. 설명이 자세하게 나와있습니다.

예외: tsconfig.json - allowImportingTsExtensions

타입스크립트 5버전 이상부터는 아래와 같이 tsconfig 설정을 해줄 경우, 불러올 모듈에 타입스크립트 확장자를 사용할 수 있다고 했는데요.

"allowImportingTsExtensions": true,
"noEmit": true

이 tsconfig 설정은 ESLint 설정파일에 plugin:import/typescript 을 추가하지 않고도, 앞서 상대 경로로서 불러온 App 모듈이 resolve 되지 않는 문제를 해결합니다.

tsExtensions-with-resolved

eslint-import-resolver-typescript

이 패키지는 eslint-plugin-import 에 타입스크립트 지원을 추가합니다.
eslint-plugin-import는 기본 모듈 리졸버로서 eslint-import-resolver-node 를 사용하기 때문에, 기본적으로는 js 확장자를 가진 모듈만 resolve 합니다.

이 패키지의 내부 코드를 살펴보면, 불러올 모듈의 확장자가 전달되지 않았다면, 먼저 타입스크립트 확장자를 가진 파일에서 모듈 경로를 찾고, 찾지 못할 경우 자바스크립트 확장자 파일에서 모듈 경로를 찾습니다.
./app.js, ./app.jsx 와 같이 자바스크립트 확장자를 명시한 경우에는, 이를 타입스크립트 확장자로 변환한 후 모듈 경로를 찾고, 찾지 못할 경우 자바스크립트 확장자 파일에서 모듈 경로를 찾습니다.

tsconfig.jsonpaths 에 설정된 경로 역시 이 과정을 거쳐 실제 모듈의 경로를 가져옵니다.

ESLint 설정 파일에는 다음과 같은 설정을 추가합니다. (무조건 이 설정을 적용하는 것은 아닙니다.)

// in .eslintrc.cjs
module.exports = {
  	settings: {
    	'import/resolver': {
        	typescript: {
            	alwaysTryTypes: true,
            },
        },
    },
};

import/resolver 키를 지정하고 객체 형태로 값을 전달했는데요. 키로 전달한 typescript 는 설치한 리졸버 패키지명에서 가져오는 것입니다. 방법은 여러가지가 있으니 참고하시면 되지만, 이 방법이 가장 간단합니다.

이 키에 어떤 값을 전달할지는 eslint-import-resolver-typescript Configuration에 나와있는 것을 토대로 전달하면 됩니다.

project

제 경우를 살펴보면, 현재 ESLint가 절대 경로로 매핑한 별칭을 인식하지 못하므로, 이를 인식할 수 있도록 이 값을 정의한 tsconfig.json 가 위치한 경로를 알려줘야 합니다.
문서를 보시면, 이 값은 project 라는 키에 전달하며, 저 같은 경우에는 프로젝트의 루트 디렉토리에 config가 존재하므로 이 키 자체를 전달하는 것을 생략했습니다.
이렇게 project 키 자체를 생략하게 되면 기본적으로 현재 프로젝트의 루트 디렉토리에 있는 tsconfig.json 파일을 사용합니다.
project 키 값과 관련된 내부 코드를 확인해보면, 문서의 내용을 조금 더 자세히 알 수 있습니다.

paths 옵션을 설정한 tsconfig.json 이 여러개이거나, 중첩된 구조 내에 있다면 전달해야 할 값이 달라질 수 있습니다.

alwaysTryTypes

이 옵션은 문서의 설명만으로는 이해가 잘 안돼서 관련 내용을 찾아봤습니다.

예를 들어, @types/unist에 정의된 .d.ts 파일에서의 타입을 필요로 하는데, 실제 이 타입과 관련된 모듈의 이름이 unist 가 아닌 경우가 있을 수 있습니다.
이 경우, @types/unist 패키지를 설치했더라도 unist 모듈을 불러오게 되면 린팅 과정에서 해당 모듈을 resolve 하지 못해 에러가 발생합니다.

import { Code } from 'unist'; // Unable to resolve path to module 'unist';

이때 alwaysTryTypes: true 를 설정하면, unist 모듈, 그러니까 이에 대한 자바스크립트 파일 없이도 @types/unist 에 정의된 .d.ts 파일을 사용할 수 있도록 해줍니다.

(영어 실력이 부족해 링크의 내용을 잘못 해석했을 수도 있습니다. 저의 해석보다는 링크를 직접 참고하시는 것을 추천드립니다. 관련 코드)

eslint-import-resolver-webpack

웹팩은 이에 대한 리졸버가 별도로 구현되어 있어, 위의 eslint-import-resolver-typescript 패키지 대신 eslint-import-resolver-webpack 패키지를 이용해 문제를 해결할 수도 있습니다.

이 패키지를 설치했다면, 다음과 같이 린팅 설정을 추가합니다.

// in .eslintrc.cjs
module.exports = {
  	// ...생략
	settings: {
      	extends: ['plugin:import/typescript'],
    	'import/resolver': {
        	webpack: {
            	config: 'webpack.config.ts',
            },
        },
    },
};

이번에는 리졸버가 eslint-import-resolver-webpack 이므로, webpack을 키 값으로 하고 값을 전달합니다.

여기서는 경로를 매핑한 웹팩 구성 파일을 잘 지정해줘야 합니다. 기본적으로 이 리졸버는 웹팩 구성 파일로 webpack.config.js 을 우선적으로 찾기 때문에, 이를 명시적으로 직접 작성한 구성 파일인 webpack.config.ts 로 변경해야 합니다. webpack.config.ts 로만 제한되는 것이 아니고 작성한 config 파일명이 있다면 그 파일명을 전달하면 됩니다.

프로젝트의 디렉토리에 기반하여 상대 경로를 전달하는 방식도 가능합니다. 저 같은 경우에는 프로젝트의 최상단 디렉토리에 구성 파일이 존재하므로 './webpack.config.ts' 를 전달할 수 있겠네요.

문서를 자세히 보시면, 여러가지 방법이 나와있으니 잘 읽어보시고 맞는 방식을 선택하시면 되겠습니다.

이렇게 변경하고 나면, 해당 config 파일에서 resolve 옵션이 존재하면 이에 해당하는 모듈을 resolve 해줍니다.

post-custom-banner

0개의 댓글