편집자 주
Vue 또는 VUE로 혼용하나 공식 사이트의 표기에 맞춰 아래와 같이 통일함
-Vue
-Vuex
-Vue-Router


목차
1.Controller
2.VIEW
3.webpack Vue 소스 진입점
4.webpack 설정
5.Package.json
6.Vue-Router
7.Vuex
8.공통 처리 mixin
9.요약
10.마치며



시작하며

드디어 브랜디 관리자 서비스에 Vue를 도입하고자 떠났던 여정의 마지막 장입니다. 브랜디 관리자 서비스는 PHP Codeigniter와 jQuery로 구성되어 있습니다. 사실 잘 운영되고 있는 서비스에 리스크가 큰 신기술을 도입하는 것은 도박에 가깝습니다. 몇 시간만 운영이 정지되어도 회사에 엄청난 피해를 안겨줄 수도 있으니까요. 하지만 여러 번의 검증과 실험으로 도박에서 이길 확률을 100%에 가깝게 끌어올린다면 한번 도전해볼 만하지 않을까요?

이전 글인 PHP Codeigniter 환경에서 VUE 사용해보기에서 기본적인 webpack + Vue + Codeigniter 환경 구축 방법을 알아봤는데요. 하지만 단순히 webpack과 Vue만 적용했다고 해서 “우리 시스템은 UI 프레임워크로 Vue를 사용하고 있습니다.”라고 말할 순 없습니다. 아주 중요한 숙제가 남았죠.

Vue에는 활용도를 대폭 끌어올려주는 Vue-Router와 Vuex Store1)가 있는데 그중 Vue-Router를 이번 글에서 자세히 다루려고 합니다.2) Vue-Router는 Vue.js의 공식 라우터입니다. 공식 홈페이지의 소개는 아래와 같습니다.

  • 중첩된 라우트/뷰 매핑
  • 모듈화된, 컴포넌트 기반의 라우터 설정
  • 라우터 파라미터, 쿼리, 와일드카드
  • Vue.js의 트랜지션 시스템을 이용한 트랜지션 효과
  • 세밀한 내비게이션 컨트롤
  • active CSS 클래스를 자동으로 추가해주는 링크
  • HTML5 히스토리 모드 또는 해시 모드(IE9에서 자동으로 폴백)
  • 사용자 정의 가능한 스크롤 동작



한마디로 정리하면 입력된 URL에 반응해 <router-view> 부분에 해당 URL의 view를 보여주는 기능인 것입니다. 다시 말해 URL이 변경될 때 한 페이지에서 화면 전체를 갈아끼우거나, 화면의 일부분(<router-view>부분)을 치환해주는 역할을 한다는 것이죠. 더 나아가 해당 화면이 로드되기 전후로 전처리, 후처리 기능까지 가능합니다.



착안점

Vue와 Vue-Rotuer를 알게 되었을 땐 PHP 기반 프로젝트에 Vue-Router를 적용할 수 없으니 처음부터 새로 만들어야 한다고 생각했습니다. 로그인 인증 문제, 메뉴의 권한 관리 등 모든 것이 Vue 아래에 있어야 한다고 생각했기 때문입니다.

어느 날 관리자 서비스에 TDD를 구현해보려고 Python Flask + webpack + Vue 프로젝트를 구성하고 있었습니다. 그러던 중 우연히 Flask + Vue-Router에서 404 페이지를 처리하려면 Flask Fallback 페이지를 Vue-Router 메인 페이지(<router-view>가 있는 페이지)로 보내고, Vue-Router에서 진짜 매핑된 URL이 없으면 404 처리를 하는 식으로 구성한다는 글을 읽고 문득 호기심이 생겼습니다.

‘관리자 서비스에서도 컨트롤러로 여러 URL을 한 가지 페이지로 보낸다면?’
PHP를 거쳐 페이지로 이동한 것이므로 권한 관리와 메뉴 트리까지는 PHP에서 처리되면서 URL이 변할 것이고, 실제로 화면을 보여주는 Contents 영역만 <router-view>를 사용한다면 어떻게 될지 궁금해졌습니다. 바로 하던 일을 멈추고 관리자 소스에 Vue-Router를 활용한 테스트 소스코드를 작성해봤습니다.

02


예상했던 대로 PHP의 로그인 인증 처리를 거치면서 실제로 보이는 부분에는 <router-view> 부분만 정상적으로 치환되었습니다. 이 간단한 실험을 바탕으로 통계 시스템의 일부를 구현하는 데에 Vue-Router와 Vuex Store, 공통 처리 Mixin을 추가해 제작했습니다.



1.Controller

4개의 페이지를 가진 통계 시스템의 Codeigniter 컨트롤러 모습입니다. 기존의 서비스 URL들이 존재하기 때문에 Fallback을 통으로 Vue-Router로 보낼 순 없었고, 라우터를 사용할 페이지들을 하나의 페이지로 보냈습니다.

1-1) /application/controllers/[컨트롤러 경로]

... 생략 
    /*
        [라우터 view]에서 <router-view/> 태그를 포함하고 있습니다.
    */
    public function salesAnalysisProduct() {
        $this->load->view('[라우터 view]');
    }
    
    public function salesAnalysisSeller() {
        $this->load->view('[라우터 view]');
    }
    
    public function trendAnalysisProduct() {
        $this->load->view('[라우터 view]');
    }
    
    public function trendAnalysisSeller() {
        $this->load->view('[라우터 view]');
    }
... 생략 





2.VIEW

Codeigniter 환경에 반영하는 것이므로 CI에서 인식시킬 PHP view와 webpack에서 빌드 결과를 자동으로 바인딩할 html 파일로 구성됩니다.

2-1)/application/views/[Vue용 view 경로]/index.php

// [index.php] Vue 를 매핑할 php파일 컨트롤러의 view로딩용 [라우터 view]입니다.
... header, menu 생략 ...
 
//바인딩될 부분
<div id="app"></div>
 
//자동매핑 html 인클루드
<?php include('index.html'); ?>
 
... footer 생략 ...



2-2)/application/views/[Vue용 view 경로]/index.html webpack의 빌드 결과로 자동으로 생성되는 파일입니다.

<div id="app"></div><script type="text/javascript" src="/include/scripts/WEBPACK_VUE/./app.1506e7f71b1061866220.js"></script>


위는 webpack의 HtmlWebpackPlugin에 의해 자동으로 바인딩된 모습입니다. 빌드되기 전 index.html은 다음 항목에 있습니다.



3.webpack Vue 소스 진입점

관리자에서는 프로젝트 폴더 안에 webpack과 Vue 용 서브 폴더를 두고 webpack.config.js에서 output 옵션을 통해 빌드 결과를 삽입하는 구조입니다. webpack 루트 폴더는 application 폴더와 같은 레벨에 위치하며, 폴더 구조나 파일 위치는 어디에 둬도 상관없습니다. webpack.config.js에서 entry 속성으로 잡아주시면 됩니다.

3-1)/[webpack루트]/index.html

// HtmlWebpackPlugin으로 스크립트를 삽입하기 위한 빈 템플릿 파일



3-2)/[webpack루트]/index.js

/**
 * 진입용 index.js
 */
import Vue from 'vue'
import axios from 'axios'
import router from './router'
import App from './App.vue'
 
Vue.prototype.$http = axios
 
new Vue({
    el : '#app',
    router,
    components : { App },
    template : '<App/>'
});



3-3)/[webpack루트]/App.vue

<template>
    <div id="app">
        <router-view/>
    </div>
</template>
 
<script>
    import mixin from './common/common-mixin.js'
    import store from './vuex/store'
    export default {
        store,
        name : 'App'
    }
</script>


Vuex와 통신 모듈 axios, Vue-Router 등을 루트 Vue 객체에 추가해줍니다. 브랜디 관리자의 webpack은 babel을 사용하고 있기 때문에 위의 store처럼 축약해서 작성하면 빌드된 파일에는 store: store와 같이 입력됩니다.

Vue-Router는 <router-view> 태그에 자동으로 매핑되며, 위와 같은 구조로 상위 컴포넌트에서 할당되어 있어야 합니다. Vuex와 Vue-Router 설정은 글 아래에서 다루겠습니다.



4.Webpack 설정

이번에 Vue-Router와 Vuex를 도입하면서 webpack의 설정도 실제 서비스용과 개발용으로 분리했습니다. 폴더는 편의상 추가하였으며, package.json에서 자신이 설정한 경로로 설정하면 됩니다.

Webpack 설정 파일은 Webpack의 시작과 끝이라고도 할 수 있습니다. Webpack 설정 파일에서 빌드할 소스의 경로와 빌드 결과 파일의 명명 규칙을 정하고, html 파일에 스크립트파를 자동으로 주입시키거나, Babel 플러그인을 통해 최신 스크립트 작성법을 브라우저를 신경쓰지 않고 사용할 수도 있습니다.

그중에서도 중요한 옵션이 있는데 바로 Code Splitting에 관련된 옵션입니다.

관리자 초기 Vue 모델에는 Vue-Router가 없었기 때문에 js 번들 파일의 크기가 그렇게 크지 않았습니다. 하지만 Vue-Router를 사용해 싱글 페이지 어플리케이션이 되거나 화면의 UI가 복잡해 컴포넌트 수가 많아지면 번들 js 파일의 크기가 매우 커집니다. 즉, 캐시를 사용하지 않는 익스플로러라면 소스에서 한 글자만 바뀌더라도 모든 페이지에서 거대한 번들 js를 새로 로딩하게 되고, 상당한 서버 자원을 소모합니다.

03
Code Splitting 적용 전


위의 이미지는 Code Splitting을 적용하기 전의 번들 js 정보입니다. 실제로 완성된 Vue 프로젝트의 번들 js는 더욱 큽니다. 정말 단순한 페이지 하나를 띄우는데 매번 뚱뚱한 js를 로딩해야 하는 것은 서비스 제공자와 서비스 사용자를 모두 괴롭게 할 것입니다.

04
Code Splitting 적용 후


하지만 위처럼 작은 조각으로 나눠 필요한 시점마다 필요한 번들 js만 로드하면 매우 빠른 페이지를 제작할 수 있습니다. 따라서 Code Split 기능은 매우 중요한 이슈입니다.

물론 개발을 진행하다 보면 역시 어느 것 하나 쉽게 넘어가지지 않습니다. 관리자의 웹팩은 4.x 버전대를 사용하고 있습니다. 예전에 TF에서는 Webpakc 3.x 버전대를 사용하였는데 당시에는 CommonChunkPlugin 설정을 통해 Code Splitting을 사용할 수 있었습니다. 그대로 관리자에 적용하려 했는데..

05


06


Removed라고 쓰여 있습니다. 찾아보니 CommonChunkPlugin이 옵티마이즈 옵션 하위의 splitChunk 속성으로 들어가면서 설정 방법이 바뀌었더군요. 머리를 싸매고 설정을 잡습니다.

4-1) /[webpack루트]/build/webpakc.config.js : 공통 설정파일

'use strict'
const HtmlWebpackPlugin = require('html-webpack-plugin'); 
const { VueLoaderPlugin } = require('vue-loader');
const path = require('path');
 
module.exports = {
    entry: { //string, object, array 가능 - 기본은 ./src
        app: path.join('[스크립트 파일 경로]', 'index.js') //진입점 스크립트 파일입니다.
    },
    output: { 
        path: '[빌드된 js 목적지 경로]',
        publicPath: '[이미지등의 웹상 리소스 경로]', 
        filename: './[name].[chunkhash].js', // 엔트리 파일명명규칙
        chunkFilename: '[id]_[chunkhash].js', // chunk파일 명명 규칙 
        // --mode development에서는 [id]에도 chunkName들어갑니다.
    },
    //vue와 js, css 로드 규칙을 설정합니다.
    module: {
        rules: [
            {
                test: /\.vue$/,
                loader: 'vue-loader',
                include: [
                    /[Vue 소스 경로]/
                ]
            },
            {
                test: /\.js$/,
                use: {
                    loader: 'babel-loader?cacheDirectory',
                },
                include: [
                    /[Vue 소스 경로]/
                ]
            },
            {
                test: /\.css$/,
                oneOf: [
                    {
                        use: [
                            'vue-style-loader',
                            'css-loader'
                        ]
                    }
                ]
            }
        ]
    },
    resolve: {
        alias: {
            '@': '[Vue소스 경로]', // 편의상 소스단축경로를 설정합니다.
        },
        //파일 확장자 자동인식 임포트시 해당 확장자는 생략가능합니다.
        extensions: ['.js', '.vue', '.json'], 
    },
    plugins: [
        // Vue 파일 로더
        new VueLoaderPlugin(),
        // html 자동 바인딩
        // 아래의 플러그인으로 인해 index.html에 해시네임으로 빌드된 index.js가 자동으로 매핑됩니다.
        new HtmlWebpackPlugin({
            // index.php에서 include할 파일이 생성될 경로와 파일명 입니다.
            filename: path.join('[View경로]', 'index.html'), 
            // 자동으로 매핑할 진입점파일을 지정합니다.
            template: path.join('[Vue소스 경로]', 'index.html'),
            inject: true,
            minify: {
                removeComments: true,
                collapseWhitespace: true,
                removeAttributeQuotes: true
            }
        }),
    ],
    optimization: {
        //웹팩 4.x 버전에서 옵티마이즈 속성으로 추가된 CodeSplitting 기능입니다.
        splitChunks: {
            //initial - static파일만 분리, async - 동적로딩파일만 분리, all - 모두 분리
            chunks: 'async', 
            minSize: 30000,
            minChunks: 1,
            maxAsyncRequests: 5, //병렬 요청 chunk수
            maxInitialRequests: 3, //초기 페이지로드 병렬 요청 chunk수
            automaticNameDelimiter: '_', //vendor, default등 prefix 구분문자 (default : '~')
            name: true, //development모드일때 파일에 청크이름 표시여부
            cacheGroups: {
                default: {
                    minChunks: 2, //2개 이상의 chunk
                    priority: -20,
                    reuseExistingChunk: true //minChunks이상에서 사용할경우 공통사용
                },
                //axios, vue 같은 공통 모듈은 vendor로 관리합니다.
                vendors: {
                    test: /[\\/]node_modules[\\/]/,
                    priority: -10
                }
            }
        }
    }
};



4-2) /[webpack루트]/build/webpack.dev.config.js 개발용 설정 파일 (네이밍은 자유)

'use strict'
const merge = require('webpack-merge')
const webpack = require('webpack')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 
const baseWebpackConfig = require('./webpack.config')
const config = require('../config').dev //개발용설정
 
const devWebpackConfig = merge(baseWebpackConfig, {
    //mode는 chunk[id], 디버깅 코드 등에 영향을 줍니다. webpack 3.x 버전에서는 Env 속성을 통해 관리했다고 합니다.
    mode: 'development', 
    plugins: [
        new BundleAnalyzerPlugin(), //번들 무게 분석기 제대로 스플릿 되었는지 확인할 때 사용합니다.
        new webpack.DefinePlugin({
            env : config.env
        }),
    ],
    watch: true, //코드의 변화를 감지해 자동으로 재빌드해주는 옵션입니다.
    cache: true, //캐시 사용을 활성화하면 변경사항이 있는 코드만 재빌드합니다.
    optimization: {
        //uglify 플러그인 코드 압축 여부를 설정합니다 압축 시 용량을 매우 줄일 수 있으나 빌드 속도가 크게 저하되므로 개발 시에는 꺼줍니다.
        minimize: false, 
    }
})
 
module.exports = new Promise((resolve, reject) => {
    resolve(devWebpackConfig);
})


4-3) /[webpack루트]/build/webpack.prod.config.js 서비스용 설정파일 (네이밍은 자유)

'use strict'
const merge = require('webpack-merge') // 설정파일 결합에 사용합니다.
const webpack = require('webpack') 
const baseWebpackConfig = require('./webpack.config') //베이스 설정파일
const config = require('../config').prod //서비스용 설정
 
const prodWebpackConfig = merge(baseWebpackConfig, {
    mode: 'production', //chunk[id], 디버깅 코드등 영향 있음
    plugins: [
        new webpack.DefinePlugin({
            env : config.env
        }),
    ],
    //개발용과 반대로 용량은 줄이고 필요 없는 기능은 꺼줍니다.
    watch: false,
    cache: false,
    optimization: {
        minimize: true,
    }
})
 
module.exports = prodWebpackConfig





5.package.json

웹팩 설정 파일이 분리되면서 package.json의 런 스크립트도 변경했습니다.

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





6.Vue-Router

Vue-Router는 위에서 설명한 대로 Vue의 컴포넌트와 밀접하게 결합된 라우터입니다. 그런데 여기서 webpack의 Code Split을 사용하려면 컴포넌트 import 방법이 매우 중요한데요.

import './testComponent' 


위처럼 import 구문을 사용해 컴포넌트를 불러오면 코드가 쪼개지지 않고 한 덩어리로 빌드되므로 아래와 같은 형태로 사용해야 합니다.

const testComponent = () => import('./testComponent') 


webpack 공식 문서에도 나와있듯이 위처럼 ES2015 Loader spec에 있는 import()를 사용하여 컴포넌트를 생성해야 번들 js가 제대로 분리되며, Dynamic Import가 가능해집니다.

Vue-Router를 쓰는 순간 싱글 페이지 어플리케이션이 되기 때문에 이곳에서 설정을 잘못 잡아주는 순간 육중한 컴포넌트 한 덩어리가 튀어나오면서 Code Splitting은 물거품이 되어버립니다. 조심합시다!

또한 import 함수 안쪽엔 아래와 같은 주석을 달아야 청크 이름이 적용됩니다.

const testComponent = () => import( /* webpackChunkName: '[청크이름]'*/ './testComponent') 



라우터 경로 속성인 path 와 Codeigniter의 컨트롤러 경로를 맞춰주는 것이 포인트입니다!

6-1) /[webpack루트]/router/index.js - 경로와 파일명은 자유입니다!

import Vue from 'vue'
import Router from 'vue-router'
 
// 주석의 webpackChunkName = 코드 스플릿 chunk Name으로 사용됩니다.
// 꼭 컴포넌트와 청크 이름을 같게 설정할 필요는 없습니다.
const SalesAnalysisProduct = () => import(/* webpackChunkName: "salesAnalysisProduct" */ '[컴포넌트 파일 경로]')
const SalesAnalysisSeller = () => import(/* webpackChunkName: "salesAnalysisSeller" */ '[컴포넌트 파일 경로]')
const TrendAnalysisProduct = () => import(/* webpackChunkName: "trendAnalysisProduct" */  '[컴포넌트 파일 경로]')
const TrendAnalysisSeller = () => import(/* webpackChunkName: "trendAnalysisSeller" */  '[컴포넌트 파일 경로]')
 
Vue.use(Router)
 
const router = new Router({
    mode: 'history',
    routes: [
        /* 통계 */
        {
            path: '[CI컨트롤러 url]/salesAnalysisProduct',
            name: 'salesAnalysisProduct',
            component: SalesAnalysisProduct
        },
        {
            path: '[CI컨트롤러 url]/salesAnalysisSeller',
            name: 'salesAnalysisSeller',
            component: SalesAnalysisSeller
        },
        {
            path: '[CI컨트롤러 url]/trendAnalysisProduct',
            name: 'trendAnalysisProduct',
            component: TrendAnalysisProduct
        },
        {
            path: '[CI컨트롤러 url]/trendAnalysisSeller',
            name: 'trendAnalysisSeller',
            component: TrendAnalysisSeller
        },
    ]
})
 
// 아래의 함수로 전처리 후처리도 가능합니다!
router.beforeEach((to, from, next) => {
  // ...
})
 
router.afterEach((to, from) => {
  // ...
})
 
export default router





7.Vuex

앞서 Vue와 Vuex, 컴포넌트간 통신과 상태 관리에서 소개했던 상태 관리와 통신을 위한 Vuex도 추가합니다. Vuex는 하나의 Store만 쓸 경우 상태 변수의 과포화로 인해 유지 보수가 어려워질 수 있으므로 namespace: true 옵션을 통해 도메인별로 관리합니다.

7-1) /[webpack루트]/vuex/store.js - Vuex 진입파일

import Vue from 'vue'
import Vuex from 'vuex'
 
// 각 도메인별 store들이 들어있는 modules 를 임포트해줍니다.
import * as modules from './modules/index'
 
Vue.use(Vuex)
 
export default new Vuex.Store({
    state : { },
    getter: { },
    mutations : { },
    actions : { },
    modules : modules.default
})



7-2) /[webpack루트]/vuex/modules/index.js - 도메인별 Store 자동 바인딩 스크립트

const files = require.context('.', false, /\.js$/)
const modules = {}
 
//자신(index.js)를 제외한 파일들을 파일이름을 Key로 modules에 담습니다.
files.keys().forEach((key) => {
    if (key === './index.js') return
    modules[key.toLowerCase().replace(/(\.\/|\.js)/g, '')] = files(key).default
})
 
export default modules



7-3) /[webpack루트]/vuex/modules/statistics.js(통계 store 파일) - 예시입니다.

export default {
    namespaced : true, //해당 속성을 통해 파일명을 namespace로 사용합니다.
    state: { /* 상태값 및 데이터 */
        ...
    },
    getters: {
    },
    mutations: { /* state 변경처리 */
        ...
    },
    actions: { /* 통신처리 */
        ...
    }
}



namespace: true로 되어있으므로 파일명인 statistics를 namespace로 사용하게 됩니다. 따라서 store 각 항목에 대한 접근은 다음과 같이 이뤄지며 computed 속성에 state: this.$store.state.statistics 처럼 정의해두면 편리합니다.

  • dispatch는 this.$store.dispatch(‘statistics/[action 이름]’)
  • commit은 this.$store.commit(‘statistics/[mutation 이름]’)
  • state 변수 접근은 this.$store.state.statistics.[state 이름]



8.공통 처리 mixin

api 통신에 사용되는 통신 라이브러리와 그 라이브러리의 복잡한 설정 코드, 단순한 Toast 출력 함수, 로딩 이펙트를 보여주는 함수 등 모든 항목들이 매 페이지마다 있으면, 통일되지 못한 UI, 페이지마다 일관되지 못한 설정 등으로 휴먼 에러가 발생할 확률이 높아집니다. 유지 보수 측면에서도 비용이 높아집니다. 이러한 단순 반복 코드들은 한번만 정의하고 재사용하는 것이 바람직합니다. 나중에 수정할 때도 용이하죠.

공통사항을 묶어 Vue 전역 믹스인으로 Vue 루트 객체에 추가합시다. 단, global 옵션인 만큼 조심해서 써야 합니다. 시스템에 영향을 줄 것 같으면 하위 컴포넌트 mixins 속성에 넣어 해당 스코프에서만 사용하는 것이 바람직합니다.

8-1) /[webpack루트]/common/common-mixin.js (파일이름, 경로는 자유입니다!)

import Vue from 'vue'
import Vue from 'axios'
import Cookies from 'js-cookie'
 
const TIMEOUT = '[타임아웃 시간(ms)]'
 
/*
    mixin의 기본 형태는 Vue 컴포넌트의 형태와 동일합니다.
    주로 전역 통신과 상태 관리는 vuex store에서,
    전역 data 속성과 전역 함수는 mixin에서 관리합니다.
*/
Vue.mixin({
    /* 전역 사용 data속성 선언 */
    data: () => {
        return {
            ... //이곳에 선언하는 data 속성은 전역에서 this로 접근 가능합니다.
        }
    },
    created: function() {
        // 공용 axios 객체 생성
        this.axios = axios.create({
            timeout: TIMEOUT,
            withCredentials: true,
            //공통해더는 여기에
            headers : { }
        });
        
        //axios 의 success와 error를 mixin method에서 처리 하도록 등록
        this.axios.interceptors.response.use(this.onSuccess, this.onError)
    },
    /* 전역 사용 함수 선언 */
    methods: {
        /* axios의 response handling 함수*/
        onSuccess : response => { },
        onError : function (error) { },
 
        /*GET, POST 등의 통신 함수, Toast(alert) 표출함수, 에러핸들링함수 등 선언*/
 
        /*... 내용이 너무 길어서 생략 ...*/
    }
});





9.요약

지금까지의 내용은 파일 경로를 토대로 요약하면 다음과 같습니다. 참고로 아래의 폴더 구조는 절대적인것은 아닙니다. 모든 폴더 구조는 자율이며, 폴더 구조에 맞게 webpack.config.js에서 조정해주면 됩니다.

[프로젝트 루트]
    └ [웹팩 루트]
        └ package.json
        └ [Vue 소스 루트]
            └ [common]
            └ [router]
                 └ index.js // 라우터 설정파일 - CI 컨트롤러와 url 맞춰줘야함
            └ [vuex]
                 └ index.js // 도메인별 store module export 스크립트
                 └ [modules]
                      └ 도메인별 store.js
            └ [컴포넌트 폴더] //예시에서는 ststistics
            └ App.vue //진입점 vue파일 Vuex와 전역 mixin 세팅 
            └ index.html //index.js가 주입될 껍데기
            └ index.js  //진입점 js Vue-Router와 App.vue 세팅
        └ [build] // 빌드파일경로
            └ webpack.config.js //베이스 설정파일
            └ webpack.dev.config.js //개발용 설정파일
            └ webpack.prod.config.js //서비스용 설정파일
    └ [application] //Codeigniter 루트
        └ [controllers]
            └ [컨트롤러 경로] // 예시의 통계부분
        └ [views]
            └ [웹팩빌드 결과 폴더]
                └ [index.php] // CI 에서 로드하는 view (index.html include)
                └ [index.html] // js 번들이 자동 주입된 빌드결과 파일
    └ [include]
        └ [scripts]
            └ [빌드결과 js 경로] //public path 속성 경로
                └ 빌드 결과 js chunk들 





마치며

관리자 서비스에서 완전한 Vue를 사용하기 위해 꽤 험난한 과정을 거쳤습니다. 지금도 잘 돌아가는 서비스에 리스크를 감수하면서도 새로운 것을 도입하려는 이유를 찾아야 했고, 한동안은 레거시와 Vue로 된 소스를 2중으로 개발해야 했습니다.

게다가 이 글을 작성하기 시작했을 땐 Code Splitting 설정 방법이 바뀌어 적용하지도 못한 상황이었기 때문에 사실 Code Splitting 내용이 없었습니다. 그런데 글을 작성하면서 splitChunk옵션을 성공해버렸어요! 덕분에 이 글도 모두 수정해야 했죠. Vue의 도입을 고려하는 개발자분들에게 도움이 되길 바라는 마음으로 글을 마칩니다.



참고
1)Vuex Store는 Vue와 Vuex, 컴포넌트간 통신과 상태 관리에 자세히 정리해두었다.

2) 브랜디 관리자 서비스는 jQuery로 작성되어 있다. 따라서 jQuery를 베재할 수만은 없는 상황이었다. 이에 따라 기존 jQuery 컴포넌트들에 대한 해결책은 천보성 팀장님이 작성한 JQuery 프로젝트에 VUE를 점진적으로 도입하기를 참고했다. props와 emit 기능을 이용해 jQuery로 제작한 컴포넌트를 깔끔하게 Wrapping 하는 방법에 대해 자세히 기술되어 있으며, 이를 활용하면 레거시 UI 플러그인을 마치 네이티브 Vue 플러그인처럼 사용할 수 있다.


강원우 과장 | R&D 개발2팀
kangww@brandi.co.kr
브랜디, 오직 예쁜 옷만