방구석에 놔둔 개발 노트

1년차 웹 / 앱 프론트엔드 엔지니어의 좌충우돌 얼렁뚱땅 앞뒤짱구 생존기

2023-11-05: Webpack으로 프로젝트 세팅하(배껴보)기 (2)

💪 tsconfig.json부터 배포까지

지난 시간에는 Webpack과 여러 패키지를 설치했고, Webpack에 관한 설정을 이것저것 찾아봤다.

이번엔 후반부로서 TypeScript를 사용할 거라면 알아야 하는 TypeScript의 설정과 마지막으로 React에 App.tsx로 연결까지 완료해 페이지를 띄우는 것까지 진행해보는 것을 해보려고 한다.

  1. tsconfig.json를 열어서 설정하기

TypeScript 패키지를 설치했다면, 디렉토리에 tsconfig.json 파일이 생성된 것을 볼 수 있을 것이다. 없다면 직접 생성해주거나 패키지를 재설치 해보도록 하자.

해당 파일을 열면 아래와 같이 수많은 설정들을 볼 수 있다.

{
  "compilerOptions": {
    /* Visit https://aka.ms/tsconfig to read more about this file */

    /* Projects */
    // "incremental": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
    // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
    // "tsBuildInfoFile": "./.tsbuildinfo",              /* Specify the path to .tsbuildinfo incremental compilation file. */
    // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */
    // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
    // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */

    /* Language and Environment */
    "target": "es2016",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    // "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
    // "jsx": "preserve",                                /* Specify what JSX code is generated. */
    // "experimentalDecorators": true,                   /* Enable experimental support for TC39 stage 2 draft decorators. */
    // "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
    // "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
    // "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
    // "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
    // "reactNamespace": "",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
    // "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
    // "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
    // "moduleDetection": "auto",                        /* Control what method is used to detect module-format JS files. */

    /* Modules */
    "module": "commonjs",                                /* Specify what module code is generated. */
    // "rootDir": "./",                                  /* Specify the root folder within your source files. */
    // "moduleResolution": "node",                       /* Specify how TypeScript looks up a file from a given module specifier. */
    // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
    // "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
    // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
    // "typeRoots": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */
    // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
    // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
    // "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */
    // "resolveJsonModule": true,                        /* Enable importing .json files. */
    // "noResolve": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */

    /* JavaScript Support */
    // "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
    // "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
    // "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */

    /* Emit */
    // "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
    // "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
    // "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
    // "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
    // "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
    // "outDir": "./",                                   /* Specify an output folder for all emitted files. */
    // "removeComments": true,                           /* Disable emitting comments. */
    // "noEmit": true,                                   /* Disable emitting files from a compilation. */
    // "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
    // "importsNotUsedAsValues": "remove",               /* Specify emit/checking behavior for imports that are only used for types. */
    // "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
    // "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
    // "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
    // "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
    // "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
    // "newLine": "crlf",                                /* Set the newline character for emitting files. */
    // "stripInternal": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
    // "noEmitHelpers": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */
    // "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
    // "preserveConstEnums": true,                       /* Disable erasing 'const enum' declarations in generated code. */
    // "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
    // "preserveValueImports": true,                     /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */

    /* Interop Constraints */
    // "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
    // "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
    // "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */

    /* Type Checking */
    "strict": true,                                      /* Enable all strict type-checking options. */
    // "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied 'any' type. */
    // "strictNullChecks": true,                         /* When type checking, take into account 'null' and 'undefined'. */
    // "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
    // "strictBindCallApply": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
    // "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
    // "noImplicitThis": true,                           /* Enable error reporting when 'this' is given the type 'any'. */
    // "useUnknownInCatchVariables": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */
    // "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
    // "noUnusedLocals": true,                           /* Enable error reporting when local variables aren't read. */
    // "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read. */
    // "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
    // "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
    // "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
    // "noUncheckedIndexedAccess": true,                 /* Add 'undefined' to a type when accessed using an index. */
    // "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
    // "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */
    // "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
    // "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */

    /* Completeness */
    // "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
    "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
  }
}

이 수많은 설정들을 다 설정하자니 복잡해질 것 같으므로 해당 사항은 별책부록으로 나중에 뜯어보기로 하고, 지금은 vite나 next 패키지 설치 시의 tsconfig.json 파일 내용 속에서 몇 가지를 추려내 적용해봤다.

{
  "compilerOptions": {
        /** 언어 및 환경 설정 **/

        // Typescript의 코드를 어느 버전의 javascript 코드로 컴파일할지 결정
    "target": "esnext",
        // 컴파일에 포함할 라이브러리 파일 목록을 Array로 묶음
    "lib": ["dom", "dom.iterable", "esnext"],
        // TypeScript를 Javascript으로 트랜스파일링할 때, 모듈을 어떤 시스템 기준으로 설정할지 결정
        // 트랜스 파일링: 한 언어로 작성한 코드를 다른 언어로 비슷한 수준으로 하여금 추상화하는 과정
    "module": "esnext",
        // ts, tsx 파일에 js, jsx 파일을 import해서 쓰게할 수 있는지,
        // 즉, javascript 파일을 컴파일하는 것을 허용할지 여부 결정
    "allowJs": true,
        // 모든 '.d.ts' 파일의 타입 체크를 스킵함.
    "skipLibCheck": true,
        // 컴파일된 결과물인 자바스크립트 파일을 생성하지 않을지 결정.
    "noEmit": false,

        /** lint 적용 사항 **/

        // 타입 체크를 엄격하게 할 것인지, 아닌지 여부 결정  
    "strict": true,
        // 사용되지 않은 지역 변수에 대해 알려줄지 결정
    "noUnusedLocals": true,
        // 사용하지 않은 파라미터에 대해 알려줄지 결정
    "noUnusedParameters": true,
        // switch 문에서 fallthrough 케이스가 발생할 수 있을 때, 알려줄지 결정
    "noFallthroughCasesInSwitch": true,

    /* 번들러 적용 사항 */

        // 모듈 경로를 어디서부터 시작하여 탐색할지 결정
        // node로 하면 node-modules부터 찾기 시작.
    "moduleResolution": "node",
        // .json 파일들을 import하는 것을 허용할지 여부 결정
    "resolveJsonModule": true,
        // 타입스크립트 파일 내에 import와 export 문이 없을 시, 해당 파일을 전역 공간으로 정의할지 여부 결정
    "isolatedModules": true,
        // tsx 파일을 jsx 파일로 어떻게 컴파일할 것인지 여부 결정
        // preserve 혹은 react, react-jsx가 많이 쓰인다.
    "jsx": "react-jsx",
        // 같은 파일이라도, 참조 방식이 일관되지 않을 경우 이를 허용해줄지 등을 결정
    "forceConsistentCasingInFileNames": true,
        // commonjs 모듈 방식을 import를 이용해서 가져올수 있도록 허용할지 결정
    "esModuleInterop": true,
  },
    // 컴파일할 대상을 array로 묶음
  "include": ["src"],
    // 컴파일에서 제외할 대상을 array로 묶음
  "exclude": ["node_modules"]
}
  1. /src 디렉토리 생성

위의 설정까지 마쳤다면 실제 코드가 작성될 위치를 지정해줄 필요가 있다.

이전에 Create-React-App을 설치해봤으면 많이 봤을 index와 App 파일을 설정해주자.

생성시에는 src 디렉토리를 파고, 그 안에 index.tsx와 App.tsx 파일을 추가하면 된다.

import App from "./App";
import React from "react";
import ReactDOM from "react-dom/client";

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
const App = () => {
  return <div>짜잔~ Webpack으로 개발 환경을 구축했습니다!</div>;
};

export default App;
  1. /public 디렉토리 생성

여기까지 했다면 이제 남은 건, 이 React 코드들이 반영될 가장 최상위 경로인 index.html을 생성하는 것이다.

React는 index.html 파일 내에 있는 id=”root”가 기입된 div 요소 속에서 모든 내용이 반영되도록 설계되어 있다.

아래와 같이 public 폴더를 파고 index.html를 추가하고 내용을 입력해주자.

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Webpack으로 React 환경 구축까지!</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
  1. 마지막으로 다시 package.json에서 몇 가지 실행 스크립트를 분리!

여기까지 했으면 이제 마지막으로 개발 환경, 배포 환경, 빌드 등을 적용할 스크립트 명령어를 커스텀해보자.

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "watch": "webpack --watch",
    // 개발 환경
  **"dev": "npx webpack serve --config webpack.dev.js",
  "dev_build": "webpack --config webpack.dev.js"
  "prod_build": "webpack --config webpack.prod.js",**
},
  • dev
    • 웹팩 서버를 실행시키되, dev 설정 기준으로 적용되도록 했다.
  • dev_build
    • 프로젝트를 빌드하되, dev 설정 기준으로 빌드되도록 적용했다.
  • prod_build
    • 프로젝트를 빌드하되, prod 설정 기준으로 빌드되도록 적용했다.

여기까지 완료했다면 터미널에 이제 ‘npm run dev’를 입력하고 실행해보자.

$ npm run dev

> webpack-setting@1.0.0 dev
> npx webpack serve --config webpack.dev.js

<i> **[webpack-dev-server] Project is running at:**
<i> **[webpack-dev-server] Loopback:** **http://localhost:3000/**
<i> **[webpack-dev-server] On Your Network (IPv4): http://192.168.200.132:3000/**
<i> **[webpack-dev-server] Content not from webpack is served from** **'./dist'** directory
<i> **[webpack-dev-server] 404s will fallback to '/index.html'**
<i> **[webpack-dev-middleware] wait until bundle finished:** **/**
asset **bundle.js** 1.42 MiB **[emitted]** (name: main)
asset **index.html** 321 bytes **[emitted]**
runtime modules 27.5 KiB 13 modules
modules by path ./node_modules/ 1.29 MiB
  modules by path ./node_modules/webpack-dev-server/client/ 71.8 KiB 16 modules
  modules by path ./node_modules/webpack/hot/*.js 5.3 KiB 4 modules
  modules by path ./node_modules/react/ 127 KiB 4 modules
  modules by path ./node_modules/html-entities/lib/*.js 81.8 KiB 4 modules
  modules by path ./node_modules/react-dom/ 1000 KiB 3 modules
  modules by path ./node_modules/scheduler/ 17.3 KiB
    ./node_modules/scheduler/index.js 198 bytes **[built] [code generated]**
    ./node_modules/scheduler/cjs/scheduler.development.js 17.1 KiB **[built] [code generated]**
  ./node_modules/ansi-html-community/index.js 4.16 KiB **[built] [code generated]**
  ./node_modules/events/events.js 14.5 KiB [built] [code generated]
modules by path ./src/*.tsx 512 bytes
  ./src/index.tsx 275 bytes **[built] [code generated]**
  ./src/App.tsx 237 bytes **[built] [code generated]**
webpack 5.89.0 compiled **successfully** in 2818 ms

위와 같이 ‘webpack 5.89.0 compiled **successfully**’가 보이면 컴파일이 완료되면서 아래와 같은 화면이 출력된다.

Untitled

그럼 무사히 Webpack으로 React 적용이 완료된 것이다. Let’s Coding!

현재까지 작업했던 내용에 관해서는 아래 레포지토리를 통해 확인할 수 있다.

GitHub - DrunkenNeoguri/bundle-practice: the repository for practice bundling react project vite and webpack

🚧 학습 중 에러사항 정리

  1. jsx 설정 이슈
**Compiled with problems:**
ERROR in ./src/index.tsx 5:12
Module parse failed: Unexpected token (5:12)
File was processed with these loaders:
 * ./node_modules/ts-loader/index.js
You may need an additional loader to handle the result of these loaders.
| import ReactDOM from "react-dom/client";
| const root = ReactDOM.createRoot(document.getElementById("root"));
> root.render(<React.StrictMode>
|     <App />
|   </React.StrictMode>);

위의 사항은 현재 ts.config의 옵션 중 jsx 설정이 ts-loader 기준에서 맞지 않아서 발생한 문제이다.

  • react’, ‘react-jsx’, ‘preserve’ 중에 적용되는 게 있는지 바꿔보도록 하자.

  • noEmit allowImportingTsExtensions 설정 충돌

**Compiled with problems:**
ERROR in ./src/index.tsx
Module build failed (from ./node_modules/ts-loader/index.js):
Error: TypeScript emitted no output for D:\code\project\bundle-practice\webpack-setting\src\index.tsx.  
    at makeSourceMapAndFinish (D:\code\project\bundle-practice\webpack-setting\node_modules\ts-loader\dist\index.js:55:18)
    at successLoader (D:\code\project\bundle-practice\webpack-setting\node_modules\ts-loader\dist\index.js:42:5)
    at Object.loader (D:\code\project\bundle-practice\webpack-setting\node_modules\ts-loader\dist\index.js:23:5)

위의 학습 내용 작성 당시 위의 이슈를 계속해서 발견해서 어떤 부분이 문제인지 확인해봤는데, 처음에는 noEmit과 관련한 설정이라고 생각해서 값만 변경했더니만 그걸로 해결되지 않았다.

어떤 부분이 문제였을까 기존의 블로그 작성하신 분의 글을 보면서 하나하나 비교하다가 깨달았는데, 찾은 답은 allowImportingTsExtensions 설정 때문이었다.

allowImportingTsExtensions 설정은 TypeScript 5버전부터 추가된 설정인데, .ts, .tsx, .mts와 같은 TypeScript 확장자를 각 파일에 서로 import하는 걸 허가해줄지 여부를 결정한다.

내가 설정한 값에서는 noEmitallowImportingTsExtensions 둘 다 true였던 상태였는데, 정석대로라면 이렇게 한 쪽이 true라면 다른 쪽도 true로 잡혀야한다고 하지만 계속해서 위와 같은 이슈가 발생했다.

결국 해결 방법은 noEmitfalse로 처리하고, allowImportingTsExtensions 를 지움으로서 해당 문제를 해결됐다.

둘 다 true였을 때 어떤 걸 더 설정했어야 하는지는 추후에 좀 더 알아봐야겠다.

  • 참고: webpack.common.jsresolveextensions에는 ‘.ts’ 확장자를 포함한 상태이다. 해당 확장자를 포함하지 않을 경우에도, 위의 이슈가 발생할 수 있다고 한다.

🔖 참고 자료

tsconfig.json

Typescript 컴파일시 세부설정 (tsconfig.json)

TSConfig Reference - Docs on every TSConfig option

Typescript emitted no output · Issue #767 · TypeStrong/ts-loader