[webpack] CRA없이 리액트(w/ 타입스크립트) A to Z 환경 구축하기(+ 배포까지)

Chloe K·2023년 5월 30일
4

Webpack

목록 보기
5/5
post-thumbnail

create-react-app

리액트 앱을 CRA없이 처음부터 만들려면 웹팩, 바벨 등 설정해줘야하는 사항들이 정말 많다. 이번에 웹팩으로 리액트 앱을 만들면서 많이 헤맸는데 프로젝트를 시작하기도 전에 힘이 빠지는 느낌이었다.

사실 그동안 CRA가 얼마나 편리하고 편하게 개발했는지 느끼게 된 계기가 되었다. CRA 공홈에 가면

You don't need to learn and configure many build tools. Instant reloads help you focus on development. When it's time to deploy, your bundles are optimized automatically.

다양한 툴에 대해서 배우고 환경설정할 필요없이 개발에 집중할 수 있게 해준다고 써있다. 커맨드라인 하나로 정말 간단하게 리액트 앱을 만들어주고 바벨, 웹팩 config가 다 이미 만들어져있다.

하지만 이미 만들어진 설정은 변경하기가 굉장히 힘들다. 설정을 변경하기 위해서 필요한 모듈을 또 설치하고 config.js파일을 override하기 위해서 다시 작성해줘야하는 번거로움이 있다. 또한 정말 다양한 모듈들이 깔려 있기 때문에 성능문제가 발생하기 쉽다.

🔨 Creating React App from scratch

그래서 시도해본 CRA 도움을 받지않고 개발자가 원하는 방식대로 webpack, babel 등을 사용해서 리액트 앱을 만들어 보았다.

1. 프로젝트 파일 생성하기

$ cd Desktop
$ mkdir webpack-react
$ cd webpack-react
$ code .

2. package.json 생성

$ npm init -y
  • package.json을 생성하기 위해 사용한다.

3. 리액트 필수 라이브러리 설치하기

$ npm i react react-dom

4. Typescript 설치하기

$ npm i -D typescript @types/react @types/react-dom ts-loader
$ tsc --init
  • ts-loader: typescript로 작성된 코드가 javascript로 변환하게 해주는 loader
  • tsc --init : tsconfig.json 파일 생성

5. webpack 설치하기

$ npm i -D webpack webpack-cli
  • webpack: 웹팩 라이브러리
  • webpack-cli: 웹팩을 명령어로 조작하기 위한 라이브러리

6. webpack 플러그인, 테스트 서버 설치하기

$ npm i -D html-webpack-plugin webpack-dev-server
  • html-webpack-plugin: 번들링 된 js 파일을 html 파일에 삽입해준다.
  • webpack-dev-server: 로컬에서 개발하기 위한 테스트 서버

7. Babel 관련 라이브러리 설치하기

$ npm i -D babel-loader @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript
  • babel-loader: 바벨 config 파일을 읽어 설정에 맞게 변환한다.
  • @babel/core: 바벨의 코어기능을 포함하고 있어 반드시 설치해야 한다.
  • @babel/preset-env: ECMAScript2015+를 변환할 때 사용한다. 바벨7이전에는 babel-reset-es2015와 같이 버전별로 제공되었지만, 현재는 통합되어 사용하기 편리해졌다.
  • @babel/preset-react: react를 변환한다. (jsx -> js)
  • @babel/preset-typescript: 타입스크립트를 변환한다. (ts,tsx -> js)

8. 루트에 babel.config.ts 생성하기

module.exports = {
  presets: [
    ["@babel/preset-react", { runtime: "automatic" }],
    "@babel/preset-env",
    "@babel/preset-typescript",
  ],
};
  • 프리셋(preset): 여러 개의 플러그인(어떻게 코드를 변환할 지에 대한 룰을 정의해준다.)을 모아놓은 것을 프리셋이라고 한다.
@babel/preset-react
@babel/preset-env
@babel/preset-typescript
@babel/preset-flow
@babel/preset-jest

9. loader 설치하기 (for css, style)

npm i -D css-loader style-loader
  • css-loader : CSS를 JS파일 내에서 불러올 수 있게 하기 (나중에 styled-components로 변경해서 적용할 예정)
  • style-loader : CSS를 DOM(style 태그) 안에 담기

10. webpack.config.ts 생성하기

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

module.exports = {
  mode: "development",
  entry: "./src/index.tsx", // src/index.js or src/index.ts가 기본값
  output: {
	path: path.resolve(__dirname, "build"),
	filename: "bundle.js", // main.js가 기본값
  },
  module: {
    rules: [
      {
          test: /\.(ts|tsx)$/,
          use: ['babel-loader', 'ts-loader'],
          exclude: ['/node_modules'],
        },
       {
          test: /\.css$/i,
          use: [
            {
              loader: 'style-loader',
            },
            { loader: 'css-loader' },
          ],
        },
    ],
  },
  plugins: [
	new HtmlWebPackPlugin({
	  template: './public/index.html'
	})
  ],
  devServer: {
    historyApiFallback: true,
    port: 8080,
    hot: true,
  }, // test dev 서버 설정하기
};

11. src/index.tsx, public/index.html 생성하기

// ./src/index.tsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import Home from './pages/Home';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);

root.render(
    <Home /> // ./src/pages/Home.tsx 생성하고 index.tsx에 import 해주기
);
<!-- ./public/index.html -->

<!DOCTYPE html>
<html lang="en">
  <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>
new HtmlWebPackPlugin({
	  template: './public/index.html'
	})

webpack.config.ts에서 설정한 HtmlWebPackPlugin 에 의해서 번들링 후 생성된 ./build/index.html은 템플릿에 설정된 파일 ./public/index.html의 동일한 내용을 자동으로 주입한다.

12. package.json 스크립트 수정하기

  "scripts": {
    "dev": "webpack serve --open --mode development",
    "start": "webpack --mode development",
    "build": "webpack --mode production"
  },

13. 번들 실행하기

$ npm dev

or 

$ npm start

or 

$ npm run build

👀 더 알아보기

📥 image, font 등 asset loader 설치하기

$ npm i -D file-loader

웹팩 사용시 이미지, 폰트 등을 로더없이 번들을 시도하면 실패한다. 에셋을 읽지 못하고 변환하지 못한 것이다. 그래서 file-loader or url-loader가 필요하다.

  • file-loader: 파일을 모듈로 사용할 수 있게 만들어준다.
  • url-lodaer: 파일을 base64 URL로 변환한다.
// webpack.config.ts에 추가하기

module.exports = {
  mode: "development",
  entry: "./src/index.tsx", // src/index.js or src/index.ts가 기본값
  output: {
	path: path.resolve(__dirname, "build"),
	filename: "bundle.js", // main.js가 기본값
  },
  module: {
    rules: [
      {
          test: /\.(ts|tsx)$/,
          use: ['babel-loader', 'ts-loader'],
          exclude: ['/node_modules'],
        },
       {
          test: /\.css$/i,
          use: [
            {
              loader: 'style-loader',
            },
            { loader: 'css-loader' },
          ],
        },
       // 이미지 로더
       {
          test: /\.(png|svg|jpg|jpeg|gif)$/i,
          use: [
            {
              loader: 'file-loader',
              options: {
                outputPath: 'static/media',
              },
            },
          ],
        },
        // 폰트 로더
        {
          test: /\.(woff|woff2|eot|ttf|otf)$/i,
          use: [
            {
              loader: 'file-loader',
            },
          ],
        },
    ],
  },
  plugins: [
	new HtmlWebPackPlugin({
	  template: './public/index.html'
	})
  ],
  devServer: {
    historyApiFallback: true,
    port: 8080,
    hot: true,
  }, // test dev 서버 설정하기
};
  • outputPath: public 폴더에 있는 파일이 아니면 번들 후 ./build/static/media에 저장되도록 설정

📁 public 디렉토리 관리하기

리액트에서 public 디렉토리는 정적 파일을 넣어서 사용한다. (index.html, images, fonts 등) 번들링할 때도 webpack으로 처리되지 않고, 원본이 build 폴더에 복사된다.

public 폴더 내에 있는 이미지를 절대경로로 불러와 사용하면 dev 서버에서는 보이지만, build시 build 폴더에 불러온 이미지가 포함되지 않는다. 이럴 때 정적파일을 원본 그대로 불러오는 플러그인을 사용해야한다.

$ npm i -D copy-webpack-plugin
// webpack.config.ts 플러그인에 추가하기

  plugins: [
	new HtmlWebPackPlugin({
	  template: './public/index.html'
	})
    new CopyWebpackPlugin({
      patterns: [
        {
          from: 'public/',
            globOptions: {
              ignore: ['**/index.html'],
            },
        },
      ],
    }),
  ],
  • 복사할 때 globOptions - ignore에 html 파일을 제외하고 복사해야 한다 (그냥 복사하면 build/index.html와 충돌나서 에러가 발생한다.)

빌드에 성공하면 public에 있는 fonts 와 images가 그대로 복사되었고 src 디렉토리에 저장된 이미지는 static/media에 랜덤해쉬값과 함께 빌드되었다.

📌 빌드할 때마다 바뀌는 해쉬값 고정하기

static/media에 저장된 이미지 파일명이 랜덤값으로 build할 때마다 바뀌는데 이걸 원본 파일명을 그대로 쓰고 싶어서 알아본 방법

아주 간단하다. webpack.config.ts에서 설정한 file-loader 옵션에 이름을 고정하는 것이다.


 {
   test: /\.(png|svg|jpg|jpeg|gif)$/i,
      use: [
        {
          loader: 'file-loader',
          options: {
             outputPath: 'static/media',
             name: '[name].[ext]',
          },
        },
      ],
 },

원본 파일명 유지

📎 css 사용시 파일 번들 후 추출하기

css 파일을 만들어서 import 해준 후 dev 서버를 확인해보면 아래와 같이 <style> 태그에 스타일이 적용이 되었다.

css 파일이 여러개일 때 <style>이 수없이 늘어나서 찾기가 힘들 것이다. 이러한 css 파일을 하나로 묶어서 추출해주는 플러그인이 바로 mini-css-extract-plugin이다.

$ npm i -D mini-css-extract-plugin
// webpack.config.ts 플러그인에 추가하기

  plugins: [
	new HtmlWebPackPlugin({
	  template: './public/index.html'
	})
    new CopyWebpackPlugin({
      patterns: [
        {
          from: 'public/',
            globOptions: {
              ignore: ['**/index.html'],
            },
        },
      ],
    }),
    new MiniCssExtractPlugin({ filename: 'static/css/app.css' }), // 파일경로 & 파일명 지정하기
  ],

개발 모드에서는 CSS를 여러 번 수정하고 DOM에 <style> 요소의 코드로 주입하는 것이 훨씬 빨리 작동하므로 "style-loader"를 사용하고, 배포 모드에서는 MiniCssExtractPlugin.loader를 사용하는 것이다 좋다고 한다. 개발, 배포모드에 따라서 다르게 적용하기 위해서는 환경변수를 사용해서 분리해야한다.

   {
      test: /\.css$/i,
      use: [
            {
              loader: isProd ? MiniCssExtractPlugin.loader : 'style-loader',
            },
            { loader: 'css-loader' },
          ],
   },

❗️ MiniCssExtractPlugin.loaderstyle-loader는 함께 사용할 수 없다. 둘 중 하나만 사용하거나 모드를 나눠서 사용하면 된다.

<link />에 파일이 추출된 것을 확인할 수 있다.

🖼️ 이미지(png, jpg, jpeg 등) 모듈화

pulic 디렉토리에 저장된 이미지를 절대 경로로 불러와서 이미지를 사용할 경우 문제없이 이미지가 렌더링된다. 그럼 src 디렉토리에 있는 이미지를 사용할 경우는 어떨까? 바로 아래와 같은 에러가 발생한다.

import React from 'react';
import Button from '../components/Button';
import JavaScript from '../assets/5968292.png'; // img from assets

const Home = () => {
  console.log('webpack test');
  return (
    <div>
      <h2>Home</h2>
      <Button>
        <div>
          HTML
          <img src="./images/1126012.png" />
          // image from public directory
        </div>
      </Button>
      <Button>
        <div>
          JavaScript
          <img src={JavaScript} />
          // image from src directory
        </div>
      </Button>
    </div>
  );
};

export default Home;

TS2307: Cannot find module '../assets/5968292.png' or its corresponding type declarations.

타입스크립트에서 발생한 에러이다. Typescript에서 .d.ts 파일을 추가해줘서 image에 대한 타입을 지정해줘야한다.

// src/types/images.d.ts

declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';

모듈 이미지 타입을 지정해주고 tsconfig.json include에 작성한 src/types/images.d.ts를 추가해주면 된다.

{
  
  (...)

  "exclude": ["node_modules"],
  "include": ["**/*.ts", "**/*.tsx", "src/index.tsx", "src/types/images.d.ts"]
}

🗂️ tsconfig paths로 절대경로 설정하기

개발을 하다보면 ../../../assets/123.png와 같은 엄청난 depth의 상대경로를 보면 지저분하고 위치를 정확하게 파악하기가 어렵다. config 파일에서 경로를 설정해주면 깔끔하게 정리할 수 있다.

tsconfig.json에서 baseUrl"path" 부분에 사용하고자하는 경로를 설정해주면 된다.

{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@components/*": ["./components/*"],
      "@assets/*": ["./assets/*"],
      "@styles/*": ["./styles/*"],
      "@contexts/*": ["./contexts/*"],
      "@hooks/*": ["./hooks/*"],
      "@utils/*": ["./utils/*"]
    }
  }
}

tsconfig.json을 아래와 같이 수정한 후

{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@src/*": ["src/*"]
    }
  }
}

웹팩에서도 경로를 처리할 수 있도록 수정해줘야 한다. (webpack.config.ts)

// webpack.config.ts

   (...)
 
 
    entry: './src/index.tsx',
    resolve: {
      extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
      alias: {
        '@src': path.resolve(__dirname, 'src'),
      },
    }, // 확장자나 경로를 알아서 처리할 수 있도록 설정
    output: {
	  path: path.resolve(__dirname, "build"),
	  filename: "bundle.js", // main.js가 기본값
    },
      
   (...)  
// 절대경로 사용

import React from 'react';
import Button from '@src/components/Button';
import JavaScript from '@src/assets/5968292.png'; // img from assets

const Home = () => {
  console.log('webpack test');
  return (
    <div>
      <h2>Home</h2>
      <Button>
        <div>
          HTML
          <img src="./images/1126012.png" />
          // image from public directory
        </div>
      </Button>
      <Button>
        <div>
          JavaScript
          <img src={JavaScript} />
          // image from src directory
        </div>
      </Button>
    </div>
  );
};

export default Home;

💅 styled-components 사용

styled-components 설치하기

$ npm i styled-components

styled-components 사용하기

import React from 'react';
import styled from 'styled-components';

interface IButton {
  children: React.ReactNode;
}

const Button = ({ children }: IButton) => {
  return <SButton>{children}</SButton>;
};

export default Button;

const SButton = styled.button`
  width: auto;
  height: 50px;
  color: black;
  background-color: #eee;
  padding: 10px 20px;
  border-radius: 10px;
  border: none;
  cursor: pointer;

  div {
    display: flex;
    align-items: center;
    gap: 10px;
  }

  img {
    width: 18px;
  }
`;

styled-components를 설치하고 사용하고 webpack.config.ts에서 따로 추가하거나 수정해줄 필요없이 바로 사용이 가능하다.

궁금해서 테스트해보기 위해 import한 css 파일을 주석처리하고 styled-components만 남겨두고 webpack.config.ts에서 설정한 css-loader, style-loader를 다 지워보고 번들링을 해봤다. 결과는 오류없이 성공했다.

여기서 웹팩이 styled-components에서 준 스타일 속성들을 어떻게 처리하는지에 대해서 찾지 못했다.. (더 구글링해봐야겠다..✍🏻)

결론은 styled-components는 css-loader, style-loader없이 사용이 가능하다!

💻 배포하기

배포는 vercel를 사용했다. vecel은 깃허브랑 연동해서 repo를 바로 불러올 수 있다.

배포를 진행하면서 실패의 연속을 경험했다. vercel에서 배포 시 아래와 같이 몇 개의 설정을 할 수 있다.

환경변수만 설정해두고 배포를 했는데 계속해서 빈화면만 반복해서 나왔다. 개발자도구로 확인해도 <script>에서 번들된 js 파일을 불러와야하는데 <script> 조차도 없는 상태.

빌드 실패 + 에러만 나는 상황에서 혹시나하고 setting 보드에 들어가서 Output directory를 수정해봤다.

문제는 번들링하고 생성된 build 폴더를 찾지 못해서 index.html을 열지 못한 것이었다. 디렉토리를 설정해주니 ./build/index.html를 성공적으로 불러왔다.


✍🏻 Webpack-react를 마치며..

우여곡절이 많았던 CRA없이 웹팩으로 리액트 환경 구축하기. 아직 더 추가로 설정해야할 부분들이 있기 때문에 앞으로 더 추가할 예정입니다.

실수의 연속이었던 Repo 링크와 함께...🙇‍♀️

profile
Frontend Developer

0개의 댓글