Refresh

This website github-wiki-see.page/m/boostcampwm-2022/web33-Mildo/wiki/%EB%B0%9C%ED%91%9C-%EC%98%81%EC%83%81-%EC%B4%AC%EC%98%81 is currently offline. Cloudflare's Always Online™ shows a snapshot of this web page from the Internet Archive's Wayback Machine. To check for the live version, click Refresh.

발표 영상 촬영 - boostcampwm-2022/web33-Mildo GitHub Wiki

파트 분배

  • 윤우 - 인구 밀도 데이터 조회, suspense
  • 현정 - 인구 밀도 데이터 조회, 데이터 캐싱, suspense
  • 상준 - 반응형 웹
  • 한빈 - 인구 밀도 데이터 조회, 성능 개선

1페이지~6페이지 - 한빈

7페이지~12페이지 - 윤우

13페이지~16페이지 - 현정

17페이지~18페이지 - 상준

19페이지~21페이지 - 한빈

페이지별 시나리오

1페이지

안녕하세요 저희는 Web33조 우아한 아이들입니다. 저희는 서울시 실시간 인구 밀도 정보 제공 서비스 Mildo를 제작하였습니다.

2페이지

저희 발표는 프로젝트 소개, 기획 의도, 기술적 고민과 도전 순으로 진행됩니다.

3페이지

첫 번째로 간단한 프로젝트 소개입니다.

4페이지

Mildo는 반응형 웹으로 제작되어 언제 어디서든 서울시 주요 50곳의 인구 밀도 정보를 쉽게 확인할 수 있는 서비스입니다.

5페이지

다음으로 기획 의도에 대해 말씀드리겠습니다.

6페이지

기존에 이미 서울시에서 도시 데이터 서비스를 통해 실시간 인구 밀도 정보를 제공하고 있었습니다. 다만, 직접 사용해보니 모바일 환경에서 이용하기 어려웠고, 원하는 정보를 빠르게 찾기 힘들다는 아쉬움이 있었습니다. 그래서 저희는 직접 이러한 단점을 보완하고 이용자 편의 기능을 추가하여 실시간 인구 밀도 정보만 쉽게 확인할 수 있는 서비스를 제작하고자 하였습니다.

7페이지

이어서 기술적 고민과 도전 입니다.

8페이지

저희는 크게 인구 밀도 데이터 조회, 반응형 웹, 데이터 캐싱, 리액트 서스펜스 그리고 성능 개선이라는 5가지 고민을 하게 되었습니다.

9페이지

첫번째 인구 밀도 데이터 조회에 대한 고민입니다.

문제점은 서울 open api에서 전체 장소에 대한 데이터를 제공하는 api가 없어 지역의 개수만큼 api 요청를 해야 되기 때문에 오랜 시간이 소요되었습니다.

이렇게 오랜 시간이 걸리면 서비스의 이용성이 떨어지므로 어떻게 하면 효율적으로 데이터를 저장하고 불러올 수 있을까?라는 고민을 하게 되었습니다.

10페이지

해결방법으로는 인구 밀도 데이터를 주기적으로 저희 DB에 저장하는 cron 서버를 구축하는 것이였습니다.

cron 서버는 30분 간격으로 서울 open api에서 인구 밀도 데이터를 저희 DB에 저장하게 됩니다.

api 서버에서는 인구 밀도 데이터가 필요하면 서울 open api에서 데이터를 요청하지 않고 이젠 저희 DB에서 가져올 수 있게 되었습니다.

또한 api 서버에 문제가 생기더라도 cron 서버만 동작하고 있다면 지속적으로 인구 밀도 데이터를 저장할 수 있어 api 서버 복구 후에도 데이터를 가져와 바로 사용할 수 있습니다.

결과적으로 저희는 인구 밀도 데이터를 요청하면 3500밀리초에서 650밀리초로 성능을 개선시킬 수 있었습니다.

11페이지

인구 밀도 데이터 조회에 대한 고민은 아직 끝나지 않았습니다.

구현을 하면서 든 생각은 인구 밀도 데이터가 지속적으로 쌓이면 최근 50곳의 인구 밀도 데이터를 DB에서 탐색하여 불러오는 시간이 증가할 것이라고 예상하였습니다.

특히 시간순서대로 정렬하는 부분에서 시간이 오래걸릴 것이라고 생각했습니다.

최근 50곳의 인구 밀도 데이터를 서버 메모리에 저장해두고 필요할 때 메모리에서 바로 데이터를 가져올 수 있지 않을까?란 생각을 하게 되었습니다.

12페이지

현재 api 서버와 cron 서버가 분리되어 있기 때문에 cron 서버 메모리를 사용할 경우 api 서버에서는 cron 서버 메모리에 접근하기 힘든 상황이였습니다.

그래서 서버 메모리가 아닌 레디스에 최근 50곳의 인구 밀도 데이터를 저장하여 해결할 수 있었습니다.

이젠 api 서버에서는 DB에서 인구 밀도 데이터를 조회하는 것이 아닌 Redis에서 key값으로 빠르게 조회할 수 있게 되었습니다.

최종적으로 인구 밀도 데이터를 요청하면 650밀리초에서 430밀리초까지 성능을 개선시킬 수 있었습니다.

13페이지

다음으로 데이터 캐싱에 대한 고민입니다.

저희는 상세 모달창을 펼칠 때마다 api서버에 24시간 인구 밀도 데이터를 요청합니다. 그래서 오른쪽 이미지를 보시면, 모달창을 펼칠 때마다 로딩이 표시되는 것을 보실 수 있습니다.

이 요청에 대한 응답은 약 1.2초에서 1.8초 정도 걸리며, 사용자는 상세 모달창을 펼칠 때마다 이 시간을 기다려야 했습니다.

그래서 이용자가 처음 요청한 데이터와 같은 값을 다시 요청한다면 이전에 요청했던 데이터를 재사용해도 되지 않을까?하는 고민을 하게 되었습니다.

14페이지

이 고민은 리액트 쿼리를 사용하여 해결했습니다. 리액트 쿼리를 이용하여 한 번 요청한 데이터는 캐싱하고, 요청을 보내지 않도록 했습니다.

그래서 오른쪽 이미지를 보시면, 서울역 모달창을 처음 열었을 때 네트워크 탭에 요청이 가는 걸 보실 수 있지만 다시 열었을 땐 요청이 가지 않는 것을 보실 수 있습니다.

이렇게 리액트 쿼리를 이용하여 로딩을 기다리는 시간을 줄일 수 있었습니다.

15페이지

다음으로 고민한 내용은 react suspense에 관련된 내용입니다.

저희는 useEffect를 이용 하여 로딩 상태를 따로 관리했습니다.

이전 코드를 보시면 useEffect로 isLoading이라는 상태를 바꿔주고, isLoading에 따라 로딩 컴포넌트를 보여줄지, 다른 컴포넌트를 보여줄지 결정하고 있습니다.

이렇게 비동기 로직과 로딩 상태를 관리하는 로직이 얽혀있어 코드가 굉장히 복잡하게 구성되어 있었습니다.

그래서 로딩 상태를 따로 관리하기보다는 리액트 suspense 기능을 이용하여 로딩을 렌더링할 수 있지 않을까?하는 고민을 하게 되었습니다.

16페이지

그래서 먼저 suspense 동작에 대해 학습하고, 이미 사용하고 있는 react query의 useQuery와 jotai의 atomsWithQuery를 이용하여 로딩을 렌더링 했습니다.

첫 번째 사진을 보시면 useQuery를 이용하고 있는데, options에 suspense true를 추가하고 상위 컴포넌트에 를 추가하여 이 컴포넌트에서 비동기 동작을 수행하고 있을 때 로딩 컴포넌트가 렌더링되게 했습니다.

또한 useQuery와 atomsWithQuery를 이용함으로써 비동기 로직과 그 데이터를 상태로 저장하는 로직이 합쳐지면서 좀 더 가독성 있는 코드를 작성할 수 있었습니다.

두 번째 사진 중 비동기 데이터 쿼리를 사용하기 이전 코드를 보시면 useEffect를 이용하여 데이터를 불러오고 setter를 이용해 상태를 변경해주는 것을 보실 수 있습니다.

하지만 맨 아래에 비동기 데이터 쿼리를 사용한 후에는 비동기 요청과 상태 업데이트를 한 번에 하여 좀 더 깔끔한 코드를 작성할 수 있었습니다.

17페이지

  • 그 다음으로 반응형 웹에 대한 설명입니다. 반응형 웹은 모바일과 데스크탑 모두를 지원하고자 했던 팀의 목표상 꼭 필요했던 고민이었습니다.
  • 그 중에서 가장 대표적으로 고민했던 부분이 바로 ‘모달창의 너비에 따라 대응하는 애니메이션 추가’였습니다. 모바일 화면의 너비가 작은 경우에는 모달창 제목 부분의 길이가 너비보다 더 길 수 있습니다. 결과적으로 모달창의 제목이 잘리는 현상이 발생하였습니다.
  • 이를 극복하고자, 저희 팀에서는 상세 모달의 제목 길이에 따라 애니메이션을 넣고 싶었습니다.
  • 그래서 애니메이션을 넣어야 하는 화면의 기준을 어떻게 잡아야 할 지, 어떻게 하면 무한 슬라이딩 애니메이션을 구현할 수 있을 지 고민하였습니다.

18페이지

  • 저희 팀에서는 먼저 애니메이션을 적용되는 조건을 useRef.current의 clientWidth 속성값과 window.innerWidth의 차이로 알아낸 다음 Title 컴포넌트를 하나 더 추가하였습니다.
  • 애니메이션 구현을 할 때 가장 중요한 점은 바로 ‘시작점과 끝점’이 맞아떨어져야 한다는 점이었습니다. 그 이유는 바로 ‘무한하게 슬라이딩되는 애니메이션’을 구현하고자 했기 때문입니다. 이를 위해 gap 속성을 50%로 주고, keyframes의 0%와 100%에 transform: translate() 속성을 주고, 이를 제목의 width를 기준으로 설정하였습니다.
  • 그 결과, 최종적으로 매끄러운 슬라이딩 애니메이션이 만들어졌습니다.

19페이지

다음으로 성능 개선 부분에 대한 고민입니다.

배포 후 Chrome의 lighthouse를 이용하여 성능을 측정한 결과 모바일 환경에서의 performance 점수가 58점으로 매우 낮게 나왔습니다.

가장 큰 문제는 번들 파일의 크기가 835KiB로 지나치게 컸고, 이로 인해 LCP 항목의 시간이 15.4초로 매우 오래 걸렸기 때문입니다.

20페이지

그래서 저희는 번들 파일의 크기와 LCP 소요 시간을 줄이기 위한 방법을 찾아 적용하였습니다.

첫 번째로 코드 스플리팅입니다. Mildo에서 그래프를 구현하며 외부 라이브러리를 사용하였는데 해당 라이브러리의 용량이 꽤 컸습니다. 그래서 React.lazy를 활용하여 해당 라이브러리를 사용한 컴포넌트에 대해 코드 스플리팅을 진행하였습니다.

두 번째로 번들러 변경입니다. 저희 팀은 cra로 프로젝트를 생성하였고, 기본적으로 제공되는 webpack으로 빌드 파일을 생성하였습니다. 그래서 vite로 번들러를 변경하였고, 이렇게 생성된 빌드 파일의 크기는 이전 대비 4분의 1 수준이었습니다.

세 번째로 nginx에서 배포 시 gzip으로 빌드 파일을 압축하여 브라우저에 응답하였습니다.

이런 과정들을 거쳐 최종적으로 빌드 파일의 크기를 초기 대비 8분의 1 수준인 104KiB로 줄였습니다.

마지막으로 번들 파일의 크기와 별개로 네이버 지도 이미지 로딩으로 인해 LCP 시간이 지연되었고, 이에 지도의 초기 줌을 확대하여 초기 렌더링 시 생성되는 지도 이미지 개수를 줄였습니다.

결과적으로 모바일 환경에서 performance 점수가 18점이 증가하여 76점이 되었고, LCP 소요 시간이 8.8초나 감소하여 6.6초가 되었습니다.

21페이지

지금까지 우아한 아이들이었습니다. www.mildo.live에 접속하시면 mildo를 직접 이용하실 수 있습니다. 발표를 들어주셔서 정말 감사합니다.

⚠️ **GitHub.com Fallback** ⚠️