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

[개인 미션 - 성능 오답노트] 해리(최현웅) 미션 제출합니다. #151

Merged
merged 19 commits into from
Sep 13, 2024

Conversation

hwinkr
Copy link

@hwinkr hwinkr commented Sep 9, 2024

🔥 결과

CloutFront 접근 경로

개선 전 성능 측정

image

개선 후 성능 측정

image

프랑스 파리 Fast 3G 환경에서 방문한 결과

image

✅ 개선 작업 목록

1. 요청 크기 줄이기

  • 소스코드 크기 줄이기

Home 페이지에서 불러오는 스크립트 리소스 크기 < 60kb ✅

image

스크립트 리소스 크기를 모두 더해보면 59.8kb인 것을 확인할 수 있습니다. 정말 아슬아슬하게 통과했네요...!

소스코드 크기를 줄이기 위한 여정

  1. 자바스크립트 리소스

자바스크립트의 리소스 크기를 개선하기 위한 계획들을 세웠으나, 웹팩 4 버젼 이상부터는 프로덕션 빌드를 할 때, 자동으로 코드를 최소화하기 위한 플러그인을 포함한다고 합니다. 그래서 따로 자바스크립트 리소스 크기 개선을 위한 작업을 진행하지는 않았습니다. (참고)

Webpack v4+의 [production mode](https://webpack.kr/configuration/mode/#mode-production)에서는 기본으로 코드를 최소화합니다.

[TerserPlugin](https://webpack.kr/plugins/terser-webpack-plugin/)은 최소화를 시작하고 기본으로 사용하기에 좋지만 다른 옵션도 있습니다.

만약 다른 최소화 플러그인을 사용하기로 결정했다면, 다른 플러그인이 [Tree shaking](https://webpack.kr/guides/tree-shaking) 가이드에 설명 된 대로 사용하지 않는 코드를 제거하고 [optimization.minimizer](https://webpack.kr/configuration/optimization/#optimizationminimizer)를 제공하는지 확인해야 합니다.

  1. CSS 리소스

css-in-js 라이브러리를 쓰거나, style-loader를 사용하게 되면 번들링 될 때, 자바스크립트 파일에 css 코드들이 포함됩니다. 그래서 자바스크립트 리소스 크기가 커집니다. 저는 여기서 의문을 좀 가졌었어요.

번들 사이즈가 커지는 것은 어쩔 수 없는 것 아닌가? 결국, 사용자가 웹 페이지에 방문하기 위해서 다운로드 받아야 하는 리소스 크기가 같을텐데 하나의 js 파일로 만드는 방법이든, js와 css 리소스를 분리하든 다른게 없지 않을까?

라는 의문을 가졌였습니다. 하지만 웹팩 공식문서에서도 확인할 수 있듯 프로덕션 빌드를 할 때는 js파일에서 css파일을 추출할 것을 권장하고 있더군요.

For production builds it's recommended to extract the CSS from your bundle being able to use parallel loading of CSS/JS resources later on. This can be achieved by using the mini-css-extract-plugin, because it creates separate css files. For development mode (including webpack-dev-server) you can use style-loader, because it injects CSS into the DOM using multiple <style></style> and works faster.

프로덕션 빌드의 경우 나중에 CSS/JS 리소스의 병렬 로딩을 사용할 수 있도록 번들에서 CSS를 추출하는 것이 좋습니다. 이는 별도의 CSS 파일을 생성하기 때문에 mini-css-extract-plugin을 사용하면 됩니다. 개발 모드(웹팩-개발 서버 포함)의 경우 여러 <스타일></스타일>을 사용하여 CSS를 DOM에 삽입하고 더 빠르게 작동하므로 style-loader를 사용할 수 있습니다.

위 글을 읽고나서 추가로, 브라우저의 렌더링 엔진은 싱글 스레드로 동작하는데 어떻게 병렬 로딩이 가능한 것인가? 라는 추가적인 의문을 가졌었어요. 해당 글을 읽은 후, 브라우저는 여러 개의 네트워크 연결을 동시에 열 수 있고 이를 통해서 여러 리소스를 병렬로 다운로드 할 수 있다는 내용을 알게되었습니다.
분리의 이점에 대해서 이해를 한 후,

두 플러그인을 활용해서 css 리소스의 크기 자체를 줄이고, 번들링된 js 파일에서 css를 분리하는 작업을 진행했습니다.

soosoo-pr soosoo-pr2

webpack-bundle-anaylzer 도구를 활용해서, 확인해 본 결과 두 플러그인을 사용한 후 번들링 된 js 파일에서 css가 사라진 것을 확인할 수 있었어요.

  • 이미지 크기 줄이기
  1. hero 이미지 크기 120kb 이하
image `92.4kb`로 개선했습니다.

이미지 크기를 개선하기 위해서 �웹팩에서 제공해 주는 이미지 크기를 줄여주는 플러그인을 활용했습니다.

hero.png, 와 3가지 webp모두 webp 확장자를 사용하도록 했습니다. gif의 경우 webm으로 변경할 지에 대한 고민을 했었는데, webp 또한 영상(gif)을 지원하기 때문에, 플러그인 설정을 통해서 모두 webp로 변경하는 방법을 적용했습니다.

2. 필요한 것만 요청하기

  • 페이지별 리소스 분리

Home 페이지에서 불러오는 스크립트 리소스에 Search 페이지의 소스 코드가 포함되지 않아야 한다. ✅

페이지별 리소스를 분리하기 위한 여정

  1. webpack splitChunks

웹팩 설정의 optimizationsplitChuncks를 활용해서 중복되는 자바스크립트 코드 덩어리들을 하나의 chuncks로 분리할 수 있도록 했어요. (관련 커밋)

  1. dynamic import, React Suspense

사용자가 현재 방문한 페이지를 그리기 위한 리소스만 다운 받을 수 있도록 하기 위해서 dynamic import, React Suspense를 활용했습니다. (관련 커밋)

  • 아이콘 패키지 Tree Shaking

react-icons 패키지에서 실제로 사용하는 아이콘 리소스만 빌드 결과에 포함되어야 한다. ✅

실제로 사용하는 리액트 아이콘만 사용하기 위해서 번들링할 때, 사용하지 않는 아이콘들을 포함하지 않는 트리 쉐이킹을 진행해야 했습니다. 해당 내용을 학습하면서 프로덕션 빌드를 할 때는 트리 쉐이킹 과정을 포함해준 다는 것을 알게되어 따로 라이브러리를 설치하거나 아이콘 패키지리를 트리 쉐이킹하기 위한 추가 작업을 진행하지 않았습니다.

soosoo-pr3

확인해 보면 사용하지 않는 아이콘은 포함되지 않아 번들 사이즈가 줄어든 것을 확인할 수 있어요.

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

  • CloudFront 캐시 설정 / S3 메타데이터 설정 (설정값, 해당 값을 설정한 이유 포함)
image

aws에서 제공하는 S3에 최적화된 캐싱 정책을 사용했고, 모모 FE 팀들과 논의한 결과로 결정된 max-age=1년을 적용하기로 했습니다. 설정할 수 있는 응답 헤더 정책의 갯수를 초과해서 저희와 같은 기간으로 설정한 투룻의 헤더 정책을 빌려서 사용했어요. 1년으로 적용한 이유는 기간과 비례해서 캐시 적중률이 높아질 수 있고, 웹팩 설정 파일에서 contenthash를 사용해 파일 내용이 변경되면 파일 이름 hash 값도 변경되도록 하여 사용자가 항상 최신 버젼을 바라볼 수 있기 때문에 기간을 가장 길게 하는 것이 좋을 것 같다고 판단했습니다.

  • GIPHY의 trending API를 Search 페이지에 들어올 때마다 새로 요청하지 않아야 한다.

5분 정도면 트렌딩 결과가 변경될 수도 있을 것 같아, 5분 동안 trending API 결과를 캐싱하기 위한 캐시 모듈을 구현했습니다. (관련 커밋)

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

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

리액트에서 제공하는 memo 함수를 사용해서 이전 props와 동일하면 memoization한 렌더링 결과를 재사용하도록 했어요. (관련 커밋)

soosoo-pr4

프로파일러로 확인해 보면, 리렌더되지 않는 것을 확인할 수 있습니다.

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

CustomCursor, 검색 결과 > hover, 도움말 패널 열고닫기 애니메이션에서 모두 layouf shift 현상이 발견되었습니다. 해당 현상을 해결하기 위해서 reflow를 발생시키지 않는 css 속성을 사용했어요. (관련 커밋)

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

Frame Drop 현상은 발생하지 않았지만, Partially Presented Frame 현상을 최소로 발생시키기 위해서 스크롤 이벤트에 requestAnimationFrame API를 적용했습니다. (관련 커밋)

적용 후, 다시 측정해 보니 크게 개선되지는 않았던 것 같아요.

soosoo-pr6 soosoo-pr7

Partially Presented Frame 현상이 발생한 폭(?)은 줄어든 것 같네요.

🧐 공유

쑤쑤의 웹 성능 최적화를 위한 더 깊은 학습을 위해서 제가 많이 참고했던 좋은 자료들을 공유드릴게요~~

- css 크기를 줄이고, 번들링 된 자바스크립트 파일에서 분리하기 위한 웹팩 도구 추가
- 이미지 크기를 최적화 하기 위한 웹팩 도구 추가
- 번들링되는 파일의 크기를 비교하기 위한 웹팩 도구 추가
- css sideEffect를 예방하기 위한 설정 추가
- ESM 구문을 사용하기 위해 modules:false 설정
- reflow를 발생시키는 css 속성을 제거하고 translate 속성을 사용
- 파일 내용이 변경될 때마다 파일 이름이 변경되도록 contenthash 사용
- 번들 사이즈 분석을 위한 플러그인 추가
- css를 js파일에서 분리하기 위한 플러그인 추가
- css 파일 크기 최적화를 위한 플러그인 추가
- 이미지, gif를 webp로 변경하기 위한 플러그인 추가
- 중복되는 코드 덩어리를 하나의 파일로 추출하기 위한 splitChunk 프로퍼티 추가
@hwinkr hwinkr requested a review from soosoo22 September 9, 2024 08:30
@hwinkr hwinkr self-assigned this Sep 9, 2024
Copy link

@soosoo22 soosoo22 left a comment

Choose a reason for hiding this comment

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

안녕하세요~ 해리!

“쑤쑤나는해리야” 아주 잘~ 봤습니다~ 저희 나이대에 인재가 참 많네요^,^~

대체적으로 요구사항을 모두 만족하신 것 같아서 몇 가지 코멘트 달았습니다! 그리고 폰트 preload도 로딩 속도 측면에서 은근히 차지하는 비율이 높아서 관련 문서 참고하시면 좋을 것 같아요!

추가로 해리 PR을 읽고 느꼈던 건데 궁금증을 가지고 접근하는 방식이 되게 좋았어요. PR 읽으면서 저도 많이 배운 것 같아요.. 그리고!!!! 공식 문서를 많이 참고하시는 것 같아서 공식 문서가 오히려 딱딱하고 이해가 잘 되질 않는 제 입장에서는 부러웠답니다🥹 공식 문서 읽는 팁이 있다면 알려주시면 감사하겠습니다~

package.json Outdated
"license": "MIT",
"dependencies": {
"@giphy/js-fetch-api": "^4.1.1",
"@giphy/js-types": "^4.2.1",
"classnames": "^2.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.4.0",
"react-icons": "5.3.0",
Copy link

@soosoo22 soosoo22 Sep 10, 2024

Choose a reason for hiding this comment

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

usedExports 옵션을 각각 true/false 로 주고 빌드해보면 사이즈 차이가 꽤나 커요. react-icons에서도 tree shaking이 적용이 되고 있다는 얘기겠죠.
스크린샷 2024-09-10 오후 11 39 20

적용은 되고 있지만 완벽하게 되는지? 확실히 모르겠지만 사용되는 아이콘 뿐만 아니라 모든 아이콘을 불러오는 것 같아요. 관련 이슈 참고 바랄게요.

React Icons Imports everything even when included 2 or 3 icons #154
The chunk is very big. #289

기존 react-icons 대신 react-icons/all-files를 이용하니 Gzipped size 기준으로 0.45 KB가 줄었네요.
스크린샷 2024-09-10 오후 11 40 27

Copy link
Author

@hwinkr hwinkr Sep 12, 2024

Choose a reason for hiding this comment

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

쑤쑤 정성 가득한 피드백 정말 감사합니다~ 미션을 진행하면서도
@react-icons/all-files 를 설치할 필요가 있을까? 에 대해서 정말 많이 고민을 했었습니다. 번들 사이즈 관점에서 큰 차이가 없는데 굳이 사용할 필요가 없을 것 같다고 판단해서 사용하지 않았었는데요. 쑤쑤의 피드백을 통해서 다시 한 번 도입 필요성에 대해서 생각해 보게 되었어요. 그 후, 생각을 바꾸고 @react-icons/all-files 라이브러리를 설치하기로 했습니다.

그 이유는 아래와 같아요.

1. 불안정성

쑤쑤가 첨부해 준 링크에 들어가서 이슈와 코멘트 내용들을 확인해 보니, 빌드 시 트리 쉐이킹 관련해서 문제가 100% 해결되지 않은 것을 알 수 있었어요.

2. 프로젝트 규모가 커져 아이콘을 많이 사용할 경우

프로젝트 규모가 커지고 라이브러리에서 사용하는 아이콘도 점점 많아질수록 1KB 이내의 차이라도 사용자에게 조금이라도 적은 번들 사이즈를 다운받도록 하는 것이 사용자 경험 측면에서 더 좋을 것 같았어요.

그래서 설치하기로 변경했답니다^^

  • 설치 전: 1.72KB
  • 설치 후: 1.28KB

0.44KB정도 줄어들었네요.

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
<Router>
<Suspense fallback={<div>loading...please wait...</div>}>
<NavBar />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/search" element={<Search />} />
</Routes>
<Footer />
</Suspense>
</Router>

Choose a reason for hiding this comment

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

NavBar와 Footer도 Suspense로 감싼 이유가 궁금합니다! NavBar와 Footer는 Home이나 Search보다 빠르게 로드되니까 Suspense로 감싸게 되면 오히려 렌더링이 늦어지는 단점이 존재할 것 같은데 해리는 어떻게 생각하시나요🤔

Choose a reason for hiding this comment

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

혹시 Layout Shift 때문에 NavBar와 Footer도 Suspense로 감싸신 거라면 Layout Shift가 일어나지 않게 로딩 화면이 화면 전체를 차지하게 만들면 일어나지 않아요. 다만 이러면 Home 페이지에서도 lazy를 적용하니까 첫 페이지(Home)에 들어갔을 때, 로딩 시간이 걸린다는 단점이 있을 수 있겠네요😭

const App = () => {
  return (
    <Router>
      <NavBar />
      <Suspense fallback={<div style={{ width: '100vw', height: '100vh' }}>Loading...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/search" element={<Search />} />
        </Routes>
      </Suspense>
      <Footer />
    </Router>
  );
};

Copy link
Author

@hwinkr hwinkr Sep 12, 2024

Choose a reason for hiding this comment

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

쑤쑤 정말 좋은 피드백 감사합니다! 😆

NavBar와 Footer도 Suspense로 감싼 이유가 궁금합니다! NavBar와 Footer는 Home이나 Search보다 빠르게 로드되니까 Suspense로 감싸게 되면 오히려 렌더링이 늦어지는 단점이 존재할 것 같은데 해리는 어떻게 생각하시나요🤔

저도 그럴 것 같다고 생각합니다! Suspense는 UI를 그릴 준비가 완료될 때까지 fallback UI를 보여주므로 현재 저와 같이 App.tsx를 구성하면 NavBar, Footer 컴포넌트는 UI를 그릴 준비가 완료되었지만 Home 컴포넌트를 다운로드 받는 동안 fallback UI를 보여주게 되는 문제가 생기겠네요! 네트워크 환경이 좋지 못한 사용자라면 특히 더 불편함을 느낄 수 있을 것 같아요.
Layout Shift 현상을 줄이기 위해서 일단 모든 컴포넌트를 Suspense로 감싸야겠다고 판단한 것도 맞습니다. 쑤쑤의 피드백! 반영해 보도록 하겠습니다.

다만 이러면 Home 페이지에서도 lazy를 적용하니까 첫 페이지(Home)에 들어갔을 때, 로딩 시간이 걸린다는 단점이 있을 수 있겠네요😭

로딩 시간이 걸린다는 단점이 있을 것이라고는 생각하지 않습니다. 사용자가 제일 처음 memegle 사이트에 방문하게 되면 Home 페이지를 그리기 위한 리소스들을 다운로드 받을 것이고, 그 시간 동안 fallback UI를 보여주게 됩니다. 네트워크 환경에 따라서 로딩 시간의 차이는 있겠지만 lazy를 적용하더라도 바로 리소스들을 다운로드 받을 것이라고 예상되어 lazy를 적용하기 때문에 로딩 시간이 더 걸릴 것이라고 생각하지는 않아요!

Choose a reason for hiding this comment

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

로딩 시간이 걸린다는 단점이 있을 것이라고는 생각하지 않습니다. 사용자가 제일 처음 memegle 사이트에 방문하게 되면 Home 페이지를 그리기 위한 리소스들을 다운로드 받을 것이고, 그 시간 동안 fallback UI를 보여주게 됩니다. 네트워크 환경에 따라서 로딩 시간의 차이는 있겠지만 lazy를 적용하더라도 바로 리소스들을 다운로드 받을 것이라고 예상되어 lazy를 적용하기 때문에 로딩 시간이 더 걸릴 것이라고 생각하지는 않아요!

흠....

저는 단순하게 사람들이 대부분 Home 페이지로 접근을 하니까 Home에도 lazy가 걸려 있으면 초기 로딩 지연이 존재할 것 같았어요. 그래서 사용자 입장에서 좋진 않을 것 같다?고 생각해서 Home에 lazy를 주지 않는 방식도 생각했어요. 그런데 그렇게 하게 되면 Home 페이지 js 파일 분리가 안 되겠네요...

Copy link
Author

Choose a reason for hiding this comment

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

Home에 lazy를 주지 않는 방식도 생각했어요

저도 이 방법을 생각했지만, 서비스에 익숙한 사용자라면 바로 url을 통해서 Search 페이지에 접근할 수도 있다고 생각했어요. (제가 딱 그렇거든요,,,자주 쓰는 서비스면 그냥 url을 외우고 입력해서 바로 들어갑니다.) 그렇다면 사용자는 Search 페이지에 있지만 Home 페이지를 그리기 위한 리소스도 함께 다운받게 되겠죠. Home 페이지에 dynamic import를 적용하는 것은 결국, trade off인 것 같습니다. 쑤쑤만의 의견을 정해보세요 :)

Comment on lines 5 to 8
import heroImage from '../../assets/images/hero.png?as=webp';
import trendingGif from '../../assets/images/trending.gif?as=webp';
import findGif from '../../assets/images/find.gif?as=webp';
import freeGif from '../../assets/images/free.gif?as=webp';

Choose a reason for hiding this comment

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

png, gif 모두 webp로 변환하셨는데 최신 브라우저에서는 webp를 모두 지원하지만 사파리 옛날 버전과 같이 webp를 지원하지 않는 경우가 은근히 있더라고요. picture 태그 관련 내용도 참고해 보시면 좋을 것 같아요!

picture 태그

https://caniuse.com/?search=webp

스크린샷 2024-09-09 오후 9 26 10

Choose a reason for hiding this comment

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

gif를 webm으로 변환할 지 고민하셨다고 했는데 최종적으로 webp로 변경하신 이유가 있으실까요?

Copy link
Author

Choose a reason for hiding this comment

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

image image

오호.. 쑤쑤 덕분에 잊고 살았던 Can i Use 웹 사이트를 다시 방문해서 jpeg, webp 확장자를 지원하는 브라우저 버젼을 비교해 보는 시간을 가졌습니다. Safari, FrieFox를 제외하면 webp가 더 많은 브라우저 버젼을 지원해 주는 것으로 확인되는데요. 그래도 훨씬 더 많은 브라우저 버젼을 지원해 주기 위해서 jpeg 확장자도 포함을 해야겠네요!

왜 png가 아닌 jepg를 사용하는지 추가로 궁금해하실 수도 있을 것 같은데요.

image

image

위 내용들을 참고해서, 이미지 최적화에는 Jpeg를 추천하고, 손실 압축을 사용하기 때문에 크기를 더 작게 만들 수 있고, 투명도가 필요한 이미지가 아니기 때문에 Jpeg를 사용하기로 했어요.

Copy link
Author

Choose a reason for hiding this comment

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

gif를 webm으로 변환할 지 고민하셨다고 했는데 최종적으로 webp로 변경하신 이유가 있으실까요?

어떤 파일 확장자를 사용할지에 대해서 정말 많은 고민을 했었는데요. gif도 webp로 변환할 수가 있고, 용량도 줄여주기 때문에 사용했었습니다. webpack 파일에 설정을 해두고 png, gif를 webp로 한 번에 변환하는 편함도 느끼기 위한것도 의사 결졍 요소 중 하나였습니다.
쑤쑤의 피드백을 받은 후 다시 한 번 리소스 파일 확장자에 대해서 고민을 했었는데, 크기를 최적화 한 gif를 사용하는 것으로 변경하게 되었습니다. 그 이유는 webp, webm, m4 모두 파일의 크기를 줄여주고 video 태그를 사용할 수 있도록 해준다는 장점은 있겠으나(mp4의 경우) 검색 결과로 나오는 리소스들은 모두 gif이고 서비스도 gif를 검색하고 보여주는 서비스이기 때문에 리소스 통일성을 위해서 gif를 사용하는 것이 나을 것 같다고 판단했어요.
webp, webm의 경우 브라우저 버젼에 따라 지원할 수 있는 범위에 한계가 있기도 하고, mp4를 사용하는 경우에는 Content-Type이 이미지에서 미디어로 변경되기 때문에 마음에 드는 움짤을 다운받으려고 하는 사용자가 당황할 수도 있겠다는 생각을 하기도 했습니다. 그래서 크기를 최적화 한 gif를 사용하는 것으로 변경하기로 했어요.

Choose a reason for hiding this comment

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

검색 결과로 나오는 리소스들은 모두 gif이고 서비스도 gif를 검색하고 보여주는 서비스이기 때문에 리소스 통일성을 위해서 gif를 사용하는 것이 나을 것 같다고 판단했어요. mp4를 사용하는 경우에는 Content-Type이 이미지에서 미디어로 변경되기 때문에 마음에 드는 움짤을 다운받으려고 하는 사용자가 당황할 수도 있겠다는 생각을 하기도 했습니다.

이 부분이 상당히 납득이 되네요👍
생각해보면 서비스 자체가 gif 중심이기 때문에, 사용자는 당연히 gif라고 생각하고 다운로드 받았다가 갑자기 mp4 동영상 파일이 다운로드되면 당황스럽겠네요!

Copy link
Author

Choose a reason for hiding this comment

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

바로 그거에요 쑤코톡!

@@ -44,7 +44,7 @@ const Home = () => {
</Link>
</div>
</section>
<CustomCursor text="memegle" />
<CustomCursor text="쑤쑤나는해리야" />

Choose a reason for hiding this comment

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

아이고~ 이런 소소한 이벤트가 있었네요^^

스크린샷 2024-09-11 오전 12 12 06

<h1 className={styles.title}>Memegle</h1>
<h1 className={styles.title}>memegle</h1>

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.

cloudfront 캐싱을 적용한 후 배포 파일이 변경되었을 때, 브라우저가 새로운 js 파일을 받아오는지 테스트 하기 위해서 약간의 변화를 줬었는데 다시 수정한다는 것을 깜빡했네요 ㅎ

opacity: 0;
transition: 0.5s all cubic-bezier(0.82, 0.085, 0.395, 0.895);
will-change: transform, opacity;

Choose a reason for hiding this comment

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

오! will-change 속성도 있군요!
하나 배워갑니다 👍 👍

Copy link
Author

Choose a reason for hiding this comment

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

네넹 해당 속성을 사용하면 브라우저가 will-changle에 명시된 스타일을 처리하기 위한 새로운 레이어를 하나 추가한다고 하네요!

위 벨로그 글에 will-change 속성을 사용하면 어떻게 되는지 시각화가 잘 되어 있어 공유해 봅니다!

"prettier": "^2.3.2",
"sharp": "0.33.5",

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.

저는 이미지 파일 확장자나 크기를 직접 변경하는 방법을 선택하지 않고, 웹팩에서 제공해 주는 플러그인을 활용했습니다.

해당 문서에서 제시하는 방법 중, sharp를 사용하게 되었습니다. image-minimizer-webpack-plugin만 설치하면 이미지 최적화를 위한 모든 기능을 사용할 수 있게 되는 줄 알았지만, 추가로 sharp 의존성이 필요했습니다.

  optimization: {
    minimizer: [
      `...`,
      new CssMinimizerPlugin(),
      new ImageMinimizerPlugin({
        minimizer: {
          implementation: ImageMinimizerPlugin.sharpMinify,
          options: {
            encodeOptions: {
              webp: {
                quality: 80
              }
            }
          }
        },
        generator: [
          {
            preset: 'webp',
            implementation: ImageMinimizerPlugin.sharpGenerate,
            options: {
              encodeOptions: {
                webp: {
                  quality: 80,
                  lossless: false
                }
              }
            }
          }
        ]
      })
    ],

그래서 설치하게 되었습니다.

Comment on lines +56 to +62
getTrendingWithCache: async () => {
return await apiClientWithCache({
queryKey: 'trending-gifs',
staleTime: 1000 * 60 * 5,
queryFn: gifAPIService.getTrending
});
},

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.

세션 스토리지, 캐시 스토리지 사용법에 익숙하지 않기도 했고 캐싱 역할을 가지는 모듈을 직접 구현해 보고 싶기도 해서 Cache 클래스를 직접 만드는 방법을 선택했습니다!

@@ -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 = `translate(${mousePosition.pageX}px, ${mousePosition.pageY}px)`;

Choose a reason for hiding this comment

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

쑤쑤의 질문 타임~

Q. translate 속성이 왜 Layout Shift를 일으키지 않을까요?

단순히 reflow를 일으키지 않아서. 안됩니다. 코린이도 이해하기 쉽게 설명해주세요^^

Copy link
Author

Choose a reason for hiding this comment

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

하하 꼭 제 조카가 물어보는 것 같군요^^

코린이도 이해할 수 있도록 정말 low level에서 설명해 보자면,

  • 리플로우가 발생하는 곳 : 부산
  • 리플로우가 발생하지 않는 곳 : 서울

translate는 서울 주민이기 때문입니다.

translate 속성을 사용하면 브라우저는 translate로 결정되는 요소들의 위치, 크기를 reflow가 발생하는 레이어(부산)이 아닌 새로운 레이어(서울)에서 처리하게 됩니다. 그래서 reflow가 발생하지 않게 됩니다. traslate 속성은 CPU가 아닌 GPU를 사용하게 되며 GPU는 브라우저의 메인 스레드와 별개로 동작하기 때문에 성능에도 좋다고 합니다.

제 설명이 부족했을수도 있지만, z-index의 동작 방식을 완전히 파악하고 있는 쑤쑤라면 제 설명을 충분히 이해할 수 있을 것이라고 믿어요!!!

Copy link

@soosoo22 soosoo22 Sep 12, 2024

Choose a reason for hiding this comment

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

10점 만점의 8.7점 드리겠습니다.

Copy link
Author

@hwinkr hwinkr Sep 13, 2024

Choose a reason for hiding this comment

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

1.3점이 깎인 부분을 설명해 주세요

Comment on lines +1 to 31
import { useEffect, useRef } from 'react';

type ScrollHandler = () => void;

const useScrollEvent = (onScroll: ScrollHandler) => {
const requestAnimationFrameRef = useRef<number | null>(null);

useEffect(() => {
const handleScroll = (event: Event) => {
onScroll();
const handleScroll = () => {
if (requestAnimationFrameRef.current) {
cancelAnimationFrame(requestAnimationFrameRef.current);
return;
}

requestAnimationFrameRef.current = requestAnimationFrame(() => {
onScroll();
requestAnimationFrameRef.current = null;
});
};

window.addEventListener('scroll', handleScroll);

return () => {
window.removeEventListener('scroll', handleScroll);

if (requestAnimationFrameRef.current) {
cancelAnimationFrame(requestAnimationFrameRef.current);
}
};
}, [onScroll]);
};

Choose a reason for hiding this comment

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

Partially Presented Frame을 최소로 발생시키기 위해 requestAnimationFrame을 활용하셨군요. 해결하지 못해서 고민하고 있었는데 한번 사용해 봐야겠네요👍 다만 크게 개선되지 않는 것 같아서 고민이네요....😅

Copy link
Author

Choose a reason for hiding this comment

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

요구사항 자체가 Partially Presented Frame 현상을 최소화하는 것이라, 아예 없애는 것은 불가능할 것 같다고 판단해서 Request Animation Frame API를 활용해서 최소화해 보는 것으로 만족했습니다!

@hwinkr
Copy link
Author

hwinkr commented Sep 12, 2024

추가로 해리 PR을 읽고 느꼈던 건데 궁금증을 가지고 접근하는 방식이 되게 좋았어요. PR 읽으면서 저도 많이 배운 것 같아요.. 그리고!!!! 공식 문서를 많이 참고하시는 것 같아서 공식 문서가 오히려 딱딱하고 이해가 잘 되질 않는 제 입장에서는 부러웠답니다🥹 공식 문서 읽는 팁이 있다면 알려주시면 감사하겠습니다~

저는 공식문서를 이렇게 읽는 것 같습니다. 추상적인 문장을 잘 이해를 하지 못하는 편이라 무조건 예시부터 보는 것 같아요. 예시가 없다면 그림을 찾습니다. 리액트 공식문서를 읽는다고 해봅시다.

리액트 공식문서 - 탈출구를 읽으면 저는 우선 탈출구부터 이해를 못합니다. 그래서 바로 예시를 찾으러 내려가는 것 같아요.
image
예시 코드를 찾거나 위 이미지를 통해 이해를 시도하는 편인 것 같아요. 공식문서가 정말 딱딱한 것 같고, 읽기도 힘들지만 예시와 시각화가 잘 되어 있다면 이해가 빠르게 되는 것 같더군요. 쑤쑤도 한 번 이렇게 읽어보시는 걸 추천드릴게요~

Copy link

@soosoo22 soosoo22 left a comment

Choose a reason for hiding this comment

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

안녕하세요! 해리!
저의 리뷰가 마음에 드셨을랑가 모르겠네요~^,^

인상 깊었던 포인트가 피드백을 무조건 반영하는 것이 아닌 명확한 근거를 대고 반영하시는 모습이 좋았어요! 리뷰하면서 저도 공부한 것 같은 느낌이...들었어요 😅 그리고 공식 문서 읽는 꿀팁! 감사해요! 앞으로 공식 문서와 친해져 볼게요~~

추가 질문들에 대한 답변 부탁드릴게요~

Comment on lines +10 to +28
<link
href="https://fonts.googleapis.com/css2?family=Josefin+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap"
rel="preload"
as="style"
/>
<link
rel="preload"
href="https://fonts.gstatic.com/s/josefinsans/v32/Qw3aZQNVED7rKGKxtqIqX5EUDXx4Vn8sig.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link
rel="preload"
href="https://fonts.gstatic.com/s/josefinsans/v32/Qw3EZQNVED7rKGKxtqIqX5EUCEx6XHgOiJM6.woff2"
as="font"
type="font/woff2"
crossorigin
/>

Choose a reason for hiding this comment

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

preload 적용해 주셨군요!!
혹시 아래 사진처럼 보이면 font를 우선적으로 불러오고 있는 건가요??
스크린샷 2024-09-13 오전 2 26 15

Copy link
Author

Choose a reason for hiding this comment

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

오 네트워크탭 활용 멋지네요~ 네 맞습니다! 쑤쑤가 보내준 사진처럼 우선적으로 불러오는 것입니다!

Comment on lines +70 to +72
"sideEffects": [
"*.css"
],

Choose a reason for hiding this comment

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

이 친구를 추가해서 어떤 변화가 있는 건가요??
sideEffects에 대해서 찾아보니까 tree shaking 관련한 코드 같은데, 제거해도 별다른 차이가 보이진 않는 것 같아서요.

Copy link
Author

Choose a reason for hiding this comment

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

sideEffects는 웹팩에게 tree shaking에서 export 하지만 import 하지 않는 코드들을 제거해도 된다고 명시하는 속성입니다. 저는 미션을 진행할 때,

"sideEffects": false,

위와 같이 설정하니 스타일이 깨지는 문제가 있어 css 파일은 tree shaking이 적용되지 않도록 했어요!

Comment on lines 12 to 27
const App = () => {
return (
<Router basename={'/perf-basecamp'}>
<Router>
<NavBar />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/search" element={<Search />} />
</Routes>
<Suspense
fallback={<div style={{ width: '100vw', height: '100vh' }}>loading...please wait...</div>}
>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/search" element={<Search />} />
</Routes>
</Suspense>
<Footer />
</Router>
);
};

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.

혹시...유조?

Copy link

@soosoo22 soosoo22 left a comment

Choose a reason for hiding this comment

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

미션하느라 고생 많으셨어요 해리!
해리의 코드를 리뷰할 수 있어서 영광이었습니다! 수고하셨습니다👍

@soosoo22 soosoo22 merged commit 2dcd06a into woowacourse:hwinkr Sep 13, 2024
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