티스토리 뷰

Firebase Storage 사용량 초과로 이미지 로딩 지연 문제가 발생했다. 클라이언트가 매번 서버에 요청을 보내는 비효율적인 흐름이 원인이었다. 이를 해결하기 위해 서비스 워커와 Cache Storage를 활용해 이미지 캐싱을 적용했다. 그러나 Response 타입이 'opaque'로 설정되어 헤더를 읽을 수 없는 문제가 발생했다. 결국 crossorigin 속성을 설정해 CORS 문제를 해결했다.//

>> Cache Storage와 crossorigin을 바로 보고 싶다면 여기를 눌러주세요 <<

 

[ 목차 ]

     

    2024년 2학기에 런칭한 '한동피드'. 많은 소식들이 올라오는 실명카톡방의 내용을 중복 없이 정리해 주는 서비스입니다. 개강 시즌에 맞춰서 다양한 동아리들이 반복적으로 홍보하는 만큼 한동피드 서비스를 홍보하기 좋은 타이밍이라고 생각해서 홍보하기 시작했습니다.

    (좌) 대학교 실명카톡방에서 서비스를 홍보하는 모습 (우)조금씩 늘고 있는 접속자 수

    캐시가 없어 발생한 문제점

    Spring 서버에 나오는 오류
    많이 사용된 대역폭

     

    어느순간 로딩 시간이 엄청 늘어나더니 이미지 로딩이 안 되는 것을 확인하였습니다. 바로 서버 로그를 확인해 보니 Firebase Usuage Limit 초과했다는 메시지와 함께 대역폭이 많이 사용되었다는 것을 알 수 있었습니다 (내 돈 ㅠㅠ)

     

    바로 Firebase Storage Plan 을 무료에서 유료로 업그레이드하고 아래와 같은 문제점을 도출했습니다.

    1. 프론트에서 접속 시 무조건 백으로 요청을 보내짐

    클라이언트에서 서비스 접속 시 무조건 백으로 요청이 보내집니다. 새로고침할 때마다 서버에 요청이 보내지게 되고 2번에 나와있듯이 서버는 매번 파이어베이스에 요청을 보내어 비효율적인 플로우가 나오게 된 것 같습니다. 따라서 최종 목표로는 어차피 한번 등록된 피드는 업데이트(수정)가 되지 않아서 서버에 반환된 데이터를 저장하는 플로우를 채택할 것 같습니다.

     

    장점으로는 현재 PWA 으로 서비스되고 있는데 오프라인 상황에서도 기존 메시지를 볼 수 있도록 설정할 수 있겠네요.

    노션 정리본

    2. 백에서 매번 피드에 저장된 모든 이미지의 임시 URL 을 발급

     

    위에 보시는 코드는 (GPT 도움을 받아) 작성한 피드의 정보를 불러와 파일 아이디들을 가져와서 (다대다) Firebase Storage API를 사용하여 임시 URL을 불러오는 코드입니다.

     

    보다시피 저번학기 공모전 및 프로젝트 데드라인을 맞추기 위해 일단 최소한의 최적화인 비동기만 구현이된 로직입니다. 따라서 프론트에서 매번 요청이 들어올 때마다 15개 정도 되는 피드를 불러와 거기에 있는 파일들 (보통 0~5개)를 불러와서 임시 주소를 발급하게 됩니다.

     

    이 과정에서 요청을 많이 보내게 되고 Firebase Quota를 넘어서 안된게 아닐까라는 추측을 합니다.

     

    발급한 임시 주소 하나당 30분정도 유효하니, 유효시간을 늘리고 Redis을 사용하여 임시 저장하여 Firebase으로 요청되는 요청의 개수를 줄이는 걸로 해결할 수 있을 것 같습니다.

    노션 정리본

    3. 프론트에서 이미지를 매번 다운로드 (이번에 해결할 문제)

    이제 이번에 해결할 이슈인 프론트에서 매번 이미지를 다운로드하는 문제입니다.

    서버에서 임시 주소가 되면 프론트에서는 <img src="..." />을 통해 이미지를 다운로드하게 됩니다.

     

    보통 저희가 제공하는 이미지같은 경우 헤더에 캐시 방법을 추가하여 브라우저에서 자동으로 캐시가 되도록 할 수 있지만, 이번 프로젝트는 임시 주소 때문인지 작동하지 않는 것을 확인할 수 있었습니다. 따라서 service worker을 통해서 Cache Storage에 추가하는 방향으로 결정합니다.

    Service Worker란 무엇인가

    이제 길었던 서론이 끝났고 이번 글의 주제인 'Cache Storage와 crossorigin' 대해서 설명하겠습니다.

    서비스 워커는 웹 응용 프로그램, 브라우저, 그리고 (사용 가능한 경우) 네트워크 사이의 프록시 서버 역할을 합니다. 서비스 워커의 개발 의도는 여러가지가 있지만, 그 중에서도 효과적인 오프라인 경험을 생성하고, 네트워크 요청을 가로채서 네트워크 사용 가능 여부에 따라 적절한 행동을 취하고, 서버의 자산을 업데이트할 수 있습니다. 또한 푸시 알림과 백그라운드 동기화 API로의 접근도 제공합니다.

    - 출처: 모질라 공식 문서

     

    캐시를 관리하기 위해 service worker 을 사용해야 하는 이유는 브라우저가 서버에 요청을 하는 것을 가로채서 기존 캐시를 사용할 수도 있고, 받아온 정보를 캐시에 추가할 수 있습니다.

     

     

    기본적인 service worker 설정 후에 다음과 같이 fetch 요청을 가로채서 캐시가 남아 있으면 캐시를 사용하고 캐시가 없으면 fetch를 해서 이후에 오래된 캐시 된 이미지를 삭제하기 위해 다운로드한 시간을 헤더에 추가하여 저장하면 끝입니다.

     

    🚨 캐시를 도입하며 문제 발생! 

    하지만 곧 문제가 발생했습니다.

    (* 위 코드는 최종 코드로써 opaque 타입은 무시됩니다 - 재현을 위해 opaque 를 확인하는 if문을 제거했습니다)

     

    1. 캐시가 언제 저장되었는지 확인하기 위해서는 헤더에 받아온 날짜 정보를 추가해야하는데 헤더 읽기 및 쓰기가 안됨.

    [ 발생하는 오류 메세지 ]

     

    service-worker.js:42 Uncaught (in promise) RangeError: Failed to construct 'Response': The status provided (0) is outside the range [200, 599].

    > Response code 가 0이므로 헤더를 추가하기 위한 Response 복제 과정 문제

     

    FeedCardImageItem.jsx:13 Failed to load image dimensions 

    > 위 오류때문에 저장이 잘 안 되어 <img 태그를 불러올 수 없는 문제 발생 (처음 한 번은 잘 저장되지만 캐시 데이터를 불러오지 못하는 문제 발생.

     

    2. 엄청 큰 파일 사이즈.

     

    일단 테스트를 위해 Response 를 복제하여 Header을 변조하는 코드를 제거하고 해 본 결과되긴 했다.

    (좌) opaque response 으로 캐시가 되는 모습 (우) 하지만 엄청나게 큰 파일 사이즈?

     

    하지만 이미지 크기에 비해 Application 탭에 보이는 저장된 Usage 값이 너무 높았습니다. 뭔가 엄청 잘못됨을 확인하고 Opaque 대해 검색해 보기 시작합니다.

    원인조사: Response Type opaque(불투명) 은 무엇인가?

    no-cors 모드에서 교차 출처 요청을 하면 응답을 서비스 워커 캐시에 저장할 수 있으며 브라우저에서 직접 사용할 수도 있습니다. 그러나 응답 본문 자체는 JavaScript를 통해 읽을 수 없습니다. 이를 불투명 응답이라고 합니다. 불투명 응답은 교차 출처 저작물의 검사를 방지하기 위한 보안 조치입니다. 여전히 교차 출처 애셋을 요청하고 캐시할 수도 있습니다. 응답 본문을 읽을 수 없으며 상태 코드도 읽을 수 없습니다.

    비불투명 응답을 생성하는 cors 요청을 명시적으로 트리거하려면 HTML에 crossorigin 속성을 추가하여 CORS 모드를 명시적으로 선택해야 합니다.
    <link crossorigin="anonymous" rel="stylesheet" href="https://example.com/path/to/style.css"> <img crossorigin="anonymous" src="https://example.com/path/to/image.png">

    불투명 응답 및 navigator.storage API 교차 도메인 정보의 유출을 방지하기 위해 스토리지 할당량 한도를 계산하는 데 사용되는 불투명 응답의 크기에 상당한 패딩을 추가합니다. 이는 navigator.storage API가 저장용량 할당량을 보고하는 방식에 영향을 미칩니다. 이 패딩은 브라우저에 따라 다르지만 Chrome의 경우 캐시된 불투명 응답 하나가 사용된 전체 저장용량에 기여하는 최소 크기는 약 7MB입니다. 캐시할 불투명 응답 수를 결정할 때 이 점을 염두에 두어야 합니다. 예상한 것보다 훨씬 빨리 저장용량 할당량을 쉽게 초과할 수 있기 때문입니다.

    출처: https://developer.chrome.com/docs/workbox/caching-resources-during-runtime/#opaque_responses

     

    하지만 뭔가 이상하죠.. 분명히 Firebase Storage에서는 CORS 설정을 했었고, 만약에 CORS 문제가 있었다면 fetch처럼 요청 자체가 안 됐을 테니깐요.

     

    사실 위 공식문서만 읽어도 감이 왔어야 했지만 갈피를 못 잡고 더 조사하기 시작합니다.

     

    웹 페이지에서 리소스로서 불투명(opaque) 응답 사용하기
    브라우저가 CORS(교차 출처 리소스 공유) 없이 교차 출처 리소스를 사용할 수 있도록 허용하는 경우, 불투명 응답(opaque response)은 웹 페이지의 리소스로 사용될 수 있습니다. 아래는 CORS 없이 교차 출처 리소스를 사용할 수 있는 일부 요소들로, Mozilla Developer Network(MDN) 문서를 참고하여 정리한 목록입니다:
    * <script>
    * <link rel="stylesheet">
    * <img>, <video>, <audio>
    * <object>, <embed>
    * <iframe>

    불투명(opaque) 응답과 캐시 스토리지 API
    개발자가 불투명 응답을 사용할 때 발생할 수 있는 문제 중 하나는 Cache Storage API와의 호환성입니다. 다음 두 가지 배경 정보가 관련됩니다.
    불투명 응답의 status 속성은 원래 요청이 성공했든 실패했든 관계없이 항상 0으로 설정됩니다. Cache Storage API의 add() 및 addAll() 메서드는 응답의 상태 코드가 2XX 범위에 없는 경우 요청을 거부합니다.

    브라우저마다 다르지만, Google Chrome의 경우 단일 불투명 응답이 캐시 저장 공간에 최소 약 7MB를 차지합니다. 따라서 불투명 응답을 캐시에 저장할 개수를 신중히 결정해야 하며, 예상보다 빠르게 저장 한도를 초과할 수 있음을 유의해야 합니다.

    출처: https://stackoverflow.com/a/39109790 (블로그를 위한 번역: ChatGPT)

     

    스택오버플로우 글을 읽고 퍼즐조각이 맞춰졌습니다. 

    ✅ 해결: Cache Storage와 crossorigin의 중요성

    https://developer.mozilla.org/ko/docs/Web/HTML/Attributes/crossorigin

    지금까지 조사한 내용을 정리해 주는 문서입니다.

     

    사실 crossorigin="anonymous" 태그만 추가해 주면 해결되는 쉬운 문제였던 것 같습니다.

    하지만 이유를 알게 되었고, CORS로 데이터를 안전하게 보호하기 위해서 추가된 기능이라는 것도 알 수 있었습니다.

     

     

    이렇게 이미지 캐시 작업을 완료했습니다.

     

    백엔드와 프론트를 다루면서 Fetch 요청에서 CORS 요청을 해결하는 방법을 자주 봐왔지만 image 태그에서도 적용되는 것은 처음 알았습니다. 다만 아직 Proxy를 사용하면 CORS 요청 없이 볼 수 있는데 왜 브라우저에서는 CORS를 중요시하는지는 더 많은 공부와 경험이 필요할 것 같습니다.

     

    작업한 PR: https://github.com/handong-app/handong-feed-prototype/pull/63

    캐시적용 결과

    캐시를 적용하고 사용된 대역폭이 반으로 감소한 모습

     

    마치며

    누군가 서비스를 열심히 사용하고 계신 모습
    계속 확인하고 계신 모습

     

    제가 서비스를 만드는 이유는 일단 제가 편할 수 있고 누군가 제 서비스를 사용해 준다는 소중한 경험 때문입니다.

     

    이번 블로그를 작성하면서 우연히 로그를 보게 되었는데 누군가 게시글을 하나씩 (/seen 요청이 하나씩 올라옴) 보고 계신 것을 확인할 수 있었습니다. 몇몇 홍보물은 마음에 드셨는지 공유버튼을 눌러 공유하신 것 같더라고요. 개인 계정을 사용하시는 것을 보아 새내기이신 것 같은데 저희 서비스로 학교생활에 기대를 하실 것이라 생각하니 너무 뿌듯하고 앞으로 더욱 많은 기능을 추가하고 싶다는 생각을 하게 되었습니다.

     

    이제 취업을 준비하고 있는데 어디가 되었건 제가 만드는 서비스로 다양한 사람들을 도와줄 수 있는 서비스를 개발하고 싶다는 다짐을 다시 한번 하게 되었습니다.

     

    참고했던 사이트

    Firebase Storage CORS 문제 때문인 줄 알아서 다시 설정할 때 참고했던 사이트

    HTML Standard

    Opaque (불투명) Response

     

    댓글