image 최적화 - by. UXKM

요약 설명

이미지 최적화는 Gulp 작업뿐만 아니라 모든 웹 환경에서 필수적인 작업으로, 웹 성능 향상을 위해 필수입니다.
Gulp를 사용하면 더 편리하게 이미지 최적화 환경을 설정할 수 있습니다.

6.3.1. 이미지 최적화가 필요한 이유

이미지 최적화가 필요한 이유는 다음과 같습니다.

  1. 로딩 속도 개선: 최적화되지 않은 이미지는 파일 크기가 크기 때문에 웹 페이지의 로딩 속도를 늦출 수 있습니다. 이미지 최적화를 통해 파일 크기를 줄여 더 빠른 로딩을 구현할 수 있습니다.
  2. 대역폭 절약: 이미지 최적화는 파일 크기를 감소시키므로, 사용자가 데이터를 다운로드하는 데 필요한 대역폭을 절약할 수 있습니다. 특히 모바일 사용자와 저속 인터넷 연결을 갖고 있는 사용자들에게 도움이 됩니다.
  3. 검색 엔진 최적화 (SEO): 웹 페이지의 로딩 속도는 검색 엔진 최적화에도 영향을 미칩니다. 최적화된 이미지를 사용하면 검색 엔진에서 더 긍정적으로 평가할 수 있습니다.
  4. 사용자 경험 향상: 이미지 최적화를 통해 웹 페이지의 성능이 향상되면 사용자 경험이 개선됩니다. 빠른 로딩 이미지는 사용자가 더 빨리 콘텐츠를 볼 수 있도록 도와줍니다.

Gulp를 사용하여 이미지를 최적화하면 이러한 이점을 쉽게 얻을 수 있습니다. Gulp 플러그인을 통해 이미지 최적화 작업을 자동화하고, 더 나은 성능과 사용자 경험을 제공할 수 있습니다.

6.3.2. 패키지 설치

터미널에서 아래 명령을 실행하여 gulp-image 패키지를 설치합니다.

npm install gulp-imagemin@7.1.0 -D
npm install gulp-newer -D


npm i gulp-imagemin@7.1.0 gulp-newer -D

gulp-imagemin

gulp-imagemin는 Gulp를 사용하여 이미지 최적화를 수행하는 플러그인으로, 다양한 이미지 형식을 지원하고 파이프라인을 활용하여 간편하게 이미지 최적화 작업을 자동화할 수 있습니다.

  • 이미지 최적화: gulp-imagemin는 다양한 이미지 최적화 도구 및 알고리즘을 사용하여 이미지를 압축하고 최적화합니다.
  • 다양한 이미지 형식 지원: JPEG, PNG, GIF, SVG 등 다양한 이미지 형식을 지원하며, 각 형식에 대한 최적화를 수행할 수 있습니다.
  • Gulp 파이프라인 통합: Gulp의 특성인 파이프라인을 활용하여 이미지 최적화 작업을 다른 Gulp 작업과 연결하여 자동화할 수 있습니다.
  • 옵션 설정: 사용자는 최적화 옵션을 설정하여 이미지 처리의 세부적인 동작을 조정할 수 있습니다.

gulp-newer

gulp-newer는 Gulp 플러그인 중 하나로, 변경된 파일만을 필터링하여 통과시키는 기능을 제공합니다. 변경된 이미지 파일만 최적화하여 전체 이미지를 다시 처리하지 않아도 되므로 빌드 시간을 단축할 수 있습니다.

  • 새로운 파일만 전달: 대상 디렉토리에 있는 파일을 기준으로 소스 파일과 대상 파일을 비교합니다. 소스 파일이 대상 파일보다 최신인 경우에만 해당 파일을 통과시킵니다.
  • 변경 감지: 파일의 수정 시간을 기준으로 변경 여부를 감지합니다. 이전 실행에서 생성된 대상 파일과 수정된 소스 파일을 비교하여 변경된 파일만을 선택합니다.
  • 빌드 최적화: 주로 이미지 최적화나 파일 복사와 같은 작업에서 사용되어 변경된 파일만을 선택하여 처리함으로써 빌드 시간을 최적화합니다.

6.3.3. gulpfile.babel.js 세팅 방향 선택

중요합니다!

이미지는 웹 사이트 대부분의 용량을 차지합니다.
본 커리큘럼에서는 한 개의 이미지만 사용하기 때문에 큰 문제가 되지 않지만, 보통 웹 사이트에서는 이미지의 총용량이 2~5MB 이상이 되는 경우가 흔합니다.
이미지가 많고 용량이 크다면 Gulp 실행 시마다 이미지 최적화는 상당한 부담이 될 수 있습니다.

그래서 이번엔 두 가지 방법으로 나눠 gulpfile.babel.js를 세팅합니다.

  1. 실시간 이미지 최적화 프로세스 설정: image task가 실행될 때 이미지 최적화도 같이 진행됩니다.
    (단, 이미지가 많고 용량이 클 경우 그만큼 초기 빌드 시간이 늘어납니다.)
  2. 독립적인 이미지 최적화 프로세스 설정: image task가 실행돼도 이미지 최적화는 실행되지 않습니다. 이미지 최적화는 필요할 때 별도로 진행합니다.
    (필요할 때만 이미지 최적화를 실행하기 때문에 초기 빌드 속도에 영향을 미치지 않습니다.)

본 UXKM Gulp 커리큘럼은 조금 더 이해하기 쉬운 1번 방식으로 마무리되지만, 만약 실제 프로젝트에 사용하고 이미지가 많다면 2번 방식을 추천합니다.
※ 아래 세팅 방법 중에서 프로젝트에 가장 적합한 방법을 선택하여 진행해 주세요.

6.3.4-1. 실시간 이미지 최적화 프로세스 설정

gulpfile.babel.js 세팅

// import -----------------------------------------------------------
// ··· 기존 import 생략 ···

import imagemin from "gulp-imagemin";
import newer from "gulp-newer";


// routes -----------------------------------------------------------



// etc --------------------------------------------------------------



// task -------------------------------------------------------------

// html task 

// css task 

// js task 

// image task 
const image = () => {
  return gulp.src( path_src.images + '/**/*' )         // 최적화 이미지 대상
  .pipe( newer( path_dist.images ) )                   // 변경된 파일 통과, 변경되지 않은 파일 건너뛰기
  .pipe( imagemin( { verbose:true } ) )                // 이미지 최적화 ( 최적화  이미지의 정보 기록 옵션 적용 )
  .pipe( gulp.dest( path_dist.images ) );              // 최적화  생성될 목적지 설정
}

// clean task 

// webserver task 

// watch task 
const watch = () => {
  // njk(html) watch  
  const html_watcher = gulp.watch(path_src.html + "/**/*", html);
  file_management(html_watcher, path_src.html, path_dist.html);

  // sass watch 
  const scss_watcher = gulp.watch(path_src.css + "/**/*", css);
  file_management(scss_watcher, path_src.css, path_dist.css);

  // js watch 
  const js_watcher = gulp.watch(path_src.js + "/**/*", js);
  file_management(js_watcher, path_src.js, path_dist.js);

  // image watch 
  const image_watcher = gulp.watch(path_src.images + "/**/*", image);
  file_management(image_watcher, path_src.images, path_dist.images);
}
// watch - 파일 감시  삭제를 위한 함수 


// series & parallel (task 그룹화) ----------------------------------

// 순차적으로 실행되어야 하는 task 그룹 
const prepare = gulp.series([ clean, image ]);

//  prepare 실행 완료  순차적으로 실행되어야 하는 task 그룹 

// 동시에 여러 개의 task 실행되어야 하는 그룹 (병렬로 실행) 


// export (gulp 실행 명령어) ----------------------------------------

// gulp build 실행 (prepare 실행  assets 실행) - build 실행 

// gulp dev 실행 (build 실행  live 실행) - build 실행 후 live 실행 
gulpfile.babel.js - full code
// import -----------------------------------------------------------
import gulp from "gulp";
import nunjucksRender from "gulp-nunjucks-render";
import plumber from "gulp-plumber";
import data from "gulp-data";
import cached from "gulp-cached";
import fs from "fs";
import del from "del";
import ws from "gulp-webserver";
import path from "path";
import gulpSass from "gulp-sass";
import dartSass from "dart-sass";
import sourcemaps from "gulp-sourcemaps";
import minificss from "gulp-minify-css";
import autoprefixer from "autoprefixer";
import postCss from "gulp-postcss";
import rename from "gulp-rename";
import dependents from "gulp-dependents";
import bro from "gulp-bro";
import babelify from "babelify";
import minify from "gulp-minify";
import imagemin from "gulp-imagemin";
import newer from "gulp-newer";


// routes -----------------------------------------------------------
const src = './src';
const dist = './dist';
const ass = '/assets';

// src 폴더의 경로 설정
const path_src = {
  html: src + '/html',
  css: src + ass + '/css',
  images: src + ass + '/images',
  js: src + ass + '/js',
}

// 빌드될 dist 폴더의 경로 설정
const path_dist = {
  html: dist,
  css: dist + ass + '/css',
  images: dist + ass + '/images',
  js: dist + ass + '/js',
};


// etc --------------------------------------------------------------
const onErrorHandler = (error) => console.log(error);  // plumber option (에러 발생  에러 로그 출력)


// task -------------------------------------------------------------

// html task
const html = () => {
  // 들여쓰기(Tab Indent) 조정을 위한 함수
  const manageEnvironment = (environment) => {
    environment.addFilter('tabIndent', (str, numOfIndents, firstLine) => {
      str = str.replace(/^(?=.)/gm, new Array(numOfIndents + 1).join('\t'));
      if(!firstLine) {
        str = str.replace(/^\s+/, "");
      }
      return str;
    });
  };

  // _gnb.json 파일 적용을 위한 변수
  const gnbJson = JSON.parse(fs.readFileSync(path_src.html + '/_templates/_json/_gnb.json'));
  const json_all = {...gnbJson};
  const datafile = () => {
    return json_all;
  }

  // njk 빌드
  return gulp.src([
    path_src.html + '/**/*',                           // 빌드할 njk 파일 경로
    '!' + path_src.html + '/**/_*',                    // 경로  제외할 njk 파일(빌드  병합될 파일)
    '!' + path_src.html + '/**/_*/**/*'                // 경로  제외할 폴더  폴더의 njk 파일(빌드  병합될 파일)
  ])
  .pipe( plumber({errorHandler:onErrorHandler}) )      // 에러 발생  gulp 종료 방지  에러 핸들링
  .pipe( data( datafile) )                             // _gnb.json 적용
  .pipe( nunjucksRender({                              // njk 적용
    envOptions: {                                      // njk 옵션 설정
      autoescape: false,                               // njk 문법의 오류가 있더라도 진행
    },
    manageEnv: manageEnvironment,                      // 들여쓰기(Tab Indent) 함수 적용
    path: [path_src.html],                             // html 폴더 전체 경로
  }) )
  .pipe( cached('html') )                              // 변경된 파일 캐시 저장
  .pipe( gulp.dest(path_dist.html) )                   // 빌드  html 파일이 생성될 목적지 설정
}

// css task
const css = () => {
  //scss 옵션 정의
  const sass = gulpSass(dartSass);                        // ECMAScript 모듈(최신 Node.js 14 이상에서 지원됨)에서 사용하기 위해 선언
  const options = {
    scss : {
      outputStyle: "expanded",                            // 컴파일 스타일: nested(default), expanded, compact, compressed
      indentType: "space",                                // 들여쓰기 스타일: space(default), tab
      indentWidth: 2,                                     // 들여쓰기   (Default : 2)
      precision: 8,                                       // 컴파일  CSS  소수점 자리수 (Type : Integer , Default : 5)
      sourceComments: true,                               // 주석 제거 여부 (Default : false)
      compiler: dartSass,                                 // 컴파일 도구
    },
    postcss: [ autoprefixer({
      overrideBrowserslist: 'last 2 versions',            // 최신 브라우저 기준 하위 2개의 버전까지 컴파일
    }) ]
  };

  return gulp.src(
    path_src.css + '/**/*.scss',                          // 파일 대상 scss파일 찾기
    { since: gulp.lastRun(css) }                          // 변경된 파일에 대해서만 컴파일 진행
  )
  .pipe( plumber({errorHandler:onErrorHandler}) )         // 에러 발생  gulp종료 방지  에러 핸들링
  // *.css 생성
  .pipe( dependents() )                                   // 현재 스트림에 있는 파일에 종속되는 모든 파일을 추가
  .pipe( sourcemaps.init() )                              // 소스맵 작성
  .pipe( sass(options.scss).on('error', sass.logError) )  // scss 옵션 적용  에러 발생  watch 멈추지 않도록 logError 설정
  .pipe( postCss(options.postcss) )                       // 하위 브라우저 고려
  .pipe( sourcemaps.write() )                             // 소스맵 적용
  .pipe( gulp.dest(path_dist.css) )                       // 컴파일  css파일이 생성될 목적지 설정
  // *.min.css 생성
  .pipe( minificss() )                                    // 컴파일된 css 압축
  .pipe( rename({ suffix: '.min' }) )                     // 압축파일 *.min.css 생성
  .pipe( sourcemaps.write() )                             // 소스맵 적용
  .pipe( gulp.dest(path_dist.css) );                      // 컴파일  css파일이 생성될 목적지 설정
}

// js task
const js = () => {
  return gulp.src([
    path_src.js + '/main.js'                                  // 트렌스파일 대상 경로 (util.js  main.js  import 하기 때문에 호출 안함)
  ])
  .pipe( sourcemaps.init({ loadMaps: true }) )                // 소스 초기화 (기존의 소스 맵을 유지하고 수정하는  사용하기 위해 옵션 설정)
  .pipe( bro({                                                // 트렌스파일 시작
    transform: [
      babelify.configure({ presets: ['@babel/preset-env'] }), // ES6 이상의 문법을 일반 브라우저가 코드를 이해할  있도록 변환
      [ 'uglifyify', { global: true } ]                       // 코드 최소화  난독화
    ]
  }) )
  .pipe( sourcemaps.write('./') )                             // 소스맵 작성
  .pipe(minify({                                              // 트렌스파일 코드 압축  min 파일 생성
    ext: { min: '.min.js' },                                  // 축소된 파일 출력하는 파일 이름의 접미사 설정
    ignoreFiles: ['-min.js']                                  // 해당 패턴과 일치하는 파일을 축소하지 않음
  }))
  .pipe( gulp.dest(path_dist.js) );                           // 트렌스파일  생성될 목적지 설정
}

// image task
const image = () => {
  return gulp.src( path_src.images + '/**/*' )         // 최적화 이미지 대상
  .pipe( newer( path_dist.images ) )                   // 변경된 파일 통과, 변경되지 않은 파일 건너뛰기
  .pipe( imagemin( { verbose:true } ) )                // 이미지 최적화 ( 최적화  이미지의 정보 기록 옵션 적용 )
  .pipe( gulp.dest( path_dist.images ) );              // 최적화  생성될 목적지 설정
}

// clean task
const clean = () => del([dist]);                       // dist 폴더 삭제

// webserver task
const webserver = () => {
  return gulp.src(dist)                                // webserver 실행  폴더 경로
  .pipe(
    ws({                                               // webserver 옵션 설정
      // port: 8000,                                   // 기본 8000, 필요  변경 가능
      livereload: true,                                // 작업  파일 저장  브라우저 자동 새로고침 (기본 false)
      open: true                                       // Gulp 실행  자동으로 브라우저를 띄우고 localhost 서버 열기 (기본 false)
    })
  );
}

// watch task
const watch = () => {
  // njk(html) watch
  const html_watcher = gulp.watch(path_src.html + "/**/*", html);
  file_management(html_watcher, path_src.html, path_dist.html);

  // sass watch
  const scss_watcher = gulp.watch(path_src.css + "/**/*", css);
  file_management(scss_watcher, path_src.css, path_dist.css);

  // js watch
  const js_watcher = gulp.watch(path_src.js + "/**/*", js);
  file_management(js_watcher, path_src.js, path_dist.js);

  // image watch
  const image_watcher = gulp.watch(path_src.images + "/**/*", image);
  file_management(image_watcher, path_src.images, path_dist.images);
}
// watch - 파일 감시  삭제를 위한 함수
const file_management = (watcher_target, src_path, dist_path) => {
  watcher_target.on('unlink', (filepath) => {
    const filePathFromSrc = path.relative(path.resolve(src_path), filepath);
    const extension_type = filePathFromSrc.split('.')[filePathFromSrc.split('.').length-1];

    // scss 삭제 (min 파일까지 삭제)
    if( extension_type === 'scss' ){
      const destFilePath_css = path.resolve(dist_path, filePathFromSrc).replace('.scss','.css');
      del.sync(destFilePath_css);
      const destFilePath_minCss = path.resolve(dist_path, filePathFromSrc).replace('.scss','.min.css');
      del.sync(destFilePath_minCss);
    }

    // js 삭제 (min 파일까지 삭제)
    else if( extension_type === 'js' ){
      const destFilePath_js = path.resolve(dist_path, filePathFromSrc);
      del.sync(destFilePath_js);
      const destFilePath_minJs = path.resolve(dist_path, filePathFromSrc).replace('.js','.min.js');
      del.sync(destFilePath_minJs);
    }

    // njk(html) 삭제
    else if( extension_type === 'njk' ){
      const destFilePath_html = path.resolve(dist_path, filePathFromSrc).replace('.njk','.html');
      del.sync(destFilePath_html);
    }

    //  파일  삭제
    else{
      const destFilePath = path.resolve(dist_path, filePathFromSrc);
      del.sync(destFilePath);
    }
  });
}


// series & parallel (task 그룹화) ----------------------------------

// 순차적으로 실행되어야 하는 task 그룹
const prepare = gulp.series([ clean, image ]);

//  prepare 실행 완료  순차적으로 실행되어야 하는 task 그룹
const assets = gulp.series([ html, css, js ]);

// 동시에 여러 개의 task 실행되어야 하는 그룹 (병렬로 실행)
const live = gulp.parallel([ webserver, watch ]);


// export (gulp 실행 명령어) ----------------------------------------

// gulp build 실행 (prepare 실행  assets 실행) - build 실행
export const build = gulp.series([ prepare, assets ]);

// gulp dev 실행 (build 실행  live 실행) - build 실행 후 live 실행
export const dev = gulp.series([ build, live ]);

gulp dev 실행

gulpfile.babel.js 파일을 세팅한 후 터미널에서 아래 명령을 실행합니다.

gulp dev

Gulp가 샐행되고 터미널에 Gulp의 작업 내용이 출력됩니다.

gulp dev 실행시 터미널에 작업내용 출력

clean 다음으로 image task가 실행되고,
gulp-imageminvisual.png 파일을 어느 정도 최적화했는지에 대한 메시지가 출력됩니다.

6.3.4-2. 독립적인 이미지 최적화 프로세스 설정

gulpfile.babel.js 세팅

// import -----------------------------------------------------------
// ··· 기존 import 생략 ···

import imagemin from "gulp-imagemin";


// routes -----------------------------------------------------------



// etc --------------------------------------------------------------



// task -------------------------------------------------------------

// html task 

// css task 

// js task 

// image task 
// 이미지를 단순히 src에서 dist 이동
const image = () => {
  return gulp.src( path_src.images + '/**/*' )         // dist 이동할 이미지 대상
  .pipe( gulp.dest( path_dist.images ) );              // 이동 목적지 설정
}
// 이미지 최적화  dist 이동
const image_optimization = () => {
  return gulp.src( path_src.images + '/**/*' )         // 최적화 이미지 대상
  .pipe( imagemin( { verbose:true } ) )                // 이미지 최적화 ( 최적화  이미지의 정보 기록 옵션 적용 )
  .pipe( gulp.dest( path_dist.images ) );              // 최적화  생성될 목적지 설정
}

// clean task 

// webserver task 

// watch task 
const watch = () => {
  // njk(html) watch  
  const html_watcher = gulp.watch(path_src.html + "/**/*", html);
  file_management(html_watcher, path_src.html, path_dist.html);

  // sass watch 
  const scss_watcher = gulp.watch(path_src.css + "/**/*", css);
  file_management(scss_watcher, path_src.css, path_dist.css);

  // js watch 
  const js_watcher = gulp.watch(path_src.js + "/**/*", js);
  file_management(js_watcher, path_src.js, path_dist.js);

  // image watch 
  const image_watcher = gulp.watch(path_src.images + "/**/*", image);
  file_management(image_watcher, path_src.images, path_dist.images);
}
// watch - 파일 감시  삭제를 위한 함수 


// series & parallel (task 그룹화) ----------------------------------

// 순차적으로 실행되어야 하는 task 그룹 
const prepare = gulp.series([ clean, image ]);

//  prepare 실행 완료  순차적으로 실행되어야 하는 task 그룹 

// 동시에 여러 개의 task 실행되어야 하는 그룹 (병렬로 실행) 


// export (gulp 실행 명령어) ----------------------------------------

// gulp build 실행 (prepare 실행  assets 실행) - build 실행 

// gulp dev 실행 (build 실행  live 실행) - build 실행 후 live 실행 

// gulp image_min 실행 (이미지 최적화 하나만을 위한 명령) 
export const image_min = gulp.series([ image_optimization ]);
gulpfile.babel.js - full code
// import -----------------------------------------------------------
import gulp from "gulp";
import nunjucksRender from "gulp-nunjucks-render";
import plumber from "gulp-plumber";
import data from "gulp-data";
import cached from "gulp-cached";
import fs from "fs";
import del from "del";
import ws from "gulp-webserver";
import path from "path";
import gulpSass from "gulp-sass";
import dartSass from "dart-sass";
import sourcemaps from "gulp-sourcemaps";
import minificss from "gulp-minify-css";
import autoprefixer from "autoprefixer";
import postCss from "gulp-postcss";
import rename from "gulp-rename";
import dependents from "gulp-dependents";
import bro from "gulp-bro";
import babelify from "babelify";
import minify from "gulp-minify";
import imagemin from "gulp-imagemin";


// routes -----------------------------------------------------------
const src = './src';
const dist = './dist';
const ass = '/assets';

// src 폴더의 경로 설정
const path_src = {
  html: src + '/html',
  css: src + ass + '/css',
  images: src + ass + '/images',
  js: src + ass + '/js',
}

// 빌드될 dist 폴더의 경로 설정
const path_dist = {
  html: dist,
  css: dist + ass + '/css',
  images: dist + ass + '/images',
  js: dist + ass + '/js',
};


// etc --------------------------------------------------------------
const onErrorHandler = (error) => console.log(error);  // plumber option (에러 발생  에러 로그 출력)


// task -------------------------------------------------------------

// html task
const html = () => {
  // 들여쓰기(Tab Indent) 조정을 위한 함수
  const manageEnvironment = (environment) => {
    environment.addFilter('tabIndent', (str, numOfIndents, firstLine) => {
      str = str.replace(/^(?=.)/gm, new Array(numOfIndents + 1).join('\t'));
      if(!firstLine) {
        str = str.replace(/^\s+/, "");
      }
      return str;
    });
  };

  // _gnb.json 파일 적용을 위한 변수
  const gnbJson = JSON.parse(fs.readFileSync(path_src.html + '/_templates/_json/_gnb.json'));
  const json_all = {...gnbJson};
  const datafile = () => {
    return json_all;
  }

  // njk 빌드
  return gulp.src([
    path_src.html + '/**/*',                           // 빌드할 njk 파일 경로
    '!' + path_src.html + '/**/_*',                    // 경로  제외할 njk 파일(빌드  병합될 파일)
    '!' + path_src.html + '/**/_*/**/*'                // 경로  제외할 폴더  폴더의 njk 파일(빌드  병합될 파일)
  ])
  .pipe( plumber({errorHandler:onErrorHandler}) )      // 에러 발생  gulp 종료 방지  에러 핸들링
  .pipe( data( datafile) )                             // _gnb.json 적용
  .pipe( nunjucksRender({                              // njk 적용
    envOptions: {                                      // njk 옵션 설정
      autoescape: false,                               // njk 문법의 오류가 있더라도 진행
    },
    manageEnv: manageEnvironment,                      // 들여쓰기(Tab Indent) 함수 적용
    path: [path_src.html],                             // html 폴더 전체 경로
  }) )
  .pipe( cached('html') )                              // 변경된 파일 캐시 저장
  .pipe( gulp.dest(path_dist.html) )                   // 빌드  html 파일이 생성될 목적지 설정
}

// css task
const css = () => {
  //scss 옵션 정의
  const sass = gulpSass(dartSass);                        // ECMAScript 모듈(최신 Node.js 14 이상에서 지원됨)에서 사용하기 위해 선언
  const options = {
    scss : {
      outputStyle: "expanded",                            // 컴파일 스타일: nested(default), expanded, compact, compressed
      indentType: "space",                                // 들여쓰기 스타일: space(default), tab
      indentWidth: 2,                                     // 들여쓰기   (Default : 2)
      precision: 8,                                       // 컴파일  CSS  소수점 자리수 (Type : Integer , Default : 5)
      sourceComments: true,                               // 주석 제거 여부 (Default : false)
      compiler: dartSass,                                 // 컴파일 도구
    },
    postcss: [ autoprefixer({
      overrideBrowserslist: 'last 2 versions',            // 최신 브라우저 기준 하위 2개의 버전까지 컴파일
    }) ]
  };

  return gulp.src(
    path_src.css + '/**/*.scss',                          // 파일 대상 scss파일 찾기
    { since: gulp.lastRun(css) }                          // 변경된 파일에 대해서만 컴파일 진행
  )
  .pipe( plumber({errorHandler:onErrorHandler}) )         // 에러 발생  gulp종료 방지  에러 핸들링
  // *.css 생성
  .pipe( dependents() )                                   // 현재 스트림에 있는 파일에 종속되는 모든 파일을 추가
  .pipe( sourcemaps.init() )                              // 소스맵 작성
  .pipe( sass(options.scss).on('error', sass.logError) )  // scss 옵션 적용  에러 발생  watch 멈추지 않도록 logError 설정
  .pipe( postCss(options.postcss) )                       // 하위 브라우저 고려
  .pipe( sourcemaps.write() )                             // 소스맵 적용
  .pipe( gulp.dest(path_dist.css) )                       // 컴파일  css파일이 생성될 목적지 설정
  // *.min.css 생성
  .pipe( minificss() )                                    // 컴파일된 css 압축
  .pipe( rename({ suffix: '.min' }) )                     // 압축파일 *.min.css 생성
  .pipe( sourcemaps.write() )                             // 소스맵 적용
  .pipe( gulp.dest(path_dist.css) );                      // 컴파일  css파일이 생성될 목적지 설정
}

// js task
const js = () => {
  return gulp.src([
    path_src.js + '/main.js'                                  // 트렌스파일 대상 경로 (util.js  main.js  import 하기 때문에 호출 안함)
  ])
  .pipe( sourcemaps.init({ loadMaps: true }) )                // 소스 초기화 (기존의 소스 맵을 유지하고 수정하는  사용하기 위해 옵션 설정)
  .pipe( bro({                                                // 트렌스파일 시작
    transform: [
      babelify.configure({ presets: ['@babel/preset-env'] }), // ES6 이상의 문법을 일반 브라우저가 코드를 이해할  있도록 변환
      [ 'uglifyify', { global: true } ]                       // 코드 최소화  난독화
    ]
  }) )
  .pipe( sourcemaps.write('./') )                             // 소스맵 작성
  .pipe(minify({                                              // 트렌스파일 코드 압축  min 파일 생성
    ext: { min: '.min.js' },                                  // 축소된 파일 출력하는 파일 이름의 접미사 설정
    ignoreFiles: ['-min.js']                                  // 해당 패턴과 일치하는 파일을 축소하지 않음
  }))
  .pipe( gulp.dest(path_dist.js) );                           // 트렌스파일  생성될 목적지 설정
}

// image task
// 이미지를 단순히 src에서 dist 이동
const image = () => {
  return gulp.src( path_src.images + '/**/*' )         // dist 이동할 이미지 대상
  .pipe( gulp.dest( path_dist.images ) );              // 이동 목적지 설정
}
// 이미지 최적화  dist 이동
const image_optimization = () => {
  return gulp.src( path_src.images + '/**/*' )         // 최적화 이미지 대상
  .pipe( imagemin( { verbose:true } ) )                // 이미지 최적화 ( 최적화  이미지의 정보 기록 옵션 적용 )
  .pipe( gulp.dest( path_dist.images ) );              // 최적화  생성될 목적지 설정
}

// clean task
const clean = () => del([dist]);                       // dist 폴더 삭제

// webserver task
const webserver = () => {
  return gulp.src(dist)                                // webserver 실행  폴더 경로
  .pipe(
    ws({                                               // webserver 옵션 설정
      port: 8300,                                      // 기본 8000, 필요  변경 가능
      livereload: true,                                // 작업  파일 저장  브라우저 자동 새로고침 (기본 false)
      open: true                                       // Gulp 실행  자동으로 브라우저를 띄우고 localhost 서버 열기 (기본 false)
    })
  );
}

// watch task
const watch = () => {
  // njk(html) watch
  const html_watcher = gulp.watch(path_src.html + "/**/*", html);
  file_management(html_watcher, path_src.html, path_dist.html);

  // sass watch
  const scss_watcher = gulp.watch(path_src.css + "/**/*", css);
  file_management(scss_watcher, path_src.css, path_dist.css);

  // js watch
  const js_watcher = gulp.watch(path_src.js + "/**/*", js);
  file_management(js_watcher, path_src.js, path_dist.js);

  // image watch
  const image_watcher = gulp.watch(path_src.images + "/**/*", image);
  file_management(image_watcher, path_src.images, path_dist.images);
}
// watch - 파일 감시  삭제를 위한 함수
const file_management = (watcher_target, src_path, dist_path) => {
  watcher_target.on('unlink', (filepath) => {
    const filePathFromSrc = path.relative(path.resolve(src_path), filepath);
    const extension_type = filePathFromSrc.split('.')[filePathFromSrc.split('.').length-1];

    // scss 삭제 (min 파일까지 삭제)
    if( extension_type === 'scss' ){
      const destFilePath_css = path.resolve(dist_path, filePathFromSrc).replace('.scss','.css');
      del.sync(destFilePath_css);
      const destFilePath_minCss = path.resolve(dist_path, filePathFromSrc).replace('.scss','.min.css');
      del.sync(destFilePath_minCss);
    }

    // js 삭제 (min 파일까지 삭제)
    else if( extension_type === 'js' ){
      const destFilePath_js = path.resolve(dist_path, filePathFromSrc);
      del.sync(destFilePath_js);
      const destFilePath_minJs = path.resolve(dist_path, filePathFromSrc).replace('.js','.min.js');
      del.sync(destFilePath_minJs);
    }

    // njk(html) 삭제
    else if( extension_type === 'njk' ){
      const destFilePath_html = path.resolve(dist_path, filePathFromSrc).replace('.njk','.html');
      del.sync(destFilePath_html);
    }

    //  파일  삭제
    else{
      const destFilePath = path.resolve(dist_path, filePathFromSrc);
      del.sync(destFilePath);
    }
  });
}


// series & parallel (task 그룹화) ----------------------------------

// 순차적으로 실행되어야 하는 task 그룹
const prepare = gulp.series([ clean, image ]);

//  prepare 실행 완료  순차적으로 실행되어야 하는 task 그룹
const assets = gulp.series([ html, css, js ]);

// 동시에 여러 개의 task 실행되어야 하는 그룹 (병렬로 실행)
const live = gulp.parallel([ webserver, watch ]);


// export (gulp 실행 명령어) ----------------------------------------

// gulp build 실행 (prepare 실행  assets 실행) - build 실행
export const build = gulp.series([ prepare, assets ]);

// gulp dev 실행 (build 실행  live 실행) - build 실행 후 live 실행
export const dev = gulp.series([ build, live ]);

// gulp image_min 실행 (이미지 최적화 하나만을 위한 명령)
export const image_min = gulp.series([ image_optimization ]);

gulp dev 실행

gulpfile.babel.js 파일을 세팅한 후 터미널에서 아래 명령을 실행합니다.

gulp dev

Gulp가 샐행되고 터미널에 Gulp의 작업 내용이 출력됩니다.

gulp dev 실행시 터미널에 작업내용 출력

clean 다음으로 image task가 실행됩니다.
실시간 이미지 최적화 프로세스 설정과는 달리, 이미지 최적화 작업을 하지 않고 단순히 src의 이미지를 dist로 옮기는 작업만 진행하기에 image task의 작업 시간도 짧습니다.

gulp image_min 실행 (이미지 최적화)

프로젝트 작업 중 이미지 최적화가 필요한 시점에 gulp dev를 종료하지 않고, 새 터미널을 열고 아래 명령을 실행합니다.
(같은 폴더에 여러 개의 터미널 오픈이 가능합니다.)

gulp image_min

image_min이 단독으로 실행되고 터미널image_min의 작업 내용이 출력됩니다.

gulp image_min 실행시 터미널에 작업내용 출력

visual.png에 대한 이미지 최적화가 진행되고, 이미지 최적화를 마친 image_min 프로세스는 자동으로 종료됩니다.

이렇게 이미지 최적화 프로세스를 별도로 실행하는 방식으로 설정하면 이미지 최적화 작업을 빌드 프로세스와 분리하여, 빌드 시간을 단축하고 이미지 최적화 작업을 효율적으로 관리할 수 있습니다.
그러나 별도로 이미지 최적화 작업을 실행해야 하는 번거로움이 있을 수 있습니다.
이러한 방식은 장단점이 모두 있지만, 프로젝트 환경에 따라 어느 쪽이 더 큰 장점을 가지는지 선택하면 됩니다.

독립적인 이미지 최적화 프로세스 설정 이외에도, 이미지 최적화를 배포 단계에서 자동으로 실행하도록 설정할 수도 있습니다.
그러나 이것은 응용 단계에 해당하여 본 커리큘럼에는 포함시키지 않았습니다.

6.3.5. 빌드된 image 결과물 확인

6.3.5.1. image가 적용된 브라우저 확인

image task를 세팅하기 전에는,
브라우저에서 이미지를 찾지 못하고, Console에서 경고 메시지가 나타났지만 이제는 모든 것이 정상으로 보이게 됩니다.

image 적용 전
[image 적용 전]
image 적용 후
[image 적용 후]

또한, 이미지 용량이 886KB에서 854KB로 줄어든 것을 확인할 수 있습니다.

gulp-imagemin을 적용하여 visual.png의 이미지 용량이 886KB 에서 854KB 로 줄어듬

6.3.5.2. dist > assets 폴더와 image 파일 확인

dist > assets > images 폴더 안에 visual.png 파일이 있는 것을 확인할 수 있습니다.

빌드 후 dist > assets > image 폴더 확인

6.3.6. 로컬 작업을 위한 Gulp 세팅을 마치며

여기까지 우리는 로컬 작업을 위한 Gulp 설정을 완료했습니다.
생소한 시스템과 언어에 처음 마주쳤지만, 웹 퍼블리싱을 위한 기본 세팅을 훌륭하게 마무리했습니다.

당연히 아직 Gulp가 익숙하지 않고, 커리큘럼 없이 혼자 세팅하는 건 어려울 수 있습니다.
하지만 앞서 안내했듯이 한 번의 세팅으로 끝나지 않고 여러 번 반복하고 응용하다 보면 Gulp가 꾀 친숙해질 것입니다.

Gulp는 여기까지만 해도 충분합니다. 다음 커리큘럼은 선택사항입니다.

다음 섹션에서는 Gulp로 설정하고 빌드한 결과물을 GitHub에 배포하기 위해 gulp-gh-pages 패키지를 활용하는 과정을 살펴봅니다. 전제로 git과 GitHub를 기본적으로 활용할 수 있다는 가정하에 커리큘럼을 진행하기 때문에, git과 GitHub가 생소하다면 커리큘럼 진행이 어려울 수 있습니다.
※ 다음 커리큘럼에서는 [실시간 이미지 최적화 프로세스로 설정]gulpfile.babel.js를 이어서 진행하며, git과 GitHub를 설명하는 내용은 다루지 않습니다.