취업 준비생 또는 현업에 종사하고 있는 많은 개발자들은 개인 프로젝트를 설계한다. 사용해보지 않은 새로운 기술 스택을 사용한다거나 디자인 패턴을 고민하며 자신의 실력을 향상시킨다. 이 글을 작성하는 본인도 취업 준비 시절에 이와 같은 목적으로 개인 프로젝트들을 했었다.
물론 개발자들이 개인 프로젝트를 하는 다른 이유도 있을 것이다. 예를 들어, 눈에 띄게 아이디어가 좋은 프로젝트라던가 슬랙처럼 기존에 많이 있는 메시지 서비스를 사용성을 좋게 만들어 많은 사람들이 이용하게 한다던가의 목표 말이다. 하지만 이런 목표까지는 없었다. 그 결과 내가 만든 서비스를 꾸준하게 이용하는 유저는 당연히 없었다.
회사에 입사하여 실제 서비스를 운영하고 있고, 이미 많은 사람들이 이용하고 있는 프로젝트를 살펴보니 고려해야 할 점들이 매우 많다는 것을 깨달았다. 취업 준비시절에도 충분히 이 부분들에 대해 공부할 수 있다고는 하지만, 사실 '그런게 있는지조차 몰랐다' 라고 느낄 때가 많다. 즉, 키워드라도 있어야 인지하여 학습하는 것이다. 그러므로 다양한 경험을 하기 위해 노력하는 것이 중요할 것이다.
본론으로 돌아와서, 오늘은 번들부터 브라우저의 캐시까지 연관지어서 살펴보고 더 나아가 근본적인 문제와 프리 캐시에 대해서도 말해볼 것이다.
프로덕션 배포가 이루어지면, 웹 애플리케이션의 모든 리소스(HTML, CSS, JS 등)는 번들로 묶여서 제공된다. 번들은 애플리케이션을 실행하기 위해 필요한 모든 코드를 포함하고 있게 된다.
인터넷이 발달하면서 웹 애플리케이션에 대한 요구사항이 많아지고, 기업들이 더 좋은 서비스를 유저에게 제공하면서 규모는 점점 커지게 된다. 이로 인해 중복되는 선언으로 인한 충돌이나 비효율적으로 서버와 티키타카 하는 문제점들이 발생한다. 커다란 규모의 웹 애플리케이션에서도 효율적으로 프로젝트를 설계할 수 있도록 번들이라는 개념이 등장한 것이다.
우리는 일반적으로 webpack
, parcel
, rollup
과 같은 번들러를 사용하여 위에서 언급한 번들링 작업을 수행하는 것이다. 이 과정을 수행할 때, 주의해야 할 점이 있다. 번들 파일의 내용이 변경될 때마다 새로운 해시 값을 생성하도록 설정해야 한다. (webpack
의 contenthash
나 chunkhash
가 그 예시이다.)
만약 번들의 해시를 사용하지 않으면 어떻게 될까? 이것은 브라우저의 캐싱과 연관이 있다.
웹 브라우저로 웹 페이지나 웹 애플리케이션을 방문하면, HTTP 캐시는 로드되는 시점에 웹 서버에서 리소스를 가져와 캐시에 저장한다. 만약 동일한 페이지를 방문하여 같은 리소스가 필요할 경우에는 캐시된 데이터를 사용하는 것이다. 그 결과 성능과 속도를 향상시킬 수 있어 사용자의 경험도 좋아진다.
브라우저 캐싱을 잘 사용하는 방법은 이 글에서 다루지 않으므로, 궁금한 사람은 토스의 웹 서비스 캐시 똑똑하게 다루기 글을 참고하자.
그렇다면 번들의 해시를 사용하지 않으면 어떤 문제가 발생할까? 사람들이 꾸준하게 이용하고 있는 서비스를 운영하고 있다고 가정하자. Git으로 관리하게 되면, master(또는 main) 브랜치 기준으로 프로덕션 배포가 되어있을 것이다. 새로운 기능이 추가되거나 버그가 발생하여 핫 픽스를 처리하는 경우에 feature와 hotfix같은 별도의 브랜치에서 작업하게 된다. 그리고 개발이 완료되면 master까지 merge를 하고, 프로덕션에 배포를 해야할 것이다.
이제 개발자는 유저가 최신 버전의 웹 애플리케이션을 이용한다고 기대한다. 하지만 유저는 다음에 다시 나의 사이트를 방문해도 과거 버전을 이용하게 될 수 있다. 그 이유는 번들과 HTTP 캐시의 특징 때문이다.
module.exports = {
output: {
filename: 'bundle.[contenthash].js',
},
// ...
};
배포시 웹 애플리케이션의 모든 리소스를 번들링한다. 하지만 번들의 해시를 사용하지 않으면, 번들 파일 안의 내용은 변경될지라도 번들 파일 이름들은 같다. 예를 들어, 브라우저가 bundle.js
라는 파일을 캐시한 경우에 웹 애플리케이션이 새로 배포되어 bundle.js
안의 내용이 변경되더라도 동일한 파일로 인식하여 캐시된 번들 파일을 사용하는 것이다.
그래서 위에 작성한 webpack
의 contenthash
같은 cache-busting
을 사용하는 것이다. 번들의 해시를 이용하여 브라우저가 변경된 파일을 인식하고 다시 리소스를 다운로드 하도록 유도하는 것이다. 하지만 이 경우에도 브라우저 캐시에 저장되어 있는 리소스가 만료되지 않았다면, 새로운 리소스를 다운로드하지 않을 수 있다는 것을 명심하자.
브라우저가 캐시된 자원이 수정되었는지 확인할 수 있도록 하는 것을 캐시 무효화라고 한다. 번들의 해시를 이용하지 않더라도, 캐시 무효화를 할 수 있는 방법들도 존재한다. 이 중 두 가지만 소개하겠다.
한 가지 방법만 사용하는 것은 아니다.
목적에 따라 여러 가지 방법을 섞어서 사용하기도 한다.
첫 번째는 캐시 제어 헤더(Cache-Control Header)를 명확하게 사용하는 것이다. HTTP 응답 헤더에 Cache-Control 헤더를 추가하여 브라우저에서 캐시를 어떻게 사용할지 명시할 수 있다. 예를 들어, Cache-Control: no-cache를 사용하면 브라우저가 캐시된 리소스를 사용하지 않고 항상 서버에서 새로운 리소스를 가져온다. 이런 부분들을 잘 설계하면 가능할 것이다.
두 번째는 ETag를 사용하는 방법이다. ETag는 서버에서 리소스의 버전을 식별하는 방법 중 하나이다. 서버는 리소스가 변경될 때마다 새로운 ETag 값을 생성하여 이를 HTTP 헤더에 포함하여 클라이언트에 전송한다. 클라이언트는 이전에 캐시한 리소스와 비교하여 ETag 값이 다르다면 서버에서 변경된 리소스를 다시 요청하게 된다. 파일이 변경될 때마다 ETag를 생성하고, HTTP 헤더에 포함하여 전송하므로 서버에 부하가 될 수 있다. 또한, ETag를 생성하는 알고리즘 복잡도에 따라 성능에도 영향을 미치니 주의하자.
이제 브라우저 HTTP 캐시를 이용하여 유저에게 더 좋은 UX를 제공하고 있다. 우리가 새로운 기능을 추가해서 프로덕션에 배포해도 캐시 무효화 덕분에, 유저가 웹 애플리케이션에 들어오거나 새로 고침을 할 경우 최신 서비스를 이용할 수 있는 것이다. 그런데 잠깐.. 방금 말에서 마음에 걸리는 부분이 있다.
내가 특정 블로그에서 글을 작성하고 있는 중이었다고 가정해보자. 작성중인 글을 주기적으로 임시저장해주는 기능이 있다면 좋겠지만, 이 블로그는 그런 기능이 없이 무조건 Submit
버튼을 눌러야 한다. Submit
버튼을 누를 경우 /create/post
라는 EndPoint로 요청이 보낸다. 하지만 이번에 새로운 프로덕션 배포때에는 /service/create/post
와 같이 EndPoinst를 변경했다고 가정해보면 무슨 일이 일어날까?
유저가 추후에 다시 방문하거나 새로 고침하지 않는 이상 과거의 버전을 이용할 것이다. 이미 BackEnd와 FrontEnd에서는 블로그 글을 생성하는 EndPoint가 변경되었지만, 유저는 과거 버전을 이용하므로 Submit
버튼을 클릭하면 오류만 발생하게 된다. 유저는 왜 에러가 발생하는지 이유도 모른채 아 뭐 이런식으로 만들었어?
라며, 영원히 이탈하게 될 지도 모른다.
이와 같은 경우의 간단한 해결 방법을 생각해보면, 새로운 배포가 발생한 경우 자동으로 새로 고침을 시키거나 새로 고침을 유도하는 팝업 메시지 같은 것을 띄우는 것이다. 이 방법이 훌륭한 해결책일 수 있지만, 위의 예시에서는 씨알도 안먹힐 것이다. 글을 열심히 작성했는데, 임시 저장도 없는 상태에서 새로 고침을 해버리면 데이터가 모두 사라지기 때문이다.
서버에서 새로운 버전의 애플리케이션을 로드할 수 있도록 라우팅을 설정하는 방법이 있다. 사실 말만 거창하지 백엔드 개발자가 귀찮을 수 있는 방법이다. 서버에서 새로운 애플리케이션을 제공하는 새로운 EndPoint를 만들고, 이전 엔드포인트에서 새로운 엔드포인트로 리다이렉트 하도록 설계하는 것이다. 이제 유저는 과거의 EndPoint에 머무르더라도 최신 서비스를 사용할 수 있을 것이다.
WebSockets이나 Long Polling을 이용하는 방법이 있다고 한다.
하지만 내가 소개할 방법은 Service Worker를 이용하여 프리 캐싱 하는 것이다. 프리 캐싱은 서비스 워커를 이용하여 캐싱할 리소스를 미리 다운로드하고 캐시에 저장하는 방법이다.
서비스 워커는 브라우저와 별개의 백그라운드 스레드에서 동작하는 JS로 작성된 파일
MDN Service Worker API
서비스 워커 캐싱 및 HTTP 캐싱
Workbox Caching
새로운 프로덕션 배포가 일어날 때마다 서비스 워커를 갱신하여 새로운 파일을 캐시할 수 있도록 설계하면 된다. 이제 애플리케이션의 최신 버전을 백그라운드에서 미리 캐싱하게 된다. 브라우저는 항상 새로운 파일을 가져와 사용할 수 있는 것이다. 주의해야 할 점은 서비스 워커의 캐시는 브라우저의 HTTP 캐시와는 별개로 생성되기 때문에, 동일한 리소스가 두 곳에서 캐시될 수 있다는 것이다. 이 경우 위에서 Cache-Control Header를 사용하여 브라우저 캐싱은 막는 것도 좋을 것이다.
이렇게 새로 고침하지 않고 새로운 리소스를 다운로드 할 수 있게 되었다. 더 나아가 서비스 워커는 브라우저와 독립적으로 동작하기 때문에 인터넷 연결이 불안정하거나 오프라인 상황에서도 웹 애플리케이션을 사용할 수 있다. 이전에 캐싱된 리소스를 이용하기 때문이다.
오늘의 글은 유저가 새로 고침하지 않고, 최신의 서비스를 이용하는 방법에는 어떤 것들이 있을가 궁금해서 작성하게 되었다. 하지만 글을 작성하면서 등장한 번들의 해시나 브라우저의 캐시같은 하나 하나의 개념들을 잘 모르는 상태였다. 지금 생각해보면 이것들을 모른채로 위의 방법만 생각했던 것이 말도 안되는 정신머리였던거 같다. 이 개념들이 서로 연관되었을 때 발생하는 상황들은 더 복잡할텐데, 개념조차 이해하지 못한채로 연관관계를 생각했기 때문이다.
그래도 이번에 글을 작성하면서 잘 몰랐던 부분에 대해서 알 수 있어 너무 재밌었다. 그리고 아직도 모르는게 정말 많다는 것을 깨달았고 더욱 열심히 정진해야겠다는 생각을 했다.
혹시 이 글에 틀린 부분이 있다면 조언 부탁드립니다.
일단 배치되면 프로덕션 시스템에 액세스하는 사용자는 소프트웨어 또는 애플리케이션의 업데이트된 버전을 사용할 수 있습니다. 배포 중 수행된 업데이트에 따라 기능, 사용자 인터페이스 또는 성능이 변경될 수 있습니다. Aetna Medicare Advantage Plans
참나 글 좀 잘쓰네