최종프로젝트 주요 구동장면 및 기술적 챌린지

Jaehyeon Ye·2023년 3월 11일
0
post-thumbnail

프로젝트 담당 기능 주요 구현내용

1. 공공데이터 API fetching 및 가공

1) axios와 useQuery를 사용하여 data fetch 및 페이지 용도에 맞게 분배, 가공
2) try, catch 문으로 에러 핸들링

2. 메뉴탭 및 지역 필터 기능 구현


1) recoil의 useRecoilState와 react-router-dom의 useLocation을 이용한 query String으로 메뉴 관리.
2) 기존에 페이지 새로고침 시 HOME으로 초기화되는 부분을 해결하고자 메뉴 상태 유지를 위해 sessionStorage를 사용해서 관리하고 있었는데 이를 url의 query를 이용한 방식으로 리팩토링

3. 페이지네이션, 캐러셀 라이브러리를 사용하지 않고 로직 직접 구현

1) 처음에는 react slick 같은 라이브러리 적용을 고려하였으나 기획 변경으로 인해 적용 시점이 늦었고 CSS가 깨지게 되어 직접 구현을 통한 커스터마이징이 더욱 빠르고 적절한 해결책이라 판단하여 직접 구현하였음.
2) 처음에는 페이지네이션 숫자 클릭시 해당 숫자 페이지로 넘어가는 로직을 useInfinitequery로 적용하였는데 페이지를 건너뛰는 fetching(예 1 -> 5페이지 이동)이 안되서 useQuery로 리팩토링 하였음. 향후 유저 피드백에서 숙소 유형별로 리스트를 보고싶다는 의견이 있어서 Promise.all 이나 useQueries를 사용하여 API DATA를 가져오고 드롭다운 메뉴를 추가하고 싶었으나 이미 디자인이 확정되어서 구현 중단.
3) API DATA 가 자주 변경되는 데이터는 아니라서 이미 fetch하여 캐싱된 데이터의 빠른 로딩을 위해 query key의 staletime을 1시간으로 적용하였음

4. 스켈레톤 UI 구현

1) fetching, Loading 상태를 근본적으로 줄이는 방향이 바람직하겠지만 공공 API 서버 상황에 따라 fetching에 시간이 좀 걸리는 경우가 있어 스켈레톤 UI 적용을 고려 및 적용
2) 스켈레톤 UI 또한 별도의 라이브러리를 사용하지 않고 직접 UI 치수에 따라 커스터마이징 구현.
스켈레톤 UI 적용 후 lighthouse에서 5점 정도의 Performance 점수 증가. 성능 향상보다는 UX적 입장에서 사용감이 좋았다는 유저 피드백이 있었음. 로딩 시 레이아웃 시프트를 방지하는 이점.

5. 각 카드별 좋아요 카운트 반영되게 구현

firestore 2개의 collection 사용하도록 DB 설계

// firestore DB 구조

bookmarks
	ㄴ 유저(uid) 목록
    	ㄴ 유저 개개인의 찜하기 목록

spot_recommendation
	ㄴ 숙소(contentid) 목록
		ㄴ API에서 가져온 숙소 하나에 대한 세부 속성들 + 내가 추가한 viewCnt, likeCnt 속성
        

1) 우리 프로젝트에서 좋아요는 곧 유저 개개인이 찜한 내용이므로 bookmarks라는 한 collection에 유저 개개개인의 doc이 있고(doc 이름은 유저 개개인 고유의 uid) doc 안에 한 유저의 찜목록들이 배열 리스트 형태로 저장되도록 설계 및 구현.
2) 또 하나는 숙소 추천 리스트 수집을 위한 stay_recommendation라는 이름의 collection을 두어 유저가 한 숙소 카드를 클릭하거나 클릭 이후 넘어가는 상세페이지에서 찜하기를 클릭하면 stay_recommendation의 해당 숙소 contentid doc 안에 내가 추가해둔 속성인 viewCnt(조회수 카운트), likeCnt(숙소 총 찜하기(=좋아요) 개수 카운트)가 증가(또는 감소)하도록 설계 및 구현.
3) 위에서 likeCnt 속성이 8개씩 카드가 나오는 화면에서도 반영되어 보이게 구현.
4) 상세페이지에서의 좋아요 카운트는 클릭 즉시 반영되어야하는데 기존의 getDoc을 이용하는 방식으로는 약간의 텀이 있고 부가적인 코드가 있어 간단하면서도 바로 상태를 반영할 수 있는 onSnapshot 함수로 리팩토링

6. Queue 자료구조를 갖는 토스트 메시지 구현

1) immutable 한 concat과 slice 함수를 사용
2) fade-out 애니메이션을 주기 위한 keyframe 사용
3) 어느 페이지나 토스트 메시지 기능이 필요한 곳에 사용가능하도록 Recoil로 관리 및 해당 로직을 커스텀 훅으로 구현

7. firestore 쿼리로 좋아요 카운트 순으로 내림차순 정렬하도록 구현

8. 마이페이지 찜목록에서 타겟 장소 제거 로직 구현

9. Kakao Map 다중 마커 구현


1) 해당 관광지(파란 마커) 기준 반경 20km 이내의 일부 숙소를 API에 요청하여 받아오고 마커로 표시

10. 이미지 최적화 도전

1) API Data에서 가져오는 이미지가 아닌 asset에 저장하는 이미지인 경우 이미지 압축 및 avif 포맷으로 변환하여 저장(메인페이지 슬라이드 배너 이미지 등)
2) img 태그를 picture 태그로 감싸고 source의 srcSet으로 avif, webp로 들어오는 이미지가 있다면 적용되게 함. loading=lazy 속성과 decoding=async 속성 적용.
3) 배너 이미지를 avif 적용했음에도 크기가 커서 첫 랜딩 시 로딩 시간이 오래걸림. useLayoutEffect로 preload되게 리팩토링
4) input에 의한 업로드 이벤트가 아닌 API fetching을 통해 가져온 이미지 포맷을 react-image-file-resizer를 사용하여 webp로 변환 및 렌더링 성공(개발환경에서)

변환 전변환 후

하지만 이 과정에서 순탄지 않았던 부분은 해당 이미지 url이 보안을 위해서인지 strict-origin-when-cross-origin CORS 정책을 사용하고 있어서 이를 우회하기 위한 http-proxy-middleware 라이브러리를 적용하여 image url을 fetch 하였다.

이미지 최적화 미해결 과제

GET http://localhost:3000/apiundefined 404 (Not Found)

해결
1) 프록시로 우회 요청을 하여 이미지를 가져와서 webp 변환에 성공은 했지만 계속 url 경로에 undefined가 들어와서 console 창에 404 에러를 발생시킴. 분명히 props가 내용이 있을 때(true)로 Narrowing을 해서 이미지 fetch 및 변환 함수를 실행하는데도 불구하고 url에 undefined 값이 들어오는 문제
원인: 이미지 url이 없는 장소의 경우 proxy의 target url이 없으므로 undefined가 들어오는 것이었음. target url이 있는 경우만 fetch되게 함으로써 404에러 문제는 해결
2) 해당 페이지 새로고침 시 내가 만든 웹페이지가 아닌 proxy target url로 이동하는 문제가 발생했고 프록시 경로 설정을 바꾸어서 해결

미해결
1) 현재 개발 환경(localhost)에서만 작동하므로 배포 버전에도 작동되게 해야함.
2) Fetch를 이중으로 하는 문제. 현재는 포맷만 다른 같은 이미지를 네트워크에서 두번(jpg 원본 하나, webp 변환본 하나) 받아오는데 이를 한번만 받아오도록 하는 것에 대해서는 해결방법을 고민중. 아마 프론트단이 아닌 서버단에서 해결해야할 문제로 추측.

const resizeFile = (file: File) =>
    new Promise((resolve) => {
      Resizer.imageFileResizer(
        file,
        440,
        600,
        'WEBP',
        100,
        0,
        (uri) => {
          resolve(uri);
        },
        'base64',
      );
    });

const resizeSpotImgFn = async (props: FetchedStayDataType) => {
  if (props.img.split('kr')[0] === 'http://tong.visitkorea.or.') {
  const imgResponse = await fetch(`/api${props?.img.split('kr')[1]}`, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Request-Method': 'GET',
        'Access-Control-Request-Headers': 'Content-Type',
      },
    });
    const data = await imgResponse.blob();
    const ext = imgResponse?.url.split('.').pop();
    const filename = imgResponse?.url
      .split('/')
      .pop()
      .split('.')[0];
    const metadata = { type: `image/${ext}` };
    const imgFile = new File([data], filename!, metadata);
    const resizedImg = await resizeFile(imgFile);
    setSpotImg(resizedImg as string);
  };
}

  useEffect(() => {
    spotRecommendationList();
    if (props) resizeSpotImgFn(props);
  }, []);

...


//setupProxy.js
const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = function(app) {
  app.use(
    createProxyMiddleware('/api', {
      target: 'https://tong.visitkorea.or.kr',
      changeOrigin: true,
      pathRewrite: {
        '^/api': '',
      },
    }),
  );
};

11. 반응형, 모바일 UI 구현


1) 미디어쿼리를 이용한 반응형 UI 구현
2) 반응형 스켈레톤 UI 적용

12. Lazy loading을 통해 code-splitting 및 코드 리팩토링을 통한 성능 개선

1) Router에서 페이지별 Lazy-loading 적용. 페이지별로 분리된 chunk가 해당 페이지 접근시 로딩되는 것을 개발자 도구의 network와 source를 통해 확인.
2) img 태그에 loading="lazy" 속성 적용
3) Network Payload를 메인페이지의 경우 초기 3~40,000 KiB -> 12,000 KiB -> 최종 7,800 KiB로 개선.
리스트페이지의 경우 초기 8~9,000KiB에서 약 3,700KiB 정도로 개선

Lighthouse 점수

개선 전개선 후
------------

1) 현재 데스크탑, 최종 배포 버전의 리스트페이지 기준
2) 환경에 따라, 페이지에 따라 점수에 변동이 있음

profile
FE Developer

0개의 댓글