자바스크립트 라이브러리 배포를 위한 웹팩(webpack) 설정 경험기

dopyo.js를 개발하면서, 배포를 위한 웹팩(webpack)을 설정 경험을 공유하는 글입니다.


평소 데이터 시각화에 관심과 svg를 공부해보고 싶어 사이드 프로젝트로 자바스크립트 차트 라이브러리인 dopyo.js를 개발하고 있습니다. 직접 자바스크립트 라이브러리를 만들고 배포까지 경험하는 것은 처음이라, 개발하는 과정을 블로그에 기록하려고 합니다. 포스트에 잘못된 부분이나 개선할 부분 등 피드백은 언제나 환영입니다.😄

웹팩(webpack)을 직접 설정하게 된 이유

최근에 자바스크립트 프레임워크, 라이브러리로 사용하고 있는 Vue, React, Angular의 경우 boilerplate를 제공해주기 때문에 사용자가 직접 개발환경을 설정할 필요가 없습니다. 필자는 기존에 Vue를 주로 사용해서 프로젝트를 진행했습니다. 그래서 Vue CLI를 통해 Vue 개발환경을 설정하고 개발해왔기 때문에 개발할 때와 빌드할 때 생성되는 웹팩(webpack)설정을 사용하고, 따로 웹팩 설정을 직접 할 일은 드물었습니다. 그래서 사이드 프로젝트를 진행할 때는 직접 웹팩 설정해서 개발환경을 구축하겠다는 목표를 세웠습니다.

그리고... 드디어 이번에 dopyo.js 프로젝트를 진행하며, 직접 웹팩 설정을 하게 되었습니다. 특히 순수 자바스크립트를 위한 웹 개발환경 설정의 경우에는 웹에서 튜토리얼을 찾기가 어렵지 않았지만, 자바스크립트 라이브러리 배포를 위한 튜토리얼은 찾기가 어려웠는데요. (좋은 튜토리얼이 있다면 피드백 남겨주시면 감사하겠습니다! 🙏) 이번 포스트를 통해 직접 경험한 라이브러리를 위하 웹팩 설정기(라고 쓰고 삽질기...)를 소개합니다.

프로젝트 기본 구조 (Input)

일단 본격적으로 시작하기 전 dopyo.js의 기본 폴더 구조는 아래와 같습니다. src 폴더 하위에 assetsjs로 css와 javascript 폴더가 구분되고, js폴더 하위에 프로젝트의 메인 파일인 chart.js가 있고 차트 관련 파일들이 있는 charts 폴더와 utility 함수들이 있는 utils 폴더로 구분되어 있습니다.

└── src
    ├── assets
    │   └── sass
    │       ├── base
    │       │   ├── _reset.scss
    │       ├── components
    │       │   └── chart.scss
    │       └── main.sass
    └── js
        ├── chart.js
        ├── charts
        │   ├── chartBasic.js
        │   ├── lineChart.js
        │   └── ...
        └── utils
            ├── calculate.js
            ├── helper.js
            └── variables.js

목표로 하는 웹팩 build 파일 구조 (Output)

웹팩을 통해 제가 빌드하고 싶은 폴더의 구조는 아래와 같습니다.

── dist
   ├── dopyo.css
   ├── dopyo.js
   ├── dopyo.min.css
   └── dopyo.min.js

웹팩(webpack) 설치

이제 본격적으로 웹팩을 시작해보겠습니다. 아래는 웹팩 공식 페이지에서 웹팩을 소개하는 이미지인데요. 웹팩은 자바스크립트를 위한 모듈 번들러로 아래 이미지의 왼쪽 부분과 같이 개발 시 작성된 다양한 javascript 파일들과 scss, css, image 등의 파일들을 이미지의 우측과 같은 형태로 번들링 해주는 자바스크립트 라이브러리입니다.

webpack

일단 웹팩을 설치 및 실행하기 위해서는 node 환경이 필요한데요. 이번 포스트에서는 npm으로 진행해보겠습니다.

노드 프로젝트 생성

package.json 파일이 이미 프로젝트 내부에 있는 경우 이 부분은 생략해도 됩니다.

npm init

웹팩 설치

웹팩4 부터는 webpack core와 webpack-cli 패키지가 분리되었습니다. 따라서 두 패키지를 각각 설치해야 합니다. webpack-cli webpack을 터미널에서 실행하기 위한 툴입니다.

npm install --save-dev webpack webpack-cli

웹팩 설정에 필요한 패키지 설치

아래 명령어를 터미널에 입력해서 패키지들을 설치합니다.

npm install --save-dev mini-css-extract-plugin webpack-merge clean-webpack-plugin terser-webpack-plugin optimize-css-assets-webpack-plugin

각 패키지가 dopyo.js의 웹팩 설정 내에서 하는 역할은 아래와 같습니다.

웹팩 config 파일 생성

프로젝트에서 사용할 webpack.prod.js파일을 프로젝트 폴더 최상위에 생성합니다. 일반적인 웹팩 설정 파일은 webpack.config.js파일 하나를 두고 사용할 수도 있는데, dopyo.js에서는 개발 시 사용한 webpack.dev.js가 따로 있기 때문에 배포를 위한 설정은 webpack.prod.js에 작성했습니다.

mode 설정

이제 단계별로 웹팩 설정을 직접 해보겠습니다. webpack.prod.js파일에 아래와 같이 mode를 먼저 설정합니다. 배포를 위한 웹팩 설정을 진행 중이기 때문에 ‘production’ 모드를 사용하겠습니다.

웹팩4에서는 기본적으로 none, development, production 세 가지를 제공하는데, development와 production에는 개발 중과 빌드 시 사용하는 옵션이 기본적으로 세팅되어 있어서 특별한 세부 설정 없이 사용할 수 있습니다. 자세한 내용은 웹팩 공식 사이트 - mode를 참고해주세요.

module.exports = {
  mode: 'production'
}

entry와 outputs 설정

모듈의 시작점을 설정하는 entry와 번들할 파일을 어디에 저장하는지 설정하는 output을 설정합니다. dopyo.js의 최상위 모듈 파일은 src/js/chart.js로 해당 파일 내부에 사용하는 scss 파일과 javascript 파일이 작성되어 있습니다.

libraryTarget 설정

이 부분에서 가장 어려움을 겪었던 것은 libraryTarget 설정인데요. 일반적인 웹프로젝트 시 또는 npm 패키지만을 사용해서 개발할 때는 설정하지 않아도 됩니다. 하지만 이번에 dopyo.js는 cdn 배포를 고려하고 있어서, script src를 통해서 링크로도 불러올 수 있어야 했는데요. 이 옵션 없이 웹팩 설정을 할 경우 다른 자바스크립트 파일에서 모듈로 빌드된 dopyo.js를 사용할 수가 없었습니다. 그래서 모듈을 사용하지 않는 경우 글로벌 변수로 등록이 필요했습니다. (문제해결에 도움을 주신 Vahn 감사합니다. 🙏)

libraryTarget 설정은 라이브러리를 내보내는 형식을 설정하는 옵션으로 여기서 사용한 umd의 경우 AMD, CommonJS2로 내보내는 것입니다. libraryTarget의 다른 옵션에 대해서는 webpack output, webpack 설정 option에 대해서를 참고해주세요.

// path 사용 설정
const path = require('path')
module.exports = {
  mode: 'production',
  // 프로젝트의 최상위 모듈 파일을 시작점으로 설정
  entry: {
    // "dopyo"의 경우 빌드한 파일의 파일명으로 "app"과 같이 원하는 파일로 설정이 가능합니다.
    // 프로젝트 최상위 파일 경로를 작성합니다. "dopyo": './src/js/chart.js'와 같이 직접 경로를 넣을 수도 있습니다.
    "dopyo": path.resolve(__dirname, 'src/js/chart.js'),
  },
  // 빌드한 파일이 저장되는 곳
  output: {
    // 프로젝트 최상위 dist폴더로 경로 설정
    path: path.resolve(__dirname, 'dist'),
    // 번들 결과물의 파일 이름 설정
    // [name]은 위에 entry에서 설정한 파일의 이름(dopyo)입니다.
    filename: '[name].js',
    // assets이 있는 경우 해당 경로 설정해주는 옵션
    publicPath: '/',
    //
    libraryTarget: "umd",
  },
}

module과 plugins 설정

다음으로 설정할 것은 module과 plugins 설정입니다. module은 번들링 하는 과정에서 해당 파일을 어떻게 로드할지를 설정합니다. 기본적으로는 rules 하위에 javascritp, css, image 등 각 파일 특성에 따른 loader를 설정해주는데요. 이 프로젝트에서는 javascript와 scss만 사용하고 있어 해당 부분에 대한 모듈을 설정했습니다. 그리고 plugins는 번들 된 결과물에 대해 어떻게 처리하는지 설정하는데, 여기서는 css파일 추출을 위해 사용을 설정했습니다.

모듈과 플러그인을 위한 패키지 설치

npm install --save-dev @babel/core @babel/plugin-syntax-dynamic-import @babel/preset-env babel-loader css-loader sass-loader  mini-css-extract-plugin

모듈, 플러그인 설정

javascript 파일에 대해서는 babel-loader를 통해 자바스크립트 파일을 컴파일하게 설정했습니다. babel 옵션은 loader하위에 옵션으로 설정할 수도 있지만 dopyo.js에서는 테스트 코드를 위한 바벨 설정이 따로 필요한 부분이 있어서 바벨 설정을 .babelrc에 따로 분리해서 작성했습니다.

scss파일에 대해서는 번들링한 결과물에서 css파일을 따로 추출할 것이기 때문에 이때 사용하는 mini-css-extract-plugin 플러그인을 사용해서 옵션을 추가했습니다. 플러그인 설정에 대해서는 plugins 하위에 추가로 설정이 필요합니다.

...
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
module.exports = {
  ...
  module: {
    rules: [
      {
        // .js 확장자를 가진 파일에 대해 적용
        test: /\.js$/,
        // node_modules 폴더 제외
        exclude: /(node_modules)/,
        use: {
          // babel-loader 사용
          loader: 'babel-loader',
        }
      },
      {
        // .sass .scss 확장자를 가진 파일에 대해 적용
        test: /\.(sass|scss)$/,
        use: [
          // loader 사용 설정
          MiniCssExtractPlugin.loader,
          "css-loader",
          "sass-loader"
        ]
      }
    ]
  },
  plugins: [
    // MiniCssExtractPlugin 플러그인 설정
    // [name]은 entry에서 설정한 "dopyo"를 의미
    new MiniCssExtractPlugin({
      filename: "[name].css",
    })
  ]
}

자바스크립트 난독화(Uglify)와 css압축(minify) 파일 추출

webpack --config webpack.prod.js

여기까지 마친 상태에서 위의 명령어를 터미널에 입력해서 빌드를 진행해보면 dist 폴더 하위에 dopyo.js, dopyo.css이 생성됩니다.

dopyo_dist

하지만 js파일은 압축이 되어있고, css파일은 압축이 되어있지 않은 상태로 빌드가 됩니다.

!function(t,n){if("object"==typeof exports&&"object"==typeof module)module.exports=n();else if("function"==typeof define&&define.amd)define([],n);else{var e=n();for(var r in e)("object"==typeof exports?exports:t)[r]=e[r]}}(window,function(){retu
...
.container {
  box-sizing: border-box;
  width: 100%;
  padding: 20px; }

#chart {
  width: 100%; }
...

위에서 이야기했듯이 제가 원하는 결과물은 아래와 같이 압축을 하지 않은 것과 압축한 결과물 두 가지입니다.

── dist
   ├── dopyo.css
   ├── dopyo.js
   ├── dopyo.min.css
   └── dopyo.min.js

그래서 먼저 시도한 방법은 자바스크립트를 난독화한 버전과 난독화하지 않은 버전 두 가지로 만드는 것입니다.

웹팩으로 자바스크립트 난독화 파일 생성하기

일반적으로 웹팩에서 기본으로 제공하는 modeproduction 옵션은 기본적으로 자바스크립트 결과물에 대해 난독화를 해줍니다. 하지만 이 프로젝트에서는 난독화와 난독화 하지 않고 babel 컴파일만 한 자바스크립트 두 파일이 필요합니다.

먼저 자바스크립트 난독화를 위한 플러그인을 설치합니다.

npm install --save-dev terser-webpack-plugin

그리고 기존 웹팩 설정을 이렇게 바꿔줍니다.

...
// terser-webpack-plugin 플러그인 추가
const TerserJSPlugin = require('terser-webpack-plugin');
module.exports = {
  // entry 파일을 2가지로 분리
  entry: {
    "dopyo": path.resolve(__dirname, 'src/js/chart.js'),
    // 난독화할 파일에 대한 설정 추가 dopyo.min이 파일의 이름이 되도록 설정
    "dopyo.min": path.resolve(__dirname, 'src/js/chart.js'),
  },
  ...
  optimization: {
    minimize: true,
    // ~.min.js 파일에 .min.js가 있는 경우에만 난독화를 하도록 설정
    minimizer: [new TerserJSPlugin({
      include: /\.min\.js$/
    })]
  }
}

이렇게 작성해보니 dist폴더를 보니 제가 원하는 구조로 설정이 완료된 것 같습니다.

dopyo_dist2

하지만 파일의 내부를 살펴보니 js파일은 원하는대로 빌드가 됐지만, css는 아니었습니다. css 파일의 경우 두 파일이 다 압축되지 않은 상태였습니다.

(function webpackUniversalModuleDefinition(root, factory) {
	if(typeof exports === 'object' && typeof module === 'object')
		module.exports = factory();
	else if(typeof define === 'function' && define.amd)
		define([], factory);
	else {
		var a = factory();
		for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
	}
})(window, function() {
...
!function(t,n){if("object"==typeof exports&&"object"==typeof module)module.exports=n();else if("function"==typeof define&&define.amd)define([],n);else{var e=n();for(var r in e)("object"==typeof exports?exports:t)[r]=e[r]}}(window,function(){retu
...
.container {
  box-sizing: border-box;
  width: 100%;
  padding: 20px; }

#chart {
  width: 100%; }
...
.container {
  box-sizing: border-box;
  width: 100%;
  padding: 20px; }

#chart {
  width: 100%; }
...

웹팩으로 css파일 압축 파일 생성하기

css 파일의 경우 어떻게 압축한 버전과 압축하지 않은 버전이 있는지 찾아봤습니다. 하지만 웹팩 css 압축 플러그인인 optimize-css-assets-webpack-plugin에서는 terser-webpack-plugin와 같이 특정 파일만 압축을 진행할 수가 없었습니다. css 파일을 multiple하게 ouput을 생성하고 싶다면 웹팩 설정을 각각 추가해서 진행할 것을 이야기했습니다. 그래서 일반 버전과 압축 버전 설정을 따로 추가했습니다.

css 압축을 위한 플러그인을 설치합니다.

npm install --save-dev optimize-css-assets-webpack-plugin

그리고 기존 웹팩 설정을 이렇게 바꿔줍니다.

...
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = [
  // 일반 버전과 압축 버전 두 가지로 웹팩 빌드 설정
  {
    entry: {
      "dopyo": path.resolve(__dirname, 'src/js/chart.js'),
    },
    ...
    // 일반 버전의 경우 production모드에서 자바스크립트 난독화가 default값으로 진행되기 때문에 난독화를 false로 설정
    optimization: {
      minimize: false
    }
  },
  {
    entry: {
      "dopyo.min": path.resolve(__dirname, 'src/js/chart.js'),
    },
    ...
    // 압축 버전에서는 js와 css의 난독화를 진행하는 옵션을 추가
    optimization: {
      minimizer: [
        new TerserJSPlugin({}),
        new OptimizeCSSAssetsPlugin({}),
      ]
    },
  }
]

이렇게 수정하고 빌드를 하면 목표로 했던 모습의 파일이 빌드된 것을 확인할 수 있습니다.

(function webpackUniversalModuleDefinition(root, factory) {
	if(typeof exports === 'object' && typeof module === 'object')
		module.exports = factory();
	else if(typeof define === 'function' && define.amd)
		define([], factory);
	else {
		var a = factory();
		for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
	}
})(window, function() {
...
!function(t,n){if("object"==typeof exports&&"object"==typeof module)module.exports=n();else if("function"==typeof define&&define.amd)define([],n);else{var e=n();for(var r in e)("object"==typeof exports?exports:t)[r]=e[r]}}(window,function(){retu
...
.container {
  box-sizing: border-box;
  width: 100%;
  padding: 20px; }

#chart {
  width: 100%; }
...
.container{box-sizing:border-box;width:100%;padding:20px}#chart{width:100%}.chart{margin:0 auto}.title{text-align:center}.axis...
...

리팩토링

원하는 결과물을 얻을 수 있었지만, 웹팩 설정에서 일반적인 옵션과 압축을 위한 옵션에 mode나 module, plugin을 보면 겹치는 부분이 있습니다. 그래서 이렇게 겹치는 부분은 어떻게 효율적으로 개선할 수 있는지 알아봤습니다.

공통 설정 분리

웹팩에는 webpack-merge라는 플러그인이 있는데, 보통 공통된 설정을 분리하고 추가한 설정을 함께 사용할 때 적용하는 플러그인입니다.

npm install --save-dev webpack-merge

플러그인을 설치 후, 공통으로 겹치는 부분을 webpack.common.js로 분리하겠습니다.

const path = require('path')
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
module.exports = {
  mode: 'production',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
    publicPath: '/',
    libraryTarget: "umd",
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /(node_modules)/,
        use: {
          loader: 'babel-loader',
        }
      },
      {
        test: /\.(sass|scss)$/,
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader",
          "sass-loader"
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].css",
    })
  ]
}

그리고 추가로 따로 변경 옵션이 필요한 부분만 남겨서 webpack.prod.js파일을 수정하겠습니다. 이 때 clean-webpack-plugin 플러그인을 추가로 설치했는데요. 이 플러그인은 기존에 빌드를 진행할 때 덮어씌워 지는 dist 폴더를 빌드를 진행할 때 폴더의 기존 파일을 삭제시키는 역할을 합니다. 혹시 모를 충돌 상황을 대비했습니다. 최종으로 설정을 끝낸 webpack.prod.js파일입니다.

const path = require('path')
// webpack-merge 플러그인 추가
const merge = require('webpack-merge')
// webpack 공통 설정 추가 
const common = require('./webpack.common.js')
// clean-webpack-plugin 추가 
const CleanWebpackPlugin = require('clean-webpack-plugin')
const TerserJSPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')

module.exports = [
  merge(common, {
    entry: {
      "dopyo": path.resolve(__dirname, 'src/js/chart.js'),
    },
    // 웹팩 빌드를 시작할 때 dist폴더를 비우도록 설정
    plugins: [
      new CleanWebpackPlugin(['dist']),
    ],
    optimization: {
      minimize: false
    }
  }),
  merge(common, {
    entry: {
      "dopyo.min": path.resolve(__dirname, 'src/js/chart.js'),
    },
    optimization: {
      minimizer: [
        new TerserJSPlugin({}),
        new OptimizeCSSAssetsPlugin({}),
      ]
    },
  })
]

그리고 마지막으로 터미널에서 package.json 파일에 아래의 scripts를 추가하면, 이제 터미널에서 npm run build 명령어를 입력하면 프로젝트 빌드를 실행하도록 설정합니다.

...
"scripts": {
  "build": "webpack --config webpack.prod.js"
},
...

느낀점

주니어 개발자로 웹팩을 처음 마주했을 때는 익숙하지 않은 문법들로 인해 선뜻 시작하지를 못했습니다. 이미 프레임워크와 라이브러리에서 기본 설정을 제공해주기 때문에 직접 설정할 필요를 느끼지 못해서 공부를 뒤로 미루곤 했습니다. 그래서 이번에 프로젝트를 진행하면서 웹팩 공부와 함께 직접 설정을 하는 것을 목표로 했습니다. 실제로 웹팩 학습과 코드를 작성하는 부분에서 시간이 걸렸고, 이 과정에서 느낀 경험기는 블로그에 꼭 남겨두고 싶어 이번 포스트를 작성했습니다. 중간중간 설명을 위한 코드를 넣다 보니 포스트가 많이 길어졌는데😅, 혹시 내용 중에 틀린 부분이나 개선할 점은 피드백을 주시면 바로 반영하겠습니다. 긴 글 읽어주셔서 감사합니다.🙏

참고문헌


Intersection Observer API의 사용법과 활용방법

Web API 중 하나인 Intersection Observer API를 알아보고 어떻게 활용할 수 있는지에 대해 정리한 글입니다.


Intersection Observer API(교차 관찰자 API)를 들어본 적이 있나요? 크롬 51버전부터 사용할 수 있는 이 Web API는 2016년 4월 구글 개발자 페이지 통해 소개되었습니다. MDN을 비롯해서 Intersection Observer를 어떻게 활용할 수 있을지 살펴보니 생각보다 다양한 곳에 적용할 수 있고, 앞으로 유용하게 쓸 수 있을 것 같습니다. MDN에서는 Intersection Observer의 필요성을 아래와 같은 예를 들어 설명하고 있습니다.

현재(2019년 1월 기준)는 웹, 모바일 크롬, 안드로이드, 파이어폭스 등에서 지원하고 있으며 아직 사파리, 모바일 사파리에서는 지원하지 않고 있습니다. 따라서 사파리, 아이폰의 경우 예시가 제대로 동작하지 않을 수 있습니다.

WebKit(2019년 2월 기준)에 따르면 Safari Technology Preview, macOS 10.14.4 beta, iOS 12.2 beta 버전에서 Intersection Observer를 사용할 수 있습니다.

기존 scroll 이벤트의 문제

웹사이트를 개발할 때 특정 위치에 도달했을 때 어떤 액션을 취해야 한다면 어떻게 구현할 수 있을까요? 보통 addEventListener()scroll 이벤트가 먼저 떠오릅니다. document에 스크롤 이벤트를 등록하고, 특정 지점을 관찰하며 엘리먼트가 위치에 도달했을 때 실행할 콜백함수를 등록하는 것이죠.

하지만 scroll 이벤트는 단시간에 수백번, 수천번 호출될 수 있고 동기적으로 실행되기 때문에 메인 스레드(Main Thread) 영향을 줍니다. 또한 한 페이지 내에 여러 scroll 이벤트(무한 스크롤, 광고 배너, 애니메이션 등)가 등록되어 있을 경우, 각 엘리먼트마다 이벤트가 등록되어 있기 때문에 사용자가 스크롤할 때마다 이를 감지하는 이벤트가 끊임없이 호출됩니다. (디바운싱(Debouncing)쓰로틀링(Throttling)을 통해 이러한 문제를 개선시킬 수도 있습니다.) 그리고 특정 지점을 관찰하기 위해서는 getBoundingClientRect() 함수를 사용해야 하는데, 이 함수는 리플로우(reflow) 현상이 발생한다는 단점이 있습니다.

리플로우(reflow): 리플로우는 브라우저가 웹 페이지의 일부 또는 전체를 다시 그려야하는 경우 발생한다.

예시를 통해 살펴보겠습니다. 간단한 addEventListener()scroll 이벤트를 구현했습니다. 특정 위치에 도달하면 box엘리먼트에 애니메이션을 동작시키는 코드입니다.

// 해당 요소가 viewport 내에 있는지 확인하는 함수
// 대표적인 예시로 사용되고 있는 stackoverflow의 예시를 가져왔습니다.
// https://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport/7557433#7557433
function isElementInViewport (el) {
  var rect = el.getBoundingClientRect();
  return (
    rect.top >= 0 &&
    rect.left >= 0 &&
    rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
    rect.right <= (window.innerWidth || document.documentElement.clientWidth)
  );
}
// scroll 이벤트를 추가하고, 해당 element에 callback 함수를 등록하는 함수
const addEventToEl = (elList) => {
  document.addEventListener('scroll', () => {
    elList.forEach(el => {
    	if (isElementInViewport(el)) {
        el.classList.add('tada');
      } else {
        el.classList.remove('tada');
      }
    })
  })
}
// 동작시킬 elements리스트에 이벤트를 등록
const boxElList = document.querySelectorAll('.box');
addEventToEl(boxElList);

해당 코드를 크롬 개발자 도구의 Performance 탭을 통해 확인해보면 getBoundingClientRect()호출하는 과정에서 Recalculate Style, 리플로우 현상이 발생하는 것을 확인할 수 있습니다.

addEventListener scroll 예시

Intersection Observer API의 등장

Intersection Observer API(교차 관찰자 API)를 사용하면 위와 같은 문제를 해결할 수 있습니다. 비동기적으로 실행되기 때문에 메인 스레드에 영향을 주지 않으면서 변경 사항을 관찰할 수 있습니다. 또한 IntersectionObserverEntry의 속성을 활용하면 getBoundingClientRect()를 호출한 것과 같은 결과를 알 수 있기 때문에 따로 getBoundingClientRect() 함수를 호출할 필요가 없어 리플로우 현상을 방지할 수 있습니다.

아래는 위에 예시와 같은 동작을 하지만 Intersection Observer API를 사용해서 구현한 예제입니다.

// IntersectionObserver 를 등록한다.
const io = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    // 관찰 대상이 viewport 안에 들어온 경우 'tada' 클래스를 추가
    if (entry.intersectionRatio > 0) {
      entry.target.classList.add('tada');
    }
    // 그 외의 경우 'tada' 클래스 제거
    else {
      entry.target.classList.remove('tada');
    }
  })
})

// 관찰할 대상을 선언하고, 해당 속성을 관찰시킨다.
const boxElList = document.querySelectorAll('.box');
boxElList.forEach((el) => {
  io.observe(el);
})

이 코드 또한 개발자 도구의 Performance 탭을 통해 확인해보면, 이 코드는 위의 예제와 달리 리플로우 현상이 발생하지 않는 것을 확인할 수 있습니다.

intersection observer 예시

Intersection Observer 사용 방법

Intersection Observer의 사용법에 대해 알아보겠습니다. MDN에서는 IntersectionObserver를 아래와 같이 정의하고 있습니다.

The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document’s viewport.

Intersection Observer API는 타겟 엘리먼트가 조상 엘리먼트, 또는 최상위 문서의 뷰포트(브라우저에서는 보통 브라우저의 viewport)의 교차영역에서 발생하는 변화를 비동기로 관찰하는 방법을 제공합니다.

기본 사용 방법은 아래와 같습니다. 여러 엘리먼트에 이벤트를 한 번에 등록하고 싶다면 콜백함수에 forEach를 사용해서 선언할 수 있습니다. IntersectionObserver를 생성하기 위해서는 교차되었을 때 실행할 callback함수를 등록해야 하고, 선택적으로 options 값을 넘겨줄 수 있습니다.

// 기본구조는 콜백함수와 옵션을 받는다.
const io = new IntersectionObserver(callback[, options])

Parameters

callback

options

위의 설명을 바탕으로 실제 어떻게 사용하는지 예시를 한 번 보겠습니다.

// options 설정
const options = {
  root: document.querySelector('.container'), // .container class를 가진 엘리먼트를 root로 설정. null일 경우 브라우저 viewport
  rootMargin: '10px', // rootMargin을 '10px 10px 10px 10px'로 설정
  threshold: [0, 0.5, 1] // 타겟 엘리먼트가 교차영역에 진입했을 때, 교차영역에 타켓 엘리먼트의 50%가 있을 때, 교차 영역에 타켓 엘리먼트의 100%가 있을 때 observe가 반응한다.
}

// IntersectionObserver 생성
const io = new IntersectionObserver((entries, observer) => {
  // IntersectionObserverEntry 객체 리스트와 observer 본인(self)를 받음
  // 동작을 원하는 것 작성
  entries.forEach(entry => {
    // entry와 observer 출력
    console.log('entry:', entry);
    console.log('observer:', observer);
  })
}, options)

Methods

IntersectionObserver.observe(targetElement)

IntersectionObserver.unobserve(targetElement)

IntersectionObserver.disconnect()

IntersectionObserver.takerecords()

IntersectionObserverEntry 객체

위에서 IntersectionObserver에 대해 설명했을 때 IntersectionObserver에서 반환하는 callback은 IntersectionObserverEntry 객체의 배열을 반환한다고 했는데요. IntersectionObserver를 사용할 때 반환되는 이 객체의 정보는 어떤 동작을 등록하거나 할 때 유용하게 사용할 수 있습니다.

Properties

사각형의 크기를 반환하는 속성

아래 세 가지 속성은 addEventListener를 설명할 때 언급했던 Element.getBoundingClientRect()를 실행한 것과 같은 결과를 반환합니다. Element.getBoundingClientRect() 함수의 경우 호출 시 리플로우(reflow) 현상이 나타나지만, 아래의 속성을 사용하면 리플로우 없이 정보를 알 수 있습니다.

`intersectionobserverentry 예시`

유용한 정보를 반환하는 속성

Lazy-Loading 예제

이제 IntersectionObserver API에 대해 알아본 것을 바탕으로 이를 활용한 Lazy-Loading 예제를 만들어보겠습니다.

<div class="example">
  <img src="https://picsum.photos/600/400/?random?0" alt="random image" class="image-default">
  <img data-src="https://picsum.photos/600/400/?random?1" alt="random image" class="image">
  <img data-src="https://picsum.photos/600/400/?random?2" alt="random image" class="image">
  <img data-src="https://picsum.photos/600/400/?random?3" alt="random image" class="image">
  <img data-src="https://picsum.photos/600/400/?random?4" alt="random image" class="image">
  <img data-src="https://picsum.photos/600/400/?random?5" alt="random image" class="image">
  <img data-src="https://picsum.photos/600/400/?random?6" alt="random image" class="image">
  <img data-src="https://picsum.photos/600/400/?random?7" alt="random image" class="image">
</div>

html의 경우 기본으로 불러올 img 하나를 제외하고 나머지 속성은 src 대신 data-src 속성에 이미지 주소를 선언했습니다.

일반적으로 <img> 태그는 src 속성을 만나면 이미지 소스를 내려받지만, 예시에서는 src 속성 대신에 data-src에 이미지 주소를 넣고 타겟 이미지가 교차 영역에 진입했을 때 타겟의 srcdata-src를 설정해주도록 했습니다.

// IntersectionObserver의 options를 설정합니다.
const options = {
  root: null,
  // 타겟 이미지 접근 전 이미지를 불러오기 위해 rootMargin을 설정했습니다.
  rootMargin: '0px 0px 30px 0px',
  threshold: 0
}

// IntersectionObserver 를 등록한다.
const io = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    // 관찰 대상이 viewport 안에 들어온 경우 image 로드
    if (entry.isIntersecting) {
      // data-src 정보를 타켓의 src 속성에 설정
      entry.target.src = entry.target.dataset.src;
      // 이미지를 불러왔다면 타켓 엘리먼트에 대한 관찰을 멈춘다.
      observer.unobserve(entry.target);
    }
  })
}, options)

// 관찰할 대상을 선언하고, 해당 속성을 관찰시킨다.
const images = document.querySelectorAll('.image');
images.forEach((el) => {
  io.observe(el);
})

자바스크립트의 경우 위와 같이 구현합니다. css의 경우 아래 jsfiddle 예제의 css 탭을 참고하시면 될 것 같습니다. 아래는 위의 코드를 구현해놓은 예제입니다. 스크롤을 내려보면, 스크롤이 해당 이미지의 위치에 도달했을 때 이미지를 로딩하고 있습니다.

크롬 개발자 도구의 Network 탭을 보면 순차적으로 이미지를 불러오는 것을 확인할 수 있습니다.

lazyloading 예시

마무리

이상으로 IntersectionObserver API에 대해 알아봤습니다. IntersectionObserver를 활용하면 기존에 사용하고 있던 scroll 이벤트 중 IntersectionObserver로 구현할 수 있는 경우에는 기존 코드 대비 성능을 개선할 수 있고, 그 외에도 웹에 게시된 광고의 수익을 활용하는 등 다양한 영역에 활용할 수 있을 것 같습니다. 아직 Safari에서 지원을 하지 않는다는 아쉬움이 있지만, polyfill을 제공하고 있기 때문에 한 번 사용해보는 것도 좋을 것 같습니다.

참고문헌


초보자를 위한 깃(Git) 사용법

버전 관리 시스템인 깃(Git)의 사용법에 대해 정리한 글입니다.


프로그래밍 프로젝트를 진행할 때 이전 버전의 코드를 보고 싶을 때, 또는 팀원들과 함께 프로젝트를 진행할 때가 있습니다. 또 직접 작성한 코드를 회사, 타인에게 보여주고 싶을 때도 있죠. 그럴 때 유용하게 사용할 수 있는 것이 깃(Git)입니다.

깃

source: wikipedia

깃(Git)은 프로그램 등의 소스 코드 관리를 위한 분산 버전 관리 시스템이다. 기하학적 불변 이론을 바탕으로 설계됐고, 빠른 수행 속도에 중점을 두고 있는 것이 특징이다. 최초에는 리누스 토르발스가 리눅스 커널 개발에 이용하려고 개발하였으며, 현재는 다른 곳에도 널리 사용되고 있다. - Wikipedia

흔히 깃(Git)과 깃허브(Github)를 동일한 것으로 생각할 때가 있는데요. 깃허브는 깃 레포지토리를 웹에 호스팅할 수 있도록 지원하는 서비스입니다. 이러한 호스팅 서비스는 깃허브를 비롯해서 깃랩(Gitlab) 등 다양한 서비스가 있습니다.

깃(Git) 설치 & 설정

깃 설치하기

Mac os에서는 Xcode가 설치되어 있다면 기본적으로 git이 설치되어 있습니다. 깃이 설치되었는지 확인하고 싶다면 터미널 창에 git --version라는 깃의 버전을 확인하는 명령어를 통해서 확인할 수 있습니다.

깃 config 설정

깃 설치를 완료했다면 git config 설정을 통해 사용 환경을 세팅할 수 있습니다.

더 자세한 설정을 알고싶다면 공식 문서를 참고하세요.

공식 가이드

깃 시작하기

깃 설치와 세팅을 완료했다면, 이제 깃을 시작해볼까요? 깃은 레포지토리(폴더)에 깃 저장소를 만들거나 다른 서버에 있는 저장소를 클론할 수 있습니다.

레포지토리(폴더)를 깃 저장소로 만들기

터미널 창에서 깃 저장소 설치를 원하는 폴더로 이동한 후 아래의 명령어를 입력하면 .git이라는 파일이 생성됩니다.

git init

.git은 기본적으로 숨김 파일이기 때문에 숨김 파일을 보이게 설정하거나 터미널 명령어에 ls -a를 입력하면 숨김 파일까지 포함한 폴더와 파일을 볼 수 있습니다.

레포지토리의 상태 확인하기

깃 저장소를 설치한 레포지토리는 내부에 파일이 수정거나 추가, 삭제와 같은 변경사항이 있을 때를 추적할 수 있습니다.

git status

위의 명령어를 입력하면 변경이 있는 파일에 대해서는 빨간색(기본)으로 Untracked상태라고 알려줍니다. Tracked 된 파일의 경우 보통 초록색으로 표시됩니다. 만약 변경된 파일이 없을 경우 nothing to commit, working directory clean이렇게 커밋할 파일이 없다고 나옵니다.

파일을 staging area로 올리기

git add

source: git-scm

위의 그림을 보면 이해가 쉬운데요. 깃 저장소는 그림과 같이 구성되는데요. untracked되었다는 것은 아직 working directory에 있다는 것이고, 그 파일을 tracked되게 하려면 아래의 명령어를 통해 파일을 staging area로 옮겨줘야 합니다.

git add 파일명     # 파일 단위로 올리는 방법
git add .        # 변경된 전체 파일을 올리는 방법

이렇게 파일을 staging area로 옮겼다면, 이제 커밋을 통해 레포지토리에 변경사항을 적용할 수 있습니다. 그리고 혹시 staging area 올린 파일을 untracked로 변경하고 싶다면 아래의 명령어를 통해 해결할 수 있습니다.

git reset HEAD 파일명

commit을 통해 레포지토리에 변경사항 적용하기

변경사항을 레포지토리에 적용시키기 위해서는 아래의 명령어를 입력해서 커밋하면 됩니다.

git commit                        # 깃 에디어를 통해 커밋 메세지 입력
git commit -m "commit message"    # 인라인으로 커밋 메세지 입력하는 법

만약 staging area에 파일을 추가하고 커밋 메세지를 입력하는 것이 번거롭다면 -a옵션을 추가해서 한 번에 해결할 수 있습니다.

git commit -a -m "commit message"

이렇게 깃 기본 사용법에 대해 알아봤습니다. 깃을 잘 활용하면 프로젝트 관리에 유용하게 사용할 수 있는데요. 만약 터미널 환경이 익숙하지 않다면 소스트리와 같은 프로그램을 활용하는 방법도 있습니다.

또한 깃에 대해 더 자세히 알고 싶다면 아래의 강의를 추천합니다. 필자는 Udacity 강의를 통해 깃을 공부했는데요. 생활코딩 또한 Git 강의를 제공하고 있습니다.

참고문헌


마크다운(Markdown) 사용법

마크다운(Markdown) 문법에 대해 정리한 포스트입니다.


개발을 공부하다보면, 특히 깃허브(github)를 사용하면 마크다운(Markdown)의 필요성을 느끼게 됩니다. 깃허브에 자신이 올린 레포지토리에 관한 설명을 적을 때나, 도큐멘테이션 작업을 할 때 마크다운을 사용하게 되는데요. 현재 제가 작성중인 지킬(jekyll) 또한 마크다운으로 작성합니다. 오늘은 마크다운으로 문서를 정리할 때 꼭 필요한 문법들을 정리해보겠습니다.

제목(Heading)

문서를 작성할 때 가장 기본이 되는 제목은 HTML의 <h1>~<h6> 태그와 유사합니다. #의 개수에 따라 글자의 크기가 달라집니다. #<h1>, ###<h3> ######<h6>

# Heading
### Heading
###### Heading

Heading

Heading

Heading

본문(paragraph)

HTML의 <p>와 같은 본문은 텍스트를 그대로 작성하면 됩니다.

Lorem ipsum dolor sit amet, consectetur adipisicing elit

Lorem ipsum dolor sit amet, consectetur adipisicing elit

인용(Blockquotes)

인용은 >를 넣어서 작성합니다.

> Lorem ipsum dolor sit amet, consectetur adipisicing elit
>> Lorem ipsum dolor sit amet, consectetur adipisicing elit

Lorem ipsum dolor sit amet, consectetur adipisicing elit

Lorem ipsum dolor sit amet, consectetur adipisicing elit

리스트

순서가 없는 리스트(Unordered List)

* 또는 -를 사용해서 순서가 없는 리스트를 작성할 수 있습니다. tab 또는 2칸 띄어쓰기를 통해 중첩된 항목을 작성할 수 있습니다.

* Frontend
  * HTML
  * CSS
  * JavaScript
    * Vue.js

- Frondend
  - HTML
  - CSS
  - JavaScript
    - Vue.js

순서가 있는 리스트(Ordered List)

1. HTML
2. CSS
3. JavaScript
  1. HTML
  2. CSS
  3. JavaScript
1. HTML
1. CSS
1. JavaScript
  1. HTML
  2. CSS
  3. JavaScript
1. Frontend
    1. HTML
    2. CSS
    3. JavaScript
        1. Vue.js
2. Backend
  1. Frontend
    1. HTML
    2. CSS
    3. JavaScript
      1. Vue.js
  2. Backend

코드블럭(Code blocks)

코드블럭은 일반 문장 사이에 단어, 짧은 문장 단위로 표현할 수 있는 방법과 여러줄의 코드를 삽입하는 방법이 있습니다.

단어, 한 줄의 코드를 감싸는 경우 `를 앞뒤로 감쌉니다.

마크다운은 코드블럭을 `<pre>`와 `<code>`로 감쌉니다.

마크다운은 코드블럭을 <pre><code>로 감쌉니다.

여러줄의 코드를 나타내는 코드블럭의 경우 코드블럭의 시작과 끝을 ```으로 감싸고 내부에 코드를 작성하면 됩니다.

function square(n) {
  return n * n;
}

수평선(Horizontal Rules)

문단과 문단 사이를 나눌 때 등 사용되는 수평선은 HTML의 <hr />과 같이 동작합니다.

* * *
***
*****
- - -
---------------------------------------





HTML의 하이퍼링크와 같은 링크는 다음과 같이 작성합니다. title은 생략이 가능합니다.

[example](http://example.com "title")

검색엔진은 [구글](https://www.google.com "구글")을 사용합니다.

example

검색엔진은 구글을 사용합니다.

강조(Emphasis)

HTML의 <em>과 같은 동작을 하는 강조는 *, _가 있고 <strong>**__를 사용합니다. 취소선은 ~~을 사용합니다.

*강조*한 텍스트
_강조_한 텍스트

강조한 텍스트

**강조**한 텍스트
__강조__한 텍스트

강조한 텍스트

~~취소~~한 텍스트

취소한 텍스트

이미지 삽입(Images)

이미지는 역시 HTML의 <img>태그와 동일하게 작동합니다. 대체 택스트를 삽입할 수 있으며, 링크 또는 로컬의 이미지파일을 연결할 수 있습니다.

![대체 텍스트](/경로/example.jpg)
![대체 텍스트](링크)
![Github](./public/img/3/github.png)

Github

![Github](https://assets-cdn.github.com/images/modules/open_graph/github-octocat.png)

Github

이상으로 마크다운의 기본 문법에 대해 알아봤습니다.

참고문헌


Jekyll 블로그 github를 통해 퍼블리싱하는 방법

Jekyll(지킬)을 활용해서 블로그 글을 작성하고 github를 통해 배포하는 방법


지난 포스트를 통해 Jekyll을 설치하고 시작하는 방법에 대해 알아봤는데요.

Jekyll로 시작하는 블로그

지난번에 만든 파일을 활용해서 Github에 연결하고 배포하는 방법을 소개해드릴게요.

.
├── Gemfile
├── Gemfile.lock
├── _config.yml
├── _posts
│   └── 2017-05-06-welcome-to-jekyll.markdown
├── _site
│   ├── about
│   │   └── index.html
│   ├── assets
│   │   └── main.css
│   ├── feed.xml
│   ├── index.html
│   └── jekyll
│       └── update
│           └── 2017
│               └── 05
│                   └── 06
│                       └── welcome-to-jekyll.html
├── about.md
└── index.md

9 directories, 11 files

Jekyll을 오류없이 설치했다면 생성한 블로그 폴더에 위와 같은 구조를 갖춘 폴더와 파일들이 설치됩니다.

블로그 기본 설정(setting)

Jekyll 설치 후 가장 먼저 해야할 일은 블로그의 타이틀을 비롯해서 필자의 정보(이메일, SNS 계정 등)을 설정하는 것인데요. 블로그 폴더 최상단에 위치한 _config.yml 파일에서 블로그 기본 세팅을 할 수 있습니다.

title: Your awesome title
email: your-email@domain.com
description: > # this means to ignore newlines until "baseurl:"
  Write an awesome description for your new site here. You can edit this line in _config.yml. It will appear in your document head meta (for Google search results) and in your feed.xml site description.
baseurl: "" # the subpath of your site, e.g. /blog
url: "" # the base hostname & protocol for your site, e.g. http://example.com
twitter_username: jekyllrb
github_username:  jekyll

블로그 글 작성

Jekyll은 마크다운(markdown) 형식으로 포스트를 작성해서 Github을 통해 퍼블리싱을 하는 방식으로 블로그를 운영하는데요. _post 폴더에 .markdown, .md 형식으로 블로그 글을 작성하고 저장하면 됩니다. 여기서 지켜야 할 규칙 몇 가지가 있는데요.

포스트 파일 형식

블로그에 올릴 각각의 포스트는 일정한 이름 규칙을 따라야 합니다. Jekyll블로그를 설치했을 때 기본적으로 생성되는 예시 포스트를 보면

2017-05-06-welcome-to-jekyll.markdown

위와 같은 형식을 갖는데요. 년-월-일-포스트 제목 순으로 파일 이름을 작성하면 됩니다.

포스트 기본 형식

그리고 포스트의 내부를 보면 상단에 포스트에 관련된 정보를 작성하는 란이 있습니다.

---
layout: post
title:  "Welcome to Jekyll!"
date:   2017-05-06 13:45:35 +0900
categories: jekyll update
---

글을 작성하기 전 상단에 위와 같은 형식으로 제목, 날짜, 카테고리 등을 설정해주면 퍼블리싱을 했을 때 글의 제목과 작성된 날짜 등이 포스트에 나타납니다.

블로그 설정

Github에 배포하기

이제 블로그를 온라인에 배포해야하는 단계가 남았는데요. Github을 통해 퍼블리싱을 할 경우 무료로 블로그를 배포할 수 있다는 장점이 있습니다. Github에 퍼블리싱을 하기 위해서는 일단 Github 계정이 있어야 합니다. 계정이 없을 경우 Github에서 계정을 만들어주세요. Github 사이트

Github 레포지토리 만들기

회원가입 혹은 로그인을 완료했다면, 우측 상단의 +아이콘을 누르고 New Repository를 클릭하거나, 우측 하단의 New repository라는 버튼을 클릭해서 레포지토리를 생성할 수 있습니다.

Github repository

Github에 블로그 폴더 업로드

레포지토리를 만드는 것까지 완료했다면, 이제 터미널 혹은 터미널이 익숙하지 않은 경우 소스트리(source tree)와 같은 프로그램을 이용해서 블로그 폴더를 Github 사이트에 업로드하면 됩니다.

아래의 명령어를 순서대로 터미널에 입력하면 됩니다.

# Jekyll 블로그를 설치한 터미널로 이동 후 git 활성화
git init
# 블로그 폴더를 git staging 상태로 올리기
git add .
# 커밋 메세지 작성
git commit -m "Add blog"
# github 레포지토리 주소 연결
git remote add origin 본인의 레포지토리 주소
# github에 파일 업로드
git push -u origin master

터미널 환경 혹은 git에 익숙하지 않다면 이 과정에서 어려움을 겪을 수 있는데요. 생활코딩과 같은 곳에서 git에 관련된 강의를 들으면 좀 더 쉽게 github를 활용할 수 있습니다.

생활코딩: 지옥에서 온 Git

위의 명령어가 제대로 동작했다면 아래와 같이 파일들이 올라간 것을 확인할 수 있습니다.

Github

Github의 git pages 기능으로 블로그 배포하기

블로그 배포를 위해서는 git pages 기능을 활용해야하는데요. 정적인 웹사이트를 Github을 통해 무료로 퍼블리싱할 수 있는 기능입니다. github 레포지토리 페이지에서 상단에 Settings를 클릭합니다. 이 곳에서 레포지토리의 이름를 바꾸고 콜라보레이터를 등록하는 등의 작업을 할 수 있는데요. 아래로 내려서 Github pages라는 부분을 찾습니다.

Github pages

그리고 source를 클릭해서 배포를 원하는 브랜치를 선택하는데요. 저는 일단 master 브랜치를 배포 대상으로 선택했습니다. 그리고 save를 클릭하면 퍼블리싱되고 있는 주소가 상단의 이미지처럼 나타납니다. 약 5~10분 정도 후에 주소를 들어가면 본인의 블로그가 퍼블리싱 된 것을 확인할 수 있습니다.

Github pages


Pagination