프론트 정적 리소스버전관리 및 독립 프로젝트로 분리


1. 프론트 정적 리소스(javascript, css) 버전 관리 및 WAS에서 분리

nginx(loadbalancing), WAS와 같은 구조로 서버를 운영할 때, 정적인 리소스도 모두 WAS에 올라가는데, 정적인 리소스는 굳이 WAS까지 갈 필요 없이 웹 서버에서 처리해도 되지 않을까 라는 생각과, 여기저기 흩어져 있는 javascript, css를 한곳에 모으기 위해 알아보기 시작

(프로젝트가 여러개가 되면 동일한 javascript 코드나 css가 여기저기 흩어져있는게 싫기도 해서 알아보기 시작했다)

결론적으로 원하는 그림대로 구성을 완료하기는 했지만 구조가 조금 복잡해진다는 단점이 생겼다

1.1. 준비물

  • git

  • nginx

  • npm

  • bower

  • grunt

  • nexus3(optional)

2. Step 1. webapp project

webapp project를 2개의 프로젝트로 나눈다. 전체적인 구조는 보통 사용하는 구조와 같고, resources 디렉토리를 gitsubmodule 프로젝트로 분리한다.

submodule 설정 관련 정보는 여기에서 확인

submodule을 등록하면 아래와 같이 .gitmodules파일이 생성된다.(굳이 git command를 사용하여 submodule을 등록하지 않고 .gitmodules파일을 생성해서 정보를 입력해줘도 된다. 이 때, 해당 디렉토리가 반드시 존재해야 된다.) submodule을 등록하기 전에 두 개의 git repository는 반드시 필요하다.

[submodule "src/main/webapp/resources"]
   path = src/main/webapp/resources
   url = git@your/repository.git

위 처럼 pathgit repository url을 등록하면 submodule등록이 완료된다.

최초 세팅 시 부모(?) 프로젝트를 받으며 submodule프로젝트까지 받을려면 아래와 같이 하면 된다.

git clone --recursive git@your/parent/repository.git

3. Step 2. resource project

resource projectnpm, bower, grunt, git, nexus(optional)을 사용한다. 우선 npm으로 필요한 패키지 정보를 받는다.

npm init
npm install -g grunt bower

이렇게 하면 gruntbower command를 사용할 수 있다.

여기서 bowernexuspackage를 등록하고, 전체적인 프론트 프로젝트(뒤에서 설명)에서 dependency를 추가하기 위해 사용한다.

bower init

위의 명령어를 사용하면 bower가 초기화 되고, bower.json파일이 생성된다.

아래는 bower.json파일의 정보다.

{
  "name": "resources",
  "version": "0.0.1",
  "description": "description of package",
  "main": [
    "css/",
    "js/",
    "img/"
  ],
  "authors": [
    "author"
  ],
  "license": "ISC",
  "homepage": "",
  "ignore": [
    "**/.*",
    "node_modules",
    "bower_components",
    "test",
    "tests",
    "package.json"
  ]
}

여기서 main은 프론트 프로젝트에서 사용할 파일, 디렉토리 목록을 나타낸 것이고, ignore는 무시할 목록들이다.

bower에서 사용할 versionbower.json에 등록(다른데다 해도 됨)한다.

bowergittag기능을 사용해서 버전을 관리한다.

예를들어 git tag 1.0.0 명령으를 사용하고, 원격 저장소에 push를 하면 해당 시점의 commit1.0.0이 되는 것이다.

매번 git tag 명령어로 버전을 등록하기는 힘드므로 grunt를 사용해서 자동화를 한다.

우선 필요한 것은 grunt-tag이다.

npm install --save-dev grunt-tag

grunt-git을 통해서도 관리를 할 수 있기는 하나, grunt-tag가 중복된 버전이 있을 시 하위에 등록된 것을 지워주고, 신규로 등록을 해주고, 원격 저장소까지 자동으로 올려줘서 grunt-tag를 선택했다.

package.json 파일과 동일한 위치에 Gruntfile.js를 생성해준다. 파일의 내용은 아래와 같다.

module.exports = function(grunt) {
   grunt.initConfig({
      tag: {
         options: {
            // Create or move the tag (default: true)
            tag: true,
            // Push the tag to remote (default: true)
            push: true,
            // File where th read the version (default: package.json)
            file: 'bower.json',
            // Name of the tag (default: '<%= version %>')
            tagName: '<%= version %&gt',
            // Message of the tag (default: 'Version <%= version %>')
            tagMsg: 'New version: <%= version %>',    // default: 'Version <%= version %>'
            // The remote where to push the tag (default: 'origin')
            remote: 'origin'
         }
      }
   });

   grunt.loadNpmTasks('grunt-tag');

   grunt.registerTask('default', ['tag']);
};

위의 내용은 bower.json 파일에서 version 정보를 가져와 git tag에 등록을 하고, remote(origin) 저장소에 push를 하라는 내용이다.

버전 등록까지 완료가 됐으니 이제 nexus에 등록을 하도록 한다.

bower의 경우 nexus에 등록을 하더라도 저장되는 정보는 어떤 package가 어떤 repository랑 연결돼 있는지에 대한 정보 뿐이니, nexus 사용에 대한 필요성을 느끼지 못한다면 굳이 사용할 필요는 없다. 이런 사람은 이 파트는 그냥 넘어가도 된다.

나는 git repository url을 사용하기 싫기도 하고, 기존에 nexus를 사용하고 있어서 nexus를 사용했다.

bowernexus 3부터 지원이 되므로 nexus 3이 필요하다. 무료 버전 다운로드는 여기에서 할 수 있다.

nexus 3를 설치를 하면 기본 설정으로 bower repository는 생성되어 있지 않아서 신규로 생성을 해 줘야 한다.

관련 정보는 여기를 참조하길 바란다.

npm을 통해 필요한 패키지를 설치해준다.

npm install --save-dev bower-nexus3-resolver

이제 .bowerrc 라는 파일을 bower.json과 같은 위치에 생성을 하고 아래와 같이 작성한다.

{
   "registry" : {
      "search" : [
         "http://your/bower/nexus/repository/bower-public/"
      ],
      "register" : "http://ID:PASSWORD@your/bower/nexus/repository/bower-snapshots/"
   },
   "resolvers" : [ "bower-nexus3-resolver" ]
}

idpasswordnexus에서 등록한 repository 등록 권한이 있는 사용자 정보를 입력하면 된다.

  • 참고로 id와 password에 특수문자를 사용할 수 있기는 하나 일반적인 url에서 규칙으로 사용되는 특수문자는 사용하면 안된다.(ex: /, #, @)*

이렇게 작성을 한 후 커맨드 창에서 아래의 명령어를 입력한다.

bower register your-package-name git@your.repository.git

이렇게 하면 등록이 완료됐다.

아래의 명령어를 입력하면 package 정보를 확인할 수 있다.

bower info your-package-name

4. Step 3. front project

이 프로젝트에서는 전체적으로 사용하는 프론트 라이브러리나, 내가 만든 프론트 소스를 관리한다.

여기서 필요한 툴은 아래와 같다.

  • npm

  • bower

  • grunt

  • git

  • nexus(optional)

npmbower를 초기화 한다.

npm init
bower init

gruntbower를 앞에서 install했으므로 해당 명령은 건너뛴다.

nexus를 사용한다면 bower-nexus3-resolver 패키지를 등록하고 .bowerrc파일을 생성해서 아래와 같이 내용을 작성해 준다.

npm install --save-dev bower-nexus3-resolver
{
   "registry" : {
      "search" : [
         "http://nexus.mitpdev.co.kr/repository/bower-public",
         "http://bower.herokuapp.com/packages"
      ]
   },
   "resolvers" : [ "bower-nexus3-resolver" ]
}

bower에서 필요한 라이브러리 패키지 설치한다.

bower install --save jquery bootstrap

이렇게 하면 bower.json파일에 dependency가 추가된다.

나는 라이브러리도 버전별로 등록을 하고 싶어서 아래와 같이 등록을 했다.

{
  "name": "package name",
  "description": "",
  "main": "index.js",
  "authors": [
    "author"
  ],
  "license": "ISC",
  "homepage": "",
  "ignore": [
    "**/.*",
    "node_modules",
    "bower_components",
    "static",
    "test",
    "tests"
  ],
  "dependencies": {
    "jquery-2.2.3": "jquery#2.2.3",
    "bootstrap-3.3.6": "bootstrap-css#3.3.6",
    "my-package-0.0.1": "my-package#0.0.1"
  }
}

만약 넥서스를 사용하지 않는다면 자신의 프로젝트를 아래와 같이 등록할 수 있다.

{
  ...
  "dependencies": {
      ...
    "my-package-0.0.1": "git@your/repository.git#0.0.1"
  }
}

이렇게 전체적인 프론트 관리 프로젝트를 별도로 두는 이유는, 한 곳으로 모아서 WAS가 아닌 Web Server에 올리기 위함도 있지만, javascriptcss파일을 난독화(minify)하고, gzip으로 압축하기 위함이다.

이제 grunt를 통해 빌드 자동화를 하도록 한다. Gruntfile.js 파일을 생성하고, 커맨드창에 아래의 명령어를 입력한다.

npm install --save-dev grunt-cli grunt-bower-task grunt-contrib-uglify grunt-contrib-cssmin grunt-contrib-compress

Gruntfile.js는 아래와 같이 내용을 채워준다.

module.exports = function(grunt) {
   var path = require('path');

   grunt.initConfig({
      pkg: grunt.file.readJSON('package.json'),
      bower: {
         install: {
            options: {
               copy: true,
               targetDir: './static',
               install: true,
               layout: function(type, component, source) {
                  var sourcePath = source.replace(/^.*\.([a-zA-Z]+)$/, '$1');

                  if (sourcePath.indexOf('bower_components') &gt= 0) {
                     sourcePath = source.replace('bower_components\\' + component + '\\', '');
                     sourcePath = sourcePath.replace('bower_components/' + component + '/', '');
                  }

                  return path.join(component, sourcePath);
               }
            }
         }
      },
      cssmin: {
         options: {
            // 'min', 'gzip'
            report: 'min'
         },
         target: {
            files: [{
               expand: true,
               cwd: 'static/',
               src: ['**/*.css', '!**/*.min.css'],
               dest: 'static/',
               ext: '.min.css',
               extDot: 'first'            }]
         }
      },
      uglify: {
         options: {
            // false, 'none', 'min', 'gzip'
            report: 'min',
            compress: {
               drop_console: true
            },
            // false, 'all', 'some', Function
            preserveComments: 'some'
         },
         target: {
            files: [{
               expand: true,
               cwd: 'static/',
               src: ['**/*.js', '!**/*.min.js'],
               dest: 'static/',
               ext: '.min.js'            }]
         }
      },
      compress: {
         options: {
            mode: 'gzip',
            level: 5
         },
         target: {
            files: [{
               expand: true,
               cwd: 'static/',
               src: ['**/*.js'],
               dest: 'static',
               ext: '.js.gz'
            }, {
               expand: true,
               cwd: 'static/',
               src: ['**/*.css'],
               dest: 'static',
               ext: '.css.gz'
            }, {
               expand: true,
               cwd: 'static/',
               src: ['**/*.min.js'],
               dest: 'static',
               ext: '.min.js.gz'
            }, {
               expand: true,
               cwd: 'static/',
               src: ['**/*.min.css'],
               dest: 'static',
               ext: 'min.css.gz'
            }]
         }
      }
   });

   grunt.loadNpmTasks('grunt-bower-task');
   grunt.loadNpmTasks('grunt-contrib-cssmin');
   grunt.loadNpmTasks('grunt-contrib-uglify');
   grunt.loadNpmTasks('grunt-contrib-compress');

   grunt.registerTask('default', ['bower', 'cssmin', 'uglify', 'compress']);
};

bower task에서 layout은 파일들의 디렉토리를 어떻게 나눌 것인지에 대한 내용이다. 기본 값은 byType인데 이대로는 내가 원하는 구조대로 되지 않아서 해당 부분을 수정했다. cssminuglify는 각각 cssjavascript 파일을 난독화(minify)하는 것이고, compressgzip으로 압축하는 것이다.

gruntregisterTask에서 등록한 순서대로 해당 명령들을 실행한다.

위같은 경우는 bower -> cssmin -> uglify -> compress 순으로 실행하게 된다.

이제 command창에서 grunt 명령어를 입력하면 bower_components 디렉토리와 static 디렉토리가 생성되는 것을 확인할 수 있다. 여기서 bower_components는 설정하기 위해 필요한 파일들을 받아오는 역할만 할 뿐이므로 지워도 된다. 여기서 확인해야 될 것은 static 디렉토리다.

각각의 패키지별로 디렉토리가 생성되고, js, css파일이 나누어져서 구성되어 있다.

또한 min 파일과 gzip으로 압축한 파일들이 들어있는데, 여기서 필요없는 파일들은 Gruntfile.js 파일을 각자 입맛대로 수정해서 지우면 된다.

이 프로젝트 또한 git repository에 등록해서 서버에서 사용한다.(나는 bower_components, node_modules, static 디렉토리에 gitignore를 걸어놓고 실제 이 프로젝트는 의존성만 관리하는 식으로 등록해놨다 실제 빌드는 서버에서 수행하도록 했다.)

5. Step 4. front project(server)

front project를 서버에 세팅한다. git으로 관리하므로 우선 clone을 받는다.

git clone git@front/project/repository.git

그리고 grunt로 빌드를 한다.

이것으로 설정은 끝났다.

6. Step 5. nginx

이제 nginx설정을 한다. nginx에서 gzip 설정을 하고, root를 등록해준다.

gzip.conf

gzip on;
gzip_disable "Mozilla/4";
gzip_disable "msie6";
gzip_static on;
gzip_vary on;
gzip_min_length 100;
gzip_buffers 4 32k;
gzip_http_version 1.1;
gzip_comp_level 2;
gzip_types
        text/plain
        text/javascript
        text/css
        application/javascript;
server {
        listen          80;
        server_name     resources.your.domain;

        root    /your/front/project/directory/static;

        access_log      /var/log/nginx/resources_acces.log;
        error_log       /var/log/nginx/resources_error.log;

        location / {
        }
}

nginx를 재실행 한다.

service nginx restart

이제 모든 설정이 끝났다.

7. ETC…

추가로 webapp 프로젝트에서 설정해 줘야 할 것이 개발시, buildresource path를 다르게 설정하는 것인데, 이것은 각자의 프로젝트에서 사용하는 빌드 툴에 따라 알아서 설정하면 된다. maven의 경우 profile을 활용해서 properties 파일을 바꿔치는 방법이 있다. 또한, buildwebapp 프로젝트에서 resources 디렉토리를 exclude를 하는 설정을 하면 모든 설정이 완료된다.

이렇게 설정함으로써 프로젝트 구조 자체는 복잡해지지만, 여러 프로젝트가 있을 경우, 공통된 javascript를 모두 각각의 서버에 올릴 필요도 없고, 버전 관리까지 되므로, 리소스 캐시에 대한 문제점도 사라지게 된다.(혼자만의 생각)

각 서버별 사용하는 리소스의 버전도 properties에 등록을 해서 관리한다면, 프론트 리소스의 버전이 올라갔을 때 properties에 등록된 버전만 수정하면 된다.