LIGHTLOG
article thumbnail

memegle 웹사이트

이번 미션은 'memegle 웹사이트의 성능을 개선하라!' 였다.

개선 전(좌)과 후(우)의 lighthouse

미션 전의 나는 '성능'에 대해서 제대로된 정의조차 할 수 없었다.

단순하게 "그냥 빠르게 돌아가면 되는 것..?" 추상적으로 떠올렸다.

 

아래의 요구사항 체크리스트를 하나씩 짚어가며 천천히 알아가보자!

✅ 미션 요구사항 및 체크리스트

정적파일 크기 줄이기

1. 소스코드 크기 줄이는 방법
1-1) Gzip 압축하기!

Gzip이란?
압축프로그램이다. 모던 브라우저에서는 모두 내장되어 있으며, 서버(ex. aws, 스프링, nginx)에서 설정가능하다.

cloudFront 편집 페이지


따라서 나는, aws CloudFront에서 지원하는 Gzip 압축 기능을 활용했다.
CloudFront 동작 설정에 `자동으로 객체 압축` 옵션을 Yes로 선택!


1-2) .css 파일 압축하기
`CssMinimizerWebpackPlugin`을 설치하여 CSS 파일에서도 불필요한 빈칸, 공백 등을 제거해 압축시켜주는 작업을 진행했다.

++ `MiniCssExtractPlugin`을 설치하여 css 파일을 추출했다. css파일을 추출하게 되면 css파일과 js파일을 병렬적으로 load할 수 있어 사용자가 페이지를 빠르게 load할 수 있다. (소스코드를 줄인 측면은 아니지만 css관련 플러그인 작업이라 해당 부분에 추가해두었습니다!)

++ 요청 우선순위를 조정하고 싶다면?
바로 필요한 파일들은 preload, prefetch, preconnect를 활용하여 빨리 불러오자.
아닌 아이들은 defer, async를 활용해보자.

2. 이미지 크기를 줄여보자

출처: HTTP Archive

평균적으로 웹페이지에서 이미지가 차지하는 용량 비율은 60%이상이다. 

 

2-1) srcSet을 활용한 이미지 최적화 

<img srcSet={`
            ${heroMobile} 375w,
            ${heroTablet} 768w,
            ${heroDesktop} 1980w,
          `}
          className={styles.heroImage}
          src={heroImage}
          alt="hero image"
/>


메인 페이지에서 사용되는 용량이 큰 이미지 파일이 있다면, 해당 img태그 내의 srcSet 속성을 활용해 각 반응형 기기들에 (Desktop, Tablet, Mobile) 최적화된 사이즈의 파일을 불러오도록 구현했다.

사이즈를 맞게 줄였는데도 여전히 용량이 크다면!
이미지 포맷을 점검하거나

이미지를 압축하거나

이미지 메타 정보를 제거하는 방법 등이 있다.

2-2) gif -> mp4

<video className={styles.featureImage} autoPlay muted loop playsInline>
    <source type="video/mp4" src={imageSrc}></source>
</video>


gif 파일은 용량이 크기 때문에 사용을 지양하자. mp4로 변환하여 용량을 줄였다. 

 

3. 페이지별 리소스 분리
3-1) code splitting

const Home = lazy(() => import(/* webpackChunkName: "home" */ './pages/Home/Home'));
const Search = lazy(() => import(/* webpackChunkName: "search" */ './pages/Search/Search'));

const App = () => {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Router>
        <NavBar />
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/search" element={<Search />} />
        </Routes>
        <Footer />
      </Router>
    </Suspense>
  );
};

lazy와 Suspense를 활용하여 페이지별로 필요한 리소스를 분리하여 불러오도록 구현했다. 

 

위에 주석은 생성되는 chunk단위의 번들파일의 이름을 부여해주기 위함이다!

module.exports = {
  entry: './src/index.tsx',
  resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'] },
  output: {
    path: path.join(__dirname, '/dist'),
    clean: true,
    filename: '[name].[contenthash].js',
    chunkFilename: '[name].[contenthash].js'
  },
...
}

위와 같이 filename과 chunkFilename을 설정해주면 이름과 해쉬값을 가진 .js 번들파일이 생성된다.

✋🏻 여기서 잠깐, 해쉬값을 넣어주는 이유?
-> cache Busting을 하기 위해서

cache Busting이란?
캐시 무효화라는 뜻으로, 파일 이름에 hash를 붙여주고 파일이 변경 될 때마다 이름 뒤에 hash 값이 변경되어 브라우저에서는 파일 변경 되었다고 인식하고 새로운 리소스를 받아서 화면에 보여주는 방식을 의미합니다.

cache Busting에는 3가지 캐시가 있다. 
hash - 모든 Chunk의 hash 값은 동일하게 생성된다.

chunkhash - webpack entry를 기반으로 생성되며 각 chunk별로 hash값이 다르다.

contenthash - 각 파일의 내용을 기반으로 hash값을 생성한다. 예를 들어 js와 css 중 한 부분의 파일에만 변경사항이 있을 경우 해당 파일만 새로운 hash값을 적용하여 cache Busting한다.

나는 이중에 contenthash를 적용하였고, 적용결과는 다음과 같다.

빌드 후 생성된 파일 목록 (/dist)

3-2) React 관련 패키지 분리해주기

module.exports = {
  ...,
  optimization: {
    minimizer: [new CssMinimizerPlugin()],
    splitChunks: {
      cacheGroups: {
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom|react-router|react-router-dom)[\\/]/,
          name: 'react',
          chunks: 'all'
        }
      }
    }
  }
};

React관련 패키지들도 따로 분리하여 빌드해주었는데, 큰 차이는 없지만 초반에 웹사이트에 로드할때 

React 관련 패키지들이 병렬적으로 로드 되어 시간을 조금이라도 단축시켜줄 수 있지 않을까~ 기대한다.

네트워크 탭의 병렬적으로 로드되는 모습


4. 아이콘 패키지 Tree Shaking
4-1) react-icons 사용과 관련하여..
용량이 크기로 악명 높은 react-icons ... 
번들파일에서 큰 부분을 차지하던 'react-icons' 라이브러리를 삭제하고 '@react-icons/all-files'를 설치하여 사용하는 아이콘만 불러와서 사용하도록 변경하였다. 

하지만! 여기서 주목할 점은, production 모드로 빌드될 때는 esm모듈이라 알아서 tree shaking이 적용된다.

따라서, size를 비교해보자면

'stat' 크기는 차이가 나지만, output size인 'parsed' 크기는 'react-icons'와 `@react-icons/all-files'가 별반 다름없다.
하지만! 빌드 속도 측면에서 `@react-icons/all-files'를 사용하는 것이 좋은 선택인 것 같다. 


5. CloudFront 캐시 설정

5-1) Cache-Control
리소스를 요청받았을 때 서버는 응답으로 주는 리소스를 검증하고 쓰라고/캐시하지 말라고 알려줄 수 있다.
• Cache-Control: no-cache
- 캐시가 가능하긴 하지만 origin 서버에 매번 캐시 유효성 검증 요청을 한다. max-age=0과 동일한 효과이다.
• Cache-Control: no-store
- 캐시 불가능! 매번 서버에서 새로 받아와야 한다.

 Cache-Control: public
- 중간 프록시(ex. CDN)에도 캐시를 저장할 수 있다. 함께 사용하는 s-maxage는 공유(shared/public) 캐시에만 적용되는 유효 기간이다.

 Cache-Control: private (default)

- 최종 끝의 클라이언트만 캐시 가능하게 한다.

 

++ 캐시 무효화하기

브라우저 캐시 무효화는 -> cache Busting
CDN 캐시 무효화는 해당 플랫폼에서 제공

6. Memoization
6-1) React.memo
추가된 목록(아이템)만 새로 렌더되어야 할때? React.memo로 컴포넌트를 Memoization 하기.

 

7. Reflow 지양하기

Reflow(Layout)이란?
화면구조(Layout)이 변경되었을 때 뷰 포트 내에서 렌더 트리상 노드의 정확한 위치과 크기를 계산하는 과정이다. element의 reflow는 DOM에 있는 모든 하위, 상위 요소의 후속 리플로우를 유발한다. 

Layout Shift 없이 애니메이션을 주기 위해 top, right, left, bottom 속성을 변경하는 것은 reflow가 발생한다.
transform 속성을 활용하면 이를 해결할 수 있다!

 

8. Frame Drop이 일어나지 않아야 한다.
  - (Chrome DevTools 기준) Partially Presented Frame 역시 최소로 발생시키기 위해서는 requestAnimationFrame 적용을 고려해볼 수 있다.

profile

LIGHTLOG

@lightOnCoding

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!