일이 지났습니다. 시의성에 유의하세요
서문
다른 사람들의 vue 멀티페이지 프레임워크를 연구해보니, 대부분 package.json 파일을 직접 복사한 후 로컬에서 npm install을 실행하거나, 공식 vue-cli 도구를 사용해 프로젝트를 생성하는 방식이었습니다. 이런 방법은 이해는 되지만 기억하기 어렵다고 느껴, .vue를 컴포넌트로 사용하는 프로젝트를 처음부터 구축해보는 연습이 필요하다고 생각했습니다. 그래서 이 프로젝트를 시작하게 되었습니다.
.vue 컴포넌트를 사용하기로 했으니, 이전처럼 jQuery 방식으로 vue.js를 페이지에 직접 삽입하는 대신, webpack + babel + 다양한 loader 도구를 함께 사용해 .vue 파일을 컴파일하고 html로 번들링하는 방식을 채택했습니다.
FBI 경고
주의: 가장 기본적인 초기 경험을 목적으로 하기 때문에, 실제 개발에 필수적인 loader와 plugin은 일부러 설치하지 않았습니다. 공식 튜토리얼을 따라 수동으로 입력해보며 개념을 익히는 데 중점을 두었고, 특히 고급 튜토리얼의 복잡한 부모-자식 컴포넌트 간 데이터 전달, 스코프 슬롯, 재귀 컴포넌트, slot 등의 내용을 깊이 이해하기 위함입니다. 따라서 이 구성은 실제 개발용 참조 설정으로 사용할 수 없으며, vue 컴포넌트 작동 원리를 이해하기 위한 연습용 프로젝트로만 활용하시기 바랍니다.
구성 설명
아래 구성에 대한 상세 설명은 후반부에서 확인할 수 있습니다. 설명을 생략하고 싶으시면 바로 아래 package.json을 복사해 사용하셔도 되지만, 개념을 확실히 이해하기 위해 직접 입력해보는 것을 강력히 권장합니다.
자세한 설명은 생략하고 시작하겠습니다.
먼저, webpack+vue 환경이므로 관련 패키지 설치가 필수적입니다. 여기서는 vue@2.2.4와 webpack@1.12.2를 사용합니다:
1 | |
다음으로 babel과 관련 loader를 설치합니다. 여기서는 es2015 프리셋을 사용하며, 최신 버전을 선택하면 됩니다:
1 | |
이어서 webpack의 필수 loader들을 설치합니다. css-loader는 css 내 url() 리소스를 처리하고, style-loader는 require로 불러온 css를 추출해 style 태그로 만들어 페이지 head 부분에 추가합니다. html-webpack-plugin은 진입 파일 js를 html로 변환하며, 진입 파일의 다양한 리소스는 각종 loader가 처리한 후 생성된 html에 삽입됩니다. extract-text-webpack-plugin은 js를 통해 style 태그로 head에 추가된 스타일을 별도의 .css 파일로 추출하는 역할을 합니다:
1 | |
마지막으로 vue 관련 도구들을 설치합니다. .vue 파일 내부에는 최소 세 개의 태그(template/style/script)가 포함되므로, 이를 처리하기 위해 세 가지 loader와 통합 vue-loader까지 총 네 가지 loader가 필요합니다. 여기서:
vue-html-loader는 webpack의 공식 html-loader를 포크한 버전으로, 작성자가 webpack.config.js의 module.export.vue 객체에서 html 옵션을 별도로 구성할 수 있도록 하기 위해 포함되었습니다(본 프로젝트에서는 vue-loader만 설치하면 충분하며, 여기서는 설명을 위해 함께 언급했습니다).
vue-style-loader는 .vue 파일의 style 섹션 내용을 처리하기 위해 사용되며, webpack의 공식 style-loader의 fork 버전입니다(이 프로젝트에서는 vue-loader를 설치하면 되며, 여기서는 추가 설명을 위해 언급합니다).
vue-template-compiler는 .vue 파일의 template 섹션 내용을 처리합니다. 단, 컴파일된 파일로 다른 작업을 할 경우에만 별도로 구성해야 합니다(즉, build tools를 작성할 때 필요하며, 그렇지 않으면 필수적이지 않습니다. vue-loader가 이미 기본적으로 이를 사용하고 있기 때문입니다)(이 프로젝트에서는 vue-loader를 설치하면 되며, 여기서는 추가 설명을 위해 언급합니다).
vue-loader는 .vue 확장자를 가진 내용을 처리하며, 관련 내용이 있을 때 위의 세 가지 loader를 호출하여 처리합니다.
1 | |
마지막으로 개발용 webpack-dev-server입니다. 여기서는 1.12.1 버전을 설치합니다:
1 | |
아래는 전체 package.json 설정 파일이며, 각 package.json 필드의 구체적인 의미는 이 사이트에서 확인할 수 있습니다.
1 | |
프로젝트 설명
자, 의존성 설치가 완료되었으니 이제 webpack 설정을 살펴보겠습니다. vue 공식 문서의 컴포넌트 부분을 빠르게 테스트하고 싶어서 모든 것을 간소화했습니다:
1 | |
이 설정에 대해 몇 가지 설명이 필요합니다.
위에서 아래로, 먼저 alias입니다. vue-router를 사용해본 사람이라면 이 설정이 필요 없을 수 있지만, .vue 컴포넌트를 사용하는 프로젝트에서는 반드시 이 설정이 필요합니다. 사용할 vue의 js 유형을 지정해야 하기 때문입니다. 이 프로젝트의 node_module/vue/dist/ 폴더 아래를 보면 vue.js와 vue.common.js 두 가지 파일이 있습니다. 여기서 vue는 template 컴포넌트를 컴파일할 때 compiler.js가 필요하며, 이는 template의 html 내용을 render 함수로 컴파일하기 위함입니다:
컴파일 전:
1 | |
그리고 vue는 compiler로 template을 컴파일한 후 생성된 js를 실행할 때 render 함수가 있으면 바로 render를 실행하며, template 필드의 내용은 무시됩니다. 컴파일된 render를 실행하는 작업은 vue.common.js가 담당합니다. 따라서:
1 | |
그래서 vue-router를 사용하는 경우, 해당 package.json의 main 필드는 node_module/dist/vue.common.js를 가리킵니다. 이를 그대로 프로젝트에 복사하면 실행 시 vue.common.js의 runtime 오류와 유사한 메시지가 표시될 수 있습니다.
두 번째로 설명해야 할 것은 css-loader와 style-loader, 그리고 vue-style-loader입니다. style-loader가 있는데 왜 vue-style-loader가 필요할까요? vue-style-loader의 설명을 살펴보니, 이는 단순히 style-loader의 fork이지만, .vue 파일을 별도로 처리하기 위해 그리고 사용자가 vue를 더 명확하게 구성할 수 있도록 webpack@1.x 설정 파일의 vue 필드에 추가된 것임을 알 수 있었습니다. extract-text-webpack-plugin 플러그인의 설정을:
1 | |
에서:
1 | |
로 변경하면 컴파일 결과가 동일함을 확인할 수 있습니다.
기본적으로 vue-loader는 자동으로 vue-style-loader를 사용합니다. 따라서 .vue 파일에서 어떤 css도 @import하지 않는 경우, vue-loader-style을 vue.loaders 필드에 수동으로 추가할 필요가 없습니다. vue-loader는 .vue 파일의 style 태그 내용을 자동으로 처리하여 style 태그로 페이지에 삽입합니다. 그러나 .vue 파일의 style 태그 내에서 @import css 파일을 사용해야 하는 경우, module.exports.vue에서 별도로 구성해야 합니다. vue 필드에서 vue-style-loader를 제거하여 테스트할 수 있습니다:
1 | |
또한, 진입점 파일 js의 require('xxx.css')는 기본적으로 module.exports.module.loader에 의해 처리됩니다. 이는 기본 loader에서 extract 플러그인을 사용하고 module.exports.vue에서는 extract 플러그인을 사용하지 않음으로써 확인할 수 있습니다. 결과를 보면 진입점 파일의 css는 추출되었지만 .vue 파일에서 @import된 css는 추출되지 않았기 때문입니다.
진입점 파일에서 require로 가져온 css 파일을 별도로 추출해야 하는 경우, module.exports.module.loader에 extract-text-webpack-plugin을 설정해야 합니다.
참고: vue-style-loader를 module.exports.vue.loaders 필드에 추가하는 것은 .vue 파일의 style 태그 내용을 별도의 .css 파일로 추출하여 페이지에 link로 삽입하기 위함입니다. style-loader와 css-loader를 기본 module.exports.module.loaders에 추가하는 것은 vue의 style 태그 내용 처리에는 효과가 없습니다. 적어도 Vue 1.x 버전과 webpack 1.x 버전에서는 효과가 없으며, webpack 2.x 버전에서는 서드파티 필드가 제거되어 module.export에 임의로 필드를 추가하는 것이 제한됩니다.
마지막으로, css-loader와 style-loader는 항상 함께 작성됩니다. 왜냐하면 css-loader의 역할은 css 파일 내의 @import와 속성 값 url()의 의존 관계를 resolve하는 것이며, 단독으로 사용하면 사실상 아무런 효과가 없기 때문입니다. style-loader가 실제로 css를 처리하고 이를 js에 번들링하여 최종적으로 head(삽입 위치는 구성 가능)에 <style> 태그 형태로 삽입하는 loader입니다.
마지막으로 extract-text-webpack-plugin에 대해 설명하겠습니다. 이 플러그인은 세 가지 매개변수를 받습니다:
첫 번째 매개변수는 선택적 매개변수로, loader를 전달합니다. css 스타일이 추출되지 않았을 때 이 loader를 사용할 수 있습니다.
두 번째 매개변수는 컴파일 및 해석을 위한 css 파일 loader로, 위 예시의 css-loader와 같이 반드시 전달해야 합니다.
세 번째 매개변수는 몇 가지 추가 옵션으로, 현재로선 publicPath만 전달할 수 있으며, 현재 loader의 경로를 지정합니다.
그렇다면 첫 번째 매개변수를 언제 전달해야 할까요? 이는 스타일이 추출되지 않는 시점을 이해해야 합니다.
code splitting에 대해 아는 분이라면, 페이지 로드 시 사용되지 않는 코드가 있을 때 code splitting을 사용하여 이 부분을 분리하고 별도의 파일로 만들어 필요 시 로드할 수 있다는 것을 알 것입니다.
만약 이러한 분리된 코드에서 require로 스타일 파일을 불러온다면, ExtractTextPlugin을 사용하더라도 이 스타일 코드는 추출되지 않습니다.
이렇게 추출되지 않는 코드에 대해 loader로 어떤 처리를 할 수 있는데, 이것이 바로 ExtractTextPlugin.extract의 첫 번째 매개변수의 역할입니다.
자, 설정 파일에 대해 이야기했으니 이제 이 프로젝트가 어떻게 작동하는지 설명하겠습니다.
한 장의 그림이 천 마디 말보다 낫습니다. 먼저 코드 인터페이스를 보겠습니다:

(이미지가 작게 표시되면 마우스 오른쪽 버튼으로 새 탭에서 열어 확인하세요)
화살표로 표시된 대로, 우선 하나의 페이지에는 최소한 하나의 컴포넌트가 있습니다. 저는 그냥 하나의 페이지에 하나의 컴포넌트를 작성했으며, 다른 컴포넌트를 import하지 않았습니다.
따라서 하나의 페이지에는 최소한 세 개의 파일이 있습니다: .vue 파일, .js 진입 파일, 그리고 .html 파일(컴포넌트가 삽입되는 파일)입니다.
html 파일에는 app이라는 컴포넌트 이름을 작성하고, 진입 파일에서 vue 인스턴스를 생성한 후 app 컴포넌트를 사용합니다. 이 app 컴포넌트의 템플릿은 index.vue에서 가져오며, 컴포넌트에 해당하는 css, js 그리고 mvvm의 특징인 데이터 바인딩도 index.vue 안에 작성됩니다.
어떤 분들은 진입 파일 js가 어떻게 동일한 디렉토리의 html 파일을 찾는지 궁금해할 수 있습니다. 사실 이는 webpack.config.js 설정 파일에 이미 작성되어 있습니다:
1 | |
이 html 생성 플러그인은 js 진입 파일에게 필요한 템플릿이 app 디렉토리의 xxx.html에서 온다고 알려주며, 최종적으로 번들링된 bundle.js도 이 안에 inject되어 최종 페이지가 생성됩니다.
또 다른 질문으로는, 진입점 js 파일에서 vue 인스턴스를 생성할 때 사용되는 components.App이 컴파일 과정에서 this_is_main_page_which_you_put_components_into.html 파일 내의 <app> 컴포넌트 참조를 찾는 것인지, 아니면 runtime 시점에 최종 번들링된 bundle.js를 실행한 후 this_is_final_filename_address_you_visit_in_browser.html 페이지에서 <app> 태그를 찾는 것인지 궁금해하는 경우가 있습니다. 정답은 후자입니다. 앞서 언급한 HtmlwebpackPlugin 플러그인은 단지 html 생성과 번들된 bundle.js 주입만 담당하며, vue가 bundle.js로 패키징된 후 vue 인스턴스화 시점에서야 비로소 <app> 태그를 찾기 때문입니다.
이렇게 보면, script 태그에서 직접 vue.js 파일을 참조한 후 렌더링하는 효과와 동일합니다. 다만 webpack 방식은 컴포넌트를 분리하여 개발 과정의 코드/컴포넌트 구조를 더 명확하게 해주며, 직접 vue.js를 참조하는 것은 프론트엔드 runtime render인 반면, webpack은 compiler render 후 바로 실행되므로 후자가 효율성이 더 높습니다.
다음은 결과 페이지입니다:

(이미지가 작게 표시되면 마우스 오른쪽 버튼으로 새 탭에서 열어 확인하세요)
이미지를 보시면 의미가 명확해질 겁니다.
추가로 궁금한 점은 문서를 참고해 주세요.
지금은 여기까지 이야기하고, vue와 webpack의 주의사항/세부사항/디자인 사고에 관해서는 더 많은 이야기가 있으니 다음에 또 얘기해요~
저는 인생의 중요한 선택의 기로에 섰을 때, 누군가 최선의 방법을 알려주어 소중한 시간을 헛되이 보내지 않기를 바라곤 합니다. 그런 마음으로 저는 자주 블로그를 쓰며, 광활한 인터넷의 이 작은 구석에 제게는 단 한 번뿐인 인생 경험을 기록하여 도움이 필요한 분들에게 도움이 되기를 바랍니다.