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
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0ccfd1e
config: 성능 개선을 위한 의존성 추가
hwinkr Sep 9, 2024
1939747
feat: trending API 호출 결과를 캐싱하기 위한 캐시 모듈 구현
hwinkr Sep 9, 2024
80e846c
config: png, gif 리소스를 webp로 사용하기 위한 타입 선언
hwinkr Sep 9, 2024
b08b3cc
refactor: 더보기 버튼을 클릭했을 때, 기존에 렌더링되어 있던 컴포넌트는 리렌더링되지 않도록 memoization 적용
hwinkr Sep 9, 2024
6a70c42
refactor: layout shift 현상 개선을 위한 css 속성 변경
hwinkr Sep 9, 2024
45df67d
refactor: 스크롤 애니메이션이 부드럽게 적용되도록 requestAnimationFrame API 활용
hwinkr Sep 9, 2024
135d4f4
chore: 커스텀 커서의 텍스트를 "쑤쑤나는해리야"로 수정
hwinkr Sep 9, 2024
2539760
refactor: Router basename props 제거, 코드 스플리팅 적용을 위한 동적 import & Suspen…
hwinkr Sep 9, 2024
a921117
chore: hero 이미지 크기 수정
hwinkr Sep 9, 2024
ceb859d
config: 번들 사이즈 최적화를 위한 웹팩 설정 변경
hwinkr Sep 9, 2024
c543650
chore: css 폰트를 빠르게 다운로드 받기 위한 preload 추가
hwinkr Sep 12, 2024
64f4449
config: @react-icons/all-files 의존성 추가
hwinkr Sep 12, 2024
3d3b433
config: gif 크기 최적화를 위한 설정 추가
hwinkr Sep 12, 2024
c8a5c31
refactor: Suspense 위치 수정, fallback UI 최소 높이 추가
hwinkr Sep 12, 2024
405fcad
chore: webp 확장자를 지원하지 않는 브라우저 버젼도 커버하기 위한 jpg 리소스 추가
hwinkr Sep 12, 2024
6666981
refactor: webp, jpg 확장자를 모두 지원하기 위한 picture 태그 추가
hwinkr Sep 12, 2024
f6b4a19
chore: 리소스 import 경로 수정
hwinkr Sep 12, 2024
77ef0c2
chore: 링크 이름 수정
hwinkr Sep 12, 2024
5497ed7
chore: 폰트 다운로드 경로 수정
hwinkr Sep 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,840 changes: 3,140 additions & 700 deletions package-lock.json

Large diffs are not rendered by default.

22 changes: 17 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,23 @@
"scripts": {
"build:dev": "webpack --mode=development",
"build:prod": "webpack --mode=production --node-env=production",
"prod": "webpack serve --mode=production",
"dev": "webpack serve --mode=development",
"watch": "webpack --watch",
"serve": "webpack serve --mode=development",
"prettier": "prettier --write .",
"deploy": "npm run build:prod && npx gh-pages -d dist"
},
"keywords": [],
"author": "woowacourse",
"homepage": "https://{username}.github.io/perf-basecamp",
"homepage": "https://hwinkr.github.io/perf-basecamp",
"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.

단순히 번들 사이즈가 줄어서 설치하는게 아닌 구체적인 이유를 함께 적어주셔서 좋네요👍

"react-router-dom": "^6.3.0"
},
"devDependencies": {
Expand All @@ -41,6 +42,7 @@
"babel-loader": "^8.2.2",
"copy-webpack-plugin": "^9.0.1",
"css-loader": "^6.2.0",
"css-minimizer-webpack-plugin": "7.0.0",
"dotenv-webpack": "^7.0.3",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.5.0",
Expand All @@ -53,18 +55,28 @@
"file-loader": "^6.2.0",
"html-loader": "^2.1.2",
"html-webpack-plugin": "^5.3.2",
"image-minimizer-webpack-plugin": "4.1.0",
"mini-css-extract-plugin": "2.9.1",
"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
                }
              }
            }
          }
        ]
      })
    ],

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

"style-loader": "^3.2.1",
"ts-loader": "^9.3.1",
"typescript": "^4.8.2",
"webpack": "^5.88.2",
"webpack-cli": "^4.7.2",
"webpack-bundle-analyzer": "4.10.2",
"webpack-cli": "4.10.0",
"webpack-dev-server": "^3.11.2"
},
"sideEffects": [
"*.css"
],
Comment on lines +70 to +72

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이 적용되지 않도록 했어요!

"babel": {
"presets": [
"@babel/preset-env",
"@babel/preset-react"
"@babel/preset-react",
{
"modules": "false"
}
],
"plugins": [
"@babel/plugin-transform-runtime"
Expand Down
21 changes: 12 additions & 9 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';

import Home from './pages/Home/Home';
import Search from './pages/Search/Search';
const Home = lazy(() => import('./pages/Home/Home'));
const Search = lazy(() => import('./pages/Search/Search'));

import NavBar from './components/NavBar/NavBar';
import Footer from './components/Footer/Footer';

import './App.css';
import { lazy, Suspense } from 'react';

const App = () => {
return (
<Router basename={'/perf-basecamp'}>
<NavBar />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/search" element={<Search />} />
</Routes>
<Footer />
<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인 것 같습니다. 쑤쑤만의 의견을 정해보세요 :)

);
};
Expand Down
12 changes: 10 additions & 2 deletions src/apis/gifAPIService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { GifsResult } from '@giphy/js-fetch-api';
import { IGif } from '@giphy/js-types';

import { GifImageModel } from '../models/image/gifImage';
import { apiClient, ApiError } from '../utils/apiClient';
import { apiClient, apiClientWithCache, ApiError } from '../utils/apiClient';

const API_KEY = process.env.GIPHY_API_KEY;
if (!API_KEY) {
Expand Down Expand Up @@ -43,7 +43,7 @@ export const gifAPIService = {
* @returns {Promise<GifImageModel[]>}
* @ref https://developers.giphy.com/docs/api/endpoint#!/gifs/trending
*/
getTrending: async (): Promise<GifImageModel[]> => {
getTrending(): Promise<GifImageModel[]> {
const url = apiClient.appendSearchParams(new URL(`${BASE_URL}/trending`), {
api_key: API_KEY,
limit: `${DEFAULT_FETCH_COUNT}`,
Expand All @@ -52,6 +52,14 @@ export const gifAPIService = {

return fetchGifs(url);
},

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

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 클래스를 직접 만드는 방법을 선택했습니다!

/**
* 검색어에 맞는 gif 목록을 가져옵니다.
* @param {string} keyword
Expand Down
Binary file modified src/assets/images/hero.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 6 additions & 6 deletions src/pages/Home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { useRef } from 'react';
import { Link } from 'react-router-dom';
import classNames from 'classnames/bind';

import heroImage from '../../assets/images/hero.png';
import trendingGif from '../../assets/images/trending.gif';
import findGif from '../../assets/images/find.gif';
import freeGif from '../../assets/images/free.gif';
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.

바로 그거에요 쑤코톡!


import FeatureItem from './components/FeatureItem/FeatureItem';
import CustomCursor from './components/CustomCursor/CustomCursor';
Expand All @@ -23,7 +23,7 @@ const Home = () => {
<section className={styles.heroSection}>
<img className={styles.heroImage} src={heroImage} alt="hero image" />
<div className={styles.projectTitle}>
<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 파일을 받아오는지 테스트 하기 위해서 약간의 변화를 줬었는데 다시 수정한다는 것을 깜빡했네요 ㅎ

<h3 className={styles.subtitle}>gif search engine for you</h3>
</div>
<Link to="/search">
Expand All @@ -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

</>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
z-index: 10;
padding: 0 16px;
color: var(--white);
will-change: transform;
}

.cursor::before {
Expand All @@ -23,6 +24,8 @@
.cursor span {
display: inline-block;
animation: wave-text 1s ease-in-out infinite;
will-change: transform;
transform: translateZ(0);
}

.cursor span:nth-of-type(1) {
Expand Down
3 changes: 1 addition & 2 deletions src/pages/Home/components/CustomCursor/CustomCursor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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점이 깎인 부분을 설명해 주세요

}
}, [mousePosition]);

Expand Down
20 changes: 17 additions & 3 deletions src/pages/Home/hooks/useScrollEvent.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
import { useEffect } from 'react';
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]);
};
Comment on lines +1 to 31

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를 활용해서 최소화해 보는 것으로 만족했습니다!

Expand Down
3 changes: 2 additions & 1 deletion src/pages/Search/components/ArtistInfo/ArtistInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { memo } from 'react';
import { Artist } from '../../../../models/help/artist';

import styles from './ArtistInfo.module.css';
Expand All @@ -21,4 +22,4 @@ const ArtistInfo = ({ artist }: ArtistProps) => {
);
};

export default ArtistInfo;
export default memo(ArtistInfo);
3 changes: 2 additions & 1 deletion src/pages/Search/components/GifItem/GifItem.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { memo } from 'react';
import { GifImageModel } from '../../../../models/image/gifImage';

import styles from './GifItem.module.css';
Expand All @@ -16,4 +17,4 @@ const GifItem = ({ imageUrl = '', title = '' }: GifItemProps) => {
);
};

export default GifItem;
export default memo(GifItem);
6 changes: 4 additions & 2 deletions src/pages/Search/components/HelpPanel/HelpPanel.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

.selectedItemContainer {
position: fixed;
right: -320px;
right: 0;
top: 0;
width: 320px;
height: 100%;
Expand All @@ -19,12 +19,14 @@
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
transform: translateX(100%);
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 속성을 사용하면 어떻게 되는지 시각화가 잘 되어 있어 공유해 봅니다!

}

.selectedItemContainer.showSheet {
right: 0;
transform: translateX(0);
opacity: 1;
}

Expand Down
2 changes: 1 addition & 1 deletion src/pages/Search/hooks/useGifSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const useGifSearch = () => {
if (status !== SEARCH_STATUS.BEFORE_SEARCH) return;

try {
const gifs = await gifAPIService.getTrending();
const gifs = await gifAPIService.getTrendingWithCache();
setGifList(gifs);
} catch (error) {
handleError(error);
Expand Down
2 changes: 2 additions & 0 deletions src/types/images.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ declare module '*.png';
declare module '*.jpg';
declare module '*.gif';
declare module '*.svg';
declare module '*.png?as=webp';
declare module '*.gif?as=webp';
23 changes: 23 additions & 0 deletions src/utils/apiClient.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import cache from './cache';

export class ApiError extends Error {
constructor(public status: number, message?: string) {
super(message);
Expand All @@ -22,3 +24,24 @@ export const apiClient = {
return newUrl;
}
};

interface ApiClientWithCacheArgs<T> {
queryFn: () => Promise<T>;
queryKey: string;
staleTime: number;
}

export const apiClientWithCache = async <T>({
queryFn,
queryKey,
staleTime
}: ApiClientWithCacheArgs<T>) => {
const cachedData = cache.get<T>(queryKey);

if (cachedData && cache.isValidCache(queryKey)) return cachedData;

const newData = await queryFn();
cache.set(queryKey, newData, staleTime);

return newData;
};
30 changes: 30 additions & 0 deletions src/utils/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
interface CacheItem<T> {
data: T;
expiredTime: number;
}

class Cache {
private cache: Record<string, CacheItem<any>> = {};

isValidCache(key: string): boolean {
const item = this.cache[key];
if (!item) return false;

return Date.now() <= item.expiredTime;
}

get<T>(key: string): T | null {
if (!this.cache[key]) return null;

return this.cache[key].data;
}

set<T>(key: string, data: T, ttl: number): void {
const expiredTime = Date.now() + ttl;
this.cache[key] = { data, expiredTime };
}
}

const cache = new Cache();

export default cache;
Loading