NextJS

트러블슈팅: 서버 배포 후 간헐적 ChunkLoadError 발생 원인

변기원 2024. 7. 10. 18:47

 

 

Nextjs 프로젝트를 배포한 뒤 유독 시크릿탭에서 , 그것도 처음에만!! 위와 같은 화면이 간헐적으로!! 목격됐다.

제일 어려운 잘되다가 또 안되다가 이유없이 잘되다가 또 이유 없이 안 되는... 그런 이슈였다.

 

콘솔창에는 ChunkLoadError라고 찍힌다. 처음 ChunkLoadError라는게 보여요~라는 제보를 받았을 때는

아~ 그거 새 배포해서 그래요 새로고침 하면 될 거예요. 하고 대답했지만 

시크릿 탭을 닫았다가 새로 켜고 접속해도 똑같은 에러가 다시 발생한다는 믿을 수 없는 추가제보가 들어왔다.

게다가 새로고침을 여러 번 하면 괜찮아진다고 한다. 정말 이상했다.

전 회사에서는 브라우저 장기접속자(?)들을 위해 ChunkLoadError를 미들웨어에서 잡은 후에 새로고침 해주는 방식으로 해결했었다.

당연히 ChunkLoadError가 발생하는 경우는 새배포가 되었는데 과거의 split 된 js chunk들을 요청하고 있기 때문이라고 생각했기 때문이다. 대부분은 이 케이스가 맞을 것이다.

 

위와 같은 새 배포로 인한 과거 청크로드 에러라면 새 시크릿탭에서 접속했을 때 그런 화면을 볼리가 없다.

캐시가 남지 않는 새 탭에서 접속하면 우리 서버까지 도달해서 새 html과 js파일을 받을 것이기 때문에...

그렇다면 이번엔 무슨 이슈일까

 

처음에는 혹시 우리 서버가 cdn을 타고 내려오는데 무효화가 잘 안 되어서

옛 청크를 요청하는 페이지가 남아있는 건 아닐지 의심했지만 아니었다. 

특정 js파일이 없다..? 헤더에 Link가 있고 폰트파일을 가리키고 있다. path로 들어가 보면 실제로 폰트파일이 다운로드된다. 근데 왜 4478-5321~~~~.js 같은 번들을 요청하는 거지? 4478-5321~~~~.js 라는 파일명이 괜히 생겼을 리 없다.
넥스트는 빌드 시에 파일내용을 반영해서 해쉬값을 만들고 있기 때문에 파일내용이 변경되지 않는 한 청크파일들의 파일명이 분명히 같을 것이다. 그럼 괜히 생겼을 리 없는 4478-5321~~~~.js파일을 찾아서 실제 서버에 들어가 보자.

첫 번째 서버에 들어가 보니. next/static/chunks 폴더에 들어가 보면 4478-5321~~~~.js 파일이 있다.

두 번째 서버에 들어가 보니 4478-5321~~~~.js이 없었다. 다른 파일명은 모두 같은데 유독 4478로 시작하는 청크파일의 파일명만 달랐다. (webpack-로 시작하는 js 파일은 예외) 

그렇다. main청크가 그와 연결된 청크들을 요청해서 페이지를 만든다. 그러면 분리된 chunk들을 가져오기 위해 브라우저에서 서버로 get요청을 보내는데, 우리 서버가 두 개라서 두 개의 서버 중에 어디에 들어가서 js파일을 서빙받을지는 모른다.

(데브옵스팀에 물어보니 라운드로빈 방식이라고 하시는 것 같다.)

서버가 두 개니까 배포할 때 1번 서버에 들어가서 빌드하고 run 해주고, 2번 서버 들어가서 빌드하고 run을 해줬는데, 빌드할 때마다 4478로 시작하는 청크이름이 다르게 빌드된다.

결과적으로 js파일을 요청하는 네트워크가 어떤 서버에 맞을지는 모르는데, 하필이면 1번 서버에서 main을 가져왔는데, 2번 서버로 4478-5321~~~~.js를 요청하면 그런 파일은 없으니 404 에러가 발생하면서 ChunkLoadError가 나오는 것이다.

 

그래서 브라우저 메모리 캐시가 없는 시크릿탭에서만 위 현상이 목격되는 것이고

새로고침을 여러 번 하면 괜찮아지는 이유도 두 개 서버 중에 우연히 한 번이라도 맞아서 4478 청크를 제대로 가져온다면

그 뒤로는 메모리 캐싱되기 때문에 서버까지 가지 않기 때문에 괜찮아지는 것이다.


그렇다면 궁금한 것은 두 가지다. 

1. 해결방법

2. 4478로 시작하는 청크는 왜 해쉬값이 달라졌는가.

 

해결방법은 단순하다. 빌드환경을 하나로 만들어야 된다. cicd를 붙여서 빌드된 파일을 양쪽 서버에서 실행하던지,

아니면 도커 이미지를 빌드해서 같은 도커이미지를 실행하는 방법도 있다. 혹은 rsync라는 것을 사용해서 한쪽 서버의 파일을 동기화하는 방식이 있다고 한다. 운영은 최대한 신중하게 배포하기 위해 cicd를 붙이실 계획이 없다고 한다. 괜찮다. 중요한 것은 빌드환경을 통일하는 것이지 cicd가 필요한 건 아니니까! 그렇다면 도커이미지를 만들까? 나도 도커를 잘 아는건 아니지만 팀원들은 도커를 전혀 사용해보지 않아서 당장 운영서버에 적용하기엔 위험부담이 있다. 그래서 rsync를 사용하는 방법으로 해결했다.

 

두 번째로 4478은 왜 해쉬값이 다를까.

파일내용기반으로 해쉬값이 결정된다 하니, 빌드결과물 파일 내용을 비교해 봤다.

빌드결과물이 다르다. 다른 부분은 vanila extract로 작성한 className이 딱 하나 빠져있다. 뭐지?

vanila extract가 next 빌드시점에 클래스명을 어떻게 결정하는가 찾아보니 빌드시점에 작성된 소스코드를 분석해서 정적으로 css파일을 만들고 클래스명을 만들어서 삽입한다. 

그래서 이러한 css파일을 가져오고 prefix_~~~ 같은 클래스명들을 확인할 수 있다.

소스코드를 분석해 본 결과 모든 소스코드에서 딱 저부분만 클래스명을 못 가져올만한 포인트가 없다..

그냥 display:flex 같은 평범한 스타일이다.

로컬에서도 여러 번 빌드해 본 결과 해쉬값이 달라지진 않았다.

계속 붙잡고 있을 순 없어서 더 이상 파보진 않았지만 css in js 라이브러리가 next 같은 프레임워크 프로젝트 빌드시 결과물 만드는 과정을 좀 더 공부해봐야 할 것 같다.