본문 바로가기
성능

브라우저 스토리지를 이용하여 네트워크 요청 줄이기

by Luke K 2023. 8. 10.

프론트엔드에서 네트워크 요청을 줄일 수 있는 두 가지 방법에 대해 다루는 글을 작성하였다.
이전 글 바로가기

첫번쨰 글에서 작성한 두 가지 방법은 다음과 같았다.

  • 적당한 단위를 산정하여 인피니티 스크롤 적용하기
  • 정적 페이지로 만들어 관리하기

이번에는 브라우저 스토리지를 이용한 캐싱 방식에 대해서 글을 작성해보려 한다.

브라우저 스토리지는 메모리보다 값을 오래 저장할 수 있도록 도와준다.

 

Table Of Content

  • 브라우저 스토리지들의 차이점
  • 장바구니 개수 컴포넌트로 생각해보는 장바구니 개수 캐싱
  • 캐싱을 이용할 때 주로 고려해야할 것들

브라우저 스토리지를 이용한 캐싱

브라우저 스토리지에는 로컬 스토리지, 세션 스토리지, 쿠키가 있다.

위 세개의 스토리지에 저장을 해두고 값이 갱신되지 않는 경우에 값을 그대로 받아오는 방식을 사용할 수 있다.

간단하게 세 개의 스토리지를 비교해보고 어떨 때 무슨 스토리지를 사용할지 정리해 보자.

  로컬 스토리지 세션 스토리지  쿠키
데이터 유지 브라우저 종료 후에도 유지 브라우저 종료 시 삭제 옵션에 따라 다름
브라우저 변경 시에도 공유 o x o
서버 사이드 사용 가능 x x o

위 데이터를 바탕으로 값을 캐싱할 때 사용할 때 상황별로 선택할만한 예제가 될만한 기획이나 상황을 생각해 보자.

사용자가 창을 닫으면 무조건 새로 요청해야 한다. -> 세션 스토리지

사용자가 크롬이었다가 사파리로 가도 유지되야 한다. -> 로컬 스토리지 혹은 쿠키

캐싱해야 할 데이터가 있는 컴포넌트를 SSR해야한다. -> 쿠키

캐싱해야할 데이터를 항상 네트워크 요청에 포함해야 한다. -> 쿠키

서버에는 데이터를 전송하지 않아도 되지만 브라우저에서 최대한 데이터를 오래 보관하여 요청을 줄이고 싶다. -> 로컬 스토리지

 

 

 

장바구니 개수 컴포넌트로 생각해 보는 장바구니 개수 캐싱

커머스를 살펴보면 다음과 같은 장바구니 버튼이 있고 장바구니 버튼에는 다음과 같은 장바구니에 담긴 상품에 숫자가 적혀있다.

쿠팡 헤더 장바구니 버튼

기획서에 다음과 같이 적혀있다고 가정해 보자

장바구니 바로가기 버튼

- 아래 10개의 페이지에서 사용되는 공통 컴포넌트입니다.

- 버튼 클릭 시 장바구니 페이지로 이동합니다.

- 장바구니에 담긴 상품 종류의 수를 함께 보여줍니다.

 

일반적으로는 위 장바구니 버튼 컴포넌트를 분리하고 이에 대한 데이터를 서버에서 받아와서 보여줄 것이다.

하지만 그렇게 할 경우 기획서에 사용되는 10개의 페이지에서는 페이지를 이동할 때마다 데이터를 요청하여 비효율적인 요청이 반복되게 된다.

그렇기 때문에 장바구니 개수 데이터를 브라우저 스토리지에 저장해 두고 필요할 때만 요청하여 갱신하는 방식을 사용할 수 있다.

다음 순서도와 같은 방식을 사용할 수 있다.

간단히 코드로 나타내면 다음과 같다.

async cartInfoPrefetch() {
    try {
      const cartData = this.getCartDataInStorage();
      const betweenTime = getBetweenTime(cartData?.maxAge);
      if (!cartData || betweenTime < 0) {
        const {count} = await this.requestCartCount();
        this.setCartCountInStorage({
          count,
          maxAge: this.getMaxAge(),
        });
      }
      set((state) => ({
        ...state,
        cartCount: cartData.count,
      }));
    } catch (error) {
      // 에러처리
    }
  },
};

 

 

이 방식을 사용할 때 고려할 것들이 있다.

  • 데이터베이스에 저장된 정보와 사용자가 보는 정보의 정합성을 어떻게 지킬 것인가?
  • 서버에서 응답을 받아오지 못하거나 에러가 났을 경우에는 어떻게 할 것인가?

이 두 가지를 해결할 방법을 생각해 보자.

 

데이터베이스에 저장된 정보와 사용자가 보는 정보의 정합성을 어떻게 지킬 것인가?

일단 항상 데이터베이스의 정보와 UI의 정보가 일치하는 것이 일반적으로 사용자가 생각하는 시점일 것이다.

그렇기에 일단 장바구니 개수가 바뀌거나, 사용자가 장바구니 개수를 확인하는 시점에 일치하도록 한다.

일단 장바구니에 상품을 등록하거나 상품을 삭제할 때가 있다.

보통 이때 post나 delete 요청을 통하게 되는데 이때 응답으로 장바구니 개수를 받아 스토리지에 갱신한다.

또 장바구니 화면에 들어올 때 장바구니 데이터들을 받으며 장바구니 개수를 갱신한다.

 

서버에서 응답을 받아오지 못하거나 에러가 났을 경우에는 어떻게 할 것인가?

개인적으로 네트워크 관련 문제가 발생했을 때 유저가 시도할 다음 행동을 정해주는 애플리케이션의 경험이 좋다고 생각한다.

혹은 유저가 알아야 하는 에러인지 몰라도 되는 에러인지를 잘 정리한 이후에 몰라도 되는 에러라면 사용자의 행동을 방해하지 않는 것 따져볼 수 있다.

일단 유저에게 다음 행동을 안내하는 방법을 봐보자.

리스트를 보여주는 페이지에서 리스트 데이터를 가져오는 요청에서 에러가 났다.

이런 경우는 리스트 데이터를 재요청하는 버튼을 보여줄 수 있다.

카카오 웹툰 리스트 불러오기 재시도 버튼

리액트 Error Boundary를 이용하여 시도할 수 있는 간단한 코드를 살펴보자.

일단 사용 방식은 다음과 같다.

export default function ListContainer() {
  return (
    <ErrorBoundary>
      <List/>
    </ErrorBoundary>
  );
};

 

Error Boundary에서는 에러를 감지하여 재시도 버튼을 띄워줄 수 있다.

import React, { Component } from "react";

class ErrorBoundary extends Component {
  state = {
    hasError: false,
    error: null,
  };

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    console.error("Error captured in boundary:", error);
    console.error("Error info:", errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
      // 재시도 버튼을 누를 경우 에러 상태를 리셋하기 위해 props로 넘겨준다.
        <RetryList resetError={() => {
          this.setState({ hasError: false, error: null });
        }}/>
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

 

사용자에게 다음 행동을 유도하지 않아도 되는 상황에는 여러 가지 선택을 해볼 수 있다.

사용자에게 재시도를 요청하는 토스트를 띄워준다.

이는 사용자는 어플을 닫고 다시 들어올 가능성이 높기 때문에 이탈률을 높일 수 있음을 고려해봐야 한다.

 

혹은 그냥 관계자들만 에러를 파악할 수 있게 한다.

에러가 발생할 때 로깅을 남겨두면 나중에 유효하게 사용할 수 있다.

그래서 에러가 발생하면 타입에 따라 적절하게 로깅을 전송하거나 남겨두면 해당 로그를 바탕으로 더 효율적인 조치를 할 수 있다.

 

사용자가 해당 페이지에서 주요하게 봐야 하는 정보를 못 보는 상황이 아닌 지 정해야 한다.

만약 아니라면 그 후 사용자에게 알리지 않고 컴포넌트에서 null을 리턴해줄 수도 있다.

 

이 부분을 할 수 있을 예제 코드를 살펴보자.

아래와 같은 로거 클래스를 간단히 만들 수 있다.

export default class ErrorLogger {
  static logError(error, metadata = {}) {
    console.error("Logged Error:", error.message);
    console.error("Metadata:", metadata);

    // 예: 다른 로깅 서비스로 전송, 로컬 스토리지에 저장 등을 추가할 수 있음
  }
}

 

또 위의 ErrorBoundary의 componentDidCatch의 다음과 같이 추가할 수 있다.

  componentDidCatch(error, errorInfo) {
    console.error("Error captured in boundary:", error);
    console.error("Error info:", errorInfo);

    const metadata = {
      componentName: this.props.componentName,
      errorType: 'CART_COUNT_REFETCHING_ERROR' // 예시  
    };
    ErrorLogger.logError(error, metadata);
  }

 

마무리

브라우저 스토리지를 이용하여 적절하게 캐싱을 하여 네트워크 요청을 줄일 수 있다.

이때 어떤 스토리지를 쓸지 정하고 어떻게 캐싱을 잘하면서도 데이터의 정합성을 지킬지가 중요하다.

데이터의 정합성을 지키기 위해 꼭 데이터를 최신화해줘야 하는 타이밍을 커뮤니케이션으로 정해 최신화할 수 있다.

또 서버에서 데이터를 다시 가져올 때 에러가 난다면 ErrorBoundary 등을 적절히 활용하여 에러가 발생했을 때 사용자 경험을 너무 안 좋지 않게 할 수 있다.

댓글