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 all 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
19 changes: 19 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@
<link rel="icon" href="./public/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<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
/>
Comment on lines +10 to +28

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.

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

<link
href="https://fonts.googleapis.com/css2?family=Josefin+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap"
rel="stylesheet"
Expand Down
23,028 changes: 14,579 additions & 8,449 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",
"@react-icons/all-files": "4.1.0",
"classnames": "^2.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.4.0",
"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
20 changes: 12 additions & 8 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
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'}>
<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>
);
};
Comment on lines 12 to 27

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.

혹시...유조?


export default App;
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 added src/assets/images/hero.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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.
2 changes: 1 addition & 1 deletion src/components/NavBar/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const NavBar = () => {
<header className={styles.header}>
<nav className={styles.nav}>
<Link to="/">
<span className={styles.logo}>memegle</span>
<span className={styles.logo}>Memegle</span>
</Link>
<Link to="/search">
<button className={styles.searchPageButton}>start search</button>
Expand Down
15 changes: 12 additions & 3 deletions src/pages/Home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { useRef } from 'react';
import { Link } from 'react-router-dom';
import classNames from 'classnames/bind';

import heroImage from '../../assets/images/hero.png';
import heroImageWebp from '../../assets/images/hero.png?as=webp';
import heroImageJpg from '../../assets/images/hero.jpg';
import trendingGif from '../../assets/images/trending.gif';
import findGif from '../../assets/images/find.gif';
import freeGif from '../../assets/images/free.gif';
Expand All @@ -21,7 +22,15 @@ const Home = () => {
return (
<>
<section className={styles.heroSection}>
<img className={styles.heroImage} src={heroImage} alt="hero image" />
<picture>
<source srcSet={heroImageWebp} type="image/webp" />
<img
className={styles.heroImage}
src={heroImageJpg}
alt="hero image"
fetchPriority="high"
/>
</picture>
<div className={styles.projectTitle}>
<h1 className={styles.title}>Memegle</h1>
<h3 className={styles.subtitle}>gif search engine for you</h3>
Expand All @@ -44,7 +53,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
3 changes: 2 additions & 1 deletion src/pages/Search/components/HelpPanel/HelpPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { AiOutlineInfo, AiOutlineClose } from 'react-icons/ai';
import { AiOutlineInfo } from '@react-icons/all-files/ai/AiOutlineInfo';
import { AiOutlineClose } from '@react-icons/all-files/ai/AiOutlineClose';
import classNames from 'classnames/bind';

import ArtistList from '../ArtistList/ArtistList';
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Search/components/SearchBar/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { AiOutlineSearch } from 'react-icons/ai';
import { AiOutlineSearch } from '@react-icons/all-files/ai/AiOutlineSearch';

import styles from './SearchBar.module.css';

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;
};
Loading