지난 시간에는 코드스플리팅에 대해서 설명했습니다.
허나 이전글에서 설명 드린것처럼 해당 기술을 무작정 적용하게되면 문제가 발생합니다.
바로 배포에서 문제가 발생하는데요. (서버 등 개발환경에 따라 다를 수 있습니다)
그래서 해당 이슈에 대해서 이해를 하고 대응할 준비가 되었을 때 적용을 하셔야 안전합니다.
자 그럼 배포에서 대체 무슨 이슈가 발생하느냐?
지금부터 천천히 설명해보겠습니다.
"프로젝트가 너무 거대해졌어 난 성능개선이 필요해, 코드를 분할해서 동적로딩을 시켜보자 성능이 많이 개선된대!"
개발자는 코드스플리팅을 적용하여 라이브 서비스까지 배포를 하였습니다.
배포가 되었으니 새로고침을 하여 서비스를 확인해봅니다.
속도가 빨라졌습니다. 개발자는 만족합니다. (미래를 예측못하고..😥)
하지만 Sentry(에러 로깅 및 트래킹 도구)에 못보던 에러들이 올라옵니다. (Error: Loading chunk failed 에러)
다급히 이슈를 트래킹 해봅니다.
아뿔싸.. js(chuck) 파일을 불러오지 못하여 사용자들의 화면도 나오지 않습니다. 개발자는 배포된걸 확인하기 위해 새로고침(refesh)을 했지만 사용자들은 새로고침(refesh)을 하지 않는다는걸 생각합니다. (특히 웹앱처럼 수명주기가 긴 서비스의 경우 타격이 더 크겠죠?)
자 이제 해결해봅시다.
원인은 생각해보면 당연하고 간단합니다.
웹팩설정을 따로 안하게 되면 기본적으로 js파일들이 청크파일로 나누어지면서 새로운 해시값을 부여받는건 다들 아실겁니다. (여기서 원인이 발생합니다)
배포 시 서버에는 위와 같은 이유로 새로운 파일(파일명이 변경)들이 배포되어 있는 상태입니다.
이 상태에서 우리 새로고침을 하지않은 사용자들은 이전 파일명을 서버에 요청하는 경우가 생깁니다. 청크파일로 나뉘어서 페이지 이동 등등 해당 파일이 필요할때 동적으로 요청을 하지만 업데이트(새로고침)를 하지않아 이전 파일명으로 서버에 요청을 하는 현상입니다.
그렇게되면? 서버에는 해당 파일이없으니 당연히 에러가 발생할겁니다 :(
예시 )
1.aaaa.js -> 배포 전 파일
1.bbbb.js -> 배포 후 파일
같은 기능을 하는 같은 파일이지만 브라우저에서는 파일명이 달라져 다른 파일로 판단합니다.
해시값을 제거(파일명 통일)하여 파일을 버전으로 관리한다고 해도, 제대로 호출된다는 보장은 되지 않습니다.
이제 원인은 파악했으니, 해결할 방안을 찾고 검토를 해봅니다.
약 아래와 같은 방안들이 있었습니다. (아래의 방안들은 모두 정답이 없으며, 기획적 요구사항 및 사용자 경험에 따라서 해결방안을 찾아 해결하시면 됩니다)
배포 시 서버에서 이전 파일들의 리소스를 지우지 않고 관리해줍니다. (static 서버 등에서 이전 파일들을 삭제하지 않고 제공해준다면 클라이언트에서의 셋팅으로 원하는 버전으로 바라 볼 수 있게 처리가 가능한 장점이 있습니다. 단점은 서버 자원을 사용해야하고, 이전 파일 리소스들을 관리해줘야 하는 이슈가 있습니다.)
service worker를 사용하여 페이지 로딩 시, chuck 파일들을 모두 로딩하여 캐싱해놓는다. 미리 로딩하니 나중에 서버에 요청할 필요가 없겠죠? (유력한 후보였으나, 저희팀에서는 service worker 관련 이슈로 다른방안으로 적용하였습니다.)
저희 프로젝트만의 구현법일 수 있습니다. (도움이 되거나 팁이 될수 있으니 안내합니다.) 저희 서비스는 크게 분류하면 두 분류로 나뉘어져 있습니다. 그래서 bundle 자체를 아예 두 분류로 output이 나오게 처리하여 서로 호출 자체를 하지 않는 방안이었습니다. (서로 호출을 하지않으나 각 프로젝트의 성격이나 셋팅에 따라 적용 할 수 있는 유무가 갈릴 것 같습니다)
이것도 저희 프로젝트만의 구현법일 수도 있지만, 혹시 프로젝트의 운영 및 설정툴을 사용하고 계신다면 배포된 설정값과 배포 후 개발자가 변경한 설정값으로 비교하여 일괄적으로 페이지 새로고침 처리를 해줍니다. (설정을 개발자쪽에서 셋팅 할수있으니 가장 안정적입니다. 허나 사용자들의 refresh가 동시다발적으로 일어나므로 서버에 부담이 갈수 있습니다.)
React의 ErrorBoundary 등을 활용하여 사용자가 업데이트를 인지할수있는 페이지로 안내 합니다. (해당 방식으로도 많이 처리를 하나, 저희 서비스는 수명주기가 긴 서비스이며 사용자 경험을 더 중시하기로하여 해당 방식은 채택하지 않았습니다.)
파일을 동적로드 할때 에러가 발생하면 새로고침으로 처리해줍니다. (저희가 선택한 방식입니다. 일괄적으로 새로고침 처리가 되지않아 서버에 부담이 없다는 장점이 있고, 페이지(라우트) 이동이 많지 않아 chunk 파일의 동적요청을 많이 하지 않는 저희 프로젝트에 알맞는 솔루션이었습니다. 혹여나 무한 새로고침의 부담이있었으나 로컬스토리지를 사용하여 해당 이슈를 처리하도록 하였습니다)
위의 내용을 참고하여, 다들 각각 프로젝트에 맞는 솔루션을 찾길 바라겠습니다. 해당 문단에서는 제가 해결한 방법인 6번 항목에 대해서 설명을 하려고합니다. 이전 글에 있었던 코드를 아래와 같이 수정하겠습니다.
import { lazy } from 'react'
export const retryLazy = (componentImport) =>
lazy(async () => {
const pageAlreadyRefreshed = JSON.parse(
window.localStorage.getItem('pageRefreshed') || 'false'
)
try {
const component = await componentImport()
window.localStorage.setItem('pageRefreshed', 'false')
return component
} catch (error) {
if (!pageAlreadyRefreshed) {
window.localStorage.setItem('pageRefreshed', 'true')
return window.location.reload()
}
throw error
}
})
유틸 함수를 위와 같이 구성합니다.
import { retryLazy } from 'utils/lazyUtil.js'
const HomeComponent = retryLazy(() => import('page/HomeComponent'))
const AboutComponent = retryLazy(() => import('page/AboutComponent'))
const OtherComponent = retryLazy(() => import('page/OtherComponent'))
let routes = [
{ redirect: true, path: "/", to: "/home" },
{ redirect: false, path: "/home", component: HomeComponent },
{ redirect: false, path: "/about", component: AboutComponent },
{ redirect: false, path: "/other", component: OtherComponent }
]
이제 라우터에서 위에서 작성한 유틸 모듈을 가져와서 lazy 부분을 교체 해줍니다.
자 이제 모두 끝났습니다!
사용자 전체를 일괄적으로 새로고침(refresh)하지 않아서 서버에 부담도 없고, 에러가 난 사용자만 해당 경우에 에러를 catch를 하여 페이지를 업데이트 시킵니다.
기획적 니즈도 만족시키고 사용자 경험도 어느정도 살리는 방식이라서 개인적으로는 맘에 드는 방안입니다. (서버부담도 덤으로 해결되었습니다😉)
물론 제가 적용한 방식이 무조건 정답은 아닙니다.
다들 프로젝트와 상황에 맞게 솔루션을 검토하여 이슈를 해결하시면 됩니다.
긴글 읽느라 다들 고생하셨습니다 :)
다음엔 위에서 언급했던 이슈트랙킹 도구, 센트리(Sentry) 적용에 대해서 글을 작성 해 보도록 하겠습니다. (정말 도움이 많이 되는 도구에요!)
이전글 보기
감사합니다!