티스토리 뷰

반응형

 

vue router 를 사용할 때 생기는 chunk 이슈를 다뤄보고자 한다.

문제 😱: Uncaught SyntaxError: Unexpected token '<'

이거, 한번 쯤 만나봤으리라.

자바스크립트가 "도저히 못읽겠어. 어떻게좀 해봐." 할 때 내뿜는 에러이다.

우리팀의 경우는, 이 에러가 굉장히 간헐적으로 나타나서 '무엇 때문에 이 에러가 나는가'를 명확히 하기 어려웠는데, 재배포시 일어나는 이슈로 우선 추측하였다.

문제 발생 상황: 재배포 → 메뉴의 이동

이건 언제 발생하느냐면, 메뉴의 이동(url 변경 → 새로운 chunk 파일 요청) 시에 나타난다.

문제를 추적해나가기 위해 상황을 좁혀,

"재배포 후, 다른 페이지에서 서버 메뉴로 진입을 하는 경우에 발생하는 문제"

라고 정해놓고 문제를 추적해 나가보자.

문제 추적 🧐 1. 브라우저는 파일을 왜 해석하지 못할까

참고로, 우리의 route config 는 아래와 같이 작성되어 있다. (펼쳐봐)

더보기
const Server = () => import(/* webpackChunkName: "Server" */ '@/views/inventory/server/pages/ServerPage.vue');

{
    path: 'inventory',
    name: INVENTORY_ROUTE._NAME,
    redirect: 'inventory/cloud-service',
    meta: { label: 'Inventory' },
    component: { template: '<router-view />' },
    children: [
        {
            path: 'server',
            meta: {
                label: 'Server',
            },
            component: { template: '<router-view />' },
            children: [
                {
                    path: '/',
                    name: INVENTORY_ROUTE.SERVER._NAME,
                    component: Server,
                },
            ],
        },
			...
		]
	...
}

 

재배포되어 더이상 Server.2937218a.js 파일이 존재하지 않다면 "응 그런 파일 없어~" 하고 네트워크 에러가 나야하는데

보다시피 정상.

 

그러나,

이런 에러가 났다.

 

뭐 어쨌든, 파일은 가져왔으니까.

결국 브라우저가 저걸 못읽겠다고 잡아떼는 상황인데.

자세히 소스 파일을 들여다봤다.

더보기
<!DOCTYPE html><html><head><base href=/ ><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel="shortcut icon" type=image/x-icon href=/favicon.ico><link rel=manifest type=image/x-icon href=/manifest.json><link rel=apple-touch-icon href=/images/icons/icon-192x192.png><link rel=stylesheet type=text/css href=/reset.css><link rel=stylesheet type=text/css href=/site-loader.css><script src=lottie.js></script><link rel=preload href=/fonts/roboto/roboto-v27-latin-regular.woff2 as=font type=font/woff2 crossorigin=anonymous><title></title><link href=/AddEventRulePage.2443967e.js rel=prefetch><link href=/AddNotificationPage.423f99ad.js rel=prefetch><link href=/AddNotificationPage~ManageNotificationPage~ProjectNotificationsPage~UserNotificationPage.262ec027.js rel=prefetch><link href=/AddServiceAccount.dd2b033d.js rel=prefetch><link href=/AddSpotGroupPage.25309be0.js rel=prefetch><link href=/AddSpotGroupPage~AlertDashboardPage~AlertListPage~CloudService~CloudServicePage~CollectorHistory~Col~1fe78708.a45199fe.js rel=prefetch><link href=/AddSpotGroupPage~SpotGroupDetailPage.4c1b9c62.js rel=prefetch><link href=/AlertDashboardPage.4e22d8b6.js rel=prefetch><link href=/AlertDashboardPage~CloudServicePage~CollectorHistory~Dashboard~ProjectDashboardPage~Server~SpotDashb~225d570e.3ef4eef0.js rel=prefetch><link href=/AlertDetailPage.6dd37180.js rel=prefetch><link href=/AlertDetailPage~EscalationPolicyPage~ProjectSettings.f647fded.js rel=prefetch><link href=/AlertListPage.5c40c793.js rel=prefetch><link href=/AlertListPage~ProjectAlert.600d25ee.js rel=prefetch><link href=/CloudService.56a70266.js rel=prefetch><link href=/CloudServicePage.0f8b0712.js rel=prefetch><link href=/CloudServicePage~PowerSchedulerPage~Server~ServiceAccount.de259d30.js rel=prefetch><link href=/CloudServiceSearch.682a59e6.js rel=prefetch><link href=/CloudServiceTypeSearch.01c8bd65.js rel=prefetch><link href=/CloudService~ProjectPage~SpotAutomationMainPage.ac5686f6.js rel=prefetch><link href=/CollectorHistory.e1b75a33.js rel=prefetch><link href=/CollectorPage.60229ab2.js rel=prefetch><link href=/CollectorPlugin.ce9943a8.js rel=prefetch><link href=/CreateCollector.fe3bbe46.js rel=prefetch><link href=/Dashboard.5b60298a.js rel=prefetch><link href=/Dashboard~ProjectDashboardPage.12a8dfb0.js rel=prefetch><link href=/Dashboard~ProjectDashboardPage~SpotGroupDetailPage.5594adf1.js rel=prefetch><link href=/DomainAdminSignIn.6aba32d3.js rel=prefetch><link href=/EscalationPolicyPage.fd9b6910.js rel=prefetch><link href=/EscalationPolicyPage~ProjectSettings.3e13a324.js rel=prefetch><link href=/KeycloakPage.81a0cd20.js rel=prefetch><link href=/ManageNotificationPage.8c7a9cc9.js rel=prefetch><link href=/ManageNotificationPage~ProjectNotificationsPage~UserNotificationPage.8e793b36.js rel=prefetch><link href=/MonitoringMainPage.8a7d7a69.js rel=prefetch><link href=/NoResource.ba963788.js rel=prefetch><link href=/PowerSchedulerLanding.af0c625f.js rel=prefetch><link href=/PowerSchedulerPage.e2700908.js rel=prefetch><link href=/PowerSchedulerPage~ResourceGroup.cccb9af0.js rel=prefetch><link href=/ProjectAlert.3320370f.js rel=prefetch><link href=/ProjectAlertPage.12f4777b.js rel=prefetch><link href=/ProjectDashboardPage.4e5d56f8.js rel=prefetch><link href=/ProjectDetailPage.7548f76f.js rel=prefetch><link href=/ProjectDetailPage~ProjectMaintenanceWindowPage.379f8f0c.js rel=prefetch><link href=/ProjectMaintenanceWindowPage.1efe29a0.js rel=prefetch><link href=/ProjectMemberPage.823e92e3.js rel=prefetch><link href=/ProjectMemberPage~ProjectPage.4d8fe64a.js rel=prefetch><link href=/ProjectNotificationsPage.861e77e0.js rel=prefetch><link href=/ProjectPage.08327029.js rel=prefetch><link href=/ProjectSettings.06976158.js rel=prefetch><link href=/ProjectTagPage.8f9d0731.js rel=prefetch><link href=/ProjectWebhook.0dba36f9.js rel=prefetch><link href=/ResourceGroup.4cb9fcc0.js rel=prefetch><link href=/Server.2cf4907a.js rel=prefetch><link href=/ServiceAccount.57e4165d.js rel=prefetch><link href=/ServiceAccountSearchPage.3593fb1d.js rel=prefetch><link href=/SignIn.e6fa5cdb.js rel=prefetch><link href=/SpotAutomationMainPage.1fc3a4bd.js rel=prefetch><link href=/SpotDashboardPage.c4780986.js rel=prefetch><link href=/SpotGroupDetailPage.38ea0f92.js rel=prefetch><link href=/SpotGroupPage.071a8566.js rel=prefetch><link href=/SupervisorPlugin.0ab663f4.js rel=prefetch><link href=/User.73334390.js rel=prefetch><link href=/UserAPIKey.61c8765e.js rel=prefetch><link href=/UserAPIKey~UserManagement.3dfa8abf.js rel=prefetch><link href=/UserAccount.2c09bb9c.js rel=prefetch><link href=/UserManagement.50749944.js rel=prefetch><link href=/UserNotificationPage.d0308613.js rel=prefetch><link href=/chunk-0ba3f7fd.994916e4.js rel=prefetch><link href=/chunk-1bc4056e.486766a9.js rel=prefetch><link href=/chunk-1be89e71.b5868b40.js rel=prefetch><link href=/chunk-2d0c95ba.41fa8489.js rel=prefetch><link href=/chunk-2d0f0b9f.a2c22464.js rel=prefetch><link href=/chunk-2d22c0b4.64ad6b33.js rel=prefetch><link href=/chunk-2e0b8c86.205aa870.js rel=prefetch><link href=/chunk-44e90382.9851c8f0.js rel=prefetch><link href=/chunk-461a2683.42df45c5.js rel=prefetch><link href=/chunk-4aaaf80d.a48cd1a6.js rel=prefetch><link href=/chunk-55bc86f8.4b05952e.js rel=prefetch><link href=/chunk-f5f99b42.ebace51f.js rel=prefetch><link href=/css/AddEventRulePage.bd08c668.css rel=prefetch><link href=/css/AddNotificationPage.7d8db5de.css rel=prefetch><link href=/css/AddNotificationPage~ManageNotificationPage~ProjectNotificationsPage~UserNotificationPage.43013aec.css rel=prefetch><link href=/css/AddServiceAccount.9a85fb46.css rel=prefetch><link href=/css/AddSpotGroupPage.ed6ed8b6.css rel=prefetch><link href=/css/AddSpotGroupPage~SpotGroupDetailPage.6276a2de.css rel=prefetch><link href=/css/AlertDashboardPage.b6c5bca9.css rel=prefetch><link href=/css/AlertDetailPage.312f4992.css rel=prefetch><link href=/css/AlertDetailPage~EscalationPolicyPage~ProjectSettings.7289a2eb.css rel=prefetch><link href=/css/AlertListPage.b4992990.css rel=prefetch><link href=/css/AlertListPage~ProjectAlert.9c01661b.css rel=prefetch><link href=/css/CloudService.d79778b1.css rel=prefetch><link href=/css/CloudServicePage.9d676694.css rel=prefetch><link href=/css/CloudService~ProjectPage~SpotAutomationMainPage.03f9a2b0.css rel=prefetch><link href=/css/CollectorHistory.978dc00e.css rel=prefetch><link href=/css/CollectorPage.52d7bc4e.css rel=prefetch><link href=/css/CollectorPlugin.db59d594.css rel=prefetch><link href=/css/CreateCollector.ed5a306e.css rel=prefetch><link href=/css/Dashboard.48fa125d.css rel=prefetch><link href=/css/Dashboard~ProjectDashboardPage.11f5c19f.css rel=prefetch><link href=/css/DomainAdminSignIn.815cc6d2.css rel=prefetch><link href=/css/EscalationPolicyPage.c882677f.css rel=prefetch><link href=/css/EscalationPolicyPage~ProjectSettings.5d111922.css rel=prefetch><link href=/css/ManageNotificationPage.1180520a.css rel=prefetch><link href=/css/ManageNotificationPage~ProjectNotificationsPage~UserNotificationPage.8ebb6cfb.css rel=prefetch><link href=/css/MonitoringMainPage.3630c535.css rel=prefetch><link href=/css/NoResource.a102c29a.css rel=prefetch><link href=/css/PowerSchedulerLanding.d670e43d.css rel=prefetch><link href=/css/PowerSchedulerPage.15b06258.css rel=prefetch><link href=/css/PowerSchedulerPage~ResourceGroup.f616876f.css rel=prefetch><link href=/css/ProjectAlert.111e2064.css rel=prefetch><link href=/css/ProjectAlertPage.5545d2a1.css rel=prefetch><link href=/css/ProjectDashboardPage.73985304.css rel=prefetch><link href=/css/ProjectDetailPage.33c7bbd9.css rel=prefetch><link href=/css/ProjectDetailPage~ProjectMaintenanceWindowPage.5588b8cb.css rel=prefetch><link href=/css/ProjectMaintenanceWindowPage.bdc07193.css rel=prefetch><link href=/css/ProjectMemberPage~ProjectPage.e4cd3c16.css rel=prefetch><link href=/css/ProjectPage.ae16d886.css rel=prefetch><link href=/css/ProjectSettings.f73a4e4f.css rel=prefetch><link href=/css/ProjectTagPage.77cd1e1e.css rel=prefetch><link href=/css/ProjectWebhook.244e7aaa.css rel=prefetch><link href=/css/Server.25f42c1f.css rel=prefetch><link href=/css/ServiceAccount.c600b0fa.css rel=prefetch><link href=/css/SignIn.07f21294.css rel=prefetch><link href=/css/SpotAutomationMainPage.25a5327b.css rel=prefetch><link href=/css/SpotDashboardPage.de09b2d3.css rel=prefetch><link href=/css/SpotGroupDetailPage.bebff5b1.css rel=prefetch><link href=/css/SpotGroupPage.cb642963.css rel=prefetch><link href=/css/SupervisorPlugin.ace392a4.css rel=prefetch><link href=/css/User.401d7e26.css rel=prefetch><link href=/css/UserAPIKey.65e3cb4e.css rel=prefetch><link href=/css/UserAPIKey~UserManagement.43c226e9.css rel=prefetch><link href=/css/UserAccount.3d6f3f4f.css rel=prefetch><link href=/css/UserManagement.cac7e050.css rel=prefetch><link href=/css/UserNotificationPage.57b523f0.css rel=prefetch><link href=/css/chunk-0ba3f7fd.cfd75d8e.css rel=prefetch><link href=/css/chunk-1bc4056e.ea70c3f0.css rel=prefetch><link href=/css/chunk-1be89e71.d34b8ff7.css rel=prefetch><link href=/css/chunk-2e0b8c86.afb7cc4a.css rel=prefetch><link href=/css/chunk-44e90382.45ef29c4.css rel=prefetch><link href=/css/chunk-461a2683.c3a06ffd.css rel=prefetch><link href=/css/chunk-4aaaf80d.5eb1f8ea.css rel=prefetch><link href=/css/chunk-55bc86f8.5a3dab04.css rel=prefetch><link href=/css/chunk-f5f99b42.a608ca90.css rel=prefetch><link href=/app.d406ce09.js rel=preload as=script><link href=/css/vendor.a107d60e.css rel=preload as=style><link href=/js/runtime~app.c8b73606.js rel=preload as=script><link href=/vendor.9fa6f766.js rel=preload as=script><link href=/css/vendor.a107d60e.css rel=stylesheet></head><body><noscript><strong>We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><script src=page-initializer.js></script><div id=site-loader-wrapper><div id=site-loader></div><div id=site-loader-text>Loading SpaceONE</div></div><script src=site-loader.js></script><div id=app><script src=/js/runtime~app.c8b73606.js></script><script src=/vendor.9fa6f766.js></script><script src=/app.d406ce09.js></script></body></html>

 

띠용.

js 파일에 웬... html 이 자리잡고 있나.

그것도 얼탱이가 없지만, href=/site-loader.css 뭐 이런식으로 브라우저가 파싱을 전혀 할 수 없는 형태.

 

얼탱방구가 없어서 network > preview 탭을 봤더니.

띠용2.

정상적인 파일들은

이렇게 자바스크립트가 들어있단 말이다.

 

파일을 로드는 해오는데, 엉뚱한걸 가져온다?

그것도 잘 동작하다가, 갑자기 어느날??

문제 추적 🧐 2. 정상적인 경우와 비교하기

감이 안와서... 정상동작하는 경우는 어떤지, 확인해보았다.

다른 페이지에 있다가 서버 페이지로 이동했더니 요청된 파일들은 아래와 같다.

서버 페이지로 갔다고 해서 서버페이지만 로드한게 아니라, 아마도 그 라우트 정보를 가지고 있는 것으로 추정되는 CloudServicePage~....js 파일을 먼저 로드한 것을 확인할 수 있다.

문제 상황에서는 아래에 있는 파일, 즉 Server.2937218a.js 이 놈만 요청했었는데 말이다.

 

앗. 그런데말입니다...

흠... 않이. 서버 페이지로 연결되는 a 태그 클릭 했는데, 왜. 도대체 저런 것들을 부르지?
엇, 그러고보니 a 태그네? 그러면 href 를 보면 되잖아?
엇, 그런데 우리 서비스는 SPA 서비스이고, a 태그로 페이지 로드 없이 이동한다고?

 

갑자기 모든게 수상쩍었다.

렌더된 a 태그는 이렇게 생겼다.

ㅋㅋㅋㅋ 그런데 저걸 누르면 난데없이

이런 파일들을 요청한다고? ㅋㅋㅋㅋㅋㅋㅋㅋㅋ 🤪🤪🤪🤪🤪🤪🤪

 

🙈 아... 바보...ㅠㅠ 이거 훼이크겠구나. 멍텅구리 ㅠㅠ

그래 말이 안되지. a 태그는 그 태생이 페이지 연결인데.
그저 해시뱅으로 책갈피 정도인데...

 

저 문제의 a 태그는 Vue Router 의 router-link를 이용해 만들어준다.

그렇다면 아마도 router-linka 태그를 만들어놓고, 내부적인 페이지 이동은 모두 자바스크립트 처리가 되어있을 터.

한번 확인해볼까.

문제 추적 🤨 3. Vue Router 의 router-link 파헤치기

router-link 는 렌더 결과는 a 태그에 href 속성이 우리가 보내고 싶은 url로 올바르게 반영된다.

그런데 그 태그를 클릭했을 때의 동작은 일반적인 a 태그의 동작과 다르다.

 

Vue Router 의 router-link가 내부적으로 어떻게 동작하는지를 보자.

// link.js
export default {
  name: 'RouterLink',
props: {
	to: {
      type: toTypes,
      required: true
    },
}
	render (h: Function) {
		...

		const { location, href } = router.resolve(this.to)

		const handler = e => {
			router.push(location)
		}

		const on = { click: handler }

		const attrs =  { href } 

		return h('a', { on, attrs }, this.$slots.default)
	}
}

// index.js
export default class VueRouter {
	constructor (options: RouterOptions = {}) {
		this.history = new HTML5History(this, options.base)
	}
	
	push (location: RawLocation) {  
    return new Promise((resolve, reject) => {
      this.history.push(location, resolve, reject)
    })
	}
}

오리지널 코드는 넘나 복잡해서, 요점만 간단히 요약해보았다.

  1. props로 주입 받은 to 객체를 통해 router.resolve() 함수로 href, location 을 알아낸다.
  2. 알아낸 location으로 router.push() 해주는 handler() 함수를 만든다.
  3. 이벤트 리스너 객체 on 을 만들어, click 이벤트에 handler()함수를 바인딩 해준다.
  4. 속성 객체 attrs 를 만들어, 알아낸 href 를 바인딩 해준다.
  5. 이벤트 리스너와 속성이 바인딩 된 a 태그를 만들어준다.

결국 보이는건 href 속성이 부여된 a 태그, 클릭하여 동작하는 것은 handler() 함수, 즉 router.push(location) 이라는 것을 알 수 있다.

공식 문서에도 이렇게 작성되어 있다.

다른 URL로 이동하려면 router.push를 사용하십시오. 이 메소드는 새로운 항목을 히스토리 스택에 넣기 때문에 사용자가 브라우저의 뒤로 가기 버튼을 클릭하면 이전 URL로 이동하게된다.
이것은 <router-link>를 클릭 할 때 내부적으로 호출되는 메소드이므로 <router-link :to="...">를 클릭하면 router.push(...)를 호출하는 것과 같습니다.

돌아 돌아서 여기까지 왔는데 버젓이 나와있다. ㅎㅎ

공식문서를 씹어먹어야 하는 이유. 알지만 항상 놓치는 mergery 같은 나.

 

여튼 결론적으로 추측해볼 수 있는 것은,

a 태그를 누르면, router.push(location) 가 동작하면서 내부적으로 저 2가지 파일을 요청할 것이라는 점.

location에 매치되는 파일(컴포넌트)을 route config 에서 찾아서 요청할 것이라는 점.

그리고 요청하여 받아온 파일 내부에서 router-view를 만나, 그 다음 파일을 요청할 것이라는 점!

따라서,

이 파일들은 대략 그 과정에서 순서대로 요청된 것이라는 점.

 

그래... 그것은 알겠다.

근데 라우터 너가 요청을 했고! 답은 엉뚱한게 왔다는게 문제다.

재배포를 해서 없다고 404가 와야하는 애가, 왜 버젓이 200으로 들어오는걸까.

그리고. 왜 js를 요청했는데 html 로 날아오는걸까.

문제 추적 🤮 4. 왜 js 를 요청했는데 html 로 날아오는걸까

우리는 웹 서버로 nginx 를 쓰고있다.

 

처음 진입하는 경우를 생각해보자.

  1. 처음에 특정 url로 브라우저에 진입한다. nginx 는 모든 요청에 대하여 index.html을 리턴하도록 되어 있는데, 거기에 script 태그로 entry 파일이 로드된다.
  2. 로드된 entry 파일이 실행되면서, router 훅도 실행되고 다양한 일이 일어날 것이다.
  3. 그리고 라우트 정보에서 현재 브라우저의 url과 매치되는 컴포넌트들을 요청한다. (Vue Router 코드를 살펴봤는데 matched 정보를 이용하더라.)

페이지 이동하는 경우를 생각해보자.

  1. router.push가 특정 url로 컴포넌트를 GET 요청한다. 그 파일이 Server.aaa.js 라고 해보자.
  2. 그 파일을 받아 와 잘 로드한다....면 좋겠지만, 여기서 로드를 못해온다. 왜? 파일이 변경되어서 Server.aaa.js는 더이상 없다. nginx 는 파일이 있으면 그대로 돌려주지만, 없으면 index.html 을 보내주도록 설정되어 있다.
    location / {
      try_files $uri /index.html;
    }​

    nginx 는 영문도 모르고 index.html 를 돌려보내준다.
  3. 브라우저는 nginx 로부터 무언가를 받긴 했는데, 그 포맷이 html 이라는 것을 모른다. 이를 실행 가능한 javascript 라고 생각하고 실행한다. 그리고... <!DOCTYPE html> 이걸 만나는 순간부터 에러가 난다.

문제 원인 😈: 잘못된 파일 요청을 index.html로 보내주는 nginx의 신박함

이제 알아버렸다. 🤩

잘못된 chunk file을 요청하였으나, 이를 index.html 로 되돌려준 nginx 설정으로부터 이 모든 것이 시작되었다!

 

저 설정은 SPA 에는 필수적인 설정이다.

왜?

모든 url의 변경사항을 웹 어플리케이션이 관장하여야, 마치 하나의 어플리케이션처럼 동작하도록 만들 수 있으니까.

 

자 그렇다면... 문제는 알았는데. 이를 어떻게 풀어나간단 말인가.

router 가 어떤 파일을 달라고 요청을 하였고,

그것에 이상이 있다는 것을 감지했다면,

그것을 처리하는 정도의 구멍은 파놓지 않았을까?

솔루션 🧙🏽‍♀️: router.onError 콜백으로 chunk load 에러 핸들링

그래... 너였구나.

우리는. 사실은. 실제로. 약간은. 원래 아래처럼 에러 핸들링을 해주고 있었다.

router.onError((error) => {
    if (/loading chunk \d* failed./i.test(error.message)) {
        window.location.reload();
    }
});

문제는 뭣이냐 하면, 이게 안먹고 있었다는 것. ^^

if 문을 타지 않고 있었다는 거엇...!

 

수많은 빌드와, 문제상황을 억지로 만들어내가며
에러는 정확히 ChunkLoadError 라는 이름으로 판별할 수 있다는걸 찾아내었고,

let nextPath: string;
router.onError((error) => {
    console.error(error);

    if (error.name === 'ChunkLoadError') {
        window.location.href = nextPath || '/';
    }
});

이렇게 마무리하였다.

 

참고로, nextPath 이 친구는 beforeEach 훅에서 to.fullPath 를 넣어주고 있다.

이유는, onError 콜백 함수의 인자로는 그 어떤 라우트 정보가 넘어오지 않기 때무네.

마치며 😅

라우터 에러 핸들링으로 처리해야 한다는걸 예상은 하고 있었지만,

왜때문에 걔가 해줘야 하는건지는 정확히 몰랐다.

정확한 원인을 파악하기 위해 이렇게 저렇게 빼애애앵 돌아돌아 명확히 하고나니..

사알짝 씁쓸하다. 에러 핸들러를 먼저 살펴볼걸... 😭

그렇지만 다시 한 번 기초를 다져보는 유의미한 삽질이 아니었나, 스스로를 다독여본다.

반응형
댓글
댓글쓰기 폼