Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[성능 베이스캠프 미션] 호프(김문희) 미션 제출합니다. #33

Merged
merged 27 commits into from
Sep 7, 2022

Conversation

moonheekim0118
Copy link

@moonheekim0118 moonheekim0118 commented Sep 3, 2022

🔥 결과

스크린샷 2022-09-01 오후 3 14 19

  • 개선 후 (CloudFront)

스크린샷 2022-09-03 오후 8 43 32

스크린샷 2022-09-03 오후 8 33 00

✅ 개선 작업 목록

1 요청 크기 줄이기

  • 소스코드 크기 줄이기

웹팩에 내장되어 있는 terser plugin 을 사용하여 압축 ( minmizer 앞부분에 '...' 추가)

스크린샷 2022-09-04 오후 1 39 55

  • 이미지 크기 줄이기

ImageMinimizerPlugin 사용하여 gif 와 webp 크기 압축 및 리사이징
webp 가 지원되지 않는 브라우저의 경우 png 파일 서빙하도록 설정

before

스크린샷 2022-09-03 오후 6 02 59

after

스크린샷 2022-09-03 오후 7 53 46

2 필요한 것만 요청하기

  • 페이지별 리소스 분리

스크린샷 2022-09-03 오후 8 37 50

  • React.lazy 를 사용하여 코드스플릿팅 하여 페이지별 (Home, Search) 번들 파일을 나눔
  • MiniCssExtractPlugin를 사용하여 CSS 파일 분리
  • CSS 파일을 분리함으로써, 해당 페이지에서 필요한 CSS 파일을 비동기적으로 로드 할 수 있도록 함
  • CssMinimizerPlugin을 사용하여 CSS 파일 압축
  • 아이콘 패키지 Tree Shaking

3 같은 건 매번 새로 요청하지 않기

  • CloudFront 캐시 설정 (설정값, 해당 값을 설정한 이유 포함)

스크린샷 2022-09-03 오후 8 39 38

  • 현재 정책 설정을 할 수 가 없어서 일단 S3 cache-control 메타데이터를 추가해주어서 리소스별로 캐싱을 설정해주었습니다.
  • 모든 리소스의 캐시 max-age 를 1년으로 주었는데, 그 이유는 정적 파일이라 어차피 변할 일이 없고, 업데이트 되어야 할 경우 s3 에 새로 업로드가 된 후 클라우드프론트 무효화를 진행해주기 때문이었습니다 !
  • 또한 캐시 기간이 길 수록 캐시 적중률이 높아진다고 하네요.. 🤔 그런데 이 부분은 더 자세히 알아보아야 할 것 같습니다.
  • GIPHY의 trending API를 Search 페이지에 들어올 때마다 새로 요청하지 않아야 한다.
    ddd

자바스크립트 코드를 작성하여 trendings api 요청 응답값이 메모리에 저장된 경우 재요청 하지 않도록 구현

4 최소한의 변경만 일으키기

  • 검색 결과 > 추가 로드시 추가된 목록만 새로 렌더되어야 한다.

React.memo 사용하여 리렌더링 필요 없는 컴포넌트의 리렌더링 방지

  • Layout Shift 없이 애니메이션이 일어나야 한다.

position을 변경하는 CSS 를 transform:tranlsate~ 을 사용하여 layout shift 를 방지했습니다.
position 을 변경하는 경우 리플로우부터 다시 일어나지만, transform 속성을 사용하는 경우 리플로우,리페인트가 발생되지 않고 바로 Composite 가 일어나기 때문입니다.

  • Frame Drop이 일어나지 않아야 한다.
    • (Chrome DevTools 기준) Partially Presented Frame 역시 최소로 발생해야 한다.

before

스크린샷 2022-09-03 오후 4 39 33

after

스크린샷 2022-09-03 오후 4 41 53

공유

  • hero 이미지의 경우 처음에는 웹팩 설정을 통해서 png 를 webp 로 변환해줘서 빌드하게 구현했는데요, 그렇게 되면 webp 를 지원하지 않는 브라우저에 이미지를 보여줄 방법이 없더라구요. 그래서 webp 로 변환된 파일 하나와, png 파일 하나를 각각 가지고, picture 태그를 통해서 이미지를 제공해주었습니다. 그런데 하다보니 든 의문이, 이런식으로 한다면 제공해줘야하는 모든 이미지에 대해서 webp 파일과 png/jpg/jpge 파일을 가지고 있어야 하는가? 입니다. 🙄
  • gif 는 mp4 로 굳이 변환하지 않았는데요, 그 이유는...저희 페이지의 유저가 홈페이지에 보여지는 gif 파일을 저장할 수 있는데 그러면 mp4확장자로 저장되어서 좋지 않은 유저경험을 발생시킨다고 생각했기 때문입니다.

새롭게 배운 내용

1. picture tag

  • 현재 저희 미션에서는 단순히 webp 를 지원하지 않으면 png 를 지원해줘! 이런 식으로 picture 태그를 사용했는데 mdn 문서를 참고한 결과 아래와 같이 미디어 쿼리에 따라서 다른 이미지를 사용자에게 서빙 해줄 수 있다구 합니다.
<picture>
  <source media="(min-width:650px)" srcset="img_pink_flowers.jpg">
  <source media="(min-width:465px)" srcset="img_white_flower.jpg">
  <img src="img_orange_flowers.jpg" alt="Flowers" style="width:auto;">
</picture>

2. S3 & cloundfront

개인적으로..왜 정적파일을 s3 에 올려두고 s3에 직접 접근하면 안되는걸까..? 에 대한 의문이 있었습니다.

  • Cloundfront 는 Ege location 이라고 하는 데이터 센터의 전 세계 네트워크를 통해 콘텐츠 서비스를 제공한다.
  • Egde 서버를 사용하여 콘텐츠를 캐싱하고 서비스를 제공하면 최종 사용자가 위치한 곳에서 더욱 가깝게 콘텐츠를 전송할 수 있어 성능이 향상된다.
  • 따라서, 사용자가 Cloundfront를 통해서 콘텐츠를 요청하면 사용자와 가까운 Edge location 으로 라우팅 된다. 이 때 Cloudfront 에 요청된 파일에 대한 캐시 사본이 있다면 cloudfront 에 의해 해당 사본을 사용자에게 빠르게 전송한다. 만약 캐싱되어있지 않다면 해당 파일이 저장된 s3 에서 원본 파일을 가져온다.
  • 즉, Cloundfront 는 콘텐츠를 Edge location에 캐싱하기 때문에 S3 버킷에 대한 부하를 줄일 수 있고, 콘텐츠를 요청하는 사용자에게도 빠르게 응답 할 수 있다.

더 공부해야 할 부분

- css 코드 압축을 위해서 css-minimizer-webpack-plugin 설치 및 웹팩 적용
- css 코드 온디맨드 로딩을 위해서 mini-css-extract-plugin 설치 및 웹팩 적용 (style-loader 대신 적용)
- terser-webpack-plugin 설치 및 적용
- 기존의 uglifyJs 가 webpack v5.0.0 을 지원해주지 않아서 terser 설치
- parallel 옵션을 사용하여 빌드 속도 개선
- 코드 스플릿팅을 적용하여, Home Page 에서 불러오는 스크립트 리소스에 gif 검색을 위한 giphy 모듈이 포함되어 있지 않도록 구현
- Suspense 를 사용하여 해당 페이지 레이지로딩 하는 동안 로딩중이라는 안내메시지 보여주도록 구현
- 이미지 확장자 webp 로 변경하도록 구현
- 이미지 압축 플러그인 설정
- 기존의 top, left를 바꾸어주던 것을 transform 속성으로 대체
- hover 시 top 을 조정하던 것을 transform 으로 대체
- right 속성을 변경하던 것을 transform-translateX 로 대체
- GifItem 컴포넌트 React.memo 로 메모이제이션하여 리렌더링 방지
- HelpPanel 컴포넌트 React.memo로 메모이제이션하여 리렌더링 방지
- trendingAPI 가 SearchPage 에 들어올 때 마다 새로 요청되지 않도록 구현
- 캐시 클래스 구현
- 기존 데이터 덮어쓰도록 수정
- APICache 클래스 제네릭 타입 수정
- 웹팩 설정 수정 (webp로 바꿔주는 rule 제거)
- webp, png 이미지 각각 추가
- 검색결과 캐싱 삭제
- devtools sourcemap 생성 false 로 수정
- 이미지 압축 png 파일도 압축하도록 플러그인 설치 및 설정 추가 (pngquant)
- 웹팩에 내장되어있기 때문에, 제거
Copy link

@kamwoo kamwoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

호프! 안녕하세요 🙂
미션 진행하신 거 잘봤습니다. 전반적으로 최적화를 잘 하셨다고 생각했어요! 많이 배웠습니다. 👍
보다가 궁금한 것들 코멘트로 남겨봤습니다!

(빨리 제출하셨는데 늦게 코멘트 달아서 죄송..합니다. 😓)


리소스들의 max-age를 1년으로 주셨다고 했는데, max-age=604800이어서 의도하신 값이랑 다른 것 같습니다! 단위가 seconds로 알고있어서 1년이라면 max-age=31536000이 맞는거 같슴다.
추가로 다들 1년으로 하길래, 1년으로 설정하는 이유가 있는지 궁금해요.


gif 는 mp4 로 굳이 변환하지 않았는데요, 그 이유는...저희 페이지의 유저가 홈페이지에 보여지는 gif 파일을 저장할 수 있는데 그러면 mp4확장자로 저장되어서 좋지 않은 유저경험을 발생시킨다고 생각했기 때문입니다.

너무 좋은 접근이라고 생각합니다. 👍👍 근데 저는 webm으로 변환했는데 다운로드 해보니깐 gif로 되더라고요. 어찌된 영문인지 몰라서 좀 더 찾아볼 예정입니다.


hero 이미지의 경우 처음에는 웹팩 설정을 통해서 png 를 webp 로 변환해줘서 빌드하게 구현했는데요, 그렇게 되면 webp 를 지원하지 않는 브라우저에 이미지를 보여줄 방법이 없더라구요. 그래서 webp 로 변환된 파일 하나와, png 파일 하나를 각각 가지고, picture 태그를 통해서 이미지를 제공해주었습니다. 그런데 하다보니 든 의문이, 이런식으로 한다면 제공해줘야하는 모든 이미지에 대해서 webp 파일과 png/jpg/jpge 파일을 가지고 있어야 하는가?

개인적인 생각으로 모든 이미지를 webp로 변환할 필요는 없다고 생각해요. 이미지 압축과 리사이징으로도 파일 크기가 상당히 줄어드는 것을 확인했고, 이미지를 많이 사용하는 서비스라면 호프가 gif 를 mp4 로 굳이 변환하지 않은 이유도 고려해야 할 사항이라고 생각합니다. 핀터레스트 메인 페이지 같은 경우에는 jpg를 사용하면서 이미지 한 개당 30~100KB를 유지하네요.. 🤔

Comment on lines 61 to 63
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.imageminGenerate,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimizer의 implementation로 ImageMinimizerPlugin.imageminGenerate를 사용할 수 있네요?
minimizer와 generator에서 할 때 어떤 차이가 있어요??

Copy link
Author

@moonheekim0118 moonheekim0118 Sep 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

와 밧드! 먼저, 짚어주셔서 감사해요.
전 그냥 웹팩에 있는대로 따라 했는데..
imageminGenerate 는 png 도 webp 확장자로 바꾸고 압축하는 역할을 하고있었네요.

그래서
implementation: ImageMinimizerPlugin.imageminMinify 로 바꿔서 압축만 해주게 해주어 원래의 의도대로 동작하도록 했어요.

만약에 generator 를 사용하고 싶다면...
밧드가 물어보신 것에 대해 답변을 하자면
minimizer 와 generator의 역할을 제가 제대로 인지 못하고 있었네요.
minimizer 에서는 압축관련 implementation 만 해주는게 올바른 것 같습니다.
위와 같이 imageminGenerate 를 해도 어찌저찌 webp 로 generate 는 하고 압축까지 해주지만, 예상치 못한 사이드이펙트가 일어날 수 있을 것 같아요.
그리고 generator 관련 implementation은 generator 에서 해주는게 맞는 것 같구요!
스크린샷 2022-09-06 오후 11 39 42

감사합니다!

58ab9f3

minimize: false
minimize: true,
minimizer: [
'...',
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오.. 👍

@@ -14,8 +14,7 @@ const CustomCursor = ({ text = '' }: CustomCursorProps) => {

useEffect(() => {
if (cursorRef.current) {
cursorRef.current.style.top = `${mousePosition.pageY}px`;
cursorRef.current.style.left = `${mousePosition.pageX}px`;
cursorRef.current.style.transform = `translate3d(${mousePosition.pageX}px, ${mousePosition.pageY}px, 0)`;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 translate3d를 사용하신 이유가 있는지 궁금합니다!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 알아본 바로는 translate3d 속성이 다른 tranlsate 속성들보다 성능에 유리한데요
그 이유가 tranlsate3d 는 GPU 를 사용하기 때문이라구 하네요.
GPU를 사용하는 속성을 사용하면 브라우저에서 렌더링 할 때 별도의 렌더 단계가 생기구, 그 후 합성 단계에서 이 렌더 단계를 합성해서 출력하는데 이 때 GPU를 사용한다고 해요!
그래서 별도의 리플로우/리페인트 과정을 거치지 않아서 훨씬 애니메이션이 부드럽기도 하고, 성능상 유리하다고 합니다!

Comment on lines +1 to +11
const cacheStorage = new Map<string, unknown>();

const cache = async <T>(key: string, apiRequestCallback: () => Promise<T>): Promise<T> => {
if (cacheStorage.has(key)) return cacheStorage.get(key) as T;

const response = await apiRequestCallback();
cacheStorage.set(key, response);
return response;
};

export default cache;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

깔~끔하네요👍

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

감사합니다 🙃

src/App.tsx Outdated
Comment on lines 14 to 23
<Suspense fallback={<h1>로딩 중 입니다.</h1>}>
<Router>
<NavBar />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/search" element={<Search />} />
</Routes>
<Footer />
</Router>
</Suspense>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사람 By 사람이라고 생각하는데 NavBar, Footer는 라우팅에 상관없이 계속 보여줘도 상관없지 않을까 생각해요🙂

Copy link
Author

@moonheekim0118 moonheekim0118 Sep 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 동의합니다 🙃 짚어주셔서 감사해요! 수정했습니다!

760d8f9

Comment on lines +18 to +22
<script>
if (typeof window.__REACT_DEVTOOLS_GLOBAL_HOOK__ === 'object') {
__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = function () { };
}
</script>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오.. 찾아보니깐 리액트 devtool 비활성화하는 방법이네여 👍👍 배워갑니다~

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그런데 무조건 prod mode 일 때, 비활성화는 아니고, React Dev Tools 가 설치되어 있지 않은 경우에만 비활성화하는 방법이긴 합니다. 😢 그래도 Index.html 에서 환경변수에 접근 할 수 없으니 이게 최선이라구 생각은 했어요.

options: {
plugins: [
['gifsicle', { interlaced: true, optimizationLevel: 3, colors: 64 }],
['webp', { quality: 50, resize: { width: 1280, height: 0 } }],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

webp 이미지는 파일 사이즈가 작을 거 같은데 여기서 quality를 50으로 설정할 이유가 있을지 궁금합니다.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

webp라서 파일 사이즈 자체가 작긴 하지만 그래도 최대한 많이 줄여보고자 50으로 줬었습니다!
그런데 저희 요구사항인 130kb 이하를 어떻게든 간당 간당히 충족시키는 선은 80정도 인 것 같아요 허허
그래서 80으로 퀄리티를 높여보고 용량을 조금 포기해보았습니다!
스크린샷 2022-09-07 오전 12 03 24
짚어주셔서 감사해요

31bee9c

return await cache<GifImageModel[]>('trendings', async () => {
const gifs = await fetch(TRENDING_GIF_API).then((res) => res.json());
return convertResponseToModel(gifs.data);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -57,4 +57,4 @@ const HelpPanel = () => {
);
};

export default HelpPanel;
export default memo(HelpPanel);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HelpPanel에 memo를 사용하면 어떤 영향을 주나요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다시 보니, HelpPanel 에는 props 가 없어서 불필요하게 리렌더링 될 일이 없었네요! 허허
오히려 memo 연산이 한번 더 들어가서 성능상 더 좋지 못하게 될 것 같구요 ㅎㅎ
짚어주셔서 감사해요!

c616428

Copy link

@kamwoo kamwoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 호프! 리뷰 남겼습니다! 확인 부탁드려요~

- implementation에서 imageminGenerate -> imageminMinify 로 변경하여, 이미지 변환이 아니라 압축 작업을 해주도록 수정
- Supsense Loader 가 NavBar 와 Footer 와 함께 렌더링 되도록 수정
- HelpPanel 에는 props 가 존재하지 않으므로 memo 성능상 이점이 없어서, 제거
- 50에서 80으로 수정
@moonheekim0118
Copy link
Author

moonheekim0118 commented Sep 6, 2022

@kamwoo 밧드~ 좋은 리뷰 감사합니다 🙂 밧드 덕분에 놓쳤던 부분도 많이 알았고, 몰랐던 것들도 많이 학습했어요.

리소스들의 max-age를 1년으로 주셨다고 했는데, max-age=604800이어서 의도하신 값이랑 다르다 것 같습니다! 단위가 로 알고있어서 1년이라면 max-age=31536000이 맞는거 같슴다.
추가로 다들 1년으로 하길래, 1년으로 설정하는 이유가 있는지 궁금해요.

허헛. 제가 실수했네요. 감사합니다. 이 부분 수정해서 적용했슴다 머쓱 🙄
그리고 1년으로 하는 이유는 .. 솔직히 저도 다들 1년으로 하길래 1년으로 했는데요.
아래의 여러 참고 문서들을 보니 HTTP/1.1 에서 권장하는 최대 max-age 가 1년이라고 하네요.
그래서 1년으로 설정해주는게 올바른 것 같아요!

개인적인 생각으로 모든 이미지를 webp로 변환할 필요는 없다고 생각해요. 이미지 압축과 리사이징으로도 파일 크기가 상당히 줄어드는 것을 확인했고, 이미지를 많이 사용하는 서비스라면 호프가 gif 를 mp4 로 굳이 변환하지 않은 이유도 고려해야 할 사항이라고 생각합니다. 핀터레스트 메인 페이지 같은 경우에는 jpg를 사용하면서 이미지 한 개당 30~100KB를 유지하네요.. 🤔

와우 좋은 이야기 감사합니다! 저의 고민/구현에 많이 공감해주셔서 감사해요 ㅎㅎ
그리고 webm 은 처음들어보는데.. gif 로 저장되는거 정말 신기하네요..! 저도 조금 더 알아보겠습니다!

혹시 더 수정할 것 있으면 언제든 편안하게 알려주세요 🙃 오늘 밧드한테 이것저것 많이 배워가네요! 감사합니다 하하

@kamwoo
Copy link

kamwoo commented Sep 7, 2022

webm은 gif로 저장되는게 아니라 적용이 안되고 있었습니다..ㅋㅋ 😅
당연하게 적용한 확장자 그대로 다운로드 되네요. 👍

Copy link

@kamwoo kamwoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요! 호프 🙂
답변 보면서 공부 많이 했습니다! 감사합니다 👍
이만 approve 하겠습니다!

@kamwoo kamwoo merged commit 0dc2f31 into woowacourse:moonheekim0118 Sep 7, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants