로아킨 프로젝트 회고록

코몽·2023년 6월 20일
0

개요

투두리스트 다음으로 두 번째로 해보는 React 프로젝트이자 TypeScript는 처음 써보는 프로젝트여서 미숙하고 제대로 안된 부분이 많았다.
제작 요청이 있어 제작을 시작한거라 기간에 대한 압박 때문에 구현 가능 여부에만 너무 중점을 둔 것 같다.
실 사용자가 있다고 생각하니 어떠한 피드백이 들어올지 두렵다.
발주가 들어오긴 했어도 요구사항 명세서 등은 없이 스스로 기획하고 설계하다 보니 중간에 내 입맛 대로 구조가 많이 바뀌어 중구난방이 된 것 같다.
다음에는 기획, 설계 및 디자인까지 피그마 등 여러 툴을 이용해 꼼꼼하게 제작하고 테스트 코드도 작성해 TDD로 해보면 좋을 것 같다.
물론 기간 압박이 없는 연습용 프로젝트에서 말이다.

중간에 1주 정도 제작하다가 원티드 프리 온보딩에 참여해서 1달 간 중단 후 다시 1주 정도 제작을 하여 마쳤다.
해당 부트캠프 및 이전의 부트캠프에서 배운 내용들 중 일부는 적용에 성공하였고 나머지도 추후 반영 예정이다.

적용 사항

부트캠프에서 로컬 캐싱과 관려하여 배웠는데 마침 내 프로젝트에서 필요한 부분이라 적용하였다.

💡 로컬 캐싱 적용

  • 게임사 호출 API 횟수 제한으로 인해 로컬 캐싱을 적용할 필요가 생겼다.
  • 로컬 캐싱에는 크게 두 가지 방법이 있고 각 방법 마다 여러가지의 방식으로 또 나뉜다.
    • Storage 방식
      • WebStorage : 저장 가능 용량도 적고 동기로 작동하여 메인 스레드 중단 위험이 있어 제외
      • CacheStorage, IndexedDB: 저장 가능 용량도 크고 비동기로 작동하여 메인 스레드에 영향을 주지 않음. 현 프로젝트에서는 둘 중 CacheStorage 채택
    • in-memory 방식
      • useRef: useRef를 일반 변수 처럼 활용하여 캐시될 데이터를 저장하는 것이다.
      • useState: useState로 캐시 데이터를 관리하는데 캐시데이터가 변할 때 리렌더링이 필요치는 않아서 useRef가 더 나아보인다.
      • JSObject: 일반 객체 리터럴에 캐시 데이터를 저장하는 것이다. 다른 모듈로 만들어 import해서 사용하는 방식을 취해야해서 useRef가 나아보인다.
  • 브라우저 새로고침 및 재실행시에도 캐시 데이터가 유지되어야 하므로 in-memory 방식이 아닌 Storage 방식의 CacheStorage 사용.
export const useCache = () => {
  const dispatch = useAppDispatch();
  const cacheKey = '/crewList'
  const cacheObj = {
    data: [],
    cachedTime: new Date().getTime()
  };
  const processCache = async (fnc: () => Promise<CharacterDetail[][]>) => {
    try {
      const cache = await caches.open('crew');
      
      const response = await cache.match(cacheKey);
      if (response) {
        const res = await response.json();
        const cachedTime = new Date(res.cachedTime).getTime();
        const currentTime = new Date().getTime();
        
        if (currentTime - cachedTime > EXPIRATION_TIME) {
          const data = await fnc();
          const newCache = { ...cacheObj, data };

          await cache.put(cacheKey, new Response(JSON.stringify(newCache), {
            headers: { 'Content-Type': 'application/json' }
          }));
          dispatch(crewActions.fetchCrew(data));
        } else {
          dispatch(crewActions.fetchCrew(res.data));
        }
      } else {
        const data = await fnc();
        const newCache = { ...cacheObj, data };
        await cache.put(cacheKey, new Response(JSON.stringify(newCache), {
          headers: { 'Content-Type': 'application/json' }
        }));
        dispatch(crewActions.fetchCrew(data));
      }
    } catch (error) {
      alert(error);
    }
  };

  return processCache;
};
  • 캐시 스토리지를 적용하여 로컬 캐싱 구현
  • expire time 기능 적용하여 일정 시간이 지나면 캐시 데이터 업데이트

💡 Promise.All 적용

  • 유저들의 대표 캐릭터명이 들어있는 배열을 순회하며 api를 호출해 동료 캐릭터 fetch
  • 리렌더링 문제 발생
    • 매번 fetch시 마다 dispatch 할 경우 수십 번 리렌더링 되는 문제 발생
  • 해결방법
    • Promise.All을 사용하여 배열내의 모든 비동기 함수가 응답이 온 후에 리렌더링을 하도록 로직 설계
const fetchCrew = async () => {
    const res = await Promise.all(crewState.main.map((item) => fetchCrewList(item)));
    res.forEach(characters => {
      characters.forEach((character: CharacterDetail) => {
        character.ItemAvgLevel = Math.trunc(+(character.ItemAvgLevel as string).replace(',', ''));
      });
      characters.sort((a: CharacterDetail, b: CharacterDetail) => (b.ItemAvgLevel as number) - (a.ItemAvgLevel as number));
    });
    const filteredList = res.map(characters => characters.filter((character: CharacterDetail) => character.CharacterLevel !== 1));
    dispatch(crewActions.fetchCrew(filteredList));
    return filteredList;
  };
  • Promise.All로 fetch 받은 리스트 정렬 및 정제 후 dispatch

트러블 슈팅

트러블 슈팅과 관련해서는 크게 5가지 문제가 있었다.

  • 전역 상태 관리 조절 여부
    : 보통 props-drilling이 2~3단계 이상 가면 전역으로 뺀다고 하는데 올바른 기준을 모르겠다. 생산성 측면에서는 이미 전역에 관련 slice가 있는 경우에는 한 번이라도 props로 내려줘야 하면 전역 상태로 빼는게 낫지 않을까 싶다. props로 내리기위해 작성하는 코드와 전역으로 빼서 가져와 쓰는 코드와 코드량 측면에서는 비슷하다고 느꼈기 때문이다. 해당 부분은 좀 더 경험을 많이 하다보면 감이 잡힐 것으로 예상된다.

  • 게임사 API 호출 횟수 제한 문제
    : 이 부분은 위의 적용 사항에서 언급한 것과 같은 부분으로 생략한다.

  • 레포지토리 분리 문제
    : 처음에는 당연히 한 레포에 FE / BE를 같이 넣어 놓았고 분리할 수 있다는 것조차 생각을 하지 못하였다. 개발 단계에서는 편리성을 위해 같이 놓고 concurrently로 서버와 리액트를 동시에 실행하였고 배포 단계에서도 그렇게 하려고 하였으나 주로 백엔드와 프론트를 따로 둔다는 글을 많이 발견하였고 백엔드는 업데이트가 적은데 반해 프론트는 자주 변경되므로 분리하는게 편리하다는 것이었다. 이 부분도 어느정도 동의하여 분리하여 다른 서버로 배포하였다.

  • 배포 문제
    : 서버 배포를 Vercel과 같은 Serverless Function으로 하면 항상 여러 문제가 발생한다. 주로 콜드 스타트 latency 관련인데 이번에는 아예 몽고 DB와 관련하여 에러가 발생하였고 검색해보니 몽고 DB측에서 해당 문제는 본인들과 상관 없는 문제라고 하였고 실제로 바로 Heroku로 테스트 해보니 정상 작동하였다.

  • 데이터 구조 설계 문제
    : 저장하고 주고 받을 데이터의 구조를 어떻게 설계할지에도 상당한 시간이 들었다. 관리하기 편하게 하나의 객체에 중첩으로 전부 때려박을지 매번 정제해서 쓸 필요 없게 프론트의 일부에서만 필요한 부분은 분리해서 하드 코딩 해놓을지 등 고민이 많았다. 결론적으로는 중심이 되는 데이터는 필요할 것 같아 하나의 객체에 중첩으로 넣어 놓고 일부분만 추출하여 하드코딩 후 사용하였다.

느낀점

전체적으로 팀 프로젝트와 달리 개인으로 하다 보니 신경써야할 부분들과 프론트 외적인 부분에 시간을 상당히 많이 쓴 것 같다. 해보면서 느낀 가장 큰 점은 왜 협업을 하는지 알겠다는 것이다. 솔직히 하나하나 분석하고 공부하고 하면서 하면 결국 되긴 되지만 시간이 오래 걸린다. 대부분의 프로젝트 특성상 제작 기간이 정해져 있어 협업을 하지 않으면 정해진 기간내에 제작하기 힘들겠다고 느꼈다.

profile
프론트엔드 웹 개발자

0개의 댓글

Powered by GraphCDN, the GraphQL CDN