티스토리 뷰
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-link는 a 태그를 만들어놓고, 내부적인 페이지 이동은 모두 자바스크립트 처리가 되어있을 터.
한번 확인해볼까.
문제 추적 🤨 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)
})
}
}
오리지널 코드는 넘나 복잡해서, 요점만 간단히 요약해보았다.
- props로 주입 받은 to 객체를 통해 router.resolve() 함수로 href, location 을 알아낸다.
- 알아낸 location으로 router.push() 해주는 handler() 함수를 만든다.
- 이벤트 리스너 객체 on 을 만들어, click 이벤트에 handler()함수를 바인딩 해준다.
- 속성 객체 attrs 를 만들어, 알아낸 href 를 바인딩 해준다.
- 이벤트 리스너와 속성이 바인딩 된 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 를 쓰고있다.
처음 진입하는 경우를 생각해보자.
- 처음에 특정 url로 브라우저에 진입한다. nginx 는 모든 요청에 대하여 index.html을 리턴하도록 되어 있는데, 거기에 script 태그로 entry 파일이 로드된다.
- 로드된 entry 파일이 실행되면서, router 훅도 실행되고 다양한 일이 일어날 것이다.
- 그리고 라우트 정보에서 현재 브라우저의 url과 매치되는 컴포넌트들을 요청한다. (Vue Router 코드를 살펴봤는데 matched 정보를 이용하더라.)
페이지 이동하는 경우를 생각해보자.
- router.push가 특정 url로 컴포넌트를 GET 요청한다. 그 파일이 Server.aaa.js 라고 해보자.
- 그 파일을 받아 와 잘 로드한다....면 좋겠지만, 여기서 로드를 못해온다. 왜? 파일이 변경되어서 Server.aaa.js는 더이상 없다. nginx 는 파일이 있으면 그대로 돌려주지만, 없으면 index.html 을 보내주도록 설정되어 있다.
location / { try_files $uri /index.html; }
nginx 는 영문도 모르고 index.html 를 돌려보내준다. - 브라우저는 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 콜백 함수의 인자로는 그 어떤 라우트 정보가 넘어오지 않기 때무네.
마치며 😅
라우터 에러 핸들링으로 처리해야 한다는걸 예상은 하고 있었지만,
왜때문에 걔가 해줘야 하는건지는 정확히 몰랐다.
정확한 원인을 파악하기 위해 이렇게 저렇게 빼애애앵 돌아돌아 명확히 하고나니..
사알짝 씁쓸하다. 에러 핸들러를 먼저 살펴볼걸... 😭
그렇지만 다시 한 번 기초를 다져보는 유의미한 삽질이 아니었나, 스스로를 다독여본다.
'Programming > Lib, Frameworks' 카테고리의 다른 글
[Vue] Vue Router - chunk load fail 로 인한 삽질기 (1) | 2021.07.17 |
---|---|
Vue.js - 컴포넌트가 뭐길래 2 (0) | 2020.02.06 |
Vue.js - 컴포넌트가 뭐길래 (0) | 2020.02.04 |
[Vue.js] 뷰 트랜지션(transition)의 사용 방법과 치명적인 문제점 (2) | 2018.09.22 |
- Total
- 190,538
- Today
- 7
- Yesterday
- 11