2022년 11월 11일
조회 : 364|7분 읽기

Next.js 12 Deep Dive

이 글은 Next.js 12버젼 기준으로 작성되었습니다.
함수나 컴포넌트의 기본적인 사용 방법, 개념은 간단히 설명합니다.

Image

next는 Image라는 컴포넌트를 제공한다. 이 컴포넌트는 HTML img 태그의 확장이며 우수한 Core Web Vitals를 달성하는데 도움이 되는 다양한 기본 성능 최적화가 포함되어있다.

Lazy Loading

이미지 로드하는 시점을 필요할 때까지 지연시키는 기술을 의미
그래서 이게 왜 최적화를 해주는 거냐 예를 들면 스크린 밖에 있는 이미지들은 로딩을 지연시키고 스크린 안에 있는 이미지만을 로드해서, 불필요한 대역폭 사용을 줄이고 필요한 이미지만 빠르게 로드할 수 있도록 하는 것이다.
일반 img 태그에선 lazy라는 속성을 줘야 위와 같은 성능 최적화를 끌어낼 수 있다. 만약 사용하지 않는다면 페이지 안에 있는 모든 img를 가져올 때까지 기다리기 때문이다. 하지만 이 컴포넌트는 자동적으로 적용이 된다. 따라서 일반 태그보다 사용하기 쉽고 최적화가 이루어지는 것이다.

이미지 사이즈 최적화

Next/Image는 디바이스 크기 별로 srcSet을 미리 지정해두고, 사용자의 디바이스에 맞는 이미지를 다운로드할 수 있게 지원한다. 또한 본래 사진이 어떤 포맷 형태이든 이미지를 webp와 같은 용량이 작은 포맷으로 이미지를 변환해서 제공한다.
위의 모든 작업은 이미지에 대한 최초 요청 시에 Next 서버에서 진행된다. 이후 요청은 캐시가 만료될 때까지 캐시된 이미지가 제공되기 때문에 첫 번째 요청보다 빠르게 서빙이 가능하다. 캐시가 만료된 후 요청이 있다면 오래된 이미지를 우선 제공한 후 백그라운드에서 이미지 최적화를 다시 진행한다.

placeholder

CLS(Cumulative Layout Shift) 방지 가능
웹 사이트를 들어갔을 때 아무것도 없는 부분에서 갑자기 팍 하고 사진이 생겨서 레이아웃이 밑으로 밀리는 것을 본 경험이 있었을 텐데 이것을 CLS라고 한다.
Image는 레이아웃이 흔들리는 현상을 방지하기 위해 placeholder를 제공한다. placeholder는 이미지가 로드되기 전에 이미지 높이만큼 영역을 표시해 로드된 후에 레이아웃이 흔들리지 않도록 하는 것이다.
완전히 배경색 빈영역으로 표현도 가능하지만 "blur"값을 이용해 blur로 보이게도 가능하다.
로컬 이미지의 경우 빌드 타임에 이미지를 기준으로 자동으로 width, height를 지정하고 base64로 인코딩된 blur 이미지가 생성되어 별도의 작업 없이 blur 사용이 가능하다.
리모트 이미지의 경우 빌드 타임에 이미지에 접근하는 것이 불가능하기 때문에 width, height 정보를 줘야 하고, blur 이미지가 생성되지 않는다.
이미지가 로드되기 전 placeholder로 blur 이미지를 사용하고 싶은 경우에는 별도로 blurDataURL 속성에 base64로 인코딩된 이미지 데이터를 작성해 줘야 한다.

사용하기 위한 조건

로컬 이미지인 경우 일반 img를 사용하는 것과 같이 바로 사용할 수 있다.
리모트 이미지인 경우 api를 통해 url을 받는 경우에는 next.config의 images에서 domain을 설정해 줘야한다.
  • 왜? 리모트 이미지의 경우 Next.js 서버에서 이미지를 가지고 있는 리모트 서버에 직접 요청을 하기 때문에 **모든 url에 대한 접근을 허용할 경우 악의를 가진 사용자에 의해 공격을 받을 가능성이 있다. ** 이를 방지하기 위해 이미지를 서빙하는 서버가 안전한 서버라는 것을 Next.js에 알려줘야 하기 때문에, next.config.js 파일에 CDN의 host를 명시해야 한다.
javascript
1const nextConfig = {
2  reactStrictMode: true,
3  images: {
4    domains: ["imagedelivery.net"],
5  },
6};


MiddleWare

일반적으로 벡엔드에서 사용하는 3계층으로 얘기하자면 제일 마지막 계층(DB)로 넘어가기 전에 2계층에 존재하며 request를 처리하는 controller전에 middleware가 존재한다.
이것은 유저가 보낸 request와 종착지의 중간에 있는 함수이기 때문에 middleware라고 불린다.
예들 들면 회원 정보를 받아올 때 DB를 가서 정보를 가져와야하는데 이때 회원이 로그인이 되어있나 어떤 회원인가를 확인하는 과정을 미들웨어의 역할이다.

사용 방법

Next.js에서 middleware를 사용하기 위해서는 12.2 버전 전에는 page의 폴더 안에 하나씩 넣는 것이었지만 업데이트가 되면서 루트에 하나만 두고 필요한 경우에는 path로 분기를 태워 사용하게 되었다.
ex)
javascript
1import type { NextRequest } from 'next/server'
2
3export function middleware(request: NextRequest) {
4  
5  if (request.nextUrl.pathname.startsWith('/about')) {
6    // This logic is only applied to /about
7    // sessionName에서 cookie를 가져옴
8 	if(request.cookies.has("sessionName")){console.log("ok")}
9    
10  }
11
12  if (request.nextUrl.pathname.startsWith('/dashboard')) {
13    // This logic is only applied to /dashboard
14  }
15}


Dynamic Imports

Next 앱의 로딩 시간을 최적화하는 방법
dynamic 컴포넌트의 핵심은 앱을 작게 나눠 필요에 따라 그 컴포넌트를 불러오는 것이다.
일반적으로 import해서 컴포넌트를 바로 사용하는게 아니라 이벤트나 특정 상황에 따라 컴포넌트를 나오게 해 다운로드하는 component를 줄인다.
바로 UI에 포함시켜 다운 받게 하는 것이 아니라 나중에 다운로드 시켜서 최적화가 가능하다.
ssr 설정을 통해 서버에서 로딩할지 csr로 로딩할지 설정이 가능하다.
javascript
1const com = dynamic(() => import("components/com"), {ssr:false});
만약에 어쩔수 없이 큰 컴포넌트를 사용해야 한다면 어떻게 해야할까
무작정 사용한다면 언제가 될지도 모르는 해당 컴포넌트가 다운이 완료될 때까지 멍하니 하얀 화면을 봐야한다.
dynamic으로 사용해야하는 컴포넌트는 작아야 성능이나 UX에 좋게 기여를 하지만 만약에 어쩔 수 없는 상황에 사용해야할 때가 오면 어떻게 해야할까
loading 속성을 사용한다. 안에는 일반 string, tag, component가 들어갈 수 있다.
javascript
1const big = dynamic(() => import("components/com"), {
2  ssr: false,
3  loading: () => <span>Loading...</span>,
4});


document

_app은 앱 전체의 청사진과 같다. 서버로 요청이 들어왔을 때 가장 먼저 실행되는 컴포넌트
_document도 마찬가지로 앱의 청사진과 같다. 하지만 이것은 app 다음에 언제나 서버에서 실행된다.
next/documnet의 Documnet로 부터 class 상속을 받아 만들어지는 class형 컴포넌트이며 서버에서 한 번만 실행된다. 주로 font를 넣을 때 사용한다.


Script Component

Next.js Script 컴포넌트인 next/script는 HTML script 태그의 확장이다.
이를 통해 개발자는 애플리케이션에서 써드 파티 스크립트의 로드되는 우선 순위를 설정할 수 있으므로 개발자 시간을 절약하면서 로드하는 성능을 향상시킬 수 있다.
이 컴포넌트에는 속성이 존재한다.
  1. strategy beforeInteractive: 페이지가 interactive 되기 전에 로드 afterInteractive: (기본값) 페이지가 interactive 된 후에 로드 lazyOnload: 다른 모든 데이터나 소스를 불러온 후에 로드 worker: (실험적인) web worker에 로드
javascript
1<Script src="~" strategy="lazyOnLoad" />
  1. onLoad 만약 script가 불러져왔다면 실행할 함수
javascript
1<Script src="~" onLoad={() => console.log("done")} />


getServerSideProps

이 함수는 서버단에서만 호출되고 페이지 컴포넌트가 서버단에서 실행된다. 유저의 요청이 발생할 때마다 실행된다.
서버에서 페이지에 필요한 데이터 값들을 가지고 올 때까지 기다렸다가 페이지가 렌더링되기 때문에 SWR과 같이 사용이 불가능하다.
따라서 static optimization, cache 같은 기능을 사용할 수 없다.

그럼 캐시 데이터를 미리 제공해서 둘 다 사용해 보자

SWRConfig 컴포넌트는 fallback이라는 속성으로 캐시 초기값을 설정할 수 있다.
javascript
1// 안에 useSWR로 데이터를 받아오고 있음
2const Home: NextPage = () =>{...}
3
4const Page: NextPage<{products:ProductsWithCount[]}>= ({products}) => {
5   return (
6      <SWRConfig value={{
7         fallback:{
8            "/api/home" :{ ok:true, products}
9         }
10      }}>
11      <Home/>
12      </SWRConfig>
13   )
14}
15
16export ssr fun ~~~
17
18// wrapping 한 페이지를 export
19export default Page
서버사이드 렌더링을 이용해서 데이터를 받아오지만 SWR을 이용해서 캐시를 설정해 주며 CSR도 동시에 사용이 가능하다.


getStaticProps (SSG)

정적인 라우트

앱 안에 있는 파일들을 정적으로 html로 만들기 때문에 node의 read를 사용해서 프로젝트 안에 있는 파일들을 읽어서 정보를 받아온다.
javascript
1export async function getStaticProps() {
2  const posts = readdirSync("./posts").map((file) => {
3    return readFileSync(`./posts/${file}`, "utf-8");
4  });
5  return {
6    props: { posts },
7  };
8}

동적인 라우트 (getStaticPaths 사용)

동적인 라우트를 갖는 페이지에서 getStaticProps를 사용할 때 꼭 필요하다.
동적 경로를 사용하는 페이지에서 getStaticPaths 함수를 export할 때 Next는 getStaticPaths에 의해 지정된 모든 경로를 정적으로 미리 렌더링한다.
javascript
1export async function getStaticPaths() {
2  const res = await AllPostId();
3
4  const paths = res?.map((post: any) => ({
5    params: {
6      id: post.id.toString(),
7    },
8  }));
9  return { paths, fallback: "blocking" };
10}
fallback의 값에 따라 getStaticPaths에서 리턴하지 않은 페이지 접속 시 return 되는 상황이 다르다.
"false" 모두 404로 연결
"true"
fallback 화면 > 페이지 반환
  1. 404 반환하지 않고 getStaticProps 함수를 이용한 값으로 HTML파일과 JSON파일을 만들어낸다.
  2. 백그라운드에서 작업이 끝나면 요청된 path에 해당하는 JSON 파일을 받아 새롭게 페이지를 렌더링함
  3. 새롭게 생성된 페이지를 기존 빌드시 프리렌더링된 페이지 리스트에 추가. 같은 path로 온 이후 요청들에 대해서는 처음 생성한 페이지를 반환함
"blocking"
  1. true일 때와 비슷하게 동작하지만 최초 만들어 놓지 않은 path에 대한 요청이 들어온 경우 fallback 상태를 보여주지 않고 SSR처럼 동작한다.
fallback이 렌더링되고 있는지 next의 router를 통해 값을 페이지에서 알 수 있다.
javascript
1const router = useRouter();
2router.isFallback; // fallback이 렌더링되고 있다면 true


ISR이란

Incremental Static Regeneration의 약자
한국말로 단계적 정적 재생성
getStaticProps, getServerSideProps 경우 유저쪽에서 로딩을 기다리는 일이 없다.
하지만 만약 서버에서 처리하는 것들이 많이 html을 만들 때까지 시간이 오래 걸린다면 유저는 로딩은 아니지만 멈춰있는 화면을 그냥 기다려야한다는 단점이 있다. ssg는 fallback이 false가 아닌경우..
ISR은 getStaticProps를 백그라운드에서 여러 번 실행이 가능하게 한다.
javascript
1export async function getStaticProps() {
2  const res = await fetch("https://.../posts");
3  const posts = await res.json();
4
5  return {
6    props: {
7      posts,
8    },
9    revalidate: 10,
10  };
11}
getStaticProps에서 revalidate를 속성을 정의한다면 (해당 값)초 마다 페이지를 재생성한다.
따라서 로딩이나 멈춰있는 화면을 기다릴 필요없이 가장 최신 데이터를 볼 수 있다.

그럼 revalidate가 뭔가

revalidate를 20이라고 가정하고 예를 들어본다. A, B, C, D 라는 유저가 웹에 도착해서 해당 페이지를 본다.
맨 처음 버젼을 본다
A --> V1
20초 후 유저가 들어온다면 백그라운드에서 V를 재생성한다. (V2의 트리거)
B --(20초)-> V1 (V2)
20초 이후 온 유저는 V2를 본다
C --(21)-> V2
40초 후 유저가 들어온다면 백그라운드에서 V3를 재생성한다. (V3의 트리거)
D --(40)-> V2 (V3)

ODR On-demand Revalidation

revalidate를 통해 새로운 데이터를 받을 수 있는 방법(원래 있던 캐시를 삭제하는 방법)은 revalidate 설정 시간이 끝나고 유저가 그 페이지를 방문하는 방법밖에 없었다.
ODR은 수동으로 캐시를 삭제하기 위한 방법을 제공한다. getStaticProps에서 API handler를 이용할 수 있다. 하지만 middleware는 사용할 수 없다.
사용 방법
사용이 필요한 api에서 사용한다.
javascript
1await res.revalidate("api url");