Nextj.js 최적화 전략

LateMarch·2023년 8월 29일
0

Next.js는 SSR의 이점을 살리기 위해 데이터 패칭을 서버에서 처리하기를 권장한다. 그런데 이미지와 스크립트, 폰트와 같은 하이퍼 미디어의 구성 요소들 또한 일종의 패칭 되어야할 데이터들이다. 따라서 이경우에도 Next.js는 SSR의 이점을 살리고 앱의 성능을 높이기 위한 최적화 전략들을 제공한다. 이 글에서는 공식 문서에 소개 된 최적화 기법들 중 일부를 소개한다.

Images

import Image from 'next/image'

Next.js에서 제공하는 <Image>는 이미지의 크기를 최적화 시켜주어 빠른 페이지 로드를 가능하게 하며 자동으로 이미지가 로드 될 자리를 선점하여 layout shift를 방지해준다. 이 외에도 여러 편의를 자동으로 제공해준다.

<Image>를 상용하는 방법 또한 간단한데, 위와 같은 편의사항들이 이미지의 크기를 명시하는 것만으로 사용 가능하게 된다.(로컬 이미지는 이마저도 필요 없다!)

import Image from 'next/image'
import profilePic from './me.png'
// loader가 './me.png'파일에 대한  srcset을 자동으로 생성해줌.
 
export default function Page() {
  return (
    <Image
      src={profilePic}
      alt="Picture of the author"
	  // 로컬 이미지는 아래의 옵션들이 자동으로 설정됨
      // width={500} automatically provided 
      // height={500} automatically provided 
      // blurDataURL="data:..." automatically provided
      // placeholder="blur" // Optional blur-up while loading
    />
  )
}

외부 이미지를 쓰는 일도 간단하다.

import Image from 'next/image'
 
export default function Page() {
  return (
    <Image
      src="https://s3.amazonaws.com/my-bucket/profile.png"
      alt="Picture of the author"
      width={500}
      height={500}
    />
  )
}

다만 이 때에는 next.config.js파일에 아래와 같이 추가하면 더 안정적인 최적화를 기대할 수 있다.

module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 's3.amazonaws.com',
        port: '',
        pathname: '/my-bucket/**',
      },
    ],
  },
}

Fonts

Next.js는 Google Fonts내의 모든 폰트를 자동으로 최적화 해준다. 그 사용법도 너무 간단한데 그저 'next/font/google'의 폰트들을 임포트 하여 사용하기만 하면 된다.

import { Inter } from 'next/font/google'
 
// If loading a variable font, you don't need to specify the font weight
const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
})
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" className={inter.className}>
      <body>{children}</body>
    </html>
  )
}

두가지 폰트를 같이 쓰는법에는 여러가지가 있다. global.css에 설정값으로 지정해서 태그에 따라 폰트를 다르게 적용할 수 있다.

import { Inter, Roboto_Mono } from 'next/font/google'
import styles from './global.css'
 
const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
  display: 'swap',
})
 
const roboto_mono = Roboto_Mono({
  subsets: ['latin'],
  variable: '--font-roboto-mono',
  display: 'swap',
})
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" className={`${inter.variable} ${roboto_mono.variable}`}>
      <body>
        <h1>My App</h1>
        <div>{children}</div>
      </body>
    </html>
  )
}

// app/global.css
html {
  font-family: var(--font-inter);
}
 
h1 {
  font-family: var(--font-roboto-mono);
}

또한 tailwind.config.js에 fontFamily를 추가하여 tailwindcss와 함꼐 사용할 수도 있다.

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
    './app/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {
      fontFamily: {
        sans: ['var(--font-inter)'],
        mono: ['var(--font-roboto-mono)'],
      },
    },
  },
  plugins: [],
}

// page.tsx
export default function Home() {
  return (
      <p className="font-sans">Sans font</p>
  );
}

Preloading & Reusing

만약 폰트 함수가 특정 페이지에서 호출 됐다면, 전역적으로 적용되지 않는다. layout에서 호출된다면 그 layout이 감싸고 있는 routes들은 모두 적용된다. 더욱이 root layout에서 preloade된다면 모든 routes에서 사용가능하니, 폰트 적용 전략에 따라 적절히 사용하면 된다. 폰트 함수는 하나의 인스턴스에서 제공될 수 있으니 하나의 공유 가능한 파일을 생겅하여 export 하여 사용하는 것이 좋다.

Scripts

Third-party script를 여러 routes에서 로드해야 하는 경우 next/script로 최적화가 가능하다.

import Script from 'next/script'
 
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <>
      <section>{children}</section>
      <Script src="https://example.com/script.js" />
    </>
  )
}

위의 외부 스크립트는 해당 layout의 route가 유저에 의해 접근될 때 로드 되며 Next.js는 이 스크립트가 유저의 여러 요청이 있더라도 한번만 로드 되는 것을 보장한다.

폰트와 마찬가지로 root layout에서 import된다면, 모든 routes에서 스크립트가 한번만 로드되어 공유된다.

Lazy Loading

Lazy loading은 라우트에서 렌더링 되어야 할 자바스크립트르 줄임으로써 앱의 초반 로딩 성능을 향상시킨다. 이 기능은 Client Components의 지연 로딩을 통해 초반 로딩에 필요한 번들만을 로드하게 해준다. (지연 로딩된 컴포넌트들은 사용자의 상호작용에 따라 동적 import 된다)

모든 서버 컴포넌트들은 자동으로 나뉘어져 Streaming할 수 있게 되므로 이 기능은 클라이언트 컴포넌트에 적용되는 기능이다.

'use client'
 
import { useState } from 'react'
import dynamic from 'next/dynamic'
 
// Client Components:
const ComponentA = dynamic(() => import('../components/A'))
const ComponentB = dynamic(() => import('../components/B'))
const ComponentC = dynamic(() => import('../components/C'), { ssr: false })
 
export default function ClientComponentExample() {
  const [showMore, setShowMore] = useState(false)
 
  return (
    <div>
      {/* Load immediately, but in a separate client bundle */}
      <ComponentA />
 
      {/* Load on demand, only when/if the condition is met */}
      {showMore && <ComponentB />}
      <button onClick={() => setShowMore(!showMore)}>Toggle</button>
 
      {/* Load only on the client side */}
      <ComponentC />
    </div>
  )
}

하지만 이 떄에도 클라이언트 컴포넌트는 Next.js에서 pre-rendered(SSR)되므로 이를 무효화 하고 싶다면 아래와 같이 설정해주어야 한다.

const ComponentC = dynamic(() => import('../components/C'), { ssr: false })

이와 같이 앱의 초기 렌더링 성능 향상을 위한 방법들이 있으며 이 방법들은 결국 화면 표시에 필요한 data fetching과 렌더링 하는 번들 사이즈의 효율화에 달렸다.

profile
latemarch

0개의 댓글