SWR 적용해서 페이지 렌더링 속도 높이기!

라코마코·2020년 12월 12일
3

JS

목록 보기
4/6

가계부 프로젝트를 진행하면서 페이지 로딩 속도를 캐시를 통해서 개선하고자 한 삽질에 대해서 작성해보고자 합니다.

혹시 틀린점이 있다면 과감히 지적해주세요

before


( 느리고 답답하다! )

after


( 이전보단 봐줄만하다 ! )

계기

저희 프로젝트에서는 거래내역을 가져와야 할 경우에는 trasaction api를 통해서 가져옵니다.

예를 들어 아래 페이지의 경우

/transactions?accountbook_id=1&start_date=2020.12.01&end_date=2021.01.01

와 같은 형식의 GET 요청을 통해서 거래 내역을 가져오고

파이 차트 페이지에서는

/transactions?accountbook_id=1&start_date=2020.10.13&end_date=2020.11.13

와 같은 GET 요청을 통해서 가져오고

그래프 페이지에서는

/transactions?accountbook_id=1&start_date=2020.12.01&end_date=2020.12.31

와 같은 형식의 요청을 통해서 거래내역을 가져오게 됩니다.

눈치 채시겠지만 메인 거래내역 페이지와 그래프 페이지의 요청 URL과 쿼리가 같습니다.

파이 차트 페이지에서도 같은 요청의 쿼리를 보내는 경우가 많았습니다.

이렇게 중복되는 요청을 캐싱하면 API 통신 시간을 줄여 더 빠른 화면 렌더링을 할 수 있다 라는 생각으로 접근하게 되었습니다.

하지만 여기에는 함정이 있습니다.

거래 내역은 사용자가 작성하는 동적인 데이터이기 때문에 항상 캐싱한 데이터만 사용해서 화면에 출력할 수는 없습니다.

다른 사용자가 작성한 새로운 데이터가 생길 수도 있기 때문입니다.

SWR

이런 문제점을 해결하기 위해서 SWR 개념을 도입했습니다.

간단하게 설명하면 사용자가 API에 요청을 보내면 우선 캐싱된 데이터를 먼저 준 후
비동기 통신으로 API 요청을 보냅니다. 이 후 응답이 오면 데이터를 캐시하고 새롭게 받은 데이터를 다시 한번 사용자에게 주는 방식입니다.

이렇게 되면 데이터가 변하지 않았다면 캐시의 효과를 누릴 수 있고.

데이터가 변했다면 바로 화면 데이터를 변경해주는 효과를 얻을 수 있습니다.

(use SWR hook https://swr.vercel.app/ )

이미 관련된 라이브러리도 있었지만 SWR을 지원하는 라이브러리들은 모두 컴포넌트에서 사용 가능한 커스텀 훅으로 이루어진 라이브러리들이었습니다.

문제는 저희 코드에서 API 요청을 컴포넌트에서 하지 않고 다른 객체에게 위임해서 호출하고 있었기에 useSWR을 쓰기 위해서는 구조를 모두 뜯어 고쳐야 해서 사용하는걸 포기했습니다.

대신 이해한 만큼 직접 구현해서 사용하기로 결심했습니다.

어떻게 만들지?

데이터를 어떻게 캐싱할지, 어떻게 구분할지에 대해서 생각을 해볼 결과 이런 결론을 내렸습니다.

  1. GET 요청만 캐싱하자
  2. URL을 Key값으로 캐싱하자

Post,Delete,Fetch와 같은 메소드 요청은 캐싱할 필요가 없다고 생각했습니다. Post Delete Fetch등은 데이터를 업데이트 하는 메소드이지 가져오는 데이터가 아니기 때문에 캐싱할 경우가 거의 없다고 생각해서 제외시켰습니다.

이 메소드들을 제외시키니 생각보다 쉽게 만들 수 있다고 생각이 들었습니다.

Get 요청시 파라미터는 쿼리로 들어가고, 쿼리문은 API URL와 함께 들어오니 문자열을 그대로 Key값으로, 응답 결과를 Value값으로 저장하는 Map 객체로 구현할 수 있다고 생각이 들었습니다.

generator

문제는 캐싱된 데이터를 먼저 리턴 한 후에, 비동기로 들어오는 응답을 다시 어떻게 리턴할지가 문제였습니다.

자바스크립트의 함수는 한번 리턴 하면 함수가 종료되기 때문에 첫 캐시 이후 들어오는 비동기 데이터를 어떻게 리턴해줄지 생각해 봐야했습니다.

고민 끝에 그 부분은 generator 함수로 해결했습니다.

generator 함수는 yield 구문을 통해서 함수를 원하는 시점에 실행시키고, 원하는 시점엔 중단시킬 수 있기 때문에 이 문제를 해결하기에 가장 적합한 방법 이라고 생각했습니다.

const IncomeCategory = new Map();
...
export default{
  getIncomeCategoryById: async function* (id: number): AsyncGenerator<Category[]> {
    const requestURL =
      categoryAPIAddress.getIncome +
      '?' +
      querystring.stringify({
        accountbook_id: id,
      });
    //캐시 리턴
    yield IncomeCategory.get(requestURL);

    const response = await instance.get(requestURL);

    //캐시 업데이트
    IncomeCategory.set(requestURL, response.data);

    yield response.data;
  },
}

API 통신은 이 getIncomeCategoryById 함수를 통해서 호출을 하는데
함수 첫 호출시에 IncomeCategory Map 객체에서 캐시된 값을 조회후에 리턴합니다.
그 후 두번째 호출이 되면 실제 서버로 API 요청을 보낸후 캐시를 업데이트 하고 그 응답값을 리턴합니다.

이 함수를 사용하는 쪽에서 먼저 generator 함수를 생성한 후에 2번 호출하며 상태값을 업데이트 하는 방식으로 사용하게 함으로써 문제를 해결했습니다.

  updateIncomeCategories = flow(function* (this: CategoryStore, id: number) {
    const generator = CategoryService.getIncomeCategoryById(id);
    const { value: cachedValue } = yield generator.next();
    if (cachedValue !== undefined) {
      // 캐시된 데이터가 있으면 우선 캐시된 데이터로 상태를 업데이트한다.
      this.changeIncomeCategories(cachedValue);
    }
    const { value: refreshedValue } = yield generator.next();
    // 그후 진짜 데이터를 이용해서 다시 상태를 변경한다.
    this.changeIncomeCategories(refreshedValue);
  });

이런 식으로 API 통신을 캐싱함으로써 페이지 렌더링 속도를 올릴 수 있게 되었습니다.

gif 이미지는 Fast 3G 환경에서 테스트한 영상입니다.
데이터를 캐싱하였기에 중복된 결과는 화면에 즉각즉각 반영되어서 반응속도가 확연히 빨라졌다는 점을 체감할 수 있었습니다.

1개의 댓글

comment-user-thumbnail
2020년 12월 13일

감사합니다 이해가 확 되네요 !!

답글 달기