[회고] 원티드 프리온보딩 프론트엔드 인턴십 - 2주차 (2-1편 cache 구현하기 - 개인)

흔한 감자·2023년 5월 3일
0
post-thumbnail

안녕하세요, 감자입니다
이번에 회고해볼 내용은 2주차 과제였던 cache 구현입니다.
제가 스스로 cache를 구현한 방식을 정리한 개인편팀의 best pratice로 작성하면서 배운점들이 정리한 팀편으로 나누어 블로깅해볼 예정입니다. 우선 제가 cache를 구현한 방식을 이야기해보도록 하겠습니다.

프리온보딩 2주차 과제

이번 과제도 10명의 팀원들과 과제를 진행하고, best pratice를 만들어서 제출하기 였습니다. 기업 과제여서 상세 내용은 공유할 수 없지만, 핵심 내용은 검색어 캐싱 구현하기 였습니다. 제가 캐싱을 구현한 방식과 팀원들이 캐싱을 구현한 방식이 크게 달랐습니다. 우선 제가 구현한 방식부터 이야기해보도록 하겠습니다.

나의 캐싱 구현하기

캐싱을 구현하는데 정말 다양한 방식이 있었는데, 저는 브라우저의 storage 사용없이 javascrip의 Map을 이용하여 구현하였습니다.

라이브러리 결정하기

기본적으로 best pratice 취합도 생각해야 했기에 지난 과제와 비슷한 셋팅을 하도록 결정하였습니다. 하지만, 다르게 선정한 부분도 있는데 바로 module.css와 vite를 사용하였습니다.

vite를 선택한 이유

  • 지난 과제에서는 제약 조건으로 CRA만 사용하도록 되어 있었지만, 이번에는 해당 제약 조건이 없어 vite도 고려
  • vite는 Esbuild 기반으로 되어있어 CRA에 비해 빌드에 빠름
  • 이러한 이유로 vite를 이번 프로젝트에 사용하기로 결정

module.css를 사용한 이유

  • 해당 과제에 허용 라이브러리에 스타일 관련 라이브러리에 대한 언급이 없어, 기본 css를 사용하기로 결정
  • css로만 했을때 class, id 명의 중복을 고려하여야 한다는 문제를 해결하고 컴포넌트별로 css를 분리하여 관리 용이성을 높이기 위해 vite template 설치시 기본으로 사용할 수 있는 module.css로 결정

캐시 구현 방식 결정하기

브라우저 스토리지 비교하기

로컬 스토리지

  • 동기 방식으로 동작하며 메인 스레드 연산을 중단
  • 최대 5MB 저장 가능
  • 문자열만 허용

세션 스토리지

  • 탭 안에서만 유효하며 탭이 닫히는 순간 스토리지 휘발
  • 로컬 스토리지와 동일하게 동기 방식으로 동작하여 메인 스레드 연산을 중단
  • 최대 5MB 저장 가능
  • 문자열만 허용

캐시 스토리지

  • 요청 및 응답에는 HTTP를 통해 전송할 수 있는 모든 타입의 데이터가 가능
  • 비동기 방식으로 메인 스레드 연산을 중단시키지 않음
  • 수백 MB에서 경우에 따라 GB 이상 저장 가능 (브라우저나 장치에 따라 다를 수 있음)

Map (javascript의 자료구조, 스토리지 x)

  • 메모리상에 캐시 데이터를 관리하여 저장/삭제 및 설정이 비교적 자유로움
  • 새로고침시 데이터가 휘발

스토리지 사용함으로써 여러 장점들이 있었지만, 검색어의 캐싱이 새로고침이나 페이지가 열고 닫힘에 따라 유지될 필요가 없다고 판단되어 스토리지를 사용하지 않고 Map으로 구현하기로 결정했습니다.

Map으로 구현하기

구현 자체는 스토리지를 사용할때보다 비교적 간단했습니다. 하지만, Map로 구현하다보니 메모리 관리의 필요성을 느끼게되었습니다. 끊임없이 저장하다보면 메모리 누수현상이 발생하지 않을까라는 걱정이 들어 접근한지 오래된 데이터 삭제하는 LRU 알고리즘을 적용하였습니다.
(참고: 캐시(Cache) 알고리즘)

구현 알고리즘

  • Map에 data(검색어 결과 리스트)와 expireTime(만료시간)과 lastAccessedTime(마지막 접근 시간)을 같이 저장
  • 접근하고자 하는 검색어 캐싱의 expireTime이 현재 시간보다 과거인 경우 캐시에서 삭제
  • 최대 저장 개수(limit)를 지정하여 최대 저장 개수로 도달한 경우 lastAccessedTime로 가장 접근한지 오래된 데이터를 삭제시킴 (LRU)

코드 보기

  • 캐시 저장 변수 Map 선언하기

    const cache = new Map<string, CachedRecommendType>();
  • 검색어 캐시 확인하기

      const fetchingSearch = async (searchKeyword: string) => {
        const cachedData = cache.get(searchKeyword); // 기존 존재하는 여부 확인
    
        // 해당 검색어의 캐시가 존재하는 경우
        if (cachedData) {
          const { data, expireTime } = cachedData;
    
          //만료 시간이 지나지 않은 경우
          if (isValidTime(expireTime)) { 
            setRecommendList(data);
    
            // lastAccessedTime(마지막 접근 시간) 현재 시간을 할당
            cache.set(searchKeyword, {
              data,
              expireTime,
              lastAccessedTime: new Date(),
            });
            return;
          }
    
          cache.delete(searchKeyword);
        }
  • 신규 검색어 받아오기 (캐시에 없는 경우)

      const data = await search(encodeURI(searchKeyword));
    
       const expireTime = addSeconds(cacheExpire);
       const lastAccessedTime = new Date();
       cache.set(searchKeyword, { data, expireTime, lastAccessedTime });
    
       if (checkCacheLimit()) {
         removeOldestAccessedItem();
       }
    
       setRecommendList(data);
  • 최대 개수 도달시 삭제하기 (LRU)

  const checkCacheLimit = () => {
    return cache.size > cacheLimit;
  };

  const removeOldestAccessedItem = () => {
    let oldestKey = '';
    let oldestAccessedTime = new Date();

    //가장 오랜된 item key 찾기
    for (const [key, value] of cache.entries()) {
      if (value.lastAccessedTime < oldestAccessedTime) {
        oldestKey = key;
        oldestAccessedTime = value.lastAccessedTime;
      }
    }

    if (oldestKey) {
      cache.delete(oldestKey);
    }
  };

마무리

limit는 테스트를 위해 100개 설정해두었는데, 브라우저를 껐다키면 휘발되기 때문에 사용자가 검색어를 100개까지 도달하는 경우가 있을까라는 생각도 들어 LRU를 과하지 않았나 싶기도했다. 하지만, 알고리즘 문제나 책에서만 보던 LRU를 실제로 구현해보니까 재미있었다.

profile
프론트엔드 개발자

0개의 댓글