프론트엔드 개발환경: 웹팩

2021. 10. 9. 23:11JS

 회사에서 대뜸 프론트엔드 개발환경을 세팅하라는 오더가 내려왔다. 사수 없이 개발팀이 꾸려진 상태라 이 상황을 어떻게 해결하나 방황하던 찰나, 문득 인프런에서 웹팩 관련 강의를 찜 해놓은게 생각났다. 곧 바로 결제하여 강의를 들었고, 웹팩 뿐만 아니라, 바벨, es-lint 등 프론트엔드 개발환경 세팅의 필수 내용이지만 쉽게 간과했던 내용을 제대로 학습할 수 있었다.  1회 수강 만으로는 부족하여 2회째 수강중인데, 정리없는 학습은 무의미함을 알기에 해당 강의를 바탕으로 웹팩 기본 내용을 정리해본다. 

 

 

 

프론트엔드 개발환경의 이해와 실습 (webpack, babel, eslint..) - 인프런 | 강의

이미 만들어 놓은 개발 환경을 이해할 수 있어요. 처음부터 직접 개발 환경을 만들 수 있어요., 웹팩, 바벨, 린트 같은 프론트엔드 기술을 이해하고 실무에 적용해 보세요! [사진] “웹팩은 팀에

www.inflearn.com

본 포스팅은 김정환 강사님의 [프론트엔드 개발환경의 이해와 실습] 웹팩 편을 수강 후 작성함을 밝힙니다.

 

1. 웹팩이란?

리액트나 자바스크립트 프로젝트를 사용한다고 생각해보자.

exports 키워드로 하나의 컴포넌트를 모듈화하여, 다른 파일에서 해당 파일을 사용하는 것을 혼히 볼 수 있다.

이때 export 키워드는 해당 함수 혹은 파일을 모듈화 하는 것으로 이해할 수 있다. 모듈화 된 대상은 다른 파일에서 사용될 때 import 키워드로 불러들여진다.

 

// math.js
exports function sum(a, b){ return a + b; }


// app.js
import * as math from "./math.js";
math.sum(1, 2); // 3

app.js 파일은 math.js 에서 선언된 sum 함수를 모듈화하여 가져온다. 만약 math.js 이외의 파일도사용한다면, 수많은 파일과 함수를 모듈화하여 가져오게 된다. 이를 모듈 시스템이라 한다.

 

이 때 app.js 는 math.js 에 의존성을 지닌다. 자바스크립트 프로젝트는 여러 의존관계를 지니며 개발되는데, 의존관계를 지닌 여러 파일들을 하나로 합쳐주는 역할을 바로 웹팩이 맡는다. 이렇게 하나의 합쳐진 파일을 bundle, 이를 하나로 묶어주는 웹팩은 bundler 라고 부른다.

 

그렇다면 웹팩은 왜 의존관계의 여러 파일을 하나로 합치는 것일까?

 

 

2. 엔트리/ 아웃풋

웹팩은 여러개의 파일을 하나의 파일로 합치는 번들러 역할을 맡는다. 

 

여러 파일을 하나로 묶는데에는 크게 2가지 이유가 있는데,

1) 여러 모듈이 의존관계를 갖으며,

2) 합치지 않으면 http 요청이 비효율적이기 때문이다.

 

위 예시에서 app.js 는 math.js 에 의존성을 갖듯이, 프로젝트가 커지면 다양하고 거대한 의존관계가 형성된다. 만약 규모가 큰 프로젝트에서 모든 개별 파일에 http 요청을 보내야하는 상황이 발생하면 통신의 비효율을 초래할 수 있다.

 

그렇기에 개별 파일에 요청을 보내기보다, 하나의 파일을 합쳐 한번의 http 커넥션을 보내면 이전보다 효율적으로 통신을 할 수 있다. 물론 여러 파일을 하나로 압축하는 효과도 기대할 수 있다.

 

웹팩은 결국 여러 모듈을 하나로 합쳐주는 역할로 이해할 수 있다. 

 

그렇다면 어떤 원리와 형태로 합치는 것일까?

우선 웹팩은 entry 와 output 이란 개념이 있다.

엔트리는 모듈시스템의 시작점을 뜻하는데, 의존관계의 첫번째 지점을 entry point 로 잡는다. 리액트 프로젝트를 떠올리면 app.js 나 main.js 에 해당된다. 아래 이미지에서 첫번째로 시작하는 .js 파일을 시작으로 화살표를 따라 여러 의존관계가 형성된다. 웹팩은 이 모든 의존관계를 하나로 아울러 하나의 결과물(bundle)을 만들어낸다. 이 결과물을 저장할 경로를 웹팩에선 output 이라 부른다. 

 

webpack 설치

$ npm init -y
$ npm install -D webpack webpack-cli

 

웹팩 패키지 설치 후, webpack -help 옵션으로 확인하면 세개의 필수값이 나온다(—mode, —entry, —output).

 

1. —mode: 웹팩의 실행 모드를 의미하며, 흔히 개발 / 운영 으로 나뉜다.

2. -entry: 모듈의 시작지점으로, 흔히 main.js 를 사용한다.

3. -output: 웹팩의 결과(번들)를 저장하는 경로 (웹팩은 여러개의 모듈을 하나의 파일로 모아주는 역할이기 때문)

 

터미널에 웹팩 실행 명령어와 mode, entry, output 옵션을 함께 적어 실행한다.

$ node_modules/.bin/webpack --mode development --entry ./src/app.js --output-path dist/main.js
// —output 은 구버전, --output-path가 업뎃

 

하지만 웹팩 옵션을 매번 CLI에 치는 일은 번거롭기 짝이없다. 

때문에 webpack.config.js 라는 웹팩 기본 설정 파일을 만들어 사용한다.

 

// webpack.config.js

const path = require('path')
module.exports = {
    mode: 'development',
    entry: {
        main: './src/app.js'
    },
    output: {
        filename: '[name].js',
        path: path.resolve('./dist')
    }
}

webpack.config.js 파일에 mode, entry, output 에 대한 정보를 작성했으니, 이제 이 파일을 읽어들여 번들링 작업을 수행한다.

package.json 의 스크립트 build 명령어에 webpack 이라고만 작성하면, 자동으로 웹팩 기본 설정파일인 webpack.config.js 파일을 읽어들인 후 실행한다.

이제 npm run build 명령어로 전보다 간단하게 webpack 작업을 지시할 수 있다.

 

entry 포인트가 꼭 하나일 필요는 없다. 여러개의 entry 를 가지는 경우라면, 아웃풋도 여러개 생성된다.

그럴 경우를 대비하여 [name].js 로 filename 을 사용한다.

 

// webpack.config.js

const path = require('path')
module.exports = {
    mode: 'development',
    // 여러개의 entry 를 갖는 경우
    entry: {
        main: './src/app.js',
        main2: './src/app2.js',
    },
    output: {
        filename: '[name].js', // 1) main.js, 2) main2.js
        path: path.resolve('./dist')
    }
}

 

 

3. Loader

- 로더는 함수 형태를 띄기 때문에, 읽어들일 파일을 인자로 받아 로더 함수 내부에서 처리한다.

 

node.js 프로젝트에는 자바스크립트 외에도 이미지와 css, font 등 여러 형식의 파일이 사용된다. 자바스크립트 이외의 형식을 자바스크립트 파일에 직접 로딩하기 위해선, 각각 형식에 맞는 'Loader' 가 필요하다.

 

로더가 필요한 이유는 웹팩이 모든 파일을 개별 모듈로 바라보기 때문이다. 개별 모듈은 import 구문을 사용하여 자바스크립트 파일 내부로 가져올 수 있고, 이 때 모듈간의 의존관계가 형성된다.

물론 import 를 통해 의존관계가 형성된다고 하여, 자바스크립트 형식이 아닌 모듈을 곧 바로 사용할 수 있는 것은 아니다. 해당 모듈을 직접 자바스크립트 파일에 로딩할 수 있는 로더의 도움이 필요하다. 

 

자주 사용되는 로더를 통해 로더의 동작원리와 사용을 알아보자.

 

3 - 1. css-loader

css-loader 는 css(스타일시트)도 import 구문으로 불러들인 css(스타일시트) 파일을 모듈로 변환하는 작업을 수행한다. 이로써 자바스크립트 파일에서 css 파일을 사용할 수 있게된다.

 

웹팩의 관점에선 번들링 과정 중 css file 을 만나면, css-loader 함수를 실행시키는 것이다.

 

css-loader 역시 npm 이나 yarn 패키지매니저를 통해 설치한다.

$ npm install -D css-loader

설치 후, webpack.config.js 파일에 로더를 추가한다.

로더는 module 이라는 이름으로 등록한다.

 

 

이렇게 css-loader 를 이용하면, css 파일의 코드가 아래처럼 자바스크립트 파일(dist/main.js) 내로 컴파일되어 입력되는 것을 확인할 수 있다.

출처: 글 하단 명시

 

3 - 2. style-loader

html코드가 dom 형태로 변해야 브라우저에서 확인할 수 있듯, css file 역시 cssDom 형태로 변환되어야 브라우저에도서 css 가 적용된다. cssDom 형태로 변환하기 위해선 css-loader 뿐만 아니라 style-loader 역시 병행돼야 한다. 바로 style-loader 를 설치하자.

$ npm install -D style-loader

이제 webpack.config.js 에 ['style-loader', 'css-loader'] 순서로 추가해준다. 배열객체의 뒤에서부터 loader 를 읽어들이기 때문에 css-loader 를 뒤에 배치하고, style-loader 를 그 전에 놓는다.

 

3 - 3. file-loader

stylesheet 뿐만 아니라 파일 역시 모듈로 변환하여 사용할 수 있다. file-loader 는 여러 형태의 파일을 로드하여 자바스크립트 파일에서 사용할 수 있게 도와준다.

우리가 html에 흔히 파일을 올릴 경우는 image 파일을 로드할 때이다.

image 파일은 모두 알다시피 css stylesheet 를 통해 간단히 로드할 수 있다.

body {
  background-image: url(bg.png);
}

웹팩은 해당 css 파일을 읽으면서 url() 함수 내부의 bg.png 를 사용하는데, 이때 파일을 로드할 수 있는 file-loader 를 동작시킨다.

이렇게 확장자에 따라 동작시킬 로더를 설정할 수 있는데, .png 나 .jpg 확장자를 만나면 file-loader 를 동작시키도록 webpack.config.js 파일에서 설정할 수 있다.

 

options : {publicPath: ".."}

options 을 통해 publicPath 도 필수적으로 설정해야한다.

웹팩으로 빌드한 파일은 웹팩 설정의 output 인 dist 폴더로 이동하기 때문이다.

publicPath 옵션을 통해 파일로더가 처리한 파일을 찾을 수 있도록 prefix 설정한 것이라 생각하자.

 

name : "[name].[ext]?[hash]"

[파일명].[확장자]?[해시무력화] => 쿼리스트링이 매번 바뀌는 해시값이기 때문에, 호출할때마다 해시값이 매번 달라져 캐시관련 문제를 해결할 수 있다.

 

 

 

3 - 4.  url-loader

url-loader 는 여러 파일을 로드할 때 file-loader 를 대체하여 사용할 수 있다.

물론 file-loader 를 사용해도 여러 파일을 로드하는데 문제는 없다. 

 

그렇다면 url-loader 는 언제 사용하는 걸까?

만약 하나의 페이지에서 작은 크기의 여러가지 이미지 파일을 사용한다면,  크기가 작은 이미지를 Base64 문자열로 인코딩하여 문자열 형태로 소스코드에 넣는 형식이다. 이러한 방법을 data-uri-scheme 이라 한다.

 

그렇다면 문자열 형태로 소스코드에 넣으면 뭐가 달라지는가?

바로 추가적인 http 요청이 일어나지 않는다. 하나의 페이지에서 크기가 작은 여러 이미지를 사용하려면, 이미지 갯수마다 http request 를 보내야한다. 이미지 갯수에 따라 http request 가 비례하여 늘어나고, 사용빈도가 높은 이미지의 경우 자주 요청을 해야하기 때문에 리소스 낭비를 초래할 수 있다. 

여러번의 요청을 보내는 대신 내부 데이터로 이미지를 포함시킬 수 있어 한번의 Http 요청으로 작업할 수 있는 이점이 있다.

 

module.exports = {
  
  ...,
  
  module :{
    rules:[
      
      ... ,
      
      {
        test:/\.(png|jpg)$/,
        loader: "url-loader",  // file-loader 대신, url-loader 로 대체
        options:{
          name:"[name].[ext]?[hash]",
          publicPath: "./dist",
          
          // 로드해야할 파일 중 10kb 이하는 url-loader 가 처리한다.
          limit: 10000, 
        }
      }
    ],
  }
}

limit 옵션을 추가하여 파일 크기에 따라 url-loader 를 동작시킨다.

limit 값보다 큰 용량의 파일은 default 로 file-loader 가 처리하게 된다.

 

limit 용량보다 작은 저용량 파일들은 문자열로 변환되어 main.js 내부에 base64 형태로 들어온다.

 

 

4. plugin

플러그인이란?

로더가 각 파일 단위로 처리하는 반면, 플로그인은 번들된 결과물을 처리한다. 아래 그림처럼 .js 라는 하나의 번들(결과물)에 배너를 추가하거나 특정 작업을 수행한다.

웹팩이 만든 결과물은 output 경로인 dist/main.js 에 나타나는데, 플러그인은 이 dist/main.js 를 대상으로 동작한다고 볼 수 있다. 꼭 main.js 만을 처리하는 것은 아니고 처리해야할 확장자 형식(ex. .css, .jpg, .png) 에 맞춰 동작한다.

 

4 - 1. BannerPlugin

main.js 같은 번들된 결과 파일에 배너 따위의 문자열을 추가할 수 있다. 웹팩이 기본으로 제공하기 때문에 따로 npm 을 통한 설치과정은 불필요하다. 보통 빌드 당시의 정보를 기입하며 빌드 날짜, 작성자, 커밋 아이디 등이 해당된다.

 

webpack.config.js

const webpack = require('webpack');

module.exports = {

  ... ,
  
  plugins: [
    new webpack.BannerPlugin({
      banner: `
      Build Date:  ${new Date().toLocaleString()} \n
       Developer : ${childProcess.execSync("git config user.name")}\n
        Commit Version: ${childProcess.execSync("git rev-parse --short HEAD")}
        `
    })
  ]

 

/dist/main.js

아래처럼 빌드될 때 배너 정보가 생성된다.

4 - 2. DefinePlugin

DefinePlugin은 환경정보를 제공하기 위해 사용된다. 여기서 말하는 환경정보란 개발환경이나, 운영환경이냐를 의미한다. 역시 배너플러그인처럼 기본 제공 플러그인이다.

물론 환경정보 이외의 값도 사용할 수 있다. 해당 프로젝트의 버전이나 domain 주소 등 빌드 타임에 결정될 값은 어플리케이션에 전달할 때 사용된다.

 

 

4 - 3. htmlTemplatePlugin

이름에서 추론할 수 있듯 html 과 관련된 작업을 수행하며, HTML 파일을 후처리하는데 사용한다. 해당 플러그인은 코드를 먼저 보며 이해해보자. 

$ npm install -D html-webpack-plugin
<!-- src/index.html -->

<!DOCTYPE html>
<html>
  <head>
    <title>타이틀<%= env %></title>
  </head>
  <body>
    <!-- 로딩 스크립트 제거 -->
    <!-- <script src="dist/main.js"></script> -->
  </body>
</html>

타이틀 태그에 ejs 문법을 사용하여 env 라는 변수를 전달한다. 이 env 라는 변수는 htmlWebpackPlugin 에서 설정하여 원하는 값을 출력하는데, 보통 빌드 당시의 개발환경을 동적으로 주입하여 명시한다.

 

env 변수를 동적으로 결정하는 과정을 살펴보자. 

// webpack.config.js

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

module.exports {
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html', //사용할 html 템플릿 경로를 지정
      templateParameters: { // 해당 템플릿에 주입할 변수 지정
        env: process.env.NODE_ENV === 'development' ? '(개발용)' : '',
      },
       minify: process.env.NODE_ENV === 'production' ? {
          collapseWhitespace: true, // 빈칸 제거
          removeComments: true, // 주석 제거
          } : false,
      })
  ]
}

1) 우선 객체 내부에 templateParameters 를 사용하면 노드 환경정보를 주입할 수 있다. 노드환경정보인 process.env.NODE_ENV 는 디폴트로 설정되는 값이다. 빌드 당시의 환경에 따라 NODE_ENV 값은 development 혹은 production 으로 변경될 수 있다. 

 

2) minify 를 사용하면, output 결과물을 한줄로 만들거나 불필요한 주석을 제거하여 파일을 압축하는 효과를 낼 수 있다.

 

4 - 4. CleanWebpackPlugin

빌드할 때마다 dist folder 를 싹 날리고, 새로운 dist 폴더를 생성한다. 헤당 플러그인을 사용하면 일일이 dist 폴더를 삭제할 필요없이, 새로운 빌드마다 새로운 dist 폴더를 갱신할 수 있다.

 

4 - 5. MiniCssExtractPlugin

이것 역시 thirdparty, output 으로 css 를 뽑아낸다.

배포 환경에서, 프로젝트 내부의 css 스타일시트가 너무 많다면, 하나의 자바스크립트 결과물로 뽑아내는 일이 용량적으로 부담스럽고 무엇보다 느릴 수 있다. 번들 결과에서 스타일시트 파일만 별도로 뽑아낸다면 속도 측면에서 좋은 성능을 낼 수 있다. 하나의 대용량 파일을 내려받는 것 보다, 여러개의 작은 파일을 동시에 다운로드 하는 것이 더 빠르기 때문이다.

// webpack.config.js

module.exports = {

  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          process.env.NODE_ENV === "production"
            ? MiniCssExtractPlugin.loader // 프로덕션 환경
            : "style-loader", // 개발 환경
          "css-loader",
        ],
      },
    ],
  },
  
  plugins: [
    ...(process.env.NODE_ENV === "production"
      ? [new MiniCssExtractPlugin({ filename: `[name].css` })]
      : []),
  ],
}

NODE_ENV = production 일 때, css 파일이 별도로 추출된 것을 확인할 수 있다. 

plugins 에서는 운영 환경일 때, Css를 추출하는 플러그인을 사용하도록 조건문을 걸어준다.

loader 에서는 운영환경일 때 style-loader 대신 miniCssExtractPlugin 의 로더를 사용하도록 조건문을 걸어준다.

이렇게 하여 개발 환경에 따라 동적으로 플러그인과 로더를 사용할 수 있게 된다.

 

 


웹팩 편이 끝나고 회사 프로젝트 세팅을 시도했다. 확실히 보이지 않던 내용이 눈에 들어오고, 이건 무엇이고, 저건 무엇인지 감이 잡혔다. 그렇다고 끝장나게 개발환경 세팅을 마칠 수 있는 것도 아니었다. 더 공부해야할 내용도 많고, 강의 내용만으로는 커버할 수 없는 부분도 존재하여 공식문서를 더 찾아봐야할 것 같다. 부디 환경세팅의 굴레를 잘 벗어날 수 있길..!