웹에서 서비스의 신규 버전이 배포되었음을 사용에게 알리는 방법(프로젝트 버전 관리)

jisu·2023년 7월 15일
0

앱스토어, 혹은 구글플레이 스토어에서 앱을 다운받을 때를 생각해봅시다.

앱은 새로운 버전이 배포된 경우, 사용자가 현재 사용중인 앱이 바로 업데이트 되는것이 아니라, 직접 앱을 업데이트 해야 수정사항들이 반영된 신규 버전으로 다운로드 됩니다. 그렇기 때문에, 사용자가 어떤 버전을 사용할지 선택할 수 있고 업데이트를 원하지 않으면 하지 않을 수도 있습니다.

반대로 앱에서도 사용자에게 업데이트를 강제하고 싶은 경우가 있을 수 있습니다. 인앱 업데이트 라고도 하며, 일반적으로 새로운 버전이 출시되었습니다. 원활한 사용을 위해 업데이트 해주세요 등의 알람이 표시되며 업데이트를 강제하도록 합니다. 강제 업데이트는 업데이트를 하지 않으면 서비스를 이용할 수 없도록 합니다. 이렇게 함으로써 구버전을 사용하므로써 발생하는 문제나 하휘호환성 고민 없이 사용자가 항상 최신의 소스 코드를 바라볼 수 있도록 합니다.


(이미지 출처: https://www.kyulabs.app/5de0e6a5-88db-4aac-8715-2413e7d4c8ad)

이처럼 앱 환경에서는 신규 업데이트에 대해서 선택 사항, 필수 사항 으로 나누어서 사용자에게 제공할 수 있습니다. 하지만 웹은 사용자의 의사와는 상관없이 새로고침 하는 순간 새로운 코드를 받아오게 되고, 그 코드가 최근에 배포되어서 바뀐 코드라면 새로운 버전이 바로 노출 되게 됩니다. 반대로 새로고침(새로운 코드를 다시 받아오는) 을 하지 않는 이상 사용자는 새로운 버전을 볼 수 없다는 의미이기도 합니다.

웹에서도 앱에서처럼 새로운 버전이 배포되었다는 것을 화면 영역, 혹은 사용자에게 알리거나 새로고침을 유도하기 위해서는 몇가지 필요한 단계가 필요합니다. 이 글에서는 웹 환경에서 새로운 버전이 배포되었음을 사용자에게 알리기 위한 방법에 대해 알아보겠습니다.

요약하자면, 서비스 영역에서 프로젝트 버전 관리의 목적과 이점은 다음과 같습니다.

  1. 사용자에게 새로운 버전을 제공할 수 있다.
    프로젝트가 새로운 버전으로 배포되면 사용자에게 업데이트를 제공해야 합니다. 버전 관리를 통해 프로젝트의 새로운 버전을 식별하고, 사용자에게 최신 버전을 제공할 수 있습니다. 이를 통해 사용자는 프로젝트의 새로운 기능이나 개선 사항을 즉시 이용할 수 있게 됩니다.
  2. 구버전 사용자에게 업데이트 유도하기 할 수 있다.
    사용자가 구버전을 사용하는 경우에는 업데이트를 유도해야 합니다. 버전 관리를 통해 현재 사용 중인 버전을 파악하고, 새로운 버전이 나왔을 때 사용자에게 업데이트를 안내할 수 있습니다. 이를 통해 구버전의 문제점이나 보안 취약점 등을 개선하고 사용자 경험을 향상시킬 수 있습니다.
  3. API 호환성 유지할 수 있다.
    만약 백엔드와 프론트엔드가 모두 변경되어 API 스펙이 변경된 경우, 구버전 화면을 사용 중인 경우에는 변경 전 API 스펙으로 요청을 보내면 실패할 수 있습니다. 버전 관리를 통해 사용 중인 버전을 파악하고, 해당 버전에 맞는 API 요청을 보낼 수 있습니다. 이를 통해 호환성 문제를 해결하고 안정적인 동작을 유지할 수 있습니다.

새로운 버전 이 출시되었음을 감지하기 위해 먼저 버전 을 관리하는 시스템이 있어야 합니다. 프로젝트 버전 관리는 소프트웨어 개발 과정에서 필수적인 요소로, 버전 정보와 커밋 정보를 추적하고 관리함으로써 개발자들 간의 협업과 코드의 변화를 효율적으로 관리할 수 있습니다. 또한 사용자에게 최신 버전을 제공하고 업데이트를 유도하여 프로젝트의 성능, 기능, 보안 등을 개선할 수 있습니다.

버전 정보 관리 방법

서비스 영역에 버전 정보를 표시하기 전에, 프로젝트의 버전정보를 어떻게 관리하는지 알아봅시다.

1. 수동으로 관리

소스 코드의 새로운 수정사항이 프로젝트가 업데이트 되었을 때마다, package.json 의 버전을 수정합니다. 코드에서 package.json 의 version 과 project name 에 접근하기 위해서는 아래처럼 require 를 사용할 수 있습니다.

  • 장점 : 명시적으로 원하는 버전을 수정하기 좋습니다.
  • 단점 : 매번 버전을 바꿔야되서 번거롭습니다. 휴먼에러의 위험이 있습니다.
// package.json

{
	"name": "webpack-test",
  	"version": "1.0.0",
  	"description": "",
 	"main": "index.js",
	...
}
// app.js

const pjson = require('./package.json');
pjson.version // 1.0.0

2. Git 정보 사용

Git 을 사용한다면 현재 브랜치의 commit 해쉬값이나, 태그로 소스 코드의 버전 정보를 표시할 수 있습니다.

# 현재 브랜치의 가장 최신 커밋 해쉬값 7글자
$ git rev-parse --short=7 HEAD

# 가장 최근 태그 해쉬값
$ git describe --abbrev=0 --tags
  • 장점 : 커밋번호나 태그로 관리 할 수 있어서 히스토리 파악에 용이합니다.
  • 단점 : 커밋이나 태그를 남겨야만 버전 관리가 가능합니다. (커밋없이 로컬에서만 배포하는 경우는 극히 드물기 때문에 단점이 아니긴 합니다.)
const { execSync } = require("child_process");

function getCurrentVersion() {
  try {
    const version = execSync("git describe --abbrev=0 --tags").toString().trim();
    return version;
  } catch (error) {
    console.error("Failed to get current version:", error);
    return "Unknown";
  }
}

3. 웹팩 플러그인 사용

웹팩(Webpack)은 자바스크립트 애플리케이션을 위한 정적 모듈 번들러(static module bundler)입니다. 웹팩은 프론트엔드 개발에서 모듈화된 코드(JavaScript, CSS, 이미지 등)를 처리하고 하나 또는 여러 개의 번들 파일로 묶어주는 역할을 합니다.

웹팩은 다양한 모듈 형식으로 작성된 프론트엔드 소스 코드를 읽고, 각 모듈 간의 의존성 관계를 파악하여 필요한 모듈들을 번들(bundle)로 생성합니다. 이를 통해 여러 개의 파일로 분산된 코드를 하나의 파일로 결합하고 최적화하여 웹 애플리케이션의 성능을 향상시킬 수 있습니다.

웹팩 플러그인(Webpack Plugin) 은 웹팩의 빌드 과정에서 추가적인 기능을 제공하는 도구입니다. 플러그인은 웹팩의 확장성을 높이고, 다양한 작업을 수행할 수 있도록 도와줍니다. 플러그인은 로더(Loader)와 달리 번들링되는 개별 모듈에 대한 변환 작업이 아니라, 번들링된 결과물에 대한 후처리 작업을 수행합니다.

웹팩 플러그인의 주요 목적은 다음과 같습니다:

  1. 번들 최적화: 플러그인을 사용하여 번들된 파일의 크기를 줄이고 성능을 최적화할 수 있습니다. 예를 들어, UglifyJsPlugin은 JavaScript 코드를 압축하고 난독화하여 파일 크기를 최소화하는 역할을 합니다.
  2. 환경 변수 주입: 플러그인을 통해 환경 변수를 주입하여 개발 환경과 프로덕션 환경 간의 차이를 관리할 수 있습니다. 이를 통해 환경별로 다른 설정 값을 사용하거나, 프로덕션 빌드 시에 경고 메시지를 제거하는 등의 작업을 수행할 수 있습니다.

다양한 플러그인 중, 버전 관리를 위해 사용할 플러그인 3개는 다음과 같습니다.

  1. DefinePlugin: DefinePlugin은 환경 변수를 정의하고, 해당 변수를 소스 코드 내에서 사용할 수 있도록 도와줍니다. 주로 개발과 프로덕션 환경 간의 다른 설정 값, API 엔드포인트 등을 관리할 때 사용됩니다. 예를 들어, 개발 환경에서는 디버그 모드를 활성화하고, 프로덕션 환경에서는 로깅을 비활성화하는 등의 작업을 수행할 수 있습니다.
  2. EnvironmentPlugin: EnvironmentPlugin은 프로세스 환경 변수를 번들에 주입하는 플러그인입니다. 환경 변수를 소스 코드에서 직접 참조하여 사용할 수 있습니다. 주로 환경별로 다른 API 키, 인증 정보 등을 관리하는 데 사용됩니다.
  3. BuildHashPlugin: 빌드의 fullHash 값을 별도의 파일로 추출합니다.

프로젝트에 적용 해보기

프로젝트 세팅

프로젝트 폴더를 만들고, init 을 해주면, 간단한 package.json 파일이 만들어집니다. webpack 을 사용하기 위해서 필요한 dependency 도 설치해줍니다.

$ mkdir webpack-test
$ cd webpack-test
$ npm init -y
$ npm i -D webpack webpack-cli
// package.json

{
  "name": "webpack-test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^5.88.1",
    "webpack-cli": "^5.1.4"
  },
}

webpack 을 사용하여 build 하도록 package.json 에 빌드 스크립트를 추가합니다.

// package.json

"scripts": {
    "build": "webpack"
},

webpack.config.js 파일을 생성합니다. mode 를 추가하지 않으면 에러가 발생할 수 있어서 mode 를 추가합니다.

module.exports = {
	mode: 'development',
};

빌드를 하기 전에, output 할 파일을 만들어줍니다. src 아래에 index.js 를 생성하고, 내용을 작성합니다.

const message = "Hello World!";
const textNode = document.createTextNode(message);
document.body.appendChild(textNode);

build 스크립트를 실행합니다.

$ npm run build 

빌드를 하고 나면 dist/ 하위에 index.js 가 생성됩니다. 빌드된 내용을 확인하기 위해 자바스크립트 코드를 로딩하는 index.html 파일을 작성합니다.

src/index.html

<!DOCTYPE html>
<html>
  <head>
    <title>Webpack Test</title>
  </head>
  <body>
    <script src="./dist/main.js"></script>
  </body>
</html>

이제, 브라우저에서 index.html 파일을 열어보면 화면에 Hello world이라고 표시가 될 것입니다.

DefinePlugin

webpack.config.js 을 아래처럼 수정해줍니다. DefinePlugin 을 사용하여 원하는 정보를 추가할 수 있는데, 이때 모든 값들은 JSON string 타입으로 바꿔서 추가해야 오류가 발생하지 않습니다.

const webpack = require("webpack");
const pjson = require('./package.json');

module.exports = {
  mode: 'development',
  plugins: [
    new webpack.DefinePlugin({
      APP_NAME: JSON.stringify(pjson.name),
      VERSION: JSON.stringify(pjson.version),
    }),
  ],
};

DefinePlugin 으로 정의한 값은 전역 변수로 추가됩니다.

const message = `Hello World! ${APP_NAME}(version: ${VERSION})`;
const paragraph = document.createElement('p')
paragraph.textContent = message
document.body.appendChild(paragraph);

수정 후 다시 빌드를 하면 화면에 아래처럼 표시됩니다.

단, DefinePlugin 이 서비스 영역에 이 값들은 추가하는 시점은 빌드타임 이기 때문에 런타임에서 이 값에 접근할 수 없습니다.

console.log(APP_NAME) // undefined

EnvironmentPlugin

EnvironmentPlugin 는 DefinePlugin 와 달리 런타임에서 사용할 수 있고, 일반적으로 process.env 에 환경변수를 등록하기 위한 플러그인입니다.

디버깅 모드인지 확인하거나 환경 변수를 추가하는 목적으로 사용합니다. EnvironmentPluginDEBUG 를 추가합니다.

const webpack = require("webpack");
const pjson = require('./package.json');

module.exports = {
  mode: 'development',
  plugins: [
    new webpack.DefinePlugin({
      APP_NAME: JSON.stringify(pjson.name),
      VERSION: JSON.stringify(pjson.version),
    }),
    new webpack.EnvironmentPlugin({
		  NODE_ENV: 'development',
		  DEBUG: true,
		});
  ],
};

런타임에서도 DEBUG 변수에 접근이 가능하기 때문에, console 에서 디버그 모드가 true인지 false인지 알 수 있습니다.

프로젝트에 제대로 적용해보기

지금까지 웹팩 플러그인을 사용하여 서비스 영역에 버전을 표시하는 방법에 대해 알아봤습니다. 이제는 본격적으로 html, css, js 를 빌드 파일에 추가하고, 빌드 해시값을 bundle 에 추가하여 빌드 버전을 관리해보겠습니다.

dependency 추가

웹팩 빌드 설정을 추가하기 위한 각종 플러그인을 설치해줍니다.

npm install -D babel-loader 
npm install -D build-hash-webpack-plugin 
npm install -D css-loader html-webpack-plugin mini-css-extract-plugin style-loader
npm install webpack-dev-server lodash
{
  "name": "webpack-test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "start": "webpack serve --open"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "babel-loader": "^9.1.3",
    "build-hash-webpack-plugin": "^1.0.4",
    "css-loader": "^6.8.1",
    "html-webpack-plugin": "^5.5.3",
    "mini-css-extract-plugin": "^2.7.6",
    "style-loader": "^3.3.3",
    "webpack": "^5.88.1",
    "webpack-cli": "^5.1.4"
  },
  "dependencies": {
    "lodash": "^4.17.21",
    "webpack-dev-server": "^4.15.1"
  }
}

src 아래에 파일들을 생성해줍니다.

  • index.css
  • index.html
  • index.js

index.html 에는 js 스크립트를 불러올 필요가 없고, entry point 로 빈파일을 두기만 하면 됩니다. 웹팩 플러그인 중 html-webpack-plugin 으로 번들링된 js, css 를 주입할 것이기 때문에 SEO 를 위한 정보들만 추가한 빈 html 을 만들면 됩니다.

webpack.config 수정

webpack.config.json 을 아래처럼 수정해줍니다.

const webpack = require("webpack");
const { execSync } = require("child_process");
const pjson = require('./package.json');
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const BuildHashPlugin = require('build-hash-webpack-plugin');

function getCurrentVersion() {
  try {
    return execSync("git rev-parse --short=7 HEAD").toString().trim();
  } catch (error) {
    console.error("Failed to get current version:", error);
    return "Unknown";
  }
}

const KR_TIME_DIFF = 9 * 60 * 60 * 1000;

function getCurrentKrTime() {
  const d = new Date();
  const utc = d.getTime() + (d.getTimezoneOffset() * 60 * 1000);
  return new Date(utc + (KR_TIME_DIFF)).toLocaleString("KR");
}

module.exports = {
  mode: 'development',
  output: {
    path: __dirname + "/dist",
    filename: "bundle.[fullhash].js"
    clean: true,
  },
  devServer: {
    static: "./dist",
    historyApiFallback: {
      index: "index.html",
    },
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html",
    }),
    new webpack.DefinePlugin({
      APP_NAME: JSON.stringify(pjson.name),
      VERSION: JSON.stringify(pjson.version),
      BUILD_TIME: JSON.stringify(getCurrentKrTime()),
      COMMIT_HASH: JSON.stringify(getCurrentVersion()),
    }),
    new webpack.EnvironmentPlugin({
      NODE_ENV: 'development'
    }),
    new MiniCssExtractPlugin({
      linkType: false,
      filename: '[name].[contenthash].css',
      chunkFilename: '[id].[contenthash].css',
    }),
    new BuildHashPlugin({filename: 'version.json'})
  ],
  module: {
    rules: [
      {
          test: /\.js$/,
          exclude: /node_modules/,
          include: path.resolve(__dirname, "src"),
          use: [{
              loader: 'babel-loader',
          }]
      },
      {
        test: /\.css$/,
        exclude: /node_modules/,
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader"],
      },
    ]
  }
};

수정된 코드를 하나씩 살펴보겠습니다.

커밋 해쉬 가져오기

git rev-parse --short=7 HEAD 는 현재 브랜치의 가장 최신 커밋을 7글자로 보여주는 커맨드입니다.

execSyncchild_process 의 동기식 메서드로, 자식 프로세스를 생성해서 커맨드를 실행할 수 있게 합니다.

function getCurrentVersion() {
  try {
    return execSync("git rev-parse --short=7 HEAD").toString().trim();
  } catch (error) {
    console.error("Failed to get current version:", error);
    return "Unknown";
  }
}

getCurrentVersion 의 return 값을 DefinePlugin 로 COMMIT_HASH 에 저장합니다.

new webpack.DefinePlugin({
      APP_NAME: JSON.stringify(pjson.name),
      VERSION: JSON.stringify(pjson.version),
      BUILD_TIME: JSON.stringify(getCurrentKrTime()),
      COMMIT_HASH: JSON.stringify(getCurrentVersion()),
    }),

HtmlWebpackPlugin

new HtmlWebpackPlugin({
      template: "./src/index.html",
    }),

HtmlWebpackPlugin 번들링된 결과물(js, css)을 html 에 주입해주고, title 이나 meta 태그도 넣어주는 플러그인입니다.

MiniCssExtractPlugin

new MiniCssExtractPlugin({
      linkType: false,
      filename: '[name].[contenthash].css',
      chunkFilename: '[id].[contenthash].css',
    }),

js 뿐만 아니라, css 도 minify 하고, 해시값을 추가할 수 있도록 해주는 플러그인입니다.

번들링 파일이름에 해쉬값 추가하기

output: {
    path: __dirname + "/dist",
    filename: "bundle.[fullhash].js"
    clean: true,
  },

[fullhash] 는 웹팩에서 기본적으로 제공하는 template 로 더 많은 옵션은 여기 서 확인할 수 있습니다. 번들링된 파일에 해쉬값을 추가할 수 있고, 빌드 결과물이 아래처럼 생성됩니다.

index.js 도 수정하여 DefinePlugin 으로 추가한 정보들을 표시합니다. 수정사항을 바로 볼 수 있도록 npm run start 로 webpack-dev-server 를 실행합니다.

BuildHashPlugin

BuildHashPlugin 빌드 해시값(fullhash) 를 별도의 파일에 추출하여 저장해주는 플러그인이다. 코드 는 여기서 볼 수 있습니다.

new BuildHashPlugin({filename: 'version.json'})

플러그인을 config 에 추가한 후 빌드를 새로 하면, dist 파일에 version.json 파일이 추가된 것을 확인할 수 있습니다.

BuildHashPlugin 는 빌드 타임에 생성되는 여러 정보를 별도 파일로 export 해주는 플러그인입니다. hash 값 외에도 추가적인 정보가 필요하다면 직접 플러그인을 만드는 것도 어렵지 않습니다. 만약 위에서 DefinePlugin 에 추가한 4개 정보(APP_NAME, VERSION, BUILD_TIME, COMMIT_HASH)들도 export 해서 런타임에 사용하고 싶다면 아래 처럼 커스텀 플러그인을 만들면 됩니다.

webpack 4^

const path = require('path');

...

class BuildHashPlugin {

	...

    apply(compiler) {
        const buildJsonFile = path.resolve(compiler.options.output.path || '.', this.options.filename);

        compiler.hooks.afterEmit.tapAsync('BuildHashWebpackPlugin', (compilation, callback) => {
          const stats = compilation.getStats().toJson({
            hash: true,
            publicPath: false,
            assets: false,
            chunks: false,
            modules: false,
            source: false,
            errorDetails: false,
            timings: false
          });

          const content = {
            buildHash: stats.hash,
            buildTime: getCurrentKrTime(),
            commitHash: getCurrentVersion(),
            projectName: pjson.name,
            projectVersion: pjson.version,
          }

          compiler.outputFileSystem.writeFile(buildJsonFile, JSON.stringify({hash: stats.hash}), callback);
        });
	}
}

module.exports = BuildHashPlugin;

webpack ~4

const path = require('path');

...

class BuildHashPlugin {

		...

    apply(compiler) {
	    const buildJsonFile = this.options.filename;
	
	    compiler.plugin('emit', (compilation, callback) => {
	      const stats = compilation.getStats().toJson({
	        hash: true,
	        publicPath: false,
	        assets: false,
	        chunks: false,
	        modules: false,
	        source: false,
	        errorDetails: false,
	        timings: false
	      });
	
	      const content = JSON.stringify({
	        buildHash: stats.hash,
	        buildTime: getCurrentKrTime(),
	        commitHash: getCurrentVersion(),
	        projectName: pjson.name,
	        projectVersion: pjson.version,
	      })
	
	      compilation.assets[buildJsonFile] = {
	        source: function() {
	          return content;
	        },
	        size: function() {
	          return content.length;
	        }
	      };
	
	      callback()
	    });
	  }
}

module.exports = BuildHashPlugin;

index.js

최종적으로 수정한 index 파일입니다. 자세한 내용은 아래에서 살펴보겠습니다.

import "./index.css"
import isEmpty from "lodash/isEmpty" 

const FIND_SCRIPTS = /(bundle)\.\w+(\.js)/;

const addParagraphToBody = (message) => {
  const paragraph = document.createElement('p')
  paragraph.textContent = message
  document.body.appendChild(paragraph);
}

const getBuildHashFromBundle =() => {
  // 스크립트 태그의 src 속성에서 파일 이름을 가져옵니다.
  const allScript = Array.from(document.getElementsByTagName("script"))
  const bundle = allScript.filter((script) => FIND_SCRIPTS.test(script.src))[0]
  const scriptSrc = bundle.src;
  const fileName = scriptSrc.substring(scriptSrc.lastIndexOf('/') + 1);

  // 파일 이름에서 해시 값을 추출합니다.
  const hash = fileName.match(/\.(.*?)\./)[1];
  return hash
}

const UPDATE_CHECK_INTERVAL = 60 * 1000 * 5; // Every 5 mins

const createUpdateCheckInterval = (onVersionUpdated) => {
  window.setInterval(async () => {
    if (navigator.onLine) {
      try {
        const response = await fetch('version.json', {cache: "no-store"});
        const oldVersion = getBuildHashFromBundle()

        if (response.ok) {
          const version = await response.json();
          if (
            !isEmpty(version) && !!oldVersion && oldVersion !== version.hash
          ) {
            onVersionUpdated();
          } else {
            console.log("nothing change")
          }
        }
      } catch (e) {
        console.error(e);
      }
    }
  }, UPDATE_CHECK_INTERVAL);
};

const onVersionUpdated = () => {
  console.log('new version is released, Please reload the APP')
}

addParagraphToBody(`Hello World! ${APP_NAME}(version: ${VERSION})`)
addParagraphToBody(`BUILD_TIME ${BUILD_TIME}`)
addParagraphToBody(`COMMIT_HASH ${COMMIT_HASH}`)
addParagraphToBody(`BUILD_HASH ${getBuildHashFromBundle()}`)

createUpdateCheckInterval(onVersionUpdated)

getBuildHashFromBundle

document 의 모든 스크립트 태그를 가져옵니다. 지금은 bundle.~~.js 하나만 타겟으로 하고 있는데, 빌드 파일이 여러개거나 버저닝 할 파일이 더 있다면, FIND_SCRIPTS 에 파일이름을 추가해줍니다.

const FIND_SCRIPTS = /(bundle)\.\w+(\.js)/;

const getBuildHashFromBundle =() => {
  // 스크립트 태그의 src 속성에서 파일 이름을 가져옵니다.
  const allScript = Array.from(document.getElementsByTagName("script"))
  const bundle = allScript.filter((script) => FIND_SCRIPTS.test(script.src))[0]
  const scriptSrc = bundle.src;
  const fileName = scriptSrc.substring(scriptSrc.lastIndexOf('/') + 1);

  // 파일 이름에서 해시 값을 추출합니다.
  const hash = fileName.match(/\.(.*?)\./)[1];
  return hash
}

createUpdateCheckInterval

웹팩 플러그인 BuildHashPlugin 를 사용하여 생성된 version.json 파일을 fetch 로 가져옵니다. 이때 같은 파일에 옵션 없이 재요청을 보내면 캐싱된 값을 가져오기 때문에, 빌드가 새로 되어서 버전이 바뀌어도 감지 할 수 없습니다. 그래서 no-store 옵션을 주거나, 랜덤한 쿼리스트링을 추가해서 캐싱하지 않고 매번 새로 요청하도록 해야합니다.

프로젝트가 다시 빌드 되어도 새로고침을 하지 않는 이상, bundle 파일의 해시는 바뀌지 않습니다. 그렇기 때문에 version.json 을 몇분 간격으로 가져와서 빌드가 되고, 새로 배포가 되어 version.json 가 바뀌는 순간 버전 정보가 달라졌다는 것을 감지 할 수 있습니다.

기존 버전(bundle.js 의 해쉬값)과 신규 버전(version.json 의 해쉬값) 을 비교해서 다른 경우 callback 을 실행합니다.

const createUpdateCheckInterval = (onVersionUpdated) => {
  window.setInterval(async () => {
    if (navigator.onLine) {
      try {
        const response = await fetch('version.json', {cache: "no-store"});
        const oldVersion = getBuildHashFromBundle()

        if (response.ok) {
          const version = await response.json();
          if (
            !isEmpty(version) && !!oldVersion && oldVersion !== version.hash
          ) {
            onVersionUpdated();
          } else {
            console.log("nothing change")
          }
        }
      } catch (e) {
        console.error(e);
      }
    }
  }, UPDATE_CHECK_INTERVAL);
};

const onVersionUpdated = () => {
  console.log('new version is released, Please reload the APP')
}

마무리

여러 플러그인을 사용했지만, 제가 초기에 목표한 프로젝트의 새로운 버전이 배포되었음을 감지하는 방법 은 아래 4단계로 요약할 수 있습니다.

  • "bundle.[fullhash].js" 을 추가하여 번들 파일이름에 해쉬값을 추가합니다.
  • BuildHashPlugin 을 사용하여 fullhash 값을 별도의 파일로 저장합니다.
  • getBuildHashFromBundle 함수를 사용하여 스크립트 파일들에서 번들 파일의 fullhash 값을 가져옵니다.
  • createUpdateCheckInterval 함수를 사용하여 일정 주기로 /version.json 을 fetch 하여 새로운 버전이 배포되었는지 확인합니다.

추가적으로 BuildHashPlugin 를 참고하여 빌드타임에서 생성되는 값들 중 원하는 변수를 파일로 추출하는 커스텀 플러그인을 만들어봤습니다. 다음에는 직접 만든 플러그인(혹은 패키지)를 npm 에 올려서 여러 프로젝트에서 사용할 수 있게 하는 방법을 알아보겠습니다.

profile
(이제부터라도) 기록하려는 프론트엔드 디벨로퍼입니다 XD

0개의 댓글