히치키치

내가 기차역이라 그런가? 사이트가 버벅거려 본문

Cognisle

내가 기차역이라 그런가? 사이트가 버벅거려

히치키치 2024. 3. 18. 09:17

최초 빌드 결과

 

1. 폰트 최적화

처리 1] 파일 otf에서 woff 로 변경

처리 2] font-display: swap 추가를 통한 폰트가 로드 되지 못했을 때 시스템 폰트를 보여줌

 

변경전

변경후

 

두자리 밀리초에서 한자리 밀리초로 변경됨

 

2. SVG 파일 Compressor 적용 후 파일 교체

 

 

왼쪽) 원래 디자이너분이 준 SVG 파일 용량

오른쪽) 인터넷에 SVG Compressor 적용 후 파일 용량

막 엄청 다이나믹하게 줄어든 것은 아니지만... 그래도 티끌모아 태산이라는 마음으로...ㅎㅎ

 

3. _app 에서 헤더와 푸더 보이도록 레이아웃 수정

 

전) _app 의 child 컴포넌트로 헤더와 푸터와 콘텐츠를 랜더링 여부를 따지는 레이아웃을 구성했는데 그랬더니 모든 페이지 컴포넌트가 다 동일하게 큰 JS 로딩이 걸림

// _app.tsx

export default function App({ Component, pageProps }: AppProps) {
  return (
    <CookiesProvider>
      <Provider store={store}>
        <AppLayout>
          <Component {...pageProps} />
        </AppLayout>
      </Provider>
    </CookiesProvider>
  )
}

 

// /components/layoust/AppLayout.tsx

const AppLayout = ({ children }: { children: ReactElement }) => {	
   // 생략

  return (
    <>
      <Head>
        <title>Cognisle</title>

        <meta
          name="viewport"
          content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
        />
      </Head>
      <AppLayoutWrapper>
        {needHeader && <Header />}
        <Container needHeader={needHeader} needBottomTab={needBottomTab}>
          {children}
        </Container>

        {needBottomTab && <BottomTab />}
      </AppLayoutWrapper>
    </>
  )
}

 

후)  _app에 헤더와 푸터와 콘텐츠를 랜더링 여부 따지고 이전에 레이아웃 구성한 AppLayout.tsx 삭제

 

4. bundle-analyzer 추가해 번들 파악 진행

// next.config.js에 추가

const withBundleAnalyzer = require("@next/bundle-analyzer")({
  enabled: process.env.ANALYZE === "true", // 환경변수 ANALYZE가 true일 때 실행
  openAnalyzer: false, // 브라우저에 자동으로 분석결과를 새 탭으로 Open하는 것을 방지
})

module.exports = withBundleAnalyzer(nextConfig)

 

// 다음의 명령어 터미널에 입력

ANALYZE=true npm run build

 

반영 결과

 

1. _app 과 /404 파일의 JS 초기 로딩이 크기 줄어듬 (154kb -> 112kb)

처음부터 레이아웃 구성을 좀 더 효율적으로 짰으면....

2. First Load JS shared by all 크기 줄어듬 (160kb -> 113kb)

띠끌모아 압축한 SVG 덕에  앱 전반에 사용되는 공용 코드, 프레임워크 코드, 웹팩 코드, CSS 등 줄어듬

3. 섬 꾸미기가 진행되는 /island 페이지에서 큰 일이 생긴 것 같다..

음.. 일단 외부라이브러리를 함부로 불러서 생긴 일은 아닌 게 맞았고 /island 가 섬을 꾸미는 페이지로 이것저것 이미지 및 svg가 많아서 생긴 문제 같다.

 

5. 섬 꾸미기 아이템 보관함에 대한 dynamic import 도입

 

왼쪽이 처음 섬꾸미기 페이지에 들어가면 보이는 것이다. 여기서 꾸미기 편집 아이콘을 누르면 아이템 보관함 컴포넌트이 보인다. 

 

다음의 코드는 vercel에서 제시한 코드 분할 및 스마트 로딩 전략으로 Next.js 앱 속도를 높이는 방법 중에서 dynamic imports에 대한 example 코드이다.

https://github.com/vercel/next.js/blob/canary/examples/with-dynamic-import/pages/index.tsx

 

그중에 아이템 보관함 컴포넌트* Load on demand *에 해당하는 경우라고 생각되었다.

이유1) 처음 페이지를 로드할 때 아직 사용자가 편집 아이콘을 누르지 않은 초기 상황에는 아이템 보관함 컴포넌트 안 보임

이유2) 아이템 보관함에는 각종 아이템에 해당하는 미디어 요소가 많아서 꼭 필요할 때만 불러와야 함

이유3) 심지어 아이템 보관함에는 carousel 기능을 위해 사용한 외부 라이브러리인 swiper도 있음

=> 진짜 필요한 경우(사용자가 편집 모드를 켜는 경우)에 그때가서 불러서 보여주자

 

아이템 보관함 컴포넌트는 접속자가 섬의 주인이고 편집 모드를 활성화한 경우에만 보이게 하는 코드이다.

 

처리: 아이템 보관함 컴포넌트(LandEdit)를 dynamic import 하는 코드로 교체했다.

 

반영 결과

1. /island가 5.19MB에서 5.17MB로 줄어듬

특히 /island의 5.03 MB 중 2.86 kB로 문제가 되었던 css/07a098635536dd3e.css 파일도 처리됨. 아마 외부 swiper 라이브러리도 dynamic import 되면서 해결된 것 같다.

2. 아니 근데 대체 왜 아직도 /island 가 문제인가...??

 

6. 아이템 SVG 컴포넌트 dynamic import 도입

이번에는 /pages/island.tsx가 아니라 constans/island.tsx가 문제인데... 이 파일은 말이다...

각 아이템에 해당하는 이미지주소, 타이틀, 번호, 컴포넌트로 랜더링된 SVG가 object 형태로 저장된 상수 (ITEM_CHOICE)가 있다. 문제는.. 사실 이 파일은 작성할 때부터 되는지부터 확인이 급해서 주먹 구구식으로 일단은 돌아가게 하자!! 하고 작성한 것이다...

// constants/island.tsx

import Item_0 from "@/public/assets/item/original/item_0.svg"
import Item_1 from "@/public/assets/item/original/item_1.svg"
...생략...
import Item_23 from "@/public/assets/item/original/item_23.svg"
import Item_24 from "@/public/assets/item/original/item_24.svg"

interface ILandItem {
  id: number
  title: string
  thumbImgSrc: string
  mainImgSrc: string
  width: number
  height: number
  svg: ReactElement
}

type ILandItemChoice = {
  [key in ItemIdProps]: ILandItem
}

const ITEM_CHOICE: ILandItemChoice = {
  0: {
    id: 0,
    title: "Item_0",
    thumbImgSrc: "/assets/item/original/item_0.svg",
    mainImgSrc: "/assets/item/original/item_0.svg",
    svg: <Item_0 width="20" height="20" />,
    ...ITEM_SIZE,
  },
  1: {
    id: 1,
    title: "Item_1",
    thumbImgSrc: "/assets/item/original/item_1.svg",
    mainImgSrc: "/assets/item/original/item_1.svg",
    svg: <Item_1 width="20" height="20" />,
    ...ITEM_SIZE,
  },
  
  ...생략...,
  
  24: {
    id: 2,
    title: "Item_2",
    thumbImgSrc: "/assets/item/original/item_2.svg",
    mainImgSrc: "/assets/item/original/item_2.svg",
    svg: <Item_2 width="20" height="20" />,
    ...ITEM_SIZE,
  }
  }

 

이미지 파일이면 다른 최적화나 사이즈를 줄이는 방법이 있겠지만..

해당 아이템을 드래깅하고 위치시키는 기능은 꼭 SVG가 ReactNode로 불러와져야만 자잘한 버그나 에러 이슈가 없고 깔끔했다. 그래서 0번부터 24번까지 해당하는 아이템의 SVG 파일 Import문을 써서 ReactNode 타입으로 불러와지게 하고, 아이템 상수 (ITEM_CHOICE)에 각 아이템에 대해 svg 키의 값으로 ReactNode화된 SVG 컴포넌트를 부여해 화면에 랜더링하는 방식을 사용했다.

// LandContent.tsx

import { ITEM_CHOICE, LAND_CHOICE } from "@/constants/island"

const LandContent = ({ isOwner }: { isOwner: boolean }) => {
 ...생략

  useEffect(() => {
    setIslandItem()
  }, [setIslandItem])
  
  return (
    <>
		...생략...
          <ItemsContainer>
            {items.length > 0 &&
              items.map((item: LocationProps) => (
                <DragItem
                  isOwner={isOwner}
                  key={item.id}
                  id={item.id}
                  title={ITEM_CHOICE[`${item.id}`].title}
                  child={ITEM_CHOICE[`${item.id}`].svg}
                  x={item.x}
                  y={item.y}
                  z={item.z}
                  active={false}
                />
              ))}
          </ItemsContainer>
        ...생략...
    </>
  )
}

 

// DragItem.tsx

const DragItem = ({ isOwner, id, x, y, z, child, title }: DraggableItem) => {

	...생략...

  return (
    <Draggable
      axis="both"
      defaultPosition={state}
      onStart={onStart}
      onDrag={(e, data) => trackPos(id, data)}
      onStop={onStop}
      disabled={!isOwner || !islandIsEdit}
    >
      <ItemContainer zIndex={state.z}>{child}</ItemContainer>
    </Draggable>
  )
}

 

문제는 유저별로 가지고 있는 아이템 키 값을 하나씩 돌면서

키 값에 매칭되는 아이템 정보는 아이템 상수 값(ReactNode화 된 SVG 컴포넌트가 아이템 갯수만큼이나 존재하는 객체 ㅎㄷㄷ)에서 불러오고 사용자가 드래깅하며 움직일 ReactNode 컴포넌트된 SVG를 child로 내려서 랜더링되도록 했다.. 매번 저 큰 상수를 불러와서 접근하고 심지어 SVG가 작은 상태도 아닌데 ㅎㄷㄷ

 

전체 아이템 목록에서 ReactNode화된 SVG 컴포넌트를 찾는 것이 아닌

필요한 아이템에 대해서만 ReactNode화된 SVG 컴포넌트를 보이면 해결될 것 같다고 생각함

 

처리:

// constants/island.tsx

import dynamic from "next/dynamic"

...생략...

interface ILandItem {
  id: number
  title: string
  thumbImgSrc: string
  mainImgSrc: string
  width: number
  height: number
}

type ILandItemChoice = {
  [key in ItemIdProps]: ILandItem
}


const ITEM_CHOICE: ILandItemChoice = {
  0: {
    id: 0,
    title: "Item_0",
    ...ITEM_SIZE,
  },
 .. 생략...
  24: {
    id: 22,
    title: "Item_24",
    ...ITEM_SIZE,
  },
}

const ITEM_SVG = {
  0: dynamic(() => import("@/public/assets/item/original/item_0.svg")),
 ...생략...
  24: dynamic(() => import("@/public/assets/item/original/item_24.svg")),
}

 

1. 아이템 상수 (ITEM_CHOICE): SVG 관련을 모두 제거하고 해당 파일을 import하는 코드도 다 지움

2. 아이템 SVG 컴포넌트 (ITEM_SVG): SVG를 ReactNode로 dynamic import로 불러오는 함수가 아이템 키에 맞게 매핑된 객체 작성 

 

// LandContent.tsx

import { ITEM_CHOICE, LAND_CHOICE } from "@/constants/island"

const LandContent = ({ isOwner }: { isOwner: boolean }) => {
 ...생략

  useEffect(() => {
    setIslandItem()
  }, [setIslandItem])
  
  return (
    <>
		...생략...
          <ItemsContainer>
            {items.length > 0 &&
              items.map((item: LocationProps) => (
                <DragItem
                  isOwner={isOwner}
                  key={item.id}
                  id={item.id}
                  title={ITEM_CHOICE[`${item.id}`].title}
                  child={ITEM_CHOICE[`${item.id}`].svg}
                  x={item.x}
                  y={item.y}
                  z={item.z}
                  active={false}
                />
              ))}
          </ItemsContainer>
        ...생략...
    </>
  )
}

 

3. 상위 컴포넌트에서 굳이 큰 SVG 리액트 노드 컴포넌트를 props로 내려줄 필요가 없어 child props 제거

import { ILandItem, ITEM_SVG } from "@/constants/island"

const DragItem = ({isOwner,id,x,y,z,title, width, height}: DraggableItem) => {
	
   ...생략...
   
  const SVGItem = ITEM_SVG[id]

  return (
    <Draggable
      axis="both"
      defaultPosition={state}
      onStart={onStart}
      onDrag={(e, data) => trackPos(id, data)}
      onStop={onStop}
      disabled={!isOwner || !islandIsEdit}
    >
      <ItemContainer zIndex={state.z}>
        <SVGWrapper width={width} height={height}>
          <SVGItem />
        </SVGWrapper>
      </ItemContainer>
    </Draggable>
  )
}

...생략...

const SVGWrapper = styled.div<{
  width: ILandItem["width"]
  height: ILandItem["height"]
}>`
  width: ${({ width }) => `${width}px`};
  height: ${({ height }) => `${height}px`};
`

export default DragItem

4. props로 전달 받은 아이템 키 값(id)를 이용해서 리액트 컴포넌트화된 SVG를 동적으로 불러오기

5. 불러온 것을 JSX로서 랜더링함

 

반영 결과

1. /island 5.17MB-> 175KB로 줄임

2. lighthouse 성능 전반적 개선 (왼쪽: 개선 전, 오른쪽: 개선 후)

3. 번들러 결과 - 진짜 SVG 파일 자체가 커서...

 

4.디자이너 분이 아이템 사이즈 용량 적은 것을 주워서 적용함

 

드디어 svg 어셋 보다는 라이브러리가 군데 군데 보인다.

 

5. SVG 뿐만 아니라 그냥 이미지 리소스도 버벅거린다.

 

현재 nextjs 프로젝트는 vercel로 자동화 배포를 하고 있고 next/Image를 통해 image 리소스를 제공하고 있다. <img> 태그와 다르게 .webp 변환해주며 이미지 크기를 최적화한다. (Vercel Image 최적화 부분 가면 되고 있는 거 확인 가능...)

문제는 우리 어플리케이션은 이미 이미지 갯수가 많다!!! (보통 20~40개 이상이면 많다고 하는 것 같다.. 아닌가....)

즉, 이미지 파일 크기는 줄었지만, 화면에 정상적으로 제공되기까지 오래 걸린다는 것이다. (당연함 이미지가 많으니까...)

처음(=최초 로딩 시에는) 수많은 이미지 중에서 불러와야하니까 느리다..

캐싱되면 처음부터 박박 뒤질 필요는 없으니까 매우 빠르게 화면에 불러올 수 있다.

 

처리

1. 우선적으로 랜더링 할 이미지에 priority 추가

 

2. 프로덕션 환경에 sharp 라이브러리 도입

 

3. next.config에 minimumCacheTTL 설정 

* 이미지 요청 시 동적으로 최적화되며 만료 시간 전까지 후속 요청에 최적화된 이미지 제공됨

* 캐싱되었지만 만료된 경우, 해당 이미지는 served stale 됨

* 이후 다시 해당 이미지에 대해 최적화가 백그라운드에서 진행되며 갱신된 만료 기한을 가지며 캐싱됨

const nextConfig = {
  reactStrictMode: false,
  images: {
    domains: ['CDN URL'],
    minimumCacheTTL: 31536000, // 기본 최대 TTL 값으로 설정함 
    formats: ['image/webp'],
  },
}

 

 

4. Serverless Function Region 서울로 설정

* 아니 이거 기본 설정이 캘러포니아 인 걸 지금 첨 알았다 레전드...

Comments