[Webpack] Loader

함성준·2021년 7월 12일
0
post-thumbnail

Webpack - Loader

웹팩 도큐먼트 내용을 요약하기 위한 문서입니다.

Loader

로더를 통해 TypeScript 로 작성된 소스코드를 JavaScript 로 변환하거나 가져온 이미지를 Base64 URL 로 변환하는 것도 가능합니다. 뿐만 아니라 최신 세대의 ECMA 표현식 또한 하위 버전으로 트랜스파일링 하는것도 예외는 아닙니다.

Configuration

우선은 별도의 설정없이, 엔트리 파일을 하나 타입스크립트로 작성하여 웹팩을 실행해보겠습니다.

// entryOne.ts
import './sample.png'
import ModuleA from "./folderOne/moduleA"
import ModuleB from "./folderOne/moduleB"

ModuleA.fnA()
ModuleB.fnB()

확장자가 .ts 이기 때문에 별도의 loader 를 설치하지 않으면 번들링이 안될거라 생각했습니다 ?

$ webpack
asset assets/8e36814b7c56efe8b9d3..png 119 KiB [emitted] [immutable] [from: src/sample.png] (auxiliary name: main)
asset main.76cba3d0783695552f0a.js 5.71 KiB [emitted] [immutable] (name: main)
runtime modules 704 bytes 4 modules
cacheable modules 300 bytes (javascript) 119 KiB (asset)
  modules by path ./src/folderOne/*.js 124 bytes
    ./src/folderOne/moduleA.js 62 bytes [built] [code generated]
    ./src/folderOne/moduleB.js 62 bytes [built] [code generated]
  ./src/entryOne.ts 134 bytes [built] [code generated]
  ./src/sample.png 42 bytes (javascript) 119 KiB (asset) [built] [code generated]
webpack 5.39.1 compiled successfully in 78 ms

!!?!

혹시 소스코드내에 타입스크립트 문법으로 작성된 부분이 없어서 그런가 아래의 소스를 추가해서 실행했습니다.

// entryOne.ts
import './sample.png'
import ModuleA from "./folderOne/moduleA"
import ModuleB from "./folderOne/moduleB"

ModuleA.fnA()
ModuleB.fnB()

function foo (a: number, b: string): void {
    console.log(a,b)
}

그랬더니 아래의 오류를 발생시킵니다.

$ webpack
asset main.1e635361f45f6ea3eccd.js 1.53 KiB [emitted] [immutable] (name: main)
./src/entryOne.ts 202 bytes [built] [code generated] [1 error]

ERROR in ./src/entryOne.ts 8:15
Module parse failed: Unexpected token (8:15)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| ModuleB.fnB()
| 
> function foo (a: number, b: string): void {
|     console.log(a,b)
| }

webpack 5.39.1 compiled with 1 error in 51 ms

일반 자바스크립트는 확장자가 .ts 여도 기본적으로 해석할 수 있는 모양입니다. 본격적으로 타입스크립트 문법을 도입했더니 해석할 수 없는 구문이라는 에러가 나네요.
그렇다면 해당 문장을 해석할 수 있는 loader 를 설치 및 웹팩 설정에 적용하는 과정을 통해 해석할 수 있도록 전처리 과정을 추가해보겠습니다.

$ npm install ts-loader -D

ts-loader 를 개발 디펜던시로 추가하고 module.rules 에 전처리 적용 규칙을 추가하였습니다. 이제 웹팩을 실행해보죠.

module.exports = {
    ...,
    module: {
        rules: [
            {
                test: /.ts$/i,
                use: 'ts-loader',
                exclude: /node_modules/
            },
            {
                test: /\.(png|svg|jpg|jpeg|gif)$/i,
                type: 'asset/resource',
            },
        ]
    }
}

결과는 아래와 같습니다.

$ webpack
asset main.0507ff4f12ca56e154dc.js 1.45 KiB [emitted] [immutable] (name: main)
./src/entryOne.ts 39 bytes [built] [code generated] [2 errors]

ERROR in ./src/entryOne.ts 
[tsl] ERROR
      TS18002: The 'files' list in config file 'tsconfig.json' is empty.
ts-loader-default_e3b0c44298fc1c14

ERROR in ./src/entryOne.ts
Module build failed (from ./node_modules/ts-loader/index.js):
Error: error while parsing tsconfig.json
    at Object.loader (/Users/jun/Desktop/Dev/wp/node_modules/ts-loader/dist/index.js:17:18)

webpack 5.39.1 compiled with 2 errors in 257 ms

tsconfig.json 파일이 비어 있거나, 존재하지 않는다고 합니다. 이것을 통해 유추해보면 ts-loader는 동작하기 위해서 사전에 작성된 tsconfig.json 설정 파일을 필요로 한다고 생각할 수 있습니다.

이 파일은 어떻게 생성할 수 있을까요? typescript 모듈 설치를 통해 쉽게 생성할 수 있습니다.

$ npm install typescript -D

해당 모듈을 설치하게 되면 tsc 명령어를 터미널에서 사용할 수 있게 됩니다.

# tsconfig.json 파일 생성
$ tsc --init

자 그럼 기본 설정파일도 생성했으니 다시 웹팩을 실행해보도록 하죠. 아마 처음 실행하는 경우라면 strict: true 혹은 noImplicitAny: true 설정에 의해 에러가 발생했을 것입니다. tsconfig.json 설정은 타입스크립트 관련 포스팅에서 상세하게 다룰 예정이니 이 옵션을 주석처리 하거나 false 로 잡아주세요.

결과적으로는, ts-loader 에 의해서 타입스크립트로 작성된 소스코드를 해석하고 자바스크립트로 변환한 결과파일을 얻게 되었습니다. 응? 근데 잠깐만.
타입스크립트가 특정 세대의 ECMA 자바스크립트로 변환해준다면 babel 은 이제 필요없는 것 아닌가 하는 생각이 들기 시작했다. 따라서 관련 문서를 좀 찾아보았다.

요약하면 babel 만 이용할 경우 tsc 가 지원하는 d.ts 타입 정의 파일을 생성할 수 없고 타입스크립트 소스코드를 자바스크립트로 트랜스파일하는 과정에서 발생하는 타입 오류를 체크할 수 없다고 하네요. 결론적으로 tsc, ts-loader 를 통해 타입스크립트 소스를 빌드 과정에서 타입 체킹 및 자바스크립트로 변환해 주고 babel 을 통해 필요한 polyfill 을 달성할 수 있는 것으로 이해가 됩니다.

그럼 목표를 하나 잡아보죠. 타입스크립트로 작성된 작은 어플리케이션을 만들어서 ts-loader, babel-loader 를 통해 타입체킹과 d.ts 파일 생성 폴리필을 만족해 봅시다.

우선 예시 구조를 만들었습니다. 확장자는 전부 .ts 로 잡았어요.

$ tree ./src
./src
├── entryOne.ts
├── folderOne
│   ├── moduleA.ts
│   └── moduleB.ts
├── folderTwo
│   ├── moduleC.ts
│   └── moduleD.ts
└── sample.png

바로 실행했더니 에러가 나네요. 아래의 오류를 내뿜습니다.

ERROR in ./src/entryOne.ts 3:16-46
Module not found: Error: Can't resolve './folderOne/moduleA'

웹팩 설정의 resolve.extensions 확장자 목록에 추가해주면 해결 할 수 있습니다. 웹팩이 기본적으로 살펴보는 파일 확장자에 .ts 가 없기 때문이죠.
그렇다면 임포트 구문에서 파일 확장자를 명확히 지정해주면 될까요? 설정을 주석처리하고 확장자를 명시해보겠습니다.

TS2691: An import path cannot end with a '.ts' extension. Consider importing './folderOne/moduleA' instead.

가져오는 경로는 .ts 로 지정할 수 없다고 하네요. resolve.extensions 목록에 추가해주는게 바람직한 접근인 것 같습니다.

혹시 ts-loader 만으로 폴리필이 달성될 수 있지 않을까 하는 마음에 제안단계에 있는 클래스 문법을 몇개 도입해서 넣어둘게요.

export class classFromA {
    #privateVar = 1;
    #privateVar2 = 2;
}

ts-loader 자체는 이 구현을 WeakMap 자료구조를 통해 자바스크립트로 변환하는 모양입니다.

/***/ ((__unused_webpack_module, exports) => {

eval("\nvar _classFromA_privateVar, _classFromA_privateVar2;\nObject.defineProperty(exports, \"__esModule\", ({ value: true }));\nexports.classFromA = exports.functionFromA = void 0;\nconst functionFromA = (A) => {\n    console.log('Function From A');\n};\nexports.functionFromA = functionFromA;\nclass classFromA {\n    constructor() {\n        _classFromA_privateVar.set(this, 1);\n        _classFromA_privateVar2.set(this, 2);\n    }\n}\nexports.classFromA = classFromA;\n_classFromA_privateVar = new WeakMap(), _classFromA_privateVar2 = new WeakMap();\n\n\n//# sourceURL=webpack://wp/./src/folderOne/moduleA.ts?");

/***/ }),

번들 파일에 WeakMap 자료구조가 포함된 것을 알 수 있고 만약 특정 브라우저에서 WeakMap 이 존재하지 않는다면 이 소스코드를 실행할 수 없을 것입니다. IE 10 이하의 버전에서는 런타임에 에러가 발생하겠군요.

폴리필 지원을 위해서 아래의 모듈을 설치합시다. 관련 문서는 여기도 잘 작성이 되어 있네요.

npm install @babel/core @babel/preset-env core-js@3 @babel/plugin-proposal-class-properties

이후 ts-loader -> babel-loader 절차를 실행하도록 해봤으나 원하는대로 동작해주지 않네요 :(

대안으로, 타입체킹이 온전히 이루어지면 바벨이 타입스크립트를 자바스크립트로 변환 및 폴리필하도록 수정했습니다.

//package.json
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build:dev": "tsc --p tsconfig.json && webpack --mode development --config webpack.config.js",
    "build:prod": "tsc --p tsconfig.json && webpack --mode production --config webpack.config.js"
  },
  "browserslist": ["IE 10"]
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",                               
    "module": "ESNext",                         
    "declaration": true,
    "emitDeclarationOnly": true,                   
    "outDir": "./dist/",                          
    "moduleResolution": "node",          
    "allowSyntheticDefaultImports": true, 
    "esModuleInterop": true,                      
    "skipLibCheck": true,                           
    "forceConsistentCasingInFileNames": true,
  },
  "include": ["./src/entryOne.ts"]
}
// .babelrc
{
    "presets": [
        ["@babel/env",{"useBuiltIns": "usage", "corejs": 3}],
        "@babel/typescript"
    ],
    "plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-transform-typescript"]
}

위의 설정으로 빌드시 타입스크립트 타입 체킹, d.ts 파일 생성, 타입스크립트를 자바스크립트로 변환, 폴리필 만족하였습니다. IE 10 환경에서 클래스 접근 제어 문법과 WeakMap 동작 확인하였습니다.

로더의 개념 이야기를 하다가 정신차려보니 여기까지 왔습니다. 위의 내용을 통해 타입스크립트 환경에서 IE 폴리필까지 적용 가능한 프로젝트 환경은 구성할 수 있겠네요. 고생하셨습니다.

0개의 댓글