Webpack 또는 Browserify & Gulp: 어느 것이 더 낫습니까?

게시 됨: 2022-03-11

웹 애플리케이션이 점점 더 복잡해짐에 따라 웹 애플리케이션을 확장 가능하게 만드는 것이 가장 중요해졌습니다. 과거에는 임시 JavaScript와 jQuery를 작성하는 것으로 충분했지만 오늘날에는 웹 앱을 구축하려면 다음과 같은 훨씬 더 높은 수준의 규율과 정식 소프트웨어 개발 관행이 필요합니다.

  • 코드 수정으로 인해 기존 기능이 손상되지 않는지 확인하기 위한 단위 테스트
  • 오류가 없는 일관된 코딩 스타일을 보장하는 Linting
  • 개발 빌드와 다른 프로덕션 빌드

웹은 또한 고유한 개발 과제 중 일부를 제공합니다. 예를 들어, 웹 페이지는 많은 비동기식 요청을 하기 때문에 수백 개의 JS 및 CSS 파일을 요청해야 하기 때문에 웹 앱의 성능이 크게 저하될 수 있습니다. 이 특정 문제는 종종 파일을 번들로 묶어 해결할 수 있으므로 수백 개의 개별 파일이 아닌 단일 번들 JS 및 CSS 파일만 요청합니다.

번들링 도구 트레이드오프: Webpack 대 Browserify

Webpack 또는 Browserify + Gulp 중 어떤 번들 도구를 사용해야 합니까? 다음은 선택 가이드입니다.
트위터

또한 기본 JS 및 CSS로 컴파일되는 SASS 및 JSX와 같은 언어 전처리기와 Babel과 같은 JS 변환기를 사용하여 ES5 호환성을 유지하면서 ES6 코드의 이점을 얻는 것도 매우 일반적입니다.

이는 웹 앱 자체의 논리를 작성하는 것과 관련이 없는 상당한 수의 작업에 해당합니다. 여기서 작업 실행기가 필요합니다. 작업 실행기의 목적은 이러한 모든 작업을 자동화하여 앱 작성에 집중하면서 향상된 개발 환경의 이점을 누릴 수 있도록 하는 것입니다. 작업 실행기가 구성되면 터미널에서 단일 명령을 호출하기만 하면 됩니다.

Gulp는 개발자에게 매우 친숙하고 배우기 쉬우며 쉽게 이해할 수 있기 때문에 작업 실행기로 사용할 것입니다.

Gulp에 대한 간략한 소개

Gulp의 API는 네 가지 기능으로 구성됩니다.

  • gulp.src
  • gulp.dest
  • gulp.task
  • gulp.watch

꿀꺽 꿀꺽 꿀꺽 꿀꺽 꿀꺽 꿀꺽 꿀꺽 꿀꺽 꿀꺽 꿀꺽 꿀꺽 꿀꺽 꿀꺽 꿀꺽 꿀꺽 마시다

예를 들어 다음은 이 네 가지 기능 중 세 가지를 사용하는 샘플 작업입니다.

 gulp.task('my-first-task', function() { gulp.src('/public/js/**/*.js') .pipe(concat()) .pipe(minify()) .pipe(gulp.dest('build')) });

my-first-task 가 수행되면 glob 패턴 /public/js/**/*.js 와 일치하는 모든 파일이 축소되어 build 폴더로 전송됩니다.

이것의 아름다움은 .pipe() 연결에 있습니다. 입력 파일 세트를 가져와 일련의 변환을 통해 파이프한 다음 출력 파일을 반환합니다. 작업을 더욱 편리하게 하기 위해 minify() 와 같은 실제 파이프 변환은 종종 NPM 라이브러리에서 수행됩니다. 결과적으로 파이프에서 파일 이름을 바꾸는 것 외에 고유한 변환을 작성해야 하는 경우는 실제로 매우 드뭅니다.

Gulp를 이해하기 위한 다음 단계는 작업 종속성의 배열을 이해하는 것입니다.

 gulp.task('my-second-task', ['lint', 'bundle'], function() { ... });

여기서 my-second-tasklintbundle 작업이 완료된 후에만 콜백 함수를 실행합니다. 이를 통해 관심사를 분리할 수 있습니다. LESSCSS 로 변환하는 것과 같이 단일 책임으로 일련의 작은 작업을 만들고 작업 종속성 배열을 통해 다른 모든 작업을 단순히 호출하는 일종의 마스터 작업을 만듭니다.

마지막으로 glob 파일 패턴에서 변경 사항을 감시하고 변경 사항이 감지되면 일련의 작업을 실행하는 gulp.watch 가 있습니다.

 gulp.task('my-third-task', function() { gulp.watch('/public/js/**/*.js', ['lint', 'reload']) })

위의 예에서 /public/js/**/*.js 와 일치하는 파일을 변경하면 lintreload 작업이 트리거됩니다. gulp.watch 의 일반적인 용도는 브라우저에서 실시간 다시 로드를 트리거하는 것입니다. 이 기능은 개발에 매우 ​​유용하여 한 번 경험하면 이 기능 없이는 살 수 없습니다.

그리고 마찬가지로 gulp 에 대해 알아야 할 모든 것을 이해하게 됩니다.

Webpack은 어디에 적합합니까?

웹팩 작동 방식

CommonJS 패턴을 사용할 때 JavaScript 파일을 묶는 것은 파일을 연결하는 것만큼 간단하지 않습니다. 오히려 파일 상단에 일련의 require 또는 import 문이 있는 진입점(일반적으로 index.js 또는 app.js 라고 함)이 있습니다.

ES5

 var Component1 = require('./components/Component1'); var Component2 = require('./components/Component2');

ES6

 import Component1 from './components/Component1'; import Component2 from './components/Component2';

종속성은 app.js 의 나머지 코드보다 먼저 해결되어야 하며 이러한 종속성 자체에는 해결해야 할 추가 종속성이 있을 수 있습니다. 또한 애플리케이션의 여러 위치에서 동일한 종속성이 require 수 있지만 해당 종속성을 한 번만 해결하려고 합니다. 상상할 수 있듯이, 종속성 트리가 몇 수준 깊이에 이르면 JavaScript를 번들로 묶는 프로세스가 다소 복잡해집니다. 이것은 Browserify 및 Webpack과 같은 번들러가 들어오는 곳입니다.

개발자가 Gulp 대신 Webpack을 사용하는 이유는 무엇입니까?

Webpack은 번들러인 반면 Gulp는 작업 실행기이므로 일반적으로 함께 사용되는 이 두 도구를 볼 수 있을 것입니다. 대신, 특히 React 커뮤니티에서 Gulp 대신 Webpack을 사용하는 추세가 증가하고 있습니다. 왜 이런거야?

간단히 말해서 Webpack은 작업 실행기를 통해 수행할 작업의 대부분을 이미 수행할 수 있는 강력한 도구입니다. 예를 들어 Webpack은 이미 번들에 대한 축소 및 소스맵 옵션을 제공합니다. 또한 Webpack은 webpack-dev-server server라는 사용자 지정 서버를 통해 미들웨어로 실행할 수 있습니다. 이 서버는 라이브 재로딩과 핫 재로딩을 모두 지원합니다(이 기능에 대해서는 나중에 설명하겠습니다). 로더를 사용하여 ES6을 ES5로 변환하고 CSS 전처리 및 후처리 프로세서를 추가할 수도 있습니다. 이는 실제로 단위 테스트와 린팅을 Webpack이 독립적으로 처리할 수 없는 주요 작업으로 남겨둡니다. 우리가 최소한 여섯 개의 잠재적인 꿀꺽 꿀꺽 꿀꺽꿀꺽 할 수 있는 작업을 2개로 줄였다는 점을 감안할 때 많은 개발자는 대신 NPM 스크립트를 직접 사용하기로 선택합니다. 이렇게 하면 프로젝트에 Gulp를 추가하는 오버헤드를 피할 수 있기 때문입니다(나중에 설명하겠습니다). .

Webpack 사용의 주요 단점은 구성하기가 다소 어렵다는 것입니다. 이는 프로젝트를 신속하게 시작하고 실행하려는 경우 매력적이지 않습니다.

3가지 태스크 러너 설정

세 가지 다른 작업 실행기 설정으로 프로젝트를 설정할 것입니다. 각 설정은 다음 작업을 수행합니다.

  • 감시된 파일 변경 사항에 대한 실시간 다시 로드로 개발 서버 설정
  • 감시된 파일 변경 사항에 대해 확장 가능한 방식으로 JS 및 CSS 파일(ES6에서 ES5로의 변환, SASS에서 CSS로의 변환 및 소스맵 포함)을 번들링합니다.
  • 독립 실행형 작업 또는 감시 모드에서 단위 테스트 실행
  • 독립 실행형 작업 또는 감시 모드에서 Linting 실행
  • 터미널에서 단일 명령을 통해 위의 모든 것을 실행할 수 있는 기능 제공
  • 축소 및 기타 최적화가 포함된 프로덕션 번들을 생성하기 위한 또 다른 명령이 있습니다.

세 가지 설정은 다음과 같습니다.

  • 꿀꺽 + 브라우저화
  • 꿀꺽꿀꺽 + 웹팩
  • Webpack + NPM 스크립트

애플리케이션은 프론트 엔드에 React를 사용합니다. 원래는 프레임워크에 구애받지 않는 접근 방식을 사용하고 싶었지만 HTML 파일이 하나만 필요하고 React가 CommonJS 패턴과 매우 잘 작동하기 때문에 React를 사용하면 실제로 작업 실행자의 책임이 간소화됩니다.

프로젝트 요구 사항에 가장 적합한 설정 유형에 대해 정보에 입각한 결정을 내릴 수 있도록 각 설정의 장점과 단점을 다룰 것입니다.

각 접근 방식(링크)에 대해 하나씩 세 가지 분기가 있는 Git 리포지토리를 설정했습니다. 각 설정을 테스트하는 것은 다음과 같이 간단합니다.

 git checkout <branch name> npm prune (optional) npm install gulp (or npm start, depending on the setup)

각 브랜치의 코드를 자세히 살펴보겠습니다...

공통 코드

폴더 구조

 - app - components - fonts - styles - index.html - index.js - index.test.js - routes.js

index.html

간단한 HTML 파일입니다. React 애플리케이션은 <div></div> 에 로드되며 번들된 단일 JS 및 CSS 파일만 사용합니다. 사실, Webpack 개발 설정에서는 bundle.css 도 필요하지 않습니다.

index.js

이것은 우리 앱의 JS 진입점 역할을 합니다. 본질적으로 우리는 앞서 언급한 id app 이 있는 div 에 React Router를 로드할 뿐입니다.

route.js

이 파일은 경로를 정의합니다. URL / , /about/contact 는 각각 HomePage , AboutPageContactPage 구성 요소에 매핑됩니다.

index.test.js

이것은 네이티브 JavaScript 동작을 테스트하는 일련의 단위 테스트입니다. 실제 프로덕션 품질 앱에서는 React 구성 요소(적어도 상태를 조작하는 구성 요소)별로 단위 테스트를 작성하여 React 관련 동작을 테스트합니다. 그러나 이 게시물의 목적을 위해서는 watch 모드에서 실행할 수 있는 기능적 단위 테스트만 있으면 충분합니다.

구성 요소/App.js

이것은 모든 페이지 보기에 대한 컨테이너로 생각할 수 있습니다. 각 페이지에는 페이지 보기 자체로 평가되는 this.props.children 뿐만 아니라 <Header/> 구성 요소가 포함되어 있습니다(예: 브라우저의 /contact 에 있는 경우 ContactPage ).

구성 요소/홈/HomePage.js

저희집 전경입니다. 부트스트랩의 그리드 시스템이 반응형 페이지를 만드는 데 탁월하기 때문에 react-bootstrap 을 사용하기로 결정했습니다. 부트스트랩을 적절히 사용하면 더 작은 뷰포트에 대해 작성해야 하는 미디어 쿼리 수가 크게 줄어듭니다.

나머지 구성 요소( Header , AboutPage , ContactPage )는 유사하게 구조화됩니다( react-bootstrap 마크업, 상태 조작 없음).

이제 스타일링에 대해 자세히 알아보겠습니다.

CSS 스타일링 접근법

내가 선호하는 React 구성 요소 스타일 지정 방식은 구성 요소당 하나의 스타일시트를 사용하는 것입니다. 이 스타일시트의 스타일 범위는 특정 구성 요소에만 적용됩니다. 내 각 React 구성 요소에서 최상위 div 에는 구성 요소 이름과 일치하는 클래스 이름이 있습니다. 예를 들어 HomePage.js 에는 다음으로 래핑된 마크업이 있습니다.

 <div className="HomePage"> ... </div>

다음과 같이 구성된 연결된 HomePage.scss 파일도 있습니다.

 @import '../../styles/variables'; .HomePage { // Content here }

이 접근 방식이 왜 그렇게 유용한가요? 이는 고도로 모듈화된 CSS를 생성하여 원치 않는 계단식 동작 문제를 크게 제거합니다.

두 개의 React 구성 요소인 Component1Component2 가 있다고 가정합니다. 두 경우 모두 h2 글꼴 크기를 재정의하려고 합니다.

 /* Component1.scss */ .Component1 { h2 { font-size: 30px; } } /* Component2.scss */ .Component2 { h2 { font-size: 60px; } }

Component1Component2h2 글꼴 크기는 구성 요소가 인접하거나 한 구성 요소가 다른 구성 요소 안에 중첩되어 있는지 여부에 관계없이 독립적입니다. 이상적으로 이것은 구성 요소의 스타일이 완전히 독립적임을 의미합니다. 즉, 구성 요소가 마크업의 어디에 배치되든 정확히 동일하게 보일 것입니다. 실제로, 항상 그렇게 간단하지는 않지만, 확실히 올바른 방향으로 가는 큰 단계입니다.

구성 요소별 스타일 외에도 특정 책임을 처리하는 SASS 부분(이 경우 글꼴 및 변수에 대해 각각 _fonts.scss_variables.scss )과 함께 전역 스타일시트 global.scss 를 포함하는 styles 폴더를 갖고 싶습니다. ). 전역 스타일시트를 사용하면 전체 앱의 일반적인 모양과 느낌을 정의할 수 있으며, 필요에 따라 구성요소별 스타일시트에서 도우미 부분을 가져올 수 있습니다.

이제 각 분기의 공통 코드를 자세히 살펴보았으므로 첫 번째 태스크 러너/번들러 접근 방식으로 초점을 이동해 보겠습니다.

Gulp + Browserify 설정

gulpfile.js

이것은 22개의 가져오기와 150줄의 코드가 포함된 놀라울 정도로 큰 gulpfile로 나옵니다. 따라서 간결함을 위해 js , css , server , watchdefault 작업만 자세히 검토하겠습니다.

JS 번들

 // Browserify specific configuration const b = browserify({ entries: [config.paths.entry], debug: true, plugin: PROD ? [] : [hmr, watchify], cache: {}, packageCache: {} }) .transform('babelify'); b.on('update', bundle); b.on('log', gutil.log); (...) gulp.task('js', bundle); (...) // Bundles our JS using Browserify. Sourcemaps are used in development, while minification is used in production. function bundle() { return b.bundle() .on('error', gutil.log.bind(gutil, 'Browserify Error')) .pipe(source('bundle.js')) .pipe(buffer()) .pipe(cond(PROD, minifyJS())) .pipe(cond(!PROD, sourcemaps.init({loadMaps: true}))) .pipe(cond(!PROD, sourcemaps.write())) .pipe(gulp.dest(config.paths.baseDir)); }

이 접근 방식은 여러 가지 이유로 보기에 좋지 않습니다. 우선 작업이 세 부분으로 나뉩니다. 먼저 Browserify 번들 객체 b 를 생성하고 일부 옵션을 전달하고 일부 이벤트 핸들러를 정의합니다. 그러면 Gulp 작업 자체가 있습니다. 이 작업은 이름이 지정된 함수를 인라인하는 대신 콜백으로 전달해야 합니다( b.on('update') 는 동일한 콜백을 사용하기 때문에). 이것은 gulp.src 를 전달하고 일부 변경 사항을 파이프하는 Gulp 작업의 우아함을 거의 가지고 있지 않습니다.

또 다른 문제는 이로 인해 브라우저에서 html , cssjs 를 다시 로드하는 데 다른 접근 방식을 사용해야 한다는 것입니다. Gulp watch 작업을 살펴보면 다음과 같습니다.

 gulp.task('watch', () => { livereload.listen({basePath: 'dist'}); gulp.watch(config.paths.html, ['html']); gulp.watch(config.paths.css, ['css']); gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); });

HTML 파일이 변경되면 html 작업이 다시 실행됩니다.

 gulp.task('html', () => { return gulp.src(config.paths.html) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); });

마지막 파이프는 NODE_ENV가 production 이 아닌 경우 NODE_ENV livereload() 를 호출하여 브라우저에서 새로 고침을 트리거합니다.

CSS watch에도 동일한 논리가 사용됩니다. CSS 파일이 변경되면 css 작업이 다시 실행되고 css 작업의 마지막 파이프가 livereload() 를 트리거하고 브라우저를 새로 고칩니다.

그러나 js watch는 js 작업을 전혀 호출하지 않습니다. 대신 Browserify의 이벤트 핸들러 b.on('update', bundle) 은 완전히 다른 접근 방식(즉, 핫 모듈 교체)을 사용하여 다시 로드를 처리합니다. 이 접근 방식의 불일치는 짜증나지만 불행히도 점진적 빌드를 위해 필요합니다. bundle 함수의 끝에서 순진하게 livereload() 를 호출했다면 개별 JS 파일 변경 시 전체 JS 번들을 다시 빌드할 것입니다. 그러한 접근 방식은 분명히 확장되지 않습니다. JS 파일이 많을수록 각 재번들이 더 오래 걸립니다. 갑자기 500ms 리번들이 30초가 걸리기 시작하여 애자일 개발을 방해합니다.

CSS 번들

 gulp.task('css', () => { return gulp.src( [ 'node_modules/bootstrap/dist/css/bootstrap.css', 'node_modules/font-awesome/css/font-awesome.css', config.paths.css ] ) .pipe(cond(!PROD, sourcemaps.init())) .pipe(sass().on('error', sass.logError)) .pipe(concat('bundle.css')) .pipe(cond(PROD, minifyCSS())) .pipe(cond(!PROD, sourcemaps.write())) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); });

여기서 첫 번째 문제는 번거로운 공급업체 CSS 포함입니다. 새 공급업체 CSS 파일이 프로젝트에 추가될 때마다 실제 소스 코드의 관련 위치에 가져오기를 추가하는 대신 gulp.src 배열에 요소를 추가하도록 gulpfile을 변경해야 한다는 것을 기억해야 합니다.

다른 주요 문제는 각 파이프의 복잡한 논리입니다. 내 파이프에 조건부 논리를 설정하기 위해 gulp-cond 라는 NPM 라이브러리를 추가해야 했으며 최종 결과는 너무 읽기 어렵습니다(어디에나 세 개의 대괄호!).

서버 작업

 gulp.task('server', () => { nodemon({ script: 'server.js' }); });

이 작업은 매우 간단합니다. 본질적으로 노드 환경에서 server.js 를 실행하는 명령줄 호출 nodemon server.js 를 둘러싼 래퍼입니다. nodemonnode 대신 사용되므로 파일을 변경하면 파일이 다시 시작됩니다. 기본적으로 nodemon 은 JS 파일 변경 시 실행 중인 프로세스를 다시 시작하므로 범위를 제한하기 위해 nodemon.json 파일을 포함하는 것이 중요합니다.

 { "watch": "server.js" }

서버 코드를 검토해 보겠습니다.

서버.js

 const baseDir = process.env.NODE_ENV === 'production' ? 'build' : 'dist'; const port = process.env.NODE_ENV === 'production' ? 8080: 3000; const app = express();

이것은 노드 환경에 따른 서버의 기본 디렉토리와 포트를 설정하고, express의 인스턴스를 생성한다.

 app.use(require('connect-livereload')({port: 35729})); app.use(express.static(path.join(__dirname, baseDir)));

이것은 connect-livereload 미들웨어(라이브 재로딩 설정에 필요)와 정적 미들웨어(정적 자산을 처리하는 데 필요)를 추가합니다.

 app.get('/api/sample-route', (req, res) => { res.send({ website: 'Toptal', blogPost: true }); });

이것은 단순한 API 경로입니다. 브라우저에서 localhost:3000/api/sample-route 로 이동하면 다음이 표시됩니다.

 { website: "Toptal", blogPost: true }

실제 백엔드에서는 API 경로 전용 전체 폴더, DB 연결 설정을 위한 별도의 파일 등이 있습니다. 이 샘플 경로는 우리가 설정한 프론트엔드 위에 백엔드를 쉽게 구축할 수 있음을 보여주기 위해 포함되었을 뿐입니다.

 app.get('*', (req, res) => { res.sendFile(path.join(__dirname, './', baseDir ,'/index.html')); });

이것은 포괄적인 경로입니다. 즉, 브라우저에 어떤 URL을 입력하든 서버는 고독한 index.html 페이지를 반환합니다. 그런 다음 클라이언트 측에서 경로를 해결하는 것은 React Router의 책임입니다.

 app.listen(port, () => { open(`http://localhost:${port}`); });

이렇게 하면 익스프레스 인스턴스가 지정한 포트를 수신 대기하고 지정된 URL의 새 탭에서 브라우저를 열도록 지시합니다.

지금까지 서버 설정에 대해 마음에 들지 않는 것은 다음과 같습니다.

 app.use(require('connect-livereload')({port: 35729}));

gulpfile에서 이미 gulp-livereload 를 사용하고 있다는 점을 감안할 때, 이것은 livereload를 사용해야 하는 두 개의 별도 위치를 만듭니다.

이제 마지막으로 중요한 것은 다음과 같습니다.

기본 작업

 gulp.task('default', (cb) => { runSequence('clean', 'lint', 'test', 'html', 'css', 'js', 'fonts', 'server', 'watch', cb); });

이것은 단순히 터미널에 gulp 를 입력할 때 실행되는 작업입니다. 한 가지 이상한 점은 작업을 순차적으로 실행하기 위해 runSequence 를 사용해야 한다는 것입니다. 일반적으로 작업 배열은 병렬로 실행되지만 이것이 항상 원하는 동작은 아닙니다. 예를 들어, 대상 폴더로 파일을 이동하기 전에 대상 폴더가 비어 있는지 확인하려면 html 이전에 clean 작업을 실행해야 합니다. gulp 4가 출시되면 기본적으로 gulp.seriesgulp.parallel 메서드를 지원하지만 지금은 설정에서 이 약간의 문제를 남겨야 합니다.

게다가 이것은 실제로 매우 우아합니다. 앱의 전체 생성 및 호스팅은 단일 명령으로 수행되며 워크플로의 일부를 이해하는 것은 실행 순서의 개별 작업을 검사하는 것만큼 간단합니다. 또한 앱 생성 및 호스팅에 대한 보다 세분화된 접근 방식을 위해 전체 시퀀스를 더 작은 청크로 나눌 수 있습니다. 예를 들어, linttest 작업을 실행하는 validate 라는 별도의 작업을 설정할 수 있습니다. 또는 serverwatch 를 실행하는 host 작업이 있을 수 있습니다. 작업을 오케스트레이션하는 이 기능은 특히 애플리케이션이 확장되고 더 많은 자동화 작업이 필요할 때 매우 강력합니다.

개발 및 프로덕션 빌드

 if (argv.prod) { process.env.NODE_ENV = 'production'; } let PROD = process.env.NODE_ENV === 'production';

yargs NPM 라이브러리를 사용하여 Gulp에 명령줄 플래그를 제공할 수 있습니다. 여기서 gulp 가 터미널의 --prod 에 전달되는 경우 노드 환경을 프로덕션으로 설정하도록 gulpfile에 지시합니다. 그런 다음 PROD 변수는 gulpfile에서 개발 및 생산 동작을 구별하기 위한 조건으로 사용됩니다. 예를 들어, browserify 구성에 전달하는 옵션 중 하나는 다음과 같습니다.

 plugin: PROD ? [] : [hmr, watchify]

이것은 browserify 가 프로덕션 모드에서 플러그인을 사용하지 않고 다른 환경에서 hmrwatchify 플러그인을 사용하도록 지시합니다.

PROD 조건문은 궁극적으로 많은 코드 반복을 포함하게 되는 프로덕션 및 개발을 위해 별도의 gulpfile을 작성하지 않아도 되기 때문에 매우 유용합니다. 대신 gulp --prod 를 사용하여 프로덕션에서 기본 작업을 실행하거나 gulp html --prod 를 사용하여 프로덕션에서 html 작업만 실행할 수 있습니다. 반면에 .pipe(cond(!PROD, livereload())) 와 같은 명령문으로 Gulp 파이프라인을 흩뜨리는 것이 가장 읽기 쉽지 않다는 것을 앞서 보았습니다. 결국 부울 변수 접근 방식을 사용할지 두 개의 개별 gulpfile을 설정할지 여부는 선호도의 문제입니다.

이제 Gulp를 작업 실행자로 계속 사용하지만 Browserify를 Webpack으로 대체할 때 어떤 일이 발생하는지 봅시다.

Gulp + Webpack 설정

갑자기 우리의 gulpfile은 12개의 가져오기를 포함하는 99줄의 길이로 이전 설정에서 상당히 감소했습니다! 기본 작업을 확인하는 경우:

 gulp.task('default', (cb) => { runSequence('lint', 'test', 'build', 'server', 'watch', cb); });

이제 전체 웹 앱 설정에 9개가 아닌 5개의 작업만 필요하므로 극적으로 개선되었습니다.

또한 livereload 의 필요성을 제거했습니다. 이제 우리의 watch 작업은 간단합니다.

 gulp.task('watch', () => { gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); });

이것은 꿀꺽 꿀꺽 꿀꺽 꿀꺽 꿀꺽꿀꺽 꿀꺽꿀꺽 꿀꺽꿀꺽 꿀꺽꿀꺽꿀꺽꿀꺽꿀꺽꿀꺽꿀꺽꿀꿀꿀꿀꿀꿀꿀꿀꿀꿀꿀꿀꿀꿀꿀꿀꿀꿀한다고한다고알아볼수있는감시자가어떤유형의재묶음동작도트리거하지않음을의미합니다. 추가 보너스로 index.htmlapp 에서 dist 또는 build 로 더 이상 전송할 필요가 없습니다.

작업 감소에 초점을 맞춰 html , css , jsfonts 작업을 모두 단일 build 작업으로 대체했습니다.

 gulp.task('build', () => { runSequence('clean', 'html'); return gulp.src(config.paths.entry) .pipe(webpack(require('./webpack.config'))) .pipe(gulp.dest(config.paths.baseDir)); });

충분히 간단합니다. cleanhtml 작업을 순서대로 실행합니다. 완료되면 진입점을 잡고 Webpack을 통해 파이프하고 webpack.config.js 파일을 전달하여 구성하고 결과 번들을 baseDir (노드 환경에 따라 dist 또는 build 중 하나)로 보냅니다.

Webpack 구성 파일을 살펴보겠습니다.

웹팩.config.js

이것은 상당히 크고 위협적인 구성 파일이므로 module.exports 개체에 설정되는 몇 가지 중요한 속성을 설명하겠습니다.

 devtool: PROD ? 'source-map' : 'eval-source-map',

이것은 Webpack이 사용할 소스맵의 유형을 설정합니다. Webpack은 기본적으로 소스맵을 지원할 뿐만 아니라 실제로 다양한 소스맵 옵션을 지원합니다. 각 옵션은 소스맵 세부 사항과 재구축 속도(변경 사항을 다시 묶는 데 걸리는 시간)의 균형을 다르게 제공합니다. 이것은 빠른 재로드를 달성하기 위해 개발용으로 "저렴한" 소스맵 옵션을 사용할 수 있고 프로덕션에서는 더 비싼 소스맵 옵션을 사용할 수 있음을 의미합니다.

 entry: PROD ? './app/index' : [ 'webpack-hot-middleware/client?reload=true', // reloads the page if hot module reloading fails. './app/index' ]

이것은 번들 진입점입니다. 배열이 전달되었음을 주목하십시오. 즉, 여러 진입점이 있을 수 있습니다. 이 경우 예상 진입점 app/index.js 와 핫 모듈 재로딩 설정의 일부로 사용되는 webpack-hot-middleware 진입점이 있습니다.

 output: { path: PROD ? __dirname + '/build' : __dirname + '/dist', publicPath: '/', filename: 'bundle.js' },

여기에서 컴파일된 번들이 출력됩니다. 가장 혼란스러운 옵션은 publicPath 입니다. 번들을 서버에서 호스팅할 기본 URL을 설정합니다. 따라서 예를 들어 publicPath/public/assets 이면 번들은 서버의 /public/assets/bundle.js 아래에 나타납니다.

 devServer: { contentBase: PROD ? './build' : './app' }

이것은 서버의 루트 디렉토리로 사용할 프로젝트의 폴더를 서버에 알려줍니다.

Webpack이 프로젝트에서 생성된 번들을 서버의 번들로 매핑하는 방법에 대해 혼란스러워한다면 다음을 기억하십시오.

  • path + filename : 프로젝트 소스 코드에서 번들의 정확한 위치
  • contentBase (루트로서, / ) + publicPath : 서버에서 번들의 위치
 plugins: PROD ? [ new webpack.optimize.OccurenceOrderPlugin(), new webpack.DefinePlugin(GLOBALS), new ExtractTextPlugin('bundle.css'), new webpack.optimize.DedupePlugin(), new webpack.optimize.UglifyJsPlugin({compress: {warnings: false}}) ] : [ new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin() ],

Webpack의 기능을 어떤 식으로든 향상시키는 플러그인입니다. 예를 들어 webpack.optimize.UglifyJsPlugin 은 축소를 담당합니다.

 loaders: [ {test: /\.js$/, include: path.join(__dirname, 'app'), loaders: ['babel']}, { test: /\.css$/, loader: PROD ? ExtractTextPlugin.extract('style', 'css?sourceMap'): 'style!css?sourceMap' }, { test: /\.scss$/, loader: PROD ? ExtractTextPlugin.extract('style', 'css?sourceMap!resolve-url!sass?sourceMap') : 'style!css?sourceMap!resolve-url!sass?sourceMap' }, {test: /\.(svg|png|jpe?g|gif)(\?\S*)?$/, loader: 'url?limit=100000&name=img/[name].[ext]'}, {test: /\.(eot|woff|woff2|ttf)(\?\S*)?$/, loader: 'url?limit=100000&name=fonts/[name].[ext]'} ]

이들은 로더입니다. 기본적으로 require() 문을 통해 로드되는 파일을 사전 처리합니다. 로더를 함께 연결할 수 있다는 점에서 Gulp 파이프와 다소 유사합니다.

로더 객체 중 하나를 살펴보겠습니다.

 {test: /\.scss$/, loader: 'style!css?sourceMap!resolve-url!sass?sourceMap'}

test 속성은 파일이 제공된 정규식 패턴(이 경우 /\.scss$/ )과 일치하는 경우 지정된 로더가 적용됨을 Webpack에 알려줍니다. loader 속성은 loader가 수행하는 작업에 해당합니다. 여기서 우리는 역순으로 실행되는 style , css , resolve-urlsass 로더를 함께 연결합니다.

loader3!loader2!loader1 구문이 매우 우아하지 않다는 것을 인정해야 합니다. 결국, 언제 프로그램에서 오른쪽에서 왼쪽으로 읽어야 합니까? 그럼에도 불구하고 로더는 웹팩의 매우 강력한 기능입니다. 사실, 방금 언급한 로더를 사용하면 SASS 파일을 JavaScript로 직접 가져올 수 있습니다! 예를 들어 진입점 파일에서 공급업체 및 전역 스타일시트를 가져올 수 있습니다.

index.js

 import React from 'react'; import {render} from 'react-dom'; import {Router, browserHistory} from 'react-router'; import routes from './routes'; // CSS imports import '../node_modules/bootstrap/dist/css/bootstrap.css'; import '../node_modules/font-awesome/css/font-awesome.css'; import './styles/global.scss'; render(<Router history={browserHistory} routes={routes} />, document.getElementById('app'));

마찬가지로 Header 구성 요소에서 import './Header.scss' 를 추가하여 구성 요소의 관련 스타일시트를 가져올 수 있습니다. 이는 다른 모든 구성 요소에도 적용됩니다.

제 생각에는 이것은 거의 JavaScript 개발 세계의 혁명적인 변화로 간주될 수 있습니다. 로더가 이 모든 것을 처리하므로 CSS 번들링, 축소 또는 소스맵에 대해 걱정할 필요가 없습니다. 핫 모듈 재로딩도 CSS 파일에서 작동합니다. 그런 다음 동일한 파일에서 JS 및 CSS 가져오기를 처리할 수 있으므로 개발이 개념적으로 더 간단해집니다. 일관성이 더 높고 컨텍스트 전환이 적으며 추론하기 쉽습니다.

이 기능이 어떻게 작동하는지 간략하게 요약하자면: Webpack은 CSS를 JS 번들에 인라인합니다. 실제로 Webpack은 이미지와 글꼴에 대해서도 이 작업을 수행할 수 있습니다.

 {test: /\.(svg|png|jpe?g|gif)(\?\S*)?$/, loader: 'url?limit=100000&name=img/[name].[ext]'}, {test: /\.(eot|woff|woff2|ttf)(\?\S*)?$/, loader: 'url?limit=100000&name=fonts/[name].[ext]'}

URL 로더는 이미지와 글꼴이 100KB 미만인 경우 데이터 URL로 인라인하고 그렇지 않으면 별도의 파일로 제공하도록 Webpack에 지시합니다. 물론 컷오프 크기를 10KB와 같은 다른 값으로 구성할 수도 있습니다.

그리고 그것은 간단히 말해서 Webpack 구성입니다. 나는 상당한 양의 설정이 있다는 것을 인정하지만 그것을 사용하는 것의 이점은 단순히 경이적입니다. Browserify에는 플러그인과 변환이 있지만 추가된 기능 측면에서 Webpack 로더와 비교할 수는 없습니다.

Webpack + NPM 스크립트 설정

이 설정에서는 작업을 자동화하기 위해 gulpfile에 의존하는 대신 npm 스크립트를 직접 사용합니다.

패키지.json

 "scripts": { "start": "npm-run-all --parallel lint:watch test:watch build", "start:prod": "npm-run-all --parallel lint test build:prod", "clean-dist": "rimraf ./dist && mkdir dist", "clean-build": "rimraf ./build && mkdir build", "clean": "npm-run-all clean-dist clean-build", "test": "mocha ./app/**/*.test.js --compilers js:babel-core/register", "test:watch": "npm run test -- --watch", "lint": "esw ./app/**/*.js", "lint:watch": "npm run lint -- --watch", "server": "nodemon server.js", "server:prod": "cross-env NODE_ENV=production nodemon server.js", "build-html": "node tools/buildHtml.js", "build-html:prod": "cross-env NODE_ENV=production node tools/buildHtml.js", "prebuild": "npm-run-all clean-dist build-html", "build": "webpack", "postbuild": "npm run server", "prebuild:prod": "npm-run-all clean-build build-html:prod", "build:prod": "cross-env NODE_ENV=production webpack", "postbuild:prod": "npm run server:prod" }

개발 및 프로덕션 빌드를 실행하려면 각각 npm startnpm run start:prod 를 입력하십시오.

이것은 99~150줄의 코드를 19개의 NPM 스크립트로 줄이거나 프로덕션 스크립트를 제외하면 12개(대부분은 노드 환경이 프로덕션으로 설정된 개발 스크립트를 미러링함)로 줄였다는 점을 감안할 때 gulpfile보다 확실히 더 컴팩트합니다. ). 단점은 이러한 명령이 Gulp 작업에 비해 다소 모호하고 표현력이 부족하다는 것입니다. 예를 들어, 단일 npm 스크립트가 특정 명령을 직렬로 실행하고 다른 명령을 병렬로 실행하도록 하는 방법(적어도 내가 알고 있는)은 없습니다. 둘 중 하나입니다.

그러나 이 접근 방식에는 큰 이점이 있습니다. 명령줄에서 직접 mocha 와 같은 NPM 라이브러리를 사용하면 각각에 대해 동등한 Gulp 래퍼(이 경우 gulp-mocha )를 설치할 필요가 없습니다.

NPM 설치 대신

  • 꿀꺽꿀꺽
  • 꿀꺽꿀꺽
  • 꿀꺽꿀꺽-노드몬

다음 패키지를 설치합니다.

  • 에슬린트
  • 모카
  • 노드몬

Cory House의 게시물, NPM 스크립트를 위해 Gulp와 Grunt를 떠난 이유 를 인용합니다.

나는 Gulp의 열렬한 팬이었습니다. 그러나 내 마지막 프로젝트에서는 gulpfile에 수백 줄과 12개 정도의 Gulp 플러그인이 있었습니다. Webpack, Browsersync, 핫 리로딩, Mocha 등을 Gulp를 사용하여 통합하는 데 어려움을 겪었습니다. 왜요? 글쎄, 일부 플러그인에는 내 사용 사례에 대한 문서가 충분하지 않았습니다. 일부 플러그인은 내가 필요로 하는 API의 일부만 노출했습니다. 하나는 소수의 파일만 감시하는 이상한 버그가 있었습니다. 명령줄에 출력할 때 다른 색상이 제거되었습니다.

그는 Gulp의 세 가지 핵심 문제를 지정합니다.

  1. 플러그인 작성자에 대한 의존성
  2. 디버그하기 귀찮다
  3. 분리된 문서

나는 이 모든 것에 동의하는 경향이 있습니다.

1. 플러그인 작성자에 대한 의존성

eslint 와 같은 라이브러리가 업데이트될 때마다 연결된 gulp-eslint 라이브러리에 해당 업데이트가 필요합니다. 라이브러리 관리자가 관심을 잃으면 라이브러리의 꿀꺽꿀꺽 버전이 기본 버전과 동기화되지 않습니다. 새 라이브러리가 생성될 때도 마찬가지입니다. 누군가가 라이브러리 xyz 를 만들고 계속 사용하면 갑자기 꿀꺽 꿀꺽 꿀꺽 마시기 작업에 사용할 해당 gulp-xyz 라이브러리가 필요합니다.

어떤 의미에서 이 접근 방식은 확장되지 않습니다. 이상적으로는 기본 라이브러리를 사용할 수 있는 Gulp와 같은 접근 방식이 필요합니다.

2. 디버그하기 귀찮다

gulp-plumber 와 같은 라이브러리가 이 문제를 상당히 완화하는 데 도움이 되지만 그럼에도 불구하고 gulp 의 오류 보고는 그다지 도움이 되지 않는 것이 사실입니다. 하나의 파이프라도 처리되지 않은 예외를 throw하는 경우 소스 코드에서 문제의 원인과 전혀 관련이 없는 것처럼 보이는 문제에 대한 스택 추적을 얻게 됩니다. 이것은 어떤 경우에는 디버깅을 악몽으로 만들 수 있습니다. Google이나 Stack Overflow에서 아무리 검색해도 오류가 이해하기 어렵거나 오해의 소지가 있는 경우 실제로 도움이 되지 않습니다.

3. 분리된 문서

종종 나는 작은 gulp 꿀꿀꿀꿀꿀꿀샘하는 라이브러리에는 문서가 매우 제한적인 경향이 있다는 것을 알았습니다. 나는 이것이 저자가 일반적으로 주로 자신의 용도로 라이브러리를 만들기 때문이라고 생각합니다. 또한 Gulp 플러그인과 기본 라이브러리 자체에 대한 문서를 모두 살펴봐야 하는 것이 일반적입니다. 이는 많은 컨텍스트 전환과 두 배의 읽기 작업을 의미합니다.

결론

각 옵션에는 장단점이 있지만 Webpack이 Browserify보다 선호되고 NPM 스크립트가 Gulp보다 선호된다는 것이 나에게는 꽤 분명한 것 같습니다. Gulp는 확실히 NPM 스크립트보다 더 표현력이 풍부하고 사용하기 편리하지만 추가된 모든 추상화에서 비용을 지불해야 합니다.

모든 조합이 앱에 완벽할 수는 없지만 압도적인 수의 개발 종속성과 실망스러운 디버깅 경험을 피하려면 NPM 스크립트가 포함된 Webpack을 사용하는 것이 좋습니다. 이 기사가 다음 프로젝트에 적합한 도구를 선택하는 데 유용하기를 바랍니다.

관련된:
  • 제어 유지: Webpack 및 React 가이드, Pt. 1
  • Gulp Under the Hood: 스트림 기반 작업 자동화 도구 구축