909 Devlog

[유튜브 클론코딩] Webpack 본문

토이 프로젝트/유튜브 클론코딩

[유튜브 클론코딩] Webpack

구공구 2023. 8. 6. 22:06
728x90

0. Webpack을 배우는 이유

제 유튜브 클론코딩 첫 번째 포스팅을 보셨다면, 프로젝트의 백엔드에서 최신 자바스크립트 문법을 사용하기 위해, Babel이란 자바스크립트 컴파일러를 거쳐 Node.js가 작동하고 있다는 것을 알고 계실 겁니다.

 

프론트엔드에서 사용하는 자바스크립트는 브라우저(크롬, 파이어폭스 등)마다 다르게 작동할 수 있습니다.

따라서, 백엔드에서 했던 자바스크립트 컴파일 작업을 프론트엔드에 사용하는 자바스크립트에도 적용시키고,

CSS 작업을 더 편리하게 할 수 있는 SCSS를 사용하고,

어느 브라우저든 이해할 수 있는 코드를 넘겨주려면, Webpack을 사용해야 합니다.

 

사실 많은 개발자들은 Webpack을 직접 사용하지 않습니다. 대부분 Webpack이 이미 포함되어 있는 툴들(react, vue, next 등)을 사용합니다.

 

하지만 주니어 개발자로서, 업계 표준인 기술이 적어도 어떻게 작동하는지 조금이라도 이해하고 넘어가는 게 좋을 것 같습니다.

1. Webpack이란?

Webpack은 위 사진과 같이 여러 개의 파일을 하나의 파일로 합쳐주는 모듈 번들러(Module bundler)입니다.

쉽게 말해 파일들을 모아 압축, 변형시켜 최소화하고, 필요한 과정을 다 거친 다음 정리된 코드를 결과물로 내놓는 도구입니다.

 

핵심 개념으로 다음과 같은 요소들이 존재합니다.

  • Entry
  • Output
  • Loaders
  • Plugins
  • Mode
  • Browser Compatibility

Browser Compatibility는 webpack이 ES5를 지원하는 모든 브라우저를 지원한다는 개념이고, 나머지는 Webpack 파일을 작성해 보면서 알아봅시다.

 

2. Webpack 설치, config 파일 생성

다음 코드를 터미널에 작성해서 webpack과 webpack-cli를 devDependencies에 설치해 줍니다.

npm i webpack webpack-cli --save-dev

그리고 Webpack 설정을 위한 webpack.config.js 파일을 생성해 줍니다.

 

3. webpack.config.js 파일 작성

webpack.config.js 파일에서 webpack의 설정들을 module.exports의 블록 안에 모두 작성해줘야 합니다.

 

3.1 entry

핵심 요소들 중 하나인 entry는 우리가 처리하고자 하는 파일의 경로를 적습니다.

제가 처리하고 싶은 파일은 src - client - js 내부의 main.js 파일입니다.

 

아래 코드는 main.js 파일이며 어떻게 컴파일되는지 보기 위해 그냥 아무렇게 작성했습니다.

// src/client/js/main.js

const hello = async () => {
    alert("hi");
    const x = await fetch("");
};

hello();

webpack.config.js 파일에 다음과 같이 작성해 줬습니다.

module.exports = {
    entry: "./src/client/js/main.js",
};

 

3.2 output

output에는 결과물이 저장될 경로와, 저장될 파일명을 정해줘야 합니다.

저는 파일 이름을 main.js로, 경로는 assets - js 폴더 내부에 저장하겠습니다.

assets - js 폴더는 현재 존재하지 않으며, 컴파일이 성공되면 자동으로 생성될 것입니다.

module.exports = {
    entry: "./src/client/js/main.js",
    output: {
        filename: "main.js",
        path: "./assets/js",
    },
};

webpack이 정상적으로 작동하는지 한번 확인해 보겠습니다.

package.json에 script를 다음과 같이 작성하고,

"scripts": {
        "blabla": "nodemon --exec babel-node src/init.js", // <- 이거 아닙니다
        "assets": "webpack --config webpack.config.js" // <- webpack 실행 script
    },

터미널에 npm run assets를 작성하고 실행시켜 보겠습니다.

npm run assets

이러면 다음과 같이 에러가 발생합니다.

에러를 읽어보면, webpack의 output 경로 설정은 absolute path(절대 경로)를 요구한다고 합니다.

 

절대 경로란, 이 프로젝트가 있는 폴더까지의 모든 경로를 다 포함하는 전체 경로를 말합니다.

예를 들어 지금 제 프로젝트의 절대 경로는 C:\Users\wjdgh\바탕 화면\코딩\프론트엔드\project\node-test 이므로 이 모든 경로를 다 작성해줘야 한다는 겁니다.

 

다행히도, 자바스크립트는 __dirname이란 상수를 제공합니다.

 

webpack.config.js에 다음과 같이 __dirname을 console.log 하는 코드를 작성하면

console.log(__dirname);

module.exports = {
    entry: "./src/client/js/main.js",
    output: {
        filename: "main.js",
        path: "./assets/js",
    },
};

터미널에 절대 경로가 뜸으로써, __dirname을 사용해서 path 설정을 쉽게 할 수 있을 것 같습니다.

 

path 설정에 __dirname을 사용하기 전에, Node.js에 기본으로 설치되어 있는 패키지인 path를 불러와서 path.resolve() 함수를 사용합니다. path.resolve()는 괄호 안에 들어가는 문자열들을 조합하여 하나의 경로를 만들어 줍니다.

 

webpack.config.js 파일을 다음과 같이 수정합니다.

const path = require("path");

module.exports = {
    entry: "./src/client/js/main.js",
    output: {
        filename: "main.js",
        path: path.resolve(__dirname, "assets", "js"),
    },
};

이제 webpack.config.js 파일을 동작시켜 보면, 

assets - js 폴더가 생성되고, main.js 파일이 생성되었습니다.

// assets/js/main.js

(async()=>{alert("hi"),await fetch("")})();

client 폴더에 저장되어 있는 main.js와 다르게 생긴 main.js 파일이 생겼는데, 이는 webpack이 성공적으로 entry 파일을 output으로 압축해 생성했다는 결과물입니다.

 

하지만 이 과정은 그저 코드를 압축시킨 것이지, 오래된 브라우저도 이해할 수 있게 변환시킨 것은 아닙니다.

 

3.3 loader

따라서, 코드 호환성을 높여봅시다.

 

webpack은 loader를 통해 코드를 전환하는데, 우리가 백엔드에서 사용했던 babel을 여기서도 사용할 수 있습니다.

 

babel을 사용하기 위해서는 터미널에 아래 명령어를 작성해 babel-loader, @babel/core, @babel/preset-env 들을 설치해야 합니다.

npm i -D babel-loader @babel/core @babel/preset-env

저는 babel-loader를 제외하고 다 설치되어 있으니 babel-loader만 설치하겠습니다.

 

webpack.config.js 파일을 다음과 같이 수정합니다.

const path = require("path");

module.exports = {
    entry: "./src/client/js/main.js",
    output: {
        filename: "main.js",
        path: path.resolve(__dirname, "assets", "js"),
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                use: {
                    loader: "babel-loader",
                    options: {
                        presets: [
                            ["@babel/preset-env", { targets: "defaults" }],
                        ],
                    },
                },
            },
        ],
    },
};

module에는 rules에 따라 사용할 loader들을 작성합니다. 

rules는 배열형식으로, 변환시킬 파일 형식에 따라 다른 옵션들을 작성합니다.

rules 배열의 객체 요소에는 test와 use를 작성하는데, test는 변환할 파일들을 식별하는 옵션이고, use는 해당 변환을 수행하는 데 사용되는 loader를 가리키는 옵션입니다.

use 옵션은 사용할 loader의 문서에서 제공하는 옵션을 그대로 따라 사용해 주는 게 좋습니다. 위 옵션은 .js 파일을 babel을 통해 변환시킨다는 것을 선언한 코드입니다.

 

이제 webpack.config.js 파일을 동작시켜 보면, 아마 output 코드가 바뀌지는 않겠지만 에러가 나지 않는다면 loader가 정상적으로 동작했을 겁니다.

원래라면 코드가 바뀌겠지만, 제가 작성한 변환 전 main.js 파일의 코드가 이제 브라우저에도 적용될 수 있게 브라우저가 업데이트되었기 때문에 바꿀 필요가 없으니 아마 그대로 출력되었을 겁니다.

 

자바스크립트 외에도 scss를 적용시키려면 다음과 같은 패키지들을 설치하고,

npm i sass-loader sass css-loader style-loader --save-dev

webpack.config.js 파일을 다음과 같이 수정해줘야 합니다.

const path = require("path");

module.exports = {
    entry: "./src/client/js/main.js",
    output: {
        filename: "main.js",
        path: path.resolve(__dirname, "assets", "js"),
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                use: {
                    loader: "babel-loader",
                    options: {
                        presets: [
                            ["@babel/preset-env", { targets: "defaults" }],
                        ],
                    },
                },
            },
            {
                test: /\.scss$/,
                use: ["style-loader", "css-loader", "sass-loader"],
            },
        ],
    },
};

아래와 같이 자바스크립트 파일에 scss를 import 해서 사용합니다.

3.4 mode

또, webpack에는 development, production, none을 작성할 수 있는 mode라는 옵션이 있는데, 

아직 개발중일 때는 변환된 코드를 에러가 있나 없나 보기 위해 development 옵션을 사용하고,

이제 개발을 다 해서 배포할 때는 코드를 완전히 압축시키기 위해 production 옵션을 사용합니다.

 

기본 옵션은 production이며, 저는 아직 개발이 끝나지 않았기 때문에 mode에 development를 설정해 주겠습니다.

const path = require("path");

module.exports = {
    entry: "./src/client/js/main.js",
    mode: "development", // <- mode 설정
    output: {
        filename: "main.js",
        path: path.resolve(__dirname, "assets", "js"),
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                use: {
                    loader: "babel-loader",
                    options: {
                        presets: [
                            ["@babel/preset-env", { targets: "defaults" }],
                        ],
                    },
                },
            },
        ],
    },
};

 

아래 코드는 webpack 설정과는 관련 없으며 제 프로젝트에 결과물을 적용시키기 위함입니다.

변환된 결과물을 express 서버에 적용시키려면, 다음 코드를 사용합니다.

app.use("/static", express.static("assets"));

pug 파일에 자바스크립트를 불러오려면, 다음 코드를 사용합니다.

script(src="/static/js/main.js")

 


3.5 plugin

loader는 파일을 해석하고 변환하는 과정에 사용되고, plugin은 그 결과물의 형태를 바꾸거나 에셋관리, 환경 변수 주입등 광범위한 작업을 수행할 수 있습니다.

저는 플러그인을 사용하여 scss로 작성되어 적용되고 있는 css를 따로 추출해 css 파일들을 관리해 보겠습니다.

npm i mini-css-extract-plugin --save-dev

webpack.config.js 파일을 다음과 같이 수정합니다.

const MiniCssExtractPlugin = require("mini-css-extract-plugin"); // <- 플러그인을 불러오고
const path = require("path");

module.exports = {
    entry: "./src/client/js/main.js",
    mode: "development",
    plugins: [ // <- 사용합니다.
        new MiniCssExtractPlugin({
            filename: "css/styles.css", 
        }),
    ],
    output: {
        filename: "js/main.js", // <- css파일과 js 파일을 분리하기 위해 경로 수정했습니다.
        path: path.resolve(__dirname, "assets"),
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                use: {
                    loader: "babel-loader",
                    options: {
                        presets: [
                            ["@babel/preset-env", { targets: "defaults" }],
                        ],
                    },
                },
            },
            {
                test: /\.scss$/,
                use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
            },
        ],
    },
};

위 코드를 작성하고, 동작시키면 아래와 같이 assets 폴더에 css와 js가 따로 분리되어 저장된 것을 확인할 수 있습니다.


아래 코드는 webpack 설정과는 관련 없으며 제 프로젝트에 결과물을 적용시키기 위함입니다.

추출한 css를 pug에 사용하려면, 다음 코드를 작성합니다.

link(rel="stylesheet", href="/static/css/styles.css")

추가로, scss나 프론트엔드 쪽 자바스크립트에서 무언가를 변경할 때마다 기존 파일들을 삭제하고 npm run assets를 실행하는 건 번거로우니, watch와 clean이라는 옵션을 적용합니다.

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const path = require("path");

module.exports = {
    entry: "./src/client/js/main.js",
    mode: "development",
    watch: true, // <- 파일 수정이 감지되면 수정한 코드 자동으로 적용
    plugins: [
        new MiniCssExtractPlugin({
            filename: "css/styles.css",
        }),
    ],
    output: {
        filename: "js/main.js",
        path: path.resolve(__dirname, "assets"),
        clean: true, // <- 적용하기 전에 output 폴더 초기화
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                use: {
                    loader: "babel-loader",
                    options: {
                        presets: [
                            ["@babel/preset-env", { targets: "defaults" }],
                        ],
                    },
                },
            },
            {
                test: /\.scss$/,
                use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
            },
        ],
    },
};

해당 옵션을 적용하면 npm run assets 명령어가 자동으로 종료되지 않으며, scss나 자바스크립트 코드를 수정하면 자동으로 적용시켜 줄 겁니다.

 

마지막으로 webpack.config.js 파일을 수정할 때마다 nodemon이 서버 구현에 필요한 파일이 수정된 줄 알고 계속 서버를 재시작시키니 nodemon에게 webpack.config.js 파일과 client 파일 그리고 assets 파일을 무시하도록 설정합니다.

nodemon.json 파일을 생성하고 아래와 같이 작성합니다.

{
    "ignore": ["webpack.config.js", "src/client/*", "assets/*"],
    "exec": "babel-node src/init.js"
}

nodemon.json 파일에 서버를 구동시키는 명령어를 작성했으니, package.json에 있는 서버 구동 script를 다음과 같이 그냥 nodemon으로 수정합니다.

"scripts": {
        "blabla": "nodemon", // <- 수정
        "assets": "webpack --config webpack.config.js"
    },

 

728x90