react, vue - Uncaught SyntaxError: Unexpected token < (흰 화면 이슈)

매번 SPA로 프로젝트를 진행할 때 마다 나를 괴롭히는 에러 메시지 ㅠ
Unexpected token '<'

Uncaught SyntaxError: Unexpected token '<'

지금까지 vue.js 로 구축하거나 react.js 로 구축하여 프로젝트를 번들링 후 배포하고 나면 종종 발생하는 에러라서 이 것 때문에 많은 대응책을 마련해 왔다.

원인을 살펴 보자면

  • 배포하고 접속하면 index.html이 브라우저의 캐시 상태이기 때문에 배포 바로 이전 버전의 index.html 이다. 그 안에 번들링한 js파일을 불러오는 부분이 배포 전 번들링 파일을 임베드하고 있기 때문에 이 오류가 난다.
    대체로 이럴 때는 새로고침하면 index.html이 새로 불러와지면서 거기에 번들링 파일 .js 를 새로 배포된 버전으로 불러와서 보여진다.


    (캐시에 남아있던 index.html 의 연결된 과거 번들링 파일)

    (새로고침하면 index.html이 바뀌면서 제대로 배포 파일을 가져온다)
  • 랜더링 되는 페이지에 api 를 통해 받은 값을 변수로 담아 이를 이용해서 인라인 으로 넣었을 때 그 값이 제대로 존재하지 않을 때, 즉 변수가 오류인 경우가 많다. 그래서 기본값을 지정해주는 센스가 필요하다.
  • webpack에서 발생하는 오류로 이 부분을 대응하는 코드도 있다.
  • jwt 토큰을 활용해서 사용하는 중이라면 토큰 만료의 경우 500에러로 인해서 같이 터지는 경우도 봤다.
  • 하이브리드앱에서 웹뷰 캐시로 인한 배포 버전 차이이다. 여기서 불특정한 기기가 다수 이슈를 발생한다.

이제 해결 방법을 하나씩 대응해보자.

방법 1.

가장 기본적인 대응은 base href를 넣는 방법이다.
index.html의 <head> 안에 아래처럼 넣는다.

<base href="/">

이렇게 하면 일단 처리는 된다. 하지만 저 base href로 지정한 경로가 사이트 구조상 경로 이슈로 사이드 이펙트가 난 적이 있으니 무조건 넣고 끝이 아니라 검수를 좀 해야한다.

방법 2.

package.json 을 열어서 "name" 이나 "version" 다음에 아래 코드도 추가한다.

"homepage": ".",

이걸 어떻게 넣는지 모를까봐 넣은 사례를 캡쳐해서 놓겠음.

방법 3.

위에 2번까지 했는데도 이슈가 있다 하면 이제 index.html을 매번 새로 번들링된 파일이 제대로 임베드 시킬 수 있도록 설정하는 것인데 가장 확실한 방법은 서버에서 index.html일 경우 http 헤더에 Cache-Control 에서 캐시를 안잡히게 설정하는 방법이다.
그러나 이걸 직접할 수 없는 경우를 위해 우선 클라이언트 단에서 처리할 수 있는 방법을 제안한다.
index.html의 <head> 안에 아래 메타 정보를 추가한다.

<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">

방법 4.

하이브리드앱의 경우 앱과 웹간의 브릿지를 만들어서 이 부분을 해결한다.

  • react.js 나 vue.js 에서 라이프 사이클 중 mount 되는 곳에 앱쪽으로 로드되었다는 A라는 브릿지를 호출한다.
  • 앱은 웹뷰로 url을 호출하면 상태에 따른 리스너가 있다. iOS, Android 동일하게 웹뷰에 대한 로드 상태 이벤트를 찾아보면 된다.
    아무튼 앱은 웹뷰를 호출하고 로드할 때 다음 3개를 미리 만들어 놓고 실행한다.
    1. 타이머 횟수를 담은 변수
    2. 타이머 함수 (30초 인터벌정도)
    3. 로딩 애니메이션 (웹뷰를 덮을 정도의 영역을 잡고 웹뷰 위에 띄운다.)
  • 웹뷰를 호출하면 타이머 함수가 작동되며 로딩 애니메이션이 작동되게 한다.
  • 지정한 30초 안에 웹뷰로 부터 위에 호출 지정한 A라는 브릿지가 호출되지 않으면 웹뷰 새로고침을 한다. 이 새로고침 할 때마다 타이머 횟수를 담은 변수에 누적 카운팅한다.
  • 새로고침 반복은 타이머 횟수 변수를 비교해서 2회 이상 반복이 진행되었다면 오류 페이지로 이동 시킨다.
  • 주의 할 점은 도메인을 분기 잘 해서 서비스하는 도메인의 웹뷰 호출이나 옵저버에서 체크해야한다. 안그러면 결제와 같은 다른 서비스 도메인을 웹뷰를 호출하면 그 쪽에서 브릿지 호출을 당연히 안하니까 로딩 돌다가 오류 페이지로 넘어가게 된다.
  • 저속 네트워크 사용자의 경우 SPA에서 번들링된 파일이 크면 다운로드 받는데 시간이 오래 걸리기 때문에 30초안에 못받을 수 있다.

대체로 이렇게 할 경우 첫 새로고침에서 해결이 된다.

방법 5.

서버에서 http 헤더에 캐시를 잡지 않도록 html 파일을 항상 새로운 파일로 받게 아래처럼 변경한다.

location ~* \.html {
            add_header Cache-Control "private, no-cache, no-store, must-revalidate";
            add_header Expires -1;
            add_header Pragma no-cache;
            add_header Last-Modified $date_gmt;
        }

이렇게 하면 html 파일들은 새로 매번 서버로 부터 받게된다. 장단점은 있다 매번 이렇게 불러오게 되면 불필요한 서버 리퀘스트가 발생할 수 있다. 이 부분은 스스로 판단하길 바란다.

위 예제는 nginx의 설정으로 /etc/nginx/nginx.conf 를 열어서  server { } 안에 추가한 것이다.

TOP