Gulp Under the Hood: 스트림 기반 작업 자동화 도구 구축
게시 됨: 2022-03-11프론트엔드 개발자는 오늘날 여러 도구를 사용하여 일상적인 작업을 자동화하고 있습니다. 가장 인기 있는 세 가지 솔루션은 Grunt, Gulp 및 Webpack입니다. 이러한 각각의 도구는 서로 다른 철학을 기반으로 구축되었지만 프론트 엔드 구축 프로세스를 간소화한다는 동일한 공통 목표를 공유합니다. 예를 들어 Grunt는 구성 기반이지만 Gulp는 거의 아무 것도 적용하지 않습니다. 사실 Gulp는 다양한 빌드 작업인 빌드 프로세스의 흐름을 구현하기 위해 코드를 작성하는 개발자에 의존합니다.
이러한 도구 중 하나를 선택할 때 개인적으로 가장 좋아하는 도구는 Gulp입니다. 대체로 간단하고 빠르며 안정적인 솔루션입니다. 이 기사에서는 Gulp와 유사한 자체 도구를 구현하여 Gulp가 내부에서 어떻게 작동하는지 확인할 것입니다.
걸프 API
Gulp에는 다음과 같은 네 가지 간단한 기능만 있습니다.
- 꿀꺽꿀꺽.태스크
- 꿀꺽꿀꺽.src
- 꿀꺽꿀꺽
- gulp.watch
이 네 가지 간단한 기능은 다양한 조합으로 Gulp의 모든 기능과 유연성을 제공합니다. 버전 4.0에서 Gulp는 gulp.series 및 gulp.parallel의 두 가지 새로운 기능을 도입했습니다. 이러한 API를 사용하면 작업을 직렬 또는 병렬로 실행할 수 있습니다.
이 네 가지 기능 중 처음 세 가지는 모든 Gulp 파일에 절대적으로 필요합니다. 명령줄 인터페이스에서 작업을 정의하고 호출할 수 있습니다. 네 번째는 파일이 변경될 때 작업이 실행되도록 허용하여 Gulp를 진정으로 자동으로 만드는 것입니다.
꿀꺽꿀꺽
이것은 기본 gulpfile입니다.
gulp.task('test', function{ gulp.src('test.txt') .pipe(gulp.dest('out')); });
간단한 테스트 작업을 설명합니다. 호출되면 현재 작업 디렉토리에 있는 test.txt 파일을 ./out 디렉토리에 복사해야 합니다. Gulp를 실행하여 시도해 보세요.
touch test.txt # Create test.txt gulp test
.pipe
메소드는 Gulp의 일부가 아니며 노드 스트림 API이며 읽을 수 있는 스트림( gulp.src('test.txt')
에 의해 생성됨)을 쓰기 가능한 스트림( gulp.dest('out')
에 의해 생성됨))과 연결합니다. gulp.dest('out')
). Gulp와 플러그인 간의 모든 통신은 스트림을 기반으로 합니다. 이렇게 하면 우아한 방식으로 gulpfile 코드를 작성할 수 있습니다.
플러그를 만나다
이제 Gulp가 작동하는 방식에 대해 어느 정도 이해했으므로 Gulp와 유사한 자체 도구인 Plug를 빌드해 보겠습니다.
우리는 plug.task API로 시작할 것입니다. 작업을 등록할 수 있어야 하며 작업 이름이 명령 매개변수에 전달되면 작업이 실행되어야 합니다.
var plug = { task: onTask }; module.exports = plug; var tasks = {}; function onTask(name, callback){ tasks[name] = callback; }
이렇게 하면 작업을 등록할 수 있습니다. 이제 이 작업을 실행 가능하게 만들어야 합니다. 일을 단순하게 유지하기 위해 별도의 작업 실행기를 만들지 않습니다. 대신 플러그 구현에 포함할 것입니다.
명령줄 매개변수에 명명된 작업을 실행하기만 하면 됩니다. 또한 모든 작업이 등록된 후 다음 실행 루프에서 이를 시도해야 합니다. 가장 쉬운 방법은 타임아웃 콜백 또는 가급적이면 process.nextTick에서 작업을 실행하는 것입니다.
process.nextTick(function(){ var taskName = process.argv[2]; if (taskName && tasks[taskName]) { tasks[taskName](); } else { console.log('unknown task', taskName) } });
다음과 같이 plugfile.js를 작성하십시오.
var plug = require('./plug'); plug.task('test', function(){ console.log('hello plug'); })
... 실행합니다.
node plugfile.js test
다음과 같이 표시됩니다.
hello plug
하위 작업
Gulp는 또한 작업 등록 시 하위 작업을 정의할 수 있습니다. 이 경우, plug.task는 이름, 하위 작업의 배열, 콜백 함수의 3가지 매개변수를 취해야 합니다. 이것을 구현해보자.
다음과 같이 작업 API를 업데이트해야 합니다.
var tasks = {}; function onTask(name) { if(Array.isArray(arguments[1]) && typeof arguments[2] === "function"){ tasks[name] = { subTasks: arguments[1], callback: arguments[2] }; } else if(typeof arguments[1] === "function"){ tasks[name] = { subTasks: [], callback: arguments[1] }; } else{ console.log('invalid task registration') } } function runTask(name){ if(tasks[name].subTasks){ tasks[name].subTasks.forEach(function(subTaskName){ runTask(subTaskName); }); } if(tasks[name].callback){ tasks[name].callback(); } } process.nextTick(function(){ if (taskName && tasks[taskName]) { runTask(taskName); } });
이제 우리의 plugfile.js가 다음과 같다면:
plug.task('subTask1', function(){ console.log('from sub task 1'); }) plug.task('subTask2', function(){ console.log('from sub task 2'); }) plug.task('test', ['subTask1', 'subTask2'], function(){ console.log('hello plug'); })
... 실행
node plugfile.js test
... 표시되어야 합니다:
from sub task 1 from sub task 2 hello plug
Gulp는 하위 작업을 병렬로 실행합니다. 그러나 일을 단순하게 유지하기 위해 구현에서는 하위 작업을 순차적으로 실행합니다. Gulp 4.0에서는 두 가지 새로운 API 기능을 사용하여 이를 제어할 수 있습니다. 이 기능은 이 기사의 뒷부분에서 구현할 것입니다.
소스 및 대상
파일을 읽고 쓰는 것을 허용하지 않으면 플러그가 거의 사용되지 않습니다. 그래서 다음으로 우리는 plug.src
를 구현할 것입니다. Gulp의 이 메서드는 파일 마스크, 파일 이름 또는 파일 마스크 배열인 인수를 예상합니다. 읽을 수 있는 노드 스트림을 반환합니다.
지금은 src
구현에서 파일 이름만 허용합니다.
var plug = { task: onTask, src: onSrc }; var stream = require('stream'); var fs = require('fs'); function onSrc(fileName){ var src = new stream.Readable({ read: function (chunk) { }, objectMode: true }); //read file and send it to the stream fs.readFile(path, 'utf8', (e,data)=> { src.push({ name: path, buffer: data }); src.push(null); }); return src; }
여기서 선택적 매개변수인 objectMode: true
를 사용합니다. 노드 스트림은 기본적으로 바이너리 스트림과 함께 작동하기 때문입니다. 스트림을 통해 JavaScript 객체를 전달/수신해야 하는 경우 이 매개변수를 사용해야 합니다.
보시다시피 인공 물체를 만들었습니다.
{ name: path, //file name buffer: data //file content }
... 스트림으로 전달했습니다.
다른 쪽 끝에서, plug.dest 메서드는 대상 폴더 이름을 수신하고 .src 스트림에서 개체를 수신할 쓰기 가능한 스트림을 반환해야 합니다. 파일 개체가 수신되는 즉시 대상 폴더에 저장됩니다.
function onDest(path){ var writer = new stream.Writable({ write: function (chunk, encoding, next) { if (!fs.existsSync(path)) fs.mkdirSync(path); fs.writeFile(path +'/'+ chunk.name, chunk.buffer, (e)=> { next() }); }, objectMode: true }); return writer; }
우리의 plugfile.js를 업데이트합시다:
var plug = require('./plug'); plug.task('test', function(){ plug.src('test.txt') .pipe(plug.dest('out')) })
... test.txt 생성

touch test.txt
... 그리고 그것을 실행하십시오:
node plugfile.js test ls ./out
test.txt 를 ./out 폴더에 복사해야 합니다.
Gulp 자체는 거의 같은 방식으로 작동하지만 인공 파일 개체 대신 비닐 개체를 사용합니다. 파일 이름과 내용뿐만 아니라 현재 폴더 이름, 파일의 전체 경로 등과 같은 추가 메타 정보도 포함되어 있어 훨씬 더 편리합니다. 전체 콘텐츠 버퍼를 포함하지 않을 수 있지만 대신 읽을 수 있는 콘텐츠 스트림이 있습니다.
비닐: 파일보다 낫다
비닐 개체로 표시된 파일을 조작할 수 있는 우수한 라이브러리 vinyl-fs가 있습니다. 기본적으로 파일 마스크를 기반으로 읽고 쓸 수 있는 스트림을 만들 수 있습니다.
비닐-fs 라이브러리를 사용하여 플러그 기능을 다시 작성할 수 있습니다. 하지만 먼저 vinyl-fs를 설치해야 합니다.
npm i vinyl-fs
이것이 설치되면 우리의 새로운 Plug 구현은 다음과 같이 보일 것입니다:
var vfs = require('vinyl-fs') function onSrc(fileName){ return vfs.src(fileName); } function onDest(path){ return vfs.dest(path); } // ...
… 그리고 그것을 시도하려면:
rm out/test.txt node plugFile.js test ls out/test.txt
결과는 여전히 동일해야 합니다.
걸프 플러그인
Plug 서비스는 Gulp 스트림 규칙을 사용하므로 기본 Gulp 플러그인을 Plug 도구와 함께 사용할 수 있습니다.
한 번 시도해 보겠습니다. gulp-rename 설치:
npm i gulp-rename
… 사용하도록 plugfile.js를 업데이트합니다.
var plug = require('./app.js'); var rename = require('gulp-rename'); plug.task('test', function () { return plug.src('test.txt') .pipe(rename('renamed.txt')) .pipe(plug.dest('out')); });
이제 plugfile.js를 실행해도 여전히 동일한 결과가 나올 것입니다.
node plugFile.js test ls out/renamed.txt
변경사항 모니터링
마지막 방법은 gulp.watch
입니다. 이 방법을 사용하면 파일 수신기를 등록하고 파일이 변경될 때 등록된 작업을 호출할 수 있습니다. 구현해 보겠습니다.
var plug = { task: onTask, src: onSrc, dest: onDest, watch: onWatch }; function onWatch(fileName, taskName){ fs.watchFile(fileName, (event, filename) => { if (filename) { tasks[taskName](); } }); }
그것을 시도하려면 다음 줄을 plugfile.js에 추가하십시오.
plug.watch('test.txt','test');
이제 test.txt 가 변경될 때마다 파일이 이름이 변경된 out 폴더에 복사됩니다.
시리즈 대 병렬
이제 Gulp API의 모든 기본 기능이 구현되었으므로 한 단계 더 나아가 보겠습니다. 다음 버전의 Gulp에는 더 많은 API 기능이 포함될 것입니다. 이 새로운 API는 Gulp를 더욱 강력하게 만듭니다.
- 꿀꺽꿀꺽.병렬
- 꿀꺽꿀꺽.시리즈
이러한 방법을 통해 사용자는 작업이 실행되는 순서를 제어할 수 있습니다. 하위 작업을 병렬로 등록하려면 현재 Gulp 동작인 gulp.parallel을 사용할 수 있습니다. 반면에 gulp.series는 하위 작업을 차례로 차례로 실행하는 데 사용할 수 있습니다.
현재 폴더에 test1.txt 와 test2.txt 가 있다고 가정합니다. 해당 파일을 out 폴더에 병렬로 복사하려면 플러그 파일을 만드십시오.
var plug = require('./plug'); plug.task('subTask1', function(){ return plug.src('test1.txt') .pipe(plug.dest('out')) }) plug.task('subTask2', function(){ return plug.src('test2.txt') .pipe(plug.dest('out')) }) plug.task('test-parallel', plug.parallel(['subTask1', 'subTask2']), function(){ console.log('done') }) plug.task('test-series', plug.series(['subTask1', 'subTask2']), function(){ console.log('done') })
구현을 단순화하기 위해 하위 작업 콜백 함수가 스트림을 반환하도록 만들어졌습니다. 이것은 스트림 수명 주기를 추적하는 데 도움이 됩니다.
API 수정을 시작할 것입니다.
var plug = { task: onTask, src: onSrc, dest: onDest, parallel: onParallel, series: onSeries };
태스크 런처가 하위 태스크를 적절하게 처리하는 데 도움이 되도록 태스크 메타 정보를 추가해야 하기 때문에 onTask 기능도 업데이트해야 합니다.
function onTask(name, subTasks, callback){ if(arguments.length < 2){ console.error('invalid task registration',arguments); return; } if(arguments.length === 2){ if(typeof arguments[1] === 'function'){ callback = subTasks; subTasks = {series: []}; } } tasks[name] = subTasks; tasks[name].callback = function(){ if(callback) return callback(); }; } function onParallel(tasks){ return { parallel: tasks }; } function onSeries(tasks){ return { series: tasks }; }
일을 단순하게 유지하기 위해 비동기 함수를 처리하여 병렬 또는 직렬로 작업을 실행하기 위한 유틸리티 라이브러리인 async.js를 사용합니다.
var async = require('async') function _processTask(taskName, callback){ var taskInfo = tasks[taskName]; console.log('task ' + taskName + ' is started'); var subTaskNames = taskInfo.series || taskInfo.parallel || []; var subTasks = subTaskNames.map(function(subTask){ return function(cb){ _processTask(subTask, cb); } }); if(subTasks.length>0){ if(taskInfo.series){ async.series(subTasks, taskInfo.callback); }else{ async.parallel(subTasks, taskInfo.callback); } }else{ var stream = taskInfo.callback(); if(stream){ stream.on('end', function(){ console.log('stream ' + taskName + ' is ended'); callback() }) }else{ console.log('task ' + taskName +' is completed'); callback(); } } }
스트림이 모든 메시지를 처리하고 닫힐 때 내보내는 노드 스트림 'end'에 의존합니다. 이는 하위 작업이 완료되었음을 나타냅니다. async.js를 사용하면 복잡한 콜백을 처리할 필요가 없습니다.
그것을 시도하기 위해 먼저 하위 작업을 병렬로 실행해 보겠습니다.
node plugFile.js test-parallel
task test-parallel is started task subTask1 is started task subTask2 is started stream subTask2 is ended stream subTask1 is ended done
그리고 동일한 하위 작업을 연속으로 실행합니다.
node plugFile.js test-series
task test-series is started task subTask1 is started stream subTask1 is ended task subTask2 is started stream subTask2 is ended done
결론
그게 다야, 우리는 Gulp의 API를 구현했고 이제 Gulp 플러그인을 사용할 수 있습니다. 물론 Gulp는 여기에서 구현한 것 이상이므로 실제 프로젝트에서 Plug를 사용하지 마십시오. 이 작은 연습이 Gulp가 내부적으로 어떻게 작동하는지 이해하는 데 도움이 되기를 바랍니다. 우리가 이를 보다 유창하게 사용하고 플러그인으로 확장할 수 있기를 바랍니다.