React + TS boilerplate 제작기 1 - 환경 구성

DD·2021년 6월 12일
44
post-thumbnail

2편 : React + TS boilerplate 제작기 - 설치 패키지 & npx

📣 경고!?

현재 react 17버전부터 가능해진 import React from 'react' 제거 작업을 진행중입니다. ts-loader에게 모든걸 맡긴 상태라 현재 아래 글에는 고려되지 않았습니다.
조만간 수정할 예정이니 참고만해주세요.

❓ 왜 시작했는가?

React를 처음 접할 때 대부분의 강의는 create-react-app(이하 CRA)을 사용합니다. 저 또한 React 프로젝트를 시작할 때 마다 일단 CRA를 실행하고, 추가적으로 필요한 의존성을 추가한 후 개발을 시작하곤 했습니다.

CRA는 webpack, babel, eslint 등 복잡한 환경 설정을 신경쓰지 않아도 되며, autoprefixer 를 지원하는 등 개발을 매우 편리하게, 시간을 절약해주는 아주 유용한 툴입니다.

🤦‍♂️ 다만, 그만큼 단점도 존재하죠.

  • CRA는 다양한 경우를 커버하기 위해 (경우에 따라) 쓸데없이 무겁다.
  • npm eject 명령으로 숨겨진 설정, 의존성을 드러낼 수 있지만 잘못 건드리면 엄청 꼬일 수 있다.
  • 그럼에도 불구하고 eject를 해야하는 상황이 있다. (현재 프로젝트에 필요한 의존성이 CRA 기존 의존성과 충돌하는 경우 등)

즉, CRA는 React는 라이브러리가 아닌 프레임워크라 부르게 만드는 이유 중 하나입니다. 이미 촘촘하게 짜여진 환경에 따라야하며, 사용자가 커스텀하기엔 큰 부담이 따르거든요.

👉 하지만 언젠가는 직접 환경설정을 해야하는 날이 올 것이기 때문에, CRA에만 의존해서는 안 되겠다는 생각이 들었습니다.

아래 설명 부분부터는 말투가 좀 바뀔겁니다..


⛏️ 그러니까 일단 한 번 만들어 보자.

사실 보일러플레이트가 뭐 별거 있겠나(사실 별거임). 이미 수많은 블로그에 다양한 정보가 넘쳐난다.

하지만 블로그마다 환경 설정이 조금씩 다르다. 근데 다 제대로 동작한다.

🤦‍♂️ 이 미묘한 차이가 초보자 입장에서 아주 골치다.

  • 어떤게 더 나은 방식인지
  • 얘는 왜 이렇게 했고, 쟤는 왜 이렇게 했지?
  • 이 설정은 어디까지 커버하는거지?
  • 이 설정 값의 의미는 뭐지? 안 하면 어떻게 되는거지?

의문이 들지만 명확한 설명은 없다. 보통의 환경 설정을 설명하는 블로그 글은 대부분 이 파일은 이렇게 작성하세요에서 그친다.. 다 그렇다는건 아니지만!

일단 한 번 따라해봐! 그럼, 짠! 동작해!

👉 가능하면 각종 설정 파일들(package.json, tsconfig ..)의 각 속성들이 어떤 의미인지 최대한 설명하겠다.. 이후 글에는 내가 진행하면서 궁금하고 찾아본 사소한(?) 내용들은 ❓로 표시하고 찾아낸 내용을 기술할 예정이다. 그러다보니 뭐 이런거까지 언급하냐 싶은 내용이 많을 수 있으니 참고해주시길..


📣 그럼 시작합니다

npm init -y

❓ -y 는 뭐지?

  • -y 옵션은 npm init시 물어보는 질문들을 모두 yes처리한다.
  • 구체적인 정보를 입력하려면 -y를 생략하고 물어오는 질문에 맞춰 작성하면 된다.

🚀 React

npm i react react-dom

  • 가장 기본적인 두 패키지만 설치하겠다.
  • router, styled-components 등등 부가적인 패키지는 원한다면 개인적으로 추가하기!

🚀 TypeScript

npm i -D typescript @types/react @types/react-dom

❓ 왜 typescript 관련 설치는 -D를 붙이나?

  • D는 해당 패키지를 devDependencies에 설치한다는 의미인데, 사실 로컬에서만 실행할 프로젝트라면 큰 상관 없기도 하다..
    • dependencies는 프로덕션 환경에서 필요한 패키지
    • devDependencies는 로컬 개발 및 테스트 단계까지만 필요한 패키지이다.
  • ts의 경우 개발을 진행할 때와 배포를 위한 빌드 파일 생성 전까지만 쓰이기 때문에 dev에 설치하는 것이고,
  • react는 프로덕션 환경에서도 필요하기에 dev가 아닌 dependencies에 설치하는 것이다!

📃 tsconfig.json

  • 이 파일이 존재하는 경로가 TypeScript 프로젝트의 root 경로가 된다.

  • compilerOptions, files, include, exclude, extends, compileOnSave 등의 속성을 작성할 수 있다. 이에 대한 설명은 간단하게만 작성하고, 자세한 내용은 검색!

    • compilerOptions : 이름 그대로 어떻게 컴파일 할 것인지 지정하는 속성
    • files : tsc 명령어 입력시 컴파일 대상 파일을 미리 지정해두는 속성
    • include : files와 비슷한 동작을 하지만 파일이 아니라 폴더 경로를 지정한다.
    • exclude : include와 반대로 컴파일하지 않을 폴더 경로를 지정한다
    • extends : 상속 받을 다른 tsconfig 설정 파일 경로를 지정한다
    • compileOnSave : 파일을 변경하면 바로 컴파일을 할 것인지 boolean값으로 지정한다. 에디터마다 동작하지 않을 수도 있다. vscode 2015, TypeScript 1.8.4 이상이어야 한다

TIP
컴파일 대상 경로를 정의하는 속성의 우선 순위
files > include = exclude // exclude에 있더라도 files에 지정된 파일은 컴파일 대상이 된다.

TIP2
exclude에 node_modules를 지정하더라도 @types 폴더는 컴파일에 포함한다!

  • 프로젝트 root 폴더에 tsconfig.json 파일을 만들어준다.

  • 타입스크립트가 글로벌로 설치되어 있다면 tsc --init 명령어로 만들어도 되지만 그러면 모든 속성값이 적힌 tsconfig.json파일이 생성된다. 궁금하면 해당 명령어로 생성해 보자.

  • 이 글에서는 직접 tsconfig.json 파일을 생성하고, 내용을 추가하는 방식을 진행한다.

{
  "compilerOptions": {
    "target": "es5", // 컴파일 된 결과물이 어느 버전의 ECMAScript를 따를 것인지
    "lib": ["DOM", "DOM.Iterable", "ESNext"], // 컴파일 시 포함시켜야하는 javascript 내장 API들의 타입 정의에 대한 정보들
    "module": "esnext", // 프로그램에서 사용할 모듈 시스템. import/export 코드가 어떤 방식의 코드로 컴파일 될지 결정한다
    "allowJs": true, // js files를 허용할 것인가
    "jsx": "react", // jsx 코드를 어떻게 컴파일 할 것인가
    "baseUrl": "./", // 비상대적 import 모듈 해석시 기준이 되는 경로
    "moduleResolution": "Node", // 모듈 해석 전략. 웬만해선 node로 고정할 것
    "sourceMap": true, // map 파일을 생성할 것인가
    "esModuleInterop": true, // es module 사용시 컴파일 단계에서 헬퍼 함수를 사용할 것인가
    "strict": true, // strict family 속성 전부를 true로 할 것인가
    "noImplicitAny": false, // any 타입으로 구현된 표현식 혹은 정의를 에러처리 할 것인가
    "isolatedModules": true, // 각 파일을 분리된 모듈로 트랜스파일링할 것인가
    "forceConsistentCasingInFileNames": true, // 사용할 파일의 이름을 대소문자까지 정확하게 작성하도록 강제할 것인가
    "declaration": false, // d.ts 파일을 생성할 것인가
    "removeComments": true, // 컴파일시 주석을 제거할 것인가
    "pretty": true, // 에러와 메세지를 색, 컨텍스트를 사용해서 스타일을 지정할 것인가
    "strictFunctionTypes": true, // 함수, 메소드의 인자 타입을 더 정확히 추론할 것인가
    "skipLibCheck": true, // 사용하는 라이브러리의 타입 검사를 skip할 것인가
    "noImplicitThis": true, // any 타입으로 암시한 this 표현식에 오류를 보고할 것인가
    "noFallthroughCasesInSwitch": true, // switch문에서 fallthrough case가 발견되면 에러를 발생시킬 것인가
    "noImplicitReturns": true, // void가 아닌 함수가 리턴을 제대로 하지 않는 경우가 있다면 에러 발생
    "noEmit": true, // 컴파일러가 js 파일 등 출력 결과물을 만들지 않을 것인가
    "noEmitOnError": true, // 에러 발생시 js 소스코드, source map, declaration 등이 생성되지 않는다
    //
    "noUnusedLocals": false,
    "downlevelIteration": true
    // 사용되지 않는 지역 변수에 대해 에러를 발생시킬 것인가
  },
  "include": ["src"],
  "exclude": ["node_modules"]
}

❓ 이 많은 옵션이 다 필요한건가?

  • 사실 tsconfig 설정을 어디까지 지정할 것인가가 가장 큰 고민인거 같다.
  • 실제 tsc --init을 실행하면 기본으로 주어지는 compilerOptions은 4가지이다
    • target
    • module
    • strict
    • esModuleInterop
  • 이 4가지는 ts를 어디에서 쓰든 가장 중요한 옵션이라고 생각해서 자체 지정한 옵션인가? 싶다.
  • 물론 이 외에도 React와 함께 사용하기 위해서 jsx, lib 등 추가적인 필수 사항이 존재한다
  • 나머지는 필수는 아니지만,
    • 얼마나 엄격한 ts 규칙을 구성할 것인가
    • 어떤 환경에서 사용할 것인가(서버, 리액트 Next.js 등)
  • 선택하는건 개인의 몫인거 같다.
  • 좀 더 고수가 돼서 능숙하게 tsconfig를 조절할 수 있는 개발자가 되자!

참고 링크

여기 상당히 잘 정리된 4개의 문서가 있다. 각자 공부해보면서 자신만의 tsconfig.json 파일을 만들어보자


🚀 Webpack

이제 모든 프로젝트를 번들링해줄 웹팩 설정을 해보자

npm i -D webpack webpack-cli webpack-dev-server
npm i -D ts-loader css-loader style-loader file-loader html-webpack-plugin
npm i dotenv

❓ loader와 plugin은 무슨 차이일까?

  • 단순히 비교하면 번들이 생성 되기 전(빌드 전)에 쓰이냐, 에 쓰이냐로 구분할 수 있다

  • loader는 webpack으로 빌드를 진행하기 전/진행 중에 개별 파일들(빌드 과정에 포함되는 각 파일들)에게 적용하기 위해 사용된다

  • plugin은 번들이 생성 된 후 결과 파일에 적용하기 위해 사용된다. 번들된 js 난독화, 압축, 복사, 특정 텍스트 추출, 별칭 등 부가 작업 등등 후처리를 한다.

  • 이것들 모두 프로덕션 배포 이전 빌드 단계에서만 사용되기 때문에 -D 옵션으로 설치한다.


📃 webpack.config.js

require("dotenv").config(); // ❓

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

const isProd = process.env.NODE_ENV === "production"; // ❓ 
const PORT = process.env.PORT || 3000;

module.exports = {
  mode: isProd ? "production" : "development",
  devtool: isProd ? "hidden-source-map" : "source-map", // development 환경에서만 source-map을 만든다.
  entry: "./src/index.tsx",
  output: {
    filename: "[name].js", // [name]은 청크의 이름을 사용한다. ❓
    path: path.join(__dirname, "/dist"), // ❓
  },
  resolve: {
    modules: ["node_modules"],
    extensions: [".js", ".jsx", ".ts", ".tsx"], // ❓
  },
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        loader: "ts-loader",
        options: {
          transpileOnly: isProd ? false : true, // ❓
        },
      },
      {
        test: /\.css?$/,
        use: ["style-loader", "css-loader"],
      },
      {
        test: /\.(webp|jpg|png|jpeg)$/,
        loader: "file-loader",
        options: {
          name: "[name].[ext]?[hash]",
        },
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "public", "index.html"),
      hash: true,
    }), // ❓
  ],
  devServer: {
    contentBase: path.resolve(__dirname, "public"),
    host: "localhost",
    port: PORT,
    open: true,
    hot: true,
    compress: true,
    historyApiFallback: true,
    overlay: true,
    stats: "errors-only",
  },
};

❓ dotenv는 뭘까?

  • 프로젝트에서 사용하는 환경변수를 별도의 파일(.env)로 관리하기 쉽게 해주는 도구
  • .env 파일을 하나만 사용할 땐 단순히 config()
  • production, development 모드 등 상황에 따라 환경 변수를 달리하려면 관련된 객체를 전달해야한다.
  • 보일러플레이트가 지정해줄 영역은 아니기에 빈값을 디폴트로 한다.

❓ NODE_ENV는 뭘까?

  • 이후 package.json에서 webpack을 실행하는 scripts 명령문에서 mode를 지정해줄 예정이다. 그 때 지정한 mode의 값이 담기는 환경변수.

❓ Chunk(청크)는 뭘까?

  • 번들링 시 모든 코드를 하나의 거대한 파일(Bundle)로 만들지 않기 위해 여러개로 나누는데 그 단위를 Chunk(청크)라고 한다.

❓ path.resolve vs path.join

  • 둘 다 인자로 전달 받는 경로를 합쳐서 문자열 형태의 path를 반환한다.
  • 차이점은 join은 전달받은 인자의 왼쪽부터, resolve는 오른쪽부터 합치면서 진행한다.
  • resolve는 합치기를 진행하다가 "/" 를 만나면 절대 경로로 인식해서 나머지 경로 인자들을 무시한다.
  • __dirname 은 현재 실행중인 경로를 의미한다.

❓ resolve 필드는 어떤 역할을 할까?

  • resolve는 웹팩이 알아서 경로, 확장자를 처리할 수 있게 도와주는 옵션이다
  • modules에 node_modules를 지정해줘야 외부 라이브러리를 바로 가져올 수 있다

    import Calender from "@jjunyjjuny/react-calendar"

  • extensions에 넣은 확장자를 웹팩이 알아서 처리한다. 따라서 import시 파일명 뒤에 확장자를 붙일 필요가 없다.

    import a from 'src/App'

❓ transpileOnly는 어떤 속성일까

  • ts-loader는 기본적으로 TS->JS로 트랜스파일링 하는 작업type check 작업을 구분 하고 같은 스레드에서 동시에 실행한다.
  • 해당 옵션을 true로 지정하면 타입체크를 수행하지 않고 트랜스파일링만 진행한다. 또한 d.ts 파일도 생성되지 않는다. 이를 통해 속도 향상을 노릴 수 있다.
  • 즉 development 모드에서는 true로 설정해서 개발 속도를 높이고, production 모드로 빌드할 때는 타입체크와 d.ts 파일 생성을 허용함으로써 안정성을 높인다.
  • 보통 속도 향상과 안정성 모두를 취하기 위해 transpileOnly는 true로하고 fork-ts-checker-webpack-plugin라는 플러그인으로 타입 체크를 한다.

❓ HtmlWebpackPlugin는 어떤 역할을 할까?

  • 빌드과정이 끝나고 따로 분리하여 bundle한 css, js 파일 등을 지정한 html 파일의 link, script 태그에 추가해준다.

📃 package.json

  • 위에서 필요한 설정을 마치고 이제 마무리로 이 파일을 정리해보자

npm i -D cross-env

webpack 실행시 NODE_ENV 값을 지정하기 위해 보통 --mode production/development 옵션을 추가해주는데, 이는 Mac에서만 동작한다

윈도우에서 NODE_ENV값을 변경하기 위해서는 cross-env 라이브러리를 설치, 사용해야한다. 서러워서 윈도우 쓰겠나..


{
  "name": "", // 실제로 공백은 불가능하다. 
  "version": "0.1.0", 
  "description": "", 
  "scripts": {
    "dev": "cross-env NODE_ENV=development webpack serve --open --hot --progress", 
    "start": "cross-env NODE_ENV=development webpack --progress",
    "build": "cross-env NODE_ENV=production webpack --progress"
  }, 
  "keywords": [], // npm 검색 키워드 
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/react": "^17.0.9",
    "@types/react-dom": "^17.0.6",
    "cross-env": "^7.0.3",
    "css-loader": "^5.2.6",
    "file-loader": "^6.2.0",
    "html-webpack-plugin": "^5.3.1",
    "styled-loader": "*",
    "ts-loader": "^9.2.3",
    "typescript": "^4.3.2",
    "url-loader": "^4.1.1",
    "webpack": "^5.38.1",
    "webpack-cli": "^4.7.2",
    "webpack-dev-server": "^3.11.2"
  },
  "dependencies": {
    "dotenv": "^10.0.0",
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  },
  "homepage": "", // git 저장소 주소 or 홈페이지 
  "repository": {
    "type": "git",
    "url": "" // git 저장소 주소
  }
}

❓ scripts

  • 위에서 설명했다시피 cross-env NODE_ENV=development/production 은 윈도우 용이다
  • mac 환경이라면 --mode development/production 만으로 충분하다
  • webpack-dev-server를 실행시키는 명령어가 webpack serve로 변경되었다. 오래된 블로그에는 여전히 webpack-dev-server 명령어가 남아있으니 주의하자. 공식문서에도 안 나와있다고 한다;;

🚀 나머지 필요 폴더 / 파일들

.gitignore

  node_modules
  /dist

  .DS_Store
  .env.local
  .env.development.local
  .env.test.local
  .env.production.local

  npm-debug.log*
  yarn-debug.log*
  yarn-error.log*

  package-lock.json
  yarn.lock

폴더/파일

// src/App.tsx
import React from "react";

export default function App() {
  return <div>sample</div>;
}
// src/index.tsx
import React from "react";
import ReactDOM from "react-dom";

import App from "./App";

const rootElement = document.getElementById("root");

ReactDOM.render(<App />, rootElement);
// public/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Title</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

📁 최종 폴더 구조

├── node_modules
├── package-lock.json
├── package.json
├── public
|  └── index.html
├── src
|  ├── App.tsx
|  └── index.tsx
├── tsconfig.json
└── webpack.config.js

👍 끝!

가능한 이건 뭐지? 싶은 내용을 최대한 설명하려고 노력했으나 여전히 부족한 점이 많습니다. 사실 의구심이 들면 스스로 찾아보면서 공부해가는게 더 개발자다운 방식이라고 생각하기도 합니다. 하지만 초보자 입장에서 환경 설정은 너무나 복잡하고 이해하기 힘든 영역이라 쪼금 더 설명을 덧붙였습니다.

다음은 이렇게 제작한 보일러플레이트를 CRA처럼 npx를 사용해 설치하는 방법에 대해 포스팅하겠습니다.

위 내용에서 오류를 발견하시면 댓글 부탁드립니다!

참고한 아티클들

profile
기억보단 기록을 / TIL 전용 => https://velog.io/@jjuny546

2개의 댓글

comment-user-thumbnail
2021년 10월 19일

똑같이 따라했는데 "Typescript emitted no output for [경로] at makeSourceMapAndFinish, successLoader,Object.loader" 라는 에러를 뱉어서 tsconfig.json에 "noEmit" 속성을 true에서 false로 바꿔줌으로써 해결했습니다.

** transpileOnly속성을 true로 주고 plugin에 ForkTsCheckerWebpackPlugin을 넣어줘도 해결이 되네요 :)

답글 달기
comment-user-thumbnail
2021년 10월 19일

mode를 development로 실행하면 해당글의 devServer 속성은
devServer: {
contentBase: path.resolve(__dirname, "public"),
host: "localhost",
port: PORT,
open: true,
hot: true,
compress: true,
historyApiFallback: true,
overlay: true,
stats: "errors-only",
},
처럼 되어있는데 webpack devServer 속성의 업데이트 때문인건지 약간 다르게 바꿔줘야하네용,

stats: "errors-only",
devServer: {
static: {
directory: path.resolve(__dirname, "public"),
},
port: PORT,
open: true,
client: {
overlay: true,
},
hot: true,
host: "localhost",
historyApiFallback: true,
compress: true,
},
stats속성은 따로 빼주어야하고, contentBase속성은 static > directory 속성으로, overlay는 client > overlay로 빼주니 되었습니다.

답글 달기