Webpack의 시대는 갔다 ?
지난 2월 14일, Create-React-App은 더 이상 사용할 수 없도록 지원이 중단되었습니다 (관련링크)
React는 공식적으로 새로운 리액트 앱 프로젝트를 생성할 때 Next.js 혹은 Remix 등의 프레임워크 툴을 활용해 만드는 것을 추천하고있습니다.
그렇다면, 프레임워크에 의존하지 않고 리액트 프로젝트를 만들 순 없을까요 ?
이에 대해서도 리액트는 대안책을 제시했습니다. (링크)
그러나 슬프게도 리액트가 제시하는 빌드 툴에는 webpack이 존재하지 않습니다.
그렇다면, 현재 대부분의 개발자들이 webpack을 사용하지 않는 것일까요 ?
절대 아닙니다.
webpack은 무려 13년 전부터 사용된 번들러로, 2025년 현재까지도 높은 점유율을 차지하고 있습니다.
즉, 여전히 많은 서비스들이 webpack을 번들러로 사용하고 있으며, 비록 Vite나 esbuild와 같은 최신 번들러들의 점유율이 증가하고 있지만, webpack은 여전히 널리 활용되고 있습니다.
이러한 점을 고려할 때, 우리는 webpack을 단순히 “구식”이라는 이유로 배제할 수 없습니다. 특히, 대규모 엔터프라이즈 프로젝트나 복잡한 빌드 설정이 필요한 환경에서는 여전히 webpack이 강력한 선택지로 남아 있습니다.
webpack은 플러그인 및 로더 시스템의 확장성, 정교한 트리 쉐이킹 및 코드 스플리팅 기능, 그리고 다양한 커뮤니티 지원 덕분에 유지보수 및 확장이 필요한 프로젝트에서 안정적인 역할을 수행하고 있습니다.
즉, 더이상 Create-React-App을 활용해 리액트 프로젝트를 만들지 못하더라도 우리는 웹팩을 다룰 수 있어야 한다 !
zero부터 webpack 리액트 프로젝트를 만들어보자 !
1. npm init
npm init 치고 귀찮으니까 다 일단 넘깁니다.
그럼 이렇게 package.json이 생겨요
package.json에서는 리액트 개발에 필요한 모든 패키지들을 넣어주면 됩니다.
우리는 리액트를 쓸거니까 react, react-dom을 설치해보겠습니다.
그러면 기존에는 없었던 node_modules가 생기고, package-lock.json이 생기게 됩니다.
그리고 package.json에 dependecies 객체에 우리가 위에서 추가했던 react와 react-dom이 설치된 것을 볼 수 있습니다.
2. 웹팩 설치
웹팩은 개발 단계에서만 필요한 패키지 입니다. 따라서 -D 를 써서 devDependencies에 설치를 해줍니다.
npm i webpack webpack-cli -D
그럼 package.json에서도 확인 가능합니다.
오 이러면 깃 변경사항에 3천개 이상의 파일이 쌓이게 됩니다.
gitignore을 설정해줍시다 !
3. 필요한 파일들 생성
웹 페이지가 만들어지고, 웹팩이 잘 돌아가게 하기 위한 파일들을 만들겠습니다.
index.html
index.html 파일을 만들어줍니다. <body> 태그 안에 id를 “root”로 갖는 div 태그를 하나 넣어주고, <script> 태그 안엔 “./dist/app.js”를 넣어줍니다.
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CRA 없이 웹팩 커스터마이징</title>
</head>
<body>
<div id="root"></div>
<script src="./dist/app.js"></script>
</body>
</html>
NoCRA.jsx
function NoCRA() {
return <div>안녕하세요 CRA 없이 웹팩을 커스텀 중입니다.</div>;
}
export default NoCRA;
client.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import NoCRA from "./NoCRA";
ReactDOM.createRoot(document.querySelector("#root")).render(<NoCRA />);
webpack.config.js
const path = require("path");
module.exports = {
name: "NoCRA",
mode: "development",
devtool: "eval",
resolve: {
extensions: [".js", ".jsx"],
},
entry: {
app: ["./client"],
}, //입력
output: {
path: path.join(__dirname, "dist"), // 실제 로컬 경로는 user/hyorinlee/github/.../dist 이겠지만, 현재 파일이 위치한 폴더(__dirname)를 기준으로 "dist" 폴더의 절대 경로를 생성
filename: "app.js",
}, //출력
};
4. 실행을 시켜봅시다
npx webpack
위 명령어를 써서 웹팩을 실행시켜보면
dist/app.js 에는 다음과 같은 코드가 생성될 것입니다.
그리고 콘솔에는 다음과 같은 에러가 뜰 것입니다.
어디서 에러가 났는지 봐볼까요?
> ReactDom.render(<NoCRA />, document.querySelector("root"));
위 코드에서 NoCRA 라는 파일의 타입을 다룰 수 없다는 에러가 났습니다.
NoCRA는 jsx 문법이기 때문에, 웹팩이 이 문법을 이해하지 못해 발생한 에러였습니다.
이를 위해서 babel이 필요합니다.
5. babel 설치
JSX 문법을 인식하기 위해서 babel을 설치하도록 하겠습니다.
npm i @babel/core @babel/preset-env @babel/preset-react babel-loader -D
babel이 무엇인가요 ?
babel은 자바스크립트 컴파일러로, ES2015+의 자바스크립트 문법이 구형 버전의 브라우저 환경에서도 잘 호환될 수 있도록 해당 브라우저에 알맞는 자바스크립트 문법으로 변환해주는 컴파일러입니다.
babel/core
babel의 핵심 라이브러리입니다. babel은 JS 코드를 변환해주는 도구인데, babel/core에서 소스 코드를 변환시키는 엔진 역할을 하게 됩니다.
babel/preset-env
예를 들어 대표적인 ES2015 문법인 화살표 함수가 있는데, 이는 ES2015 문법을 지원하지 않는 브라우저에서는 제대로 동작하지 않을 것입니다.
그러나, babel을 사용하면 화살표 함수를, 일반 함수 선언문으로 변경해주게 됩니다.
// Babel Input: ES2015 arrow function
[1, 2, 3].map(n => n + 1);
// Babel Output: ES5 equivalent
[1, 2, 3].map(function(n) {
return n + 1;
});
babel/preset-react
babel은 JSX 문법 또한 변경해줍니다. React preset 을 사용하면 됩니다.
예를 들어, 다음과 같은 JSX 문법이 있다면
const profile = (
<div>
<img src="avatar.png" className="profile" />
<h3>{[user.firstName, user.lastName].join(" ")}</h3>
</div>
);
babel/plugin-transform-react-jsx-development 가 JSX 문법을 JS로 변경해줍니다.
참고로 preset은 plugin들의 묶음이라 생각하시면 됩니다. 따라서 위 플러그인도 preset-react에 포함되어있는 것이죠.
import { jsx as _jsx } from "react/jsx-runtime";
import { jsxs as _jsxs } from "react/jsx-runtime";
const profile = _jsxs("div", {
children: [
_jsx("img", {
src: "avatar.png",
className: "profile",
}),
_jsx("h3", {
children: [user.firstName, user.lastName].join(" "),
}),
],
});
babel-loader
babel을 사용해서 ES2015의 문법을 ES5 문법으로 트랜스파일 할 수 있도록 중간다리 역할을 해주는 로더라고 생각하시면 됩니다.
loader와 관련해서는 아래에 더 자세히 써두도록 하겠습니다.
6. babel 설정 추가
webpack.config.js에 바벨과 관련된 설정(module)을 추가해주면 됩니다.
저는 entry와 output 사이에 module 설정값을 넣어주었습니다.
entry로 입력을 받고, module로 babel 설정을 하고, 그 다음 결과물인 output이 나오는 흐름이죠.
const path = require("path");
module.exports = {
name: "NoCRA",
mode: "development",
devtool: "eval",
resolve: {
extensions: [".js", ".jsx"],
},
entry: {... 중략},
module: {
rules: [
{
test: /\.jsx?/,
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
},
},
],
},
output: {... 중략},
};
7. 다시 실행
npx webpack
실행 결과를 보시면, 이전과 다르게 웹팩이 JSX 문법을 이해하고, NoCRA 라는 컴포넌트를 성공적으로 컴파일 한 것을 확인할 수 있습니다.
그리고 dist/app.js 를 보면, 이 사진 아래로 어마어마하게 긴 코드들이 있습니다.
저희가 작성한 JSX 코드들이 잘 변환된 것을 확인할 수 있습니다.
8. 서버 실행
이제 번들링된 파일들을 localhost:3000에 띄워보도록 하겠습니다.
이를 위해선 몇 가지 작업이 필요합니다.
webpack-dev-server 설치
npm i webpack-dev-server -D
webpack.config.js에 설정 추가
devServer 설정을 추가해 어디에 있는 파일을 실행시킬 것인지, 그리고 포트 번호 및 자동 브라우저 열기 설정을 추가해보겠습니다.
const path = require("path");
module.exports = {
/** 중략 */
devServer: {
static: path.join(__dirname, "dist"), // 빌드된 파일들이 위치할 폴더
port: 3000, // 서버가 실행될 포트 번호
open: true, // 서버 시작 시 자동으로 브라우저 열기
},
};
진짜 실행
아래 두 명령어를 입력해서 지금까지 했던 작업물들을 다시 한 번 번들링하고, 서버를 실행시켜보겠습니다.
npx webpack // 번들링
npx webpack-dev-server // 서버 실행
결과
그럼 아래와 같이 우리가 NoCRA 컴포넌트에서 작성했던 문구가 잘 실행되는 것을 알 수 있습니다.
9. Target Browser 설정
앞서 babel/preset-env 는 최신 JS 문법을 구형 브라우저에서도 동작할 수 있도록 변환시켜주는 역할을 한다고 배웠습니다.
그럼 정확히 어떤 브라우저까지 지원하는지 알아볼까요 ?
targets 옵션 추가
babel/preset-env 에는 targets 옵션이 있습니다. 이를 이용해서 어떤 브라우저까지 지원할 지 정할 수 있습니다.
정확한 옵션에 대해서는 링크를 참조해주세요 !
아래 보시면 특정 나라에서의 n%의 점유율을 가진 브라우저, 혹은 특정 브라우저의 n 버전까지 ~~ 등등 다양한 선택지가 있습니다.
저는 우리나라에서 점유율이 5%이상인 브라우저만을 대상으로 동작하도록 설정해보겠습니다.
module: {
rules: [
{
test: /\.jsx?/,
loader: "babel-loader",
options: {
presets: [
[
"@babel/preset-env",
{
targets: ">5% in KR",
debug: true,
},
],
"@babel/preset-react",
],
},
},
],
},
결과를 보면 chrome과 samsung 브라우저가 한국의 점유율 5% 이상인 것으로 알 수 있습니다.
그럼 이걸 1%로 낮춰볼까요 ?
module: {
rules: [
{
test: /\.jsx?/,
loader: "babel-loader",
options: {
presets: [
[
"@babel/preset-env",
{
targets: ">1% in KR",
debug: true,
},
],
"@babel/preset-react",
],
},
},
],
},
그럼 이번엔 ios, edge 브라우저까지 추가된 것을 볼 수 있습니다.
10. HMR 지원
HMR을 지원하기 위해서는 아래 라이브러리 설치가 필요합니다.
npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh
이후 webpack.config.js에 module의 플러그인 설정을 추가해주고
기본 플러그인 설정도 추가해줍니다.
그리고 devServer 설정에 hot을 true로 넣어주면 완료입니다 !
11. 자주 사용하는 loader 추가
이전에 babel 설정하며 babel/loader을 설치했었죠. babel/loader은 JavaScript 파일을 Babel과 Webpack이 트랜스파일링 가능하도록 해주는 로더였습니다.
이 외에도 다양한 loader가 있는데, 많이 사용하는 loader 몇 개를 소개해드리고 CSS-loader, style-loader을 실습해보겠습니다.
CSS-loader
CSS 파일을 자바스크립트가 불러와 사용하려면 CSS를 모듈로 변환시키는 작업이 필요합니다. 여기 CSS-loader가 그러한 역할을 해줍니다.
기존 코드에서 style.css 파일을 추가하고, NoCRA.jsx 파일에서 이를 불러와봅시다.
// style.css
div {
color: red;
}
body {
background-color: antiquewhite;
}
// NoCRA.jsx
import React from "react";
import ".style.css";
function NoCRA() {
return <div>안녕하세요 CRA 없이 웹팩을 커스텀 중입니다.</div>;
}
export default NoCRA;
그리고 컴파일을 돌리면 아래와 같이 .css 확장자 파일을 다룰 수 있는 loader가 없다는 에러 메시지를 확인할 수 있습니다.
이를 해결하기 위해 우선 css-loader 을 설치해보겠습니다.
npm install -D css-loader
웹팩에 css-loader 설정을 추가하고
다시 번들링을 해보면
npx webpack
dist/app.js에 다음과 같이 우리가 작성한 CSS 코드가 잘 들어간 것을 볼 수 있습니다.
그러나 아직 화면에는 반영이 되지 않은 것을 볼 수 있습니다.
그 이유는 CSS-loader은 CSS 코드를 자바스크립트로 변환시키는 역할만 하기 때문인데요.
모듈로 변경된 스타일 시트는 DOM에 추가되어야지만 브라우저가 해석할 수 있습니다.
따라서 자바스크립트로 변경된 스타일을 동적으로 DOM에 추가하는 역할을 하는 style-loader가 필요합니다.
style-loader
npm install -D style-loader
그리고 webpack 설정에 style-loader을 추가해줍니다. (기존 css-loader 속성 앞에 추가해주면 됩니다)
다시 번들링 하고 확인해보면
우리가 설정해둔 대로 스타일이 적용된 것을 알 수 있습니다.
결론
Webpack 커스터마이징을 통해 번들링 과정에 대해 깊이 이해할 수 있었습니다! 기존에는 Create React App(CRA)을 사용하며 JSX 문법이 어떻게 브라우저에서 동작하는지, CSS 파일들이 어떻게 처리되는지 깊게 고민해보지 않았었습니다.
그러나 직접 웹팩 설정을 구성해보면서 Babel과 로더와 플러그인의 역할 체계적으로 익힐 수 있었습니다.
다음 글은 Webpack으로 Vite 빌드 속도 따라잡기를 해보도록 하겠습니다 !
'⚓️ 개발환경' 카테고리의 다른 글
[⚓️Webpack] Webpack 커스터마이징을 통해 Vite 속도 따라잡기 (1) | 2025.03.12 |
---|---|
[개발환경] npm, pnpm, yarn let’s go (0) | 2024.09.02 |
[⚓️개발환경] 바벨이란 무엇인가? (2) | 2024.08.12 |
[⚓️개발환경] 웹팩이란 무엇인가 (6) | 2024.07.08 |
[⚓️개발환경] 프론트엔드 개발에 Node.js가 필요한 이유 (8) | 2024.06.25 |