laravel-vue-spa 따라잡기?
내친 김에 오늘은 Github 공개 프로젝트인
A Laravel-Vue SPA starter project template. 리포를 내려받고, 이 프로젝트의 프론트엔드 부분을 뜯어보고자 한다. 한때 VueJS 를 사용할 줄 안다고 생각했던 백앤드 개발자가 1년 정도의 프론트앤드 개발 공백기를 갖은다음 겪은 좌충우돌 경험기라고 보면 되겠다. 나는 아직 위 공개 프로젝트가 best practice 의 모델이 되는지 판단하기 어렵지만, 최소한 이 정도 코드 베이스를 읽고 이해할 수 있는 이해도를 갖추는 것이 이번 포스팅의 목표이자, 웹개발자로서의 편협적인 지적 한계를 벗어나기 위한 목표이기도 하다.
불과 1-2년전 Laravel 과 VueJS 로 프론트앤드 작업을 해봤었던 개발자로서, Webpack 으로 발단된 새로운 SPA 개발 flow 가, 한때 Laravel 의 뷰 단위로 VueJS 를 사용하여 웹을 개발했었던 flow 에 비하여, 중복된 concern 을 동시에 가져가야만 하는 심각한
a pain in the ass 로 변한 것 같다는 생각은 지울 수 가 없다. 이번 해 라라콘
20180726 Laracon 2018 7 Caleb Porzio 프리젠테이션을 보아도 나만 그렇게 느끼는 건 아닌듯 하지만, 소위
cool kids on the block
이 작성한 코드를 이해하지도 못한다는 사실은, 다시금 VueJS 학습에 대한 강한 동기를 마련해 주었다.
오늘은 어찌된 영문인지, Program with Erik 이란 유튜브 채널에서 VueJS 관련 방송을 하던 개발자가
5 Reasons Why You Shouldn’t Become A Software Engineer 라는 방송을 올렸던데, 비트코인 추락 속도마냥 엄청나게 빠른 속도로 1년전 기술이 도태되는 업종에서 일하는 공돌이 Ordeal 을 이야기한 내심은 잘 모르겠다. 어찌되었건, 넉넉하지 못한 시간을 쪼개서 항상 새로운 기술을 Radar 처럼 인지하고 있으면서 follow up 해야 하는 이 직업 상의 여유롭지 못함으로 인하여, 나 자신을 고지식하고 편협한 framework 속에 영원히 가두면서 살지 않았으면 좋겠다는 생각이 들었다.
- Google to the rescue!
Anyway, 현재의 나 자신은 ES6 문법도 많이 까먹었은 상태지만, Google 이 있으므로 큰 문제될 건 없을 것으로 본다. 일단 /resources 폴더의
index.blade.php
를 보니 HTML 의 scaffolding 을 라라벨 블레이드 엔진을 한번 통과하여 사용하고 있다. SPA 를 만들려고 했으면서, vue-cli 를 사용하지 않고, 굳이 이렇게 라라벨 디펜던시를 유지한건 좀 이상하지만, 문제의 핵심은 이것이 아니므로, 패스…
- JS 의 엔트리 포인트인 app.js
오케이 여기까진, 필요한 js 모듈들을 가져와서, Vue 인스턴스를 초기화 하는 거라는 건 알겠다.
vue
를 제외하곤 로컬에 있는 디렉토리를 지정한 것이므로,
~/store
부터 한번 들어가 보겠다. 기본적으로
index.js
를 읽을거 같이 생겼으니, 오래 생각 안하고 해당 파일을 열어본다.
- /js/store/index.js
처음부터 이해가 잘 안되다보니, 우선, 몇가지 키워드로 구글링해서 찾은 아래 페이지 읽었다.
VueJS Component Registration
오케이,
require.context()
는
webpack
을 이용해서, 기본으로 사용할 컴포넌트들을 global component 들로 레지스터를 하는 메서드라는 것 까지 알겠다. 그래도 드는 생각은
Duh… I still can’t get my head around the whole thing. 이때 문득, 이전에 버스 타고가다가 유투브로 본
7 Secret Patterns Vue Consultants Don’t Want You to Know – Chris Fritz 에서 들었던 내용이 떠올라 다시 보기로 한다. 요행히 어떤 사람이 이미
한글로 정리한 내용도 찾아진다. 하지만, 다시 보아도 직접적인 line by line 설명없이, 위의 코드를 이해하는데는 많은 도움이 되진 않았다. namespaced 옵션의 목적은 뭐고, require.context() 의 역할은 뭔지 등을 모르고서는, 파악이 되지 않아 또 다시 vuex 에 대한 구글링…
이번에 찾은 내용은 비디오 강좌였다.
Vuex for Everyone 이라는 무료 비디오 강좌인데, 다행히… 원하는 내용을 커버하고 있었다. 근데, 이 사람 뭥미… 발음이 완전 인도사람 발음이라 뭔소리인지 잘 안들린다. 캡션이 제공되지 않았더라면… 흠… 나중에 찾아보니, Alex Kyriakidis 라는 그리스 사람이고, VueJS core 멤버란다. 가만 들어보니 인도 발음과 또 다르네 그려. 발음만 제외하면, 굉장히 핵심적인 내용을 명료하게 짚어주는 강의였기에, 게다가 무료. 추천 VueJS 컨텐츠 등극, 쾅… 어찌 어찌해서 보게된 이 비디오강의로 인하여, 시간상으론 상당한 detour 였으나, 그 강좌를 통해 vuex 사용법을 캐치업 하고 난 다음 다시 코드를 보니
store
코드들을 잘 이해할 수 있었다.
line #9 ~ #19
Vuex 에 필요한 state
, getters
, mutations
, actions
을 namespaced module
로 불러오겠다는 의미다.
또한,
mutations
부 에서는 state 변화만 처리하는 것이 best practice 이므로, store/modules/auth.js 와 store/modules/lang.js 의
mutations
부 에서 사용하는 있는 Cookie.set(), Cookie.remove() 는 actions 으로 이전시키는 리팩터링이 필요하겠다는 생각에 까지 도달했다.
- /js/router/index.js
다음은
router
관련 코드 인데,
store
보다 많이 길고, 조금 더 난해하다.
SPA 의 라우팅을 담당하는 로직 부인데, 일단, 낯선 ‘vue-meta’, ‘vue-router’, ‘vuex-router-sync 내용을 포함하고 있으니, 해당 내용을 follow up 하기 위해 아래의 문서들을 먼저 읽었다.
vue-router 문서
vue-meta 문서
vue-router-sync 문서
라우터를 정의한 내용은 길기도 하거니와, 나 처럼 사전지식이 부족했던 사람들은 이해하기 쉽지 않을 것이므로, 상세히 line by line 설명을 적어보겠다.
line #11 ~ 12
글로벌 미들웨어, Vue Router 용어로는 Navigation Guards, 로 사용할 함수 컴포넌트 이름들 중 일부분이다.
라라벨 백그라운드를 갖고 있기에 라라벨에서 사용하는 미들웨어 용어를 사용한 듯 싶다. 어쨌든, 귀여운 시도다. 여기에서는 네이게이션 가드 함수들 중에서, app 내의 모든 페이지 전환시 공히 사용하는
글로벌 미들웨어
의 키값을 우선 정의해 놓았다.
로케일
과
인증
이므로, 납득이 가는 설정이다.
line #15 ~ 17
middleware
폴더에 존재하는 모든 .js 를 읽어서, export default 함수들을 import 한다.
코드 마지막 부분 (line #191 ~ 199) 에 있는
resolveMiddleware()
를 사용해서, 마치
store/index.js
의 하위
modules
폴더 안에 존재하는 모든 component 모듈들을
Webpack
의
require.context()
API 를 사용하여 import 한 것처럼, 여기서는
router/index.js
의 하위
middleware
폴더 안에 존재하는 모든 export default 함수 모듈들을 import 한다.
line #19 ~ 23
이 앱에서 사용할 라우터 object를 생성한 다음, vuex 의 store.state.route
와 현재 $route
의 값을 sync 시키는 vue-router-sync 플러그인을 사용하기 위해, 해당 플러그인의 sync() API 를 호출한 다음, 생성된 라우터 object 를 default export 시켰다.
라우터 object 를 생성하기 위해서, 라인 #30 ~ #41 에 정의된 createRouter() 함수를 사용하는데, 이때 이 함수는, 또 다시
Scroll Behavior 를 정의하기 위한
scrollBehavior()
함수와,
Global Before Guards 를 등록하기 위한
beforeEach
함수와,
Global After Hooks 를 등록하기 위한
afterEach
함수 등 모두 3개의 함수를 내부에서 사용한다.
line #169 ~ 185
Scroll Behavior 함수
Vue Router 문서의 Scroll Behavior 부분을 읽어보면 이해가 어렵지 않다. savedPosition 이 있으면, 그 위치를 리턴하고, 이동할 to
route
object 들의 anchor 포지션이 설정되었으면 그 selector 이름을 리턴하고, to
route
object 의 컴포넌트를 Vue Router 의
router.getMatchedComponents API 을 호출하여 알아낸다음 가장 마지막
component
object 에 (ex.
/pages/settins/profile.vue
컴포넌트 object 처럼)
scrollToTop
property 가 false 로 설정되어 있다면, 빈
{}
object 를 리턴하여, 스크롤 위치를 보존하도록 하고, 이도 저도 아닌 fallback 로직으로는
{ x:0, y:0 }
object 를 리턴하여, 무조건 최상단으로 scroll 시키겠다는 내용이다.
참고로,
router.getMatchedComponents({ ...to })
는 component 인스턴스가 아닌 definition/constructor 가 배열로 리턴되며, console.log() 으로 찍어보면 아래와 같은 내용을 볼 수 있다. 나처럼 그 값들이 궁금했다면, 참고하시길…
[
{ middleware: "auth", _compiled: true, __file: "resources/js/pages/settings/index.vue", computed: {…}, beforeCreate:[…], … },
{ scrollToTop: false, _compiled: true, __file: "resources/js/pages/settings/profile.vue", computed: {…}, created: …, data: …, metaInfo: …, methods: …, … }
]
line #50 ~ 77
beforeEach 함수
이 함수는 promise 를 리턴한다. (혹시, async + await 에 대한 내용이 익숙치 않다면, 이번 라라콘에서 CSS Grid 를 발표했었던
Wes Bos
의
관련 freeCodeCamp talk 유튜브 비디오을 봐도 되겠다. async + await 을 한줄로 설명한다면, promise 처리를 synchronously 이해할 수 있는 코드로 작성하는 방법이다.) 이젠 너무 반복된 코드의 설명은 빼고 나머지 코드의 내용을 설명하면, 61번~63번 라인은 컴포넌트 definition 에 명시적으로
loading
property 가 false 라는 값으로 설정된 경우가 아니라면,
App.vue
컴포넌트의
mounted()
life cycle 에서, Vue.$loading 으로 지정한, (다시말해 router.app.$loading 로 접근가능한)
Loading.vue
컴포넌트의
start()
메서드를 실행시킨다.
66번 라인은 match 되는 컴포넌트에서 모든 필요한 middleware 를 이전에 설정한 globalMiddleware 배열과 merge 한 결과를 구한다. 즉 컴포넌트에 middleware object 가 있는 경우, middleware 라는 이름의 배열로 리턴한다. middlewares 라고 썼음 더 좋으련만 내 코드가 아니니 또 패스.
69번~76번 라인은 각각의 미들웨어를 호출하는 부분이다. 참고로,
Global Guard의
next()
함수 설명부를 보면 파이프라인의 다음번 hook 으로 이동하는 경우는
next()
함수 호출시 인자값이 없는 경우이다. 다시 말해 next() 의 인자가 undefined 인 경우만 next() 함수가 호출되어 다음 로직 이 진행된다. 이 점 유념하고, dynamic components 에 대한 이해를 높이기 위해 아래 VueJS 문서를 다시한번 정독했다.
has anyone heard of RTFM?
flow 는 callMiddleware 함수로 넘어간다. 아래에 나열한 4개의 인자와 함께.
- middleware 배열
- Global Guard 의 beforeEach 에서 전달 받은 to Route object
- Global Guard 의 beforeEach 에서 전달 받은 from Route object
- callback 함수
여기 저기서 똑같은 next() 이름의 함수로 호출해 대니, flow 를 따라가다가, 머리 속에 그린 로직이 mixed up 되는 존나 안읽히는 코드다. 뭐가 진짜 next() hook 인지 정신 똑바로 차리지 않으면 헷갈리니, 차라리 100번째 라인의 next 인자는 nextCb 으로, 110번째 라인의 next(…args) 는 nextCb(…args) 으로 바꾼다음 읽으면 낫다. 하는 일은 배열로 전달한 각각의 middleware 를 재귀호출 하는 것이다.
여기서 재밌는 점은, 미들웨어 처리를 다한다음에, 75번째 라인의 진짜 next() hook 을 호출하기 전에, args length 가 0 이면, 즉 정상적으로 미들웨어 패스가 되고 난 후라면, Vue 인스턴스에 전달한
App.vue
컴포넌트의 layout 을 설정한다. 이 때, 첫번째 component 즉, components[0] 에 layout 가 설정되어 있는 경우는 그 페이지 layout 으로 설정하며, 아닌 경우에는 default layout 으로 설정한다. 다시 말해,
routes.js
에서 지정하는 최상위 component 에 layout 옵션을 사용하면 원하는 layout 설정이 가능하단 말이다. default 레이아웃 (
default.vue
) 은 NavBar 가 포함되어 있고, basic 레이아웃 (
basic.vue
) 은 네비게이션 바가 없는 웰컴 페이지와 같은 데서 사용한다.
재귀호출의 시각은 124 번 라인의
_next()
로 시작하는데, 이를 위해 미들웨어 배열의 아이템들을 역순으로 바꾸고 하나씩
pop()
하여, 원래 지정한 components 순서대로, 가드를 하나씩 하나씩 재귀 호출한다. VueJS 에서 어떤 Route 에 대한 multiple guards 를 natively 지원하지 않기때문에 이런 hacky 방법을 쓰는데,
vue-router-multiguard 라는 npm 패키지를 사용하면, 좀 더 읽기 편한 코드를 만들 수 있을 듯 하다. 왜 이런식으로 빙빙 돌려서 재귀호출을 하는지는 vue-router 리포의
관련 쓰레드에서 좀 더 읽어 볼 수 있다. 간단히 설명하자면, 가드가 여러개 있을때는, 어떤 가드에서 next() 를 바로 호출해 버리면 안된다. 다른 가드들을 거치면서, next() hook 이 호출되면 안되는 상황이 올 수도 있기 때문이다. 그래서 모든 가드들을 패스한 후, next() 를 마지막에 한번만 호출해야 되는 것이다.
app.js
의 Vue object 정의 맨 마지막에 들어간 App.vue 컴포넌트를 살펴본다. 기본 템플릿으론 loading 컴포넌트를 사용하고 라우터 가드들을 빠져나오면서 설정한 layout 으로 해당 layout 컴포넌트를 동적 로딩을 한다. 또한 둘다 공히
<child/>
컴포넌트를 사용하고 있다.
Child.vue
파일을 보면 알 수 있듯이, 이 컴포넌트는 vue-router 에서 각 path 마다 매치되는 컴포넌트들을 보여주는
<route-view/>
컴포넌트를 제공하는 기본 템플릿라고 보면된다. 이후, vue-router 가 브라우저의 url 에 맞는 컴포넌트를 불러서
Child
컴포넌트의
<route-view>
안에 렌더링 시킨다.

크롬브라우저의 Vue.js devtools 익스텐션을 설치후 보면 위와 같은 계층적인 DOM 구조가 보인다.
Yay!
나머지 주요 패키지로는 vue-i18n 과 vform 이 있는데, 로컬라이제이션과 form 패키지는 각각의 패키지 문서를 참고하기 바라면서 이 글을 마친다. 개인적으로, 로컬라이제이션 패키지와 폼밸리데이션은 다른 github 프로젝트들도 많이 있어서,
monterail/vuelidate 프로젝트 등을 살펴보는 것도 필요해 보인다.
다음에는 vue-cli 와 vuetify.js 를 사용한 SPA 개발에 대하여 좀더 살펴보기로 하겠다.
Happy Coding!