투두리스트 다음으로 두 번째로 해보는 React 프로젝트이자 TypeScript는 처음 써보는 프로젝트여서 미숙하고 제대로 안된 부분이 많았다.
제작 요청이 있어 제작을 시작한거라 기간에 대한 압박 때문에 구현 가능 여부에만 너무 중점을 둔 것 같다.
실 사용자가 있다고 생각하니 어떠한 피드백이 들어올지 두렵다.
발주가 들어오긴 했어도 요구사항 명세서 등은 없이 스스로 기획하고 설계하다 보니 중간에 내 입맛 대로 구조가 많이 바뀌어 중구난방이 된 것 같다.
다음에는 기획, 설계 및 디자인까지 피그마 등 여러 툴을 이용해 꼼꼼하게 제작하고 테스트 코드도 작성해 TDD로 해보면 좋을 것 같다.
물론 기간 압박이 없는 연습용 프로젝트에서 말이다.
중간에 1주 정도 제작하다가 원티드 프리 온보딩에 참여해서 1달 간 중단 후 다시 1주 정도 제작을 하여 마쳤다.
해당 부트캠프 및 이전의 부트캠프에서 배운 내용들 중 일부는 적용에 성공하였고 나머지도 추후 반영 예정이다.
부트캠프에서 로컬 캐싱과 관려하여 배웠는데 마침 내 프로젝트에서 필요한 부분이라 적용하였다.
💡 로컬 캐싱 적용
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;
};
💡 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;
};
트러블 슈팅과 관련해서는 크게 5가지 문제가 있었다.
전역 상태 관리 조절 여부
: 보통 props-drilling이 2~3단계 이상 가면 전역으로 뺀다고 하는데 올바른 기준을 모르겠다. 생산성 측면에서는 이미 전역에 관련 slice가 있는 경우에는 한 번이라도 props로 내려줘야 하면 전역 상태로 빼는게 낫지 않을까 싶다. props로 내리기위해 작성하는 코드와 전역으로 빼서 가져와 쓰는 코드와 코드량 측면에서는 비슷하다고 느꼈기 때문이다. 해당 부분은 좀 더 경험을 많이 하다보면 감이 잡힐 것으로 예상된다.
게임사 API 호출 횟수 제한 문제
: 이 부분은 위의 적용 사항에서 언급한 것과 같은 부분으로 생략한다.
레포지토리 분리 문제
: 처음에는 당연히 한 레포에 FE / BE를 같이 넣어 놓았고 분리할 수 있다는 것조차 생각을 하지 못하였다. 개발 단계에서는 편리성을 위해 같이 놓고 concurrently로 서버와 리액트를 동시에 실행하였고 배포 단계에서도 그렇게 하려고 하였으나 주로 백엔드와 프론트를 따로 둔다는 글을 많이 발견하였고 백엔드는 업데이트가 적은데 반해 프론트는 자주 변경되므로 분리하는게 편리하다는 것이었다. 이 부분도 어느정도 동의하여 분리하여 다른 서버로 배포하였다.
배포 문제
: 서버 배포를 Vercel과 같은 Serverless Function으로 하면 항상 여러 문제가 발생한다. 주로 콜드 스타트 latency 관련인데 이번에는 아예 몽고 DB와 관련하여 에러가 발생하였고 검색해보니 몽고 DB측에서 해당 문제는 본인들과 상관 없는 문제라고 하였고 실제로 바로 Heroku로 테스트 해보니 정상 작동하였다.
데이터 구조 설계 문제
: 저장하고 주고 받을 데이터의 구조를 어떻게 설계할지에도 상당한 시간이 들었다. 관리하기 편하게 하나의 객체에 중첩으로 전부 때려박을지 매번 정제해서 쓸 필요 없게 프론트의 일부에서만 필요한 부분은 분리해서 하드 코딩 해놓을지 등 고민이 많았다. 결론적으로는 중심이 되는 데이터는 필요할 것 같아 하나의 객체에 중첩으로 넣어 놓고 일부분만 추출하여 하드코딩 후 사용하였다.
전체적으로 팀 프로젝트와 달리 개인으로 하다 보니 신경써야할 부분들과 프론트 외적인 부분에 시간을 상당히 많이 쓴 것 같다. 해보면서 느낀 가장 큰 점은 왜 협업을 하는지 알겠다는 것이다. 솔직히 하나하나 분석하고 공부하고 하면서 하면 결국 되긴 되지만 시간이 오래 걸린다. 대부분의 프로젝트 특성상 제작 기간이 정해져 있어 협업을 하지 않으면 정해진 기간내에 제작하기 힘들겠다고 느꼈다.