Next.js pages router 공식문서 정리

정윤규·2023년 6월 22일
5

next.js

목록 보기
1/2
post-thumbnail

Devrank 프로젝트에서 Next.js를 처음 사용한 이후로 최근 Next.js에 관심이 많아졌다. Next.js app router도 최근 stable이 되었고 지금도 앞으로도 Next.js는 trend가 될 것 같다.

이 글은 Next.js 공식문서의 pages router 중 핵심 내용을 선별해 정리한 글이다.

Pages

  • Next.js에서 페이지는 pages 디렉토리에 존재하는 리액트 컴포넌트 파일에 해당한다. 각 페이지는 파일명에 따라 라우팅 경로와 연관된다.
  • Ex) pages/introduce.js 파일이 존재하면 /introduce 경로에서 접근가능하다.
  • Dynamic route 또한 지원한다. [] 를 통해 적용 가능. 예를 들어 pages/posts/[id].js 파일이 존재할 시 posts/1 ~ posts/n 페이지에서 접근가능하다.
    • id → dynamic parameter

Pre-rendering

기본적으로 Next.js는 모든 페이지를 pre-rendering 한다. Next.js는 각 페이지를 모두 클라이언트측 자바스크립트에서 생성하는 대신 미리 HTML 파일을 만든다.

결국 완성된 HTML 문서를 전송하기 때문에 성능 효율적이며 SEO에 유리하다.

각 HTML파일은 최소한의 자바스크립트 코드만이 필요하다. 브라우저에 페이지가 로드되면 자바스크립트가 로드되어 페이지가 완전히 동작가능하게 되는데 이 과정을 hydration 이라 한다.

Next.js는 기본적으로 SSG(Static Site Generation)와 SSR(Server-side Rendering) 두 가지 형태의 pre-rendering을 지원한다.

  • SSG: HTML 문서를 빌드 타임에 생성하고 매 요청마다 재사용한다.
  • SSR: HTML 문서가 매 요청마다 생성된다.

Next.js 측에선 SSR을 꼭 사용해야하는 상황이 아니라면 SSG를 SSR보다 적극 사용하길 추천한다. SSG는 CDN에 캐싱될 수 있기 때문에 성능효율적이다.

이외에도 Client-side data fetching 또한 가능하다. 즉, SSR, SSG, CSR 중 필요에 따라 선택적으로 사용하면 된다.

Static Generation

만약 페이지가 SSG를 사용하면 HTML 문서는 빌드타임에 미리 생성된다. (next build 를 실행시킬 때) 해당 문서는 매 요청마다 재사용된다.

data fetching이 없는 페이지일 시 Next.js는 default로 페이지를 SSG로 pre-rendering 한다.

function Page() {
	return <div>page</div>
}

export default Page;

페이지를 pre-rendering하기 위해 외부 데이터를 fetching해야 할 경우는 두가지 케이스가 존재한다.

  1. page content가 외부 데이터에 의존할 때: getStaticProps
  2. 페이지의 path가 외부 데이터에 의존할 때: getStaticPaths , getStaticProps 또한 사용가능

Case 1

페이지를 pre-render할 때 필요한 외부 데이터를 fetch 해야 하는 경우 async 함수인 getStaticProps를 같은 파일 내에서 export해야 한다. 해당 함수는 빌드 타임에 호출되고 pre-render시에 fetch된 데이터를 페이지의 props로 넘길 수 있게 해준다.

// props로 getStaticProps에서 return한 Props정보를 전달받아 사용할 수 있다.
export default function Page({data}) {
	//... some code
}

// 빌드타임에 호출
export async function getStaticProps() {
	const res = await fetch(요청경로);
	const data = res.json();
	
	return {
		props: {
			data,
		},
	}
}

Case 2

페이지의 path가 외부 데이터에 의존할 경우 getStaticPaths를 고려할 수 있다. Next.js는 dynamic route를 지원하나 어떤 dynamic값을 가지는 페이지를 빌드타임에 pre-render할 지는 외부 데이터에 의존할 수 있다.

export async function getStaticPaths() {
	const res = await fetch(요청경로);
	const items = res.json();
	
	// pre-render할 path 정보를 획득한다.
	const paths = items.map((item) => ({
		params: { id: item.id },
	));

	// paths에 해당하는 문서만 빌드타임에 pre-render
	// fallback이 false일 경우 다른 라우트 경로는 404가 된다.
	return { paths, fallback: false };
}

// 데이터를 fetching 해야할 경우 getStaticPaths에서 전달받은 path params를 사용할 수 있다.
export async function getStaticProps({params}) {
	const res = await fetch(`경로/${params.id}`);
	const item = await res.json();

	return {
		props: { item }
	};
}

export default function Item({item}) {
	// some code
}

Static Generation은 언제 써야할까?

가능한 한 사용하길 권장한다. 페이지가 한번만 만들어지고 CDN에 캐싱되어 매 요청마다 생성하는 것에 대비해 매우 빠르다.

하지만 페이지의 content가 매 요청마다 갱신되어야 한다면 문서를 미리 생성할 수 없기 때문에 SSG를 사용할 수 없다. → CSR, SSR을 사용해야 함

Server-side Rendering

SSR을 사용하면 페이지는 각 요청마다 새로 생성된다. SSR을 사용하기 위해선 async function인 getServerSideProps 를 export 해야한다. 해당 함수는 매 요청마다 호출된다.

만약 페이지의 데이터가 요청마다 빈번하게 갱신되어야 하면 사용할 수 있다.

export default function Item({item}) {
	// some code
}

export async function getServerSideProps() {
	const res = await fetch(경로);
	const item = await res.json();

	return {
		props: { item }
	};
}

Data fetching

getServerSideProps

getServerSideProps 함수를 export하면 Next.js는 매 요청마다 함수에서 반환된 데이터를 사용해 페이지를 pre-render한다.

// 페이지가 SSR로 동작하게 된다.
export async function getServerSideProps(context) {
	return {
		props: {}, // 페이지 컴포넌트에 props로 전달
	}
}

getServerSideProps 는 항상 브라우저가 아닌 server-side에서만 실행된다.

  • 페이지를 직접 요청하면 getServerSideProps 가 요청 시간에 실행되고 페이지는 반환된 props와 함께 pre-render된다.
  • 페이지가 next/link(Link)next/router(useRouter) 를 통해 클라이언트 측 페이지 이동간에 요청된다면 Next.js는 서버에 getServerSideProps를 실행시키는 API 요청을 전달한다.
💡 Note
  • getServerSideProps 는 페이지를 렌더링하는데 사용할 수 있는 JSON 데이터를 반환한다.
  • 위의 동작은 Next.js가 처리하기때문에 개발자는 신경쓸 필요가 없다.
  • getServerSideProps 는 페이지 컴포넌트에서만 사용가능하고 단독으로 export해야 한다.

언제 써야할까?

  • 페이지의 데이터가 요청시간에 fetch되어야 한다면 getServerSideProps를 사용해야 한다.
  • cache-control 헤더를 사용해서 캐싱할 수 있다.
  • 이외의 경우에는 CSR or SSG를 고려해야 한다.

getStaticProps

getStaticProps 함수를 export하면 Next.js는 빌드타임에 반환된 데이터를 사용해 페이지를 pre-render한다.

// 페이지가 SSG로 동작한다.
export async function getStaticProps(context) {
	return {
		props: {}, // props로 전달
	}
}

getStaticProps 는 항상 서버에서 실행되고 클라이언트에서는 절대 실행되지 않는다.

  • next build 중에 실행된다.
  • ISR을 사용하면 getStaticProps도 background에서 실행하여 페이지를 revalidate할 수 있다.
💡 `getStaticProps` 는 정적인 HTML문서를 생성하기 때문에 query parameter나 HTTP 헤더와 같은 요청에 대한 정보에 접근할 수 없다. 따라서 요청에 대해 접근이 필요하다면 Middleware 기능을 고려해야 한다.

언제 써야할까?

  • 페이지를 렌더링하는데 필요한 데이터가 유저 요청 이전에 빌드타임에서 사용가능할 때
  • 데이터를 headless CMS에서 가져올 때
  • 페이지가 SEO에 유리해야 하고 빠르게 표시되야 할 때 - getStaticProps가 생성하는 HTML과 JSON 파일들은 CDN에 캐시되어 성능이 좋다.
  • 데이터가 유저를 특정해 캐시되지 않아도 될 때. Middleware 기능을 통해 경로를 rewrite하면 우회할 수 있다고 설명되어 있다.

정적으로 생성되는 HTML과 JSON파일

getStaticProps로 빌드타임에 생성된 페이지는 HTML파일만이 아닌 함수 반환값으로 JSON파일 또한 정적으로 생성한다. JSON파일은 client-side에서 routing간에 페이지를 render할 때 필요한 props값을 주입하기 위해 사용된다.

즉, getStaticProps가 호출되는 것이 아닌 생성되었던 JSON파일을 재사용한다.

getStaticPaths

페이지가 dynamic route의 형태를 가지면서 getStaticProps를 사용한다면 여러개의 path를 가지는 문서를 정적으로 생성할 필요가 있다.

getStaticPaths 함수를 export하면 Next.js는 getStaticPaths에서 특정된 모든 path에 대한 문서를 pre-render한다.

// pages/items/[id].js

// items/1과 items/2 를 생성한다.
export async function getStaticPaths() {
	return {
		// 각 path에 대한 정보를 전달한다.
		paths: [{params: {id: '1' }}, {params: {id: '2' }}],
		fallback: false,
	}
}

// getStaticPaths는 getStaticProps를 함께 호출해야 한다.
export async function getStaticProps(context) {
	//
}

언제 써야 할까?

  • dynamic route를 사용하는 페이지를 pre-render해야 할 때 사용한다.
  • 이외에는 SSG와 이유가 동일하다.

Incremental Static Regeneration

ISR을 사용하면 빌드 후에도 전체 사이트를 rebuild하는 대신 페이지 별로 정적인 페이지를 갱신할 수 있다.

ISR을 사용하려면 revalidate prop을 getStaticProps 에 추가한다.

export async function getStaticProps() {
	// 코드
	return {
		props: { data },
		// 매 10초 마다 페이지 갱신
		// 요청이 올 시 페이지 갱신
		revalidate: 10, // 초 단위
	}
}

페이지가 빌드 타임에 생성되고 첫 요청은 cache된 페이지를 보여준다. 이후로는 다음과 같다.

  • 초기 요청 이후 10초 이전의 요청은 cache된 페이지를 보여준다.
  • 10초 이후 다음 요청은 cache된 페이지를 보여주나 Next.js가 페이지를 background에서 재생성한다.

On-Demand Revalidation

revalidate 시간을 60으로 하면 모든 사용자들은 1분동안 같은 화면을 보게되고 cache를 무효화하는 유일한 방법은 1분뒤 누군가가 페이지를 방문하는 것이다.

이러한 이유로 Next.js는 On-demand ISR을 제공한다. on-demand revalidation을 하기위해서는 getStaticProps에서 revalidate를 명시하지 않는다. 만약 페이지 캐시를 무효화하기 위해서는 revalidate() 함수를 호출하면 된다.

사용법은 공식문서를 참고 → https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration

client-side

다음 경우에 client-side data fetching이 유용하다.

  • SEO가 필요하지 않은 경우
  • data를 pre-render할 필요가 없을 경우
  • 페이지 내 데이터가 빈번하게 갱신되어야 할 때
  • SWR, react-query 같은 서버 상태관리 및 캐싱 도구랑 같이 쓰면 유리

Layout

Next.js에선 네비게이션바(topbar)나 푸터와 같은 페이지마다 동일한 UI는 layout으로 작성해서 페이지마다 새로 작성하지 않고 공유해서 사용할 수 있는 구조를 제시한다.

전체 페이지에 하나의 레이아웃 구조만이 필요하면 Layout 커스텀 컴포넌트를 작성하고 _app.js 에서 페이지 컴포넌트를 감싸서 재사용할 수 있다. 페이지 컴포넌트가 변화해도 레이아웃은 변화하지 않기 때문에 컴포넌트 state가 유지되는 이점이 있다.

//components/Layout.js

// Layout으로 사용할 컴포넌트를 정의
// children이 페이지 컴포넌트가 된다.
export default function Layout({children}) {
	return (
		<>
			<nav></nav>
			<main>{children}</main>
			<footer></footer>
		</>
	);
}

//pages/_app.js
export default function MyApp({ Component, pageProps }) {
	return (
		// Layout 컴포넌트로 감싸준다.
		<Layout>
			<Component {...pageProps} />
		</Layout>
	)
}

만약 여러개의 레이아웃이 필요하면 페이지에 getLayout 프로퍼티를 추가해서 페이지별 레이아웃을 정의할 수 있다.

export default function Page() {
	// JSX return
}

Page.getLayout = function getLayout(page) {
	// page를 layout nesting
}

// pages/_app.js

export default function MyApp({ Component, pageProps }) {
  // component에 getLayout 프로퍼티가 있으면 getLayout으로 페이지 별 레이아웃 적용
	// 없으면 컴포넌트를 그대로 반환하는 함수 할당
  const getLayout = Component.getLayout || ((page) => page)

  return getLayout(<Component {...pageProps} />)
}

Layout 컴포넌트는 페이지가 아니기 때문에 client side data fetching만이 가능하다.

Image Optimization

Next.js에서 제공하는 next/image 컴포넌트는 <img> 태그의 확장으로 기존 img태그를 성능을 최적화한 컴포넌트이다.

import Image from 'next/image';

다음의 최적화 기능을 제공한다.

  • 항상 디바이스별로 사이즈에 맞는 이미지를 제공한다.
  • CLS를 자동으로 방지한다. Layout shift 예방
    • Next.js가 import한 이미지파일을 토대로 width와 height를 자동으로 설정한다.
    • remote src의 이미지일 경우 width, height를 설정해주어야 함.
  • blur-up placeholder를 설정할 수 있고 viewport에 이미지가 보일 시 로드되도록 lazy load를 적용한다.
  • 원격 서버에 있는 이미지를 포함해 on-demand 이미지 resize가 가능하다.

Priority

각 페이지의 LCP(viewport에 존재하는 가장 큰 이미지나 텍스트 블록)가 될 이미지에는 priority 속성을 추가해서 Next.js가 해당 이미지를 우선 로드할 수 있게 해주어야 한다.

// LCP image
<Image
	// 속성들
	// priority를 추가 
	priority 
/>

Image sizing

이미지에 사이즈를 지정해주지 않으면 이미지가 로드되기 전 비어있던 공간이 이미지가 로드되면서 화면이 밀리는 현상이 생기는데 이는 성능에 큰 저하를 일으킨다. → (CLS, Cumulative Layout Shift)

next/image 는 이를 다음의 3가지 방법으로 방지한다.

  1. 이미지를 import하면 자동으로 사이즈를 지정해준다.
  2. width, height property를 지정한다.
  3. fill property를 추가하면 이미지가 parent element에 따라 동적으로 확장된다.
💡 이미지 사이즈를 모를때
  • fill 사용 fill 속성은 이미지 사이즈가 부모 element에 따라 변화할 수 있게 해준다.
  • 이미지 normalizing 이미지를 특정한 size로 변경하는 normalize 프로세스를 추가한다.
  • image URL을 API로 요청할때 API를 요청할 때 특정 size로 요청할 수 있을수도 있다.

더 자세한 건 API 문서를 보자. https://nextjs.org/docs/api-reference/next/image

Font optimization

next/font 는 폰트를 최적화해주고 외부 네트워크 요청을 제거해준다.

  • 폰트가 변경되면서 발생하는 layout shift를 방지
  • 모든 google font를 내장하여 google font를 구글에 요청하지 않고 바로 사용할 수 있다. next/font/google 을 통해 font import 가능
    // 아무 파일에서든 가능
    // _app.js에서 추가해준다면 하위 컴포넌트 전체에 적용
    import { Roboto } from 'next/font/google'
    
    const roboto = Roboto({
      weight: ['400', '700'], // weight 지정가능
      style: ['normal', 'italic'], // style 지정가능
      subsets: ['latin'],
    })
    
    export default function MyApp({ Component, pageProps }) {
      return (
        <main className={roboto.className}>
          <Component {...pageProps} />
        </main>
      )
    }

Local fonts

next/font/local 를 사용하면 local font 또한 향상된 성능으로 사용할 수 있다.

import localFont from 'next/font/local'

// local font src를 전달
const myFont = localFont({ src: './my-font.woff2' })

export default function MyApp({ Component, pageProps }) {
  return (
    <main className={myFont.className}>
      <Component {...pageProps} />
    </main>
  )
}
💡 Reusing font

폰트를 호출할 때마다 새로운 인스턴스로 호스팅되기 때문에 만약 여러 파일에서 동일한 폰트를 사용해야 하면 하나의 파일에서 폰트 로더를 모듈화시켜 필요한 파일에서 import 해서 사용해야 한다.

Static File Serving

Next.js는 루트 디렉토리의 public 폴더에서 정적 asset들을 제공한다. public 폴더 아래의 파일들은 코드에서 / 를 base-URL로 접근가능하다.

환경변수

Next.js는 환경변수를 세팅할 수 있는 방법을 내장으로 지원한다.

  • .env.local 사용
  • NEXT_PUBLIC_ prefix 환경변수 사용(브라우저에서 사용하려면)

Next.js는 .env.local 에 있는 환경변수들을 자동으로 process.env에 추가해준다.

variable 또한 사용 가능

HOSTNAME=localhost
PORT=8080
// $(variable) 변수로 사용
HOST=http://$HOSTNAME:$PORT

기본적으로 환경변수는 Node.js 환경에서만 사용가능하기 때문에 브라우저에 노출되지 않는다. 브라우저에 노출시키기 위해서는 환경변수에 NEXT_PUBLIC_ prefix를 붙여야 한다.

만약 환경변수를 production과 development, test 환경에 따라 나누어 적용하고 싶으면 .env.production , .env.development, .env.test 등으로 나누어 생성하면 된다.

로드순서

  1. process.env
  2. .env.$(NODE_ENV).local
  3. .env.local
  4. .env.$(NODE_ENV)
  5. .env

Optimizing Scripts

third-party scripts는 사용자나 개발자의 경험을 떨어뜨릴 수 있다.

  • third-party scripts에 의해 로딩 성능이 저하되어 사용자 경험 또한 저하시킬 수 있다.
  • 개발자는 third-party scripts가 어플리케이션 페이지성능에 영향을 줄지 확신하기 어렵다.

일반 <script> 태그는 몇가지 문제가 있다.

  • 어플리케이션이 커지면 third-party scripts의 로딩순서를 관리하기 어렵다
  • Streaming 이나 Suspense와 같은 페이지 성능을 향상시키는 기능과 호환되지 않는다.

Script 컴포넌트는 이러한 문제를 몇가지 로딩 전략을 통해 최적화한다.

import Script from 'next/script'

export default function Page() {
  return (
    <>
      <Script src="https://example.com/script.js" />
    </>
  )
}

Strategy

next/scriptstrategy property를 통해 로딩 동작을 조정할 수 있다.

  • beforeInteractive: Next.js 코드와 페이지가 하이드레이션 되기 전에 스크립트를 로드한다.
  • afterInteractive(default): 하이드레이션이 페이지에 일어나고난 직후에 스크립트를 로드한다.
  • lazyOnload: 브라우저 idle time에 스크립트를 lazy load한다.
  • worker(experimental): script 로드를 web worker에 맡긴다.

Event Handler

몇가지 event handler 속성으로 스크립트 컴포넌트 동작 이후 추가적인 코드를 실행할 수 있다.

  • onLoad: script가 로드되고 난 후 동작
  • onReady: script 로드되고 난 후와 컴포넌트가 마운트될 때마다
  • onError: 로드에 실패했을 때
import Script from 'next/script'

export default function Page() {
  return (
    <>
      <Script
        src="https://example.com/script.js"
        onLoad={() => {
          console.log('Script has loaded')
        }}
      />
    </>
  )
}

Routing

Next.js는 파일 시스템 기반 라우트 구조를 사용한다. pages 디렉토리에 추가된 파일들은 자동으로 접근 가능한 경로가 된다.

  • Index routes 디렉토리 내 index 파일은 디렉토리 루트 경로에 해당한다.
    • pages/index.js/
    • pages/blog/index.js → `
  • Nested routes 중첩된 폴더 구조 또한 routing 된다.
    • pages/board/settings.js/board/settings
  • Dynamic route segments [](bracket) 을 통해 dynamic route 또한 가능하다.
    • pages/item/[id].js/item/:id
    • pages/item/[...all].js/item/* *뒤로 어떠한 경로가 오든 match한다.

페이지간 이동

Next.js router는 Link 컴포넌트를 통해 페이지 간 이동에 SPA와 같은 client-side route 이동이 가능하게 한다.

Link 컴포넌트는 viewport에 노출되었을 시 SSG 페이지를 prefetch 한다. SSR 페이지의 경우 클릭 시 페이지 데이터를 fetch한다.

Dynamic route

dynamic route시 경로에 포함되는 dynamic parameter는 페이지에 query parameter로 전달되어 사용할 수 있다.

import { useRouter } from 'next/router'

const Post = () => {
  const router = useRouter()
  const { pid } = router.query

  return <p>Post: {pid}</p>
}

export default Post

Catch all route

[](bracket) 사이에 ... 을 추가하면 모든 경로를 커버할 수 있다.

  • pages/items/[...id].js/items/*.js 에 해당
  • 모든 쿼리 파라미터는 배열로 페이지에 전달된다. items/a/b{ “id”: [”a”, “b”] } 전달

Optional catch all route

[[...all]] catch all route를 이중 bracket으로 사용하면 optional catch all route가 된다.

  • optional의 경우 path가 없는 경우까지 커버한다. /items 또한 커버한다.

우선순위

  1. 미리 정의된 route
  2. dynamic route
  3. dynamic catch all route

next/router

client-side navigation은 링크 컴포넌트가 아닌 next/router 를 통해서도 가능하다.

import { useRouter } from 'next/router'

export default function ReadMore() {
  const router = useRouter()

  return (
		// router.push()에 전달한 경로로 이동
    <button onClick={() => router.push('/about')}>
      Click here to read more
    </button>
  )
}

Shallow routing

Shallow routing은 data fetching method(getServerSideProps, getStaticProps, getInitialProps)를 재실행하지 않고 URL을 변경할 수 있게 해준다. 기존의 상태를 유지하면서 URL만 변경하고 싶을 때 사용하면 될 것 같다.

사용법은 router에 shallow 옵션을 true로 전달하면 된다.

import { useEffect } from 'react'
import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  useEffect(() => {
		// 세번째 인자에 shallow option을 true로 전달
    router.push('/?q=asdf', undefined, { shallow: true })
  }, [])

  useEffect(() => {
		// q 쿼리 파라미터가 변경되었을 때
  }, [router.query.q])
}

위의 경우 URL은 변경되지만 페이지는 갱신되지 않는다. 즉, route 상태만 변경되었다.

💡 Note

shallow routing은 같은 페이지의 URL 변경에 대해서만 작동한다. 예를 들어 About 페이지가 있는데 다음과 같이 about페이지의 경로로 URL을 변경하면 About페이지에 대한 경로이기 때문에 shallow option을 주었어도 새 페이지로 변경하고 data fetching을 수행한다.

router.push('/?count=10', '/about?counter=10', {shallow:true})
💡 shallow routing은 페이지 리렌더링을 야기한다?

router.push든 router.replace든 일어나면 페이지가 리렌더링 된다고 한다. 이는 next/router가 Context API를 내부적으로 사용하기 때문에 router.* 를 실행하면 내부 상태 값이 변경되고 이는 리렌더링을 야기한다고 한다. (근데 왜 공식문서에 The URL will get updated to /?counter=10and the page won't get replaced 이렇게 되어있지…?)

해결 방법은 window.history.replaceState 를 사용하는 것이다.

window.history.replaceState(
	window.history.state,
	'',
	window.location.pathname + '?' + 'whatever=u_want',
)

https://yceffort.kr/2021/12/nextjs-lesson-and-learn

API Routes

API routes는 Next.js에서 serverless API 엔드포인트를 만들수 있게 해준다.

pages/api 내의 파일들은 페이지가 아닌 API 엔드포인트로 /api/* 경로로 매칭된다. 이 파일들은 server 측에서만 번들되기 때문에 클라이언트 측 번들크기에는 영향이 없다.

API route를 사용하기 위해서 파일 내에서 request handler함수를 export default 해야 한다. request handler는 다음의 두 파라미터를 전달받는다.

  • req: http.IncomingMessage의 인스턴스로 요청에 대한 객체
  • res: http.ServerResponse의 인스턴스로 응답에 대한 객체
// pages/api/user.js

// api/user의 요청에 대해 200 status code의 JSON 데이터를 응답한다.
export default function handler(req, res) {
	res.status(200).json({name: 'John Doe'});
}

API route에서 HTTP 메소드 각각에 대해서 다루기 위해서는 req.method 를 사용할 수 있다.

export default function handler(req, res) {
	// req.method에 따라 분기를 만든다.
  if (req.method === 'POST') {
    // POST 요청에 따른 작업
  } else {
    // 다른 HTTP 메소드
  }
}

Use case

  • 모든 API를 API Route를 통해 만들수도 있다.
  • 이미 API가 있는데 API route를 통해 다시 API요청을 하지 않아도 된다.
  • 만약 외부 API URL을 은닉화하고 싶으면 API route를 사용할 수 있다. /api/secrethttps://company.com/secret-url 대신사용
  • 외부 서비스에 안전하게 접근하기 위해 서버에서 환경변수를 사용할 때
  • API proxy 외부 API 호출전에 요청 및 응답객체에 접근해서 Authorization, CORS 등 여러 목적을 위한 프록시 역할 수행
💡 Note

Dynamic API routes

API routes 또한 pages와 같이 Dynamic routes를 지원한다. 예를 들어 pages/api/item/[id].js 의 경우 req.query 를 통해서 dynamic parameter를 사용할 수 있다.

// 요청 시 id값을 응답한다.
export default function handler(req, res) {
	const { id } = req.query;
	res.end(id);
}

index route 처리

index route를 처리하는 패턴으로 두 가지가 있다.

  1. 옵션1
    • /api/items.js : index route
    • /api/items/[id].js : dynamic route
  2. 옵션2
    • /api/items/index.js : index route
    • /api/items/[id].js: dynamic route

Catch all route

pages와 같이 catch all route([…all]) 과 optional catch all route([[…all]]) 둘 다 사용 가능하다. 라우트 우선순위 또한 pages와 동일하다.

API routes request helper

API routes는 요청(req)을 해석하는 내장 request helpers(미들웨어)를 제공한다.

  • req.cookies 요청에 포함된 cookie값을 포함하는 객체
  • req.query query string정보를 포함하는 객체
  • req.body body가 전달되지 않았을 땐 null or body 정보를 포함하는 객체

Custom config

모든 API Route는 config 객체를 export 할 수 있다. config를 사용하면 기본 설정을 변경할 수 있다.

export const config = {
	// 이런식으로 기본설정 custom 가능
	api: {
		bodyParser: {
			sizeLimit: '1mb'
		}
	}
}
  • api 객체에 모든 config option을 설정할 수 있다.
  • bodyParser는 기본 enabled상태. body를 Stream 이나 raw-body 로 사용하고 싶으면 false로 변경하면 된다.
  • bodyParser.sizeLimit는 parsed body의 최대 크기(byte단위)를 설정한다.
  • externalResolver 는 경로가 외부 resolver에 의해 처리되고 있음을 알리는 플래그로 이 옵션을 사용하면 확인되지 않은 요청에 대한 경고가 비활성화된다.
  • responseLimit는 기본 enabled 상태(응답 body 크기가 4MB를 넘으면 경고)
    • 서버리스 환경에서 Next.js를 사용하지 않고 CDN 또는 전용 media host를 사용하지 않을 때의 성능 영향을 이해하는 경우 이 제한을 false로 설정할 수 있다. (흠 뭔소린지 모르겠음..)
    • bytes 크기로도 설정 가능

API routes response helper

response 객체는 Express와 유사한 method set을 제공한다.

  • res.status() 응답코드 정의
  • res.json() JSON 데이터 전송
  • res.send() HTTP응답 전송 string or object or Buffer
  • res.redirect() Redirect 수행
  • res.revalidate() SSG 페이지를 재생성한다.
export default async function handler(req, res) {
	// 일반적으로 하는 방식이랑 똑같이 사용할 수 있다
  try {
    const result = await requestSomething();
    res.status(200).send({ result })
  } catch (err) {
    res.status(500).send({ error: 'error occurred' })
  }
}

Typing

handler를 type-safe하게 사용할려면 NextApiRequestNextApiResponse 타입을 사용할 수 있다.

import type { NextApiRequest, NextApiResponse } from 'next'

type ResponseData = {
  // 응답 데이터 type
}

export default function handler(
  req: NextApiRequest,
	// 응답 데이터 type을 generic으로 전
  res: NextApiResponse<ResponseData>
) {
  res.status(200).json(응답데이터)
}
💡 Note

NextAPIRequest의 body는 클라이언트가 어떤 데이터 타입의 페이로드를 전달할지 모르니 any 타입이다. 따라서 런타임에 타입을 검사해서 사용해야 한다.

Edge API routes

Edge API routes를 사용하면 기존 Node.js 기반 런타임보다 가볍고 빠른 환경을 제공한다. edge runtime을 활성화 시키고 싶으면 라우트에서 runtime config를 export 하면 된다.

export const config = {
  runtime: 'edge',
}

export default function handler(req) {
	// edge runtime에서 지원하는 API인 Response로 응답하는듯?
	return new Response('Hello world!')
}

Edge API routes는 Edge Runtime을 사용하고 API routes는 노드js 런타임을 사용한다.

Edge API routes는 표준 web API를 기반으로 만들어졌기 때문에 몇 가지 제약사항이 존재하나 서버에서 응답을 스트리밍하고 캐시된 파일에 접근한 후 실행된다. 서버 측 스트리밍은 빠른 TTFB(Time To First Byte)로 성능을 향상시킬 수 있다.

💡 Edge Runtime

Edge runtime에서 지원하는 API와 지원하지 않는 API는 공식문서에서 확인해 볼 수 있다.

https://nextjs.org/docs/api-reference/edge-runtime

Authentication

Next.js는 여러개의 인증 패턴(use case가 각각 다름)을 지원한다.

  • SSG를 사용해 로딩상태를 서버측에서 렌더링하고 client-side에서 데이터를 fetching한다.
  • 유저 데이터를 server-side에서 fetch해서 미인증 콘텐츠가 깜박이는 현상을 방지한다.

https://nextjs.org/docs/authentication 코드는 여기서 보자

SSG 인증

next.js는 getServerSidePropsgetInitialProps가 존재하지 않으면 페이지를 자동으로 정적이라고 판단한다. 대신 페이지가 서버측에서 로딩상태를 렌더링하고 클라이언트 측에선 데이터를 가져올 수 있다.

이러한 패턴은 페이지가 CDN으로부터 제공되고 next/link에 의해 preload될 수 있어 TTI가 빨라진다.

SSR 인증

만약 getServerSideProps를 사용하는 SSR 페이지일 경우 getServerSideProps 에서 유저 데이터를 props로 페이지에 전달해주어 로딩상태를 표시하지않고 바로 콘텐츠를 표시할 수 있다. 이 때 인증과정으로 인해 렌더링이 지연될 수 있으니 인증 조회가 빠른지 확인하고 지연이 발생할 시 SSG를 고려해보아야 한다.

Custom App

Next.js는 App 컴포넌트를 사용하여 페이지를 초기화한다. App을 커스텀하면 다음의 것들이 가능하다.

  • 페이지 이동시 유지되는 레이아웃 추가
  • 페이지 이동간에 상태 유지
  • 페이지에 추가적인 데이터 삽입
  • 전역 스타일 추가

App을 커스텀하려면 /pages/_app.js 를 작성한다.

// Component prop = 활성화된 페이지 컴포넌트
// pageProps = data fetching을 통해 preload된 props 객체
export default function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

Custom Document

커스텀 Document 를 통해 <html><body> 태그를 수정할 수 있다. 다큐먼트 파일은 서버에서만 렌더링 되기 때문에 이벤트 핸들러는 사용이 불가능하다.

Document를 override하기 위해서 pages/_document.js 파일을 생성한다.

import { Html, Head, Main, NextScript } from 'next/document'

export default function Document() {
  return (
		// 여기서 custom한다.
    <Html>
      <Head />
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}
💡 Note
  • <Head /> 컴포넌트는 next/head와 별개이다. <Head /> 는 모든 페이지에 공통인 head태그만을 조작할 수 있다. 다른 경우에는 각 페이지에서 next/head를 사용하는것이 옳다.
  • <Main /> 바깥의 리액트 컴포넌트는 브라우저에서 초기화되지 않으므로 로직이나 CSS를 추가해선 안된다.

Custom Error Page

Next.js는 404페이지를 기본적으로 정적생성해 제공한다. 만약 커스텀 404페이지를 제공하고 싶다면 pages폴더 아래에 404.js 파일을 생성해서 커스텀할 수 있다.

500 페이지도 동일하다. 만약 커스텀 500페이지를 생성하고 싶다면 500.js 파일을 생성하면 된다.

500 에러의 경우 클라이언트와 서버에서 Error 컴포넌트에 의해 다뤄진다. Error 컴포넌트를 override할려면 _error.js파일을 생성해 커스텀하면 된다.

Middleware

Middleware는 요청이 완료되기 전에 특정 코드를 실행할 수 있게 해준다. 이를 통해 rewrite, redirect, 요청 응답 헤더 수정 등의 작업을 할 수 있다.

  • 페이지 렌더링 전 인증 확인 or 요청 확인
  • 요청 데이터를 사전에 처리
  • 특정 API 요청을 수행하고 캐시를 관리
  • 요청에 대한 응답 변환 및 에러 처리

일단 쓸려면 next latest버전을 설치해야 한다.

npm install next@latest

middleware.ts 파일을 pages와 같은 레벨에 위치하게 생성한다.

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

// 비동기 처리를 해야 하면 async 함수로 쓰자
export function middleware(request: NextRequest) {
	// redirect 처리 등등 추가적인 동작 실행 가능
  return NextResponse.redirect(new URL('/about-2', request.url))
}

// 일치하는 경로에 대한 설정
export const config = {
  matcher: '/about/:path*',
}

Matching Paths

미들웨어는 모든 라우트에서 실행된다.

1. next.config.js의 `headers`, `redirects`
2. Middleware
3. nextjs.config.js의 `beforeFiles`
4. 파일 시스템의 모든 파일 (`public`, `_next/static/, 페이지들)
5. nextjs.config.js의 `afterFiles`
6. Dynamic 라우트들 (ex: `/card/[cardId]`)
7. nextjs.config.js의 `fallback`

미들웨어를 실행할 경로를 특정하기 위해서 다음의 두 방법이 있다.

  1. Custom matcher 설정
  2. 조건문

Matcher

matcher를 통해 특정한 경로에서 미들웨어가 실행되게 필터링할 수 있다.

export const config = {
	// 배열로 여러개의 경로도 설정 가능
  // matcher: ['/about/:path*', '/dashboard/:path*'],
	// 정규표현식도 사용가능
	// matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)'
  matcher: '매칭할 경로'
}
  • path는 / 로 시작해야 한다.
  • /about/:path 와 같이 parameter를 가질 수 있다. path 값은 동적인 값
  • /about/:path* 처럼 * 를 파라미터 뒤에 붙이면 0개 이상의 sub path가 올 수 있다. ? 는 0 or 1, + 는 1개이상
  • 정규표현식 또한 () 안에 삽입해서 사용가능하다. Ex) /about/(.*)

조건문

export function middleware(request: NextRequest) {
	// 내부에서 조건문으로 분기처리 하는 방법
  if (request.nextUrl.pathname.startsWith('/about')) {
    return NextResponse.rewrite(new URL('/about-2', request.url))
  }

  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.rewrite(new URL('/dashboard/user', request.url))
  }
}

NextResponse

NextResponse API 를 통해 다음의 동작을 수행할 수 있다.

  • 요청을 다른 URL로 리다이렉트
  • 요청에 대한 경로는 유지하고 rewrite하는 URL의 응답을 보여줌
  • API routes, getServerSideProps, rewrite될 페이지의 요청 헤더 설정
  • 응답 쿠키 설정
  • 응답 헤더 설정

미들웨어에서 response하기 위한 방법 두가지

  1. response를 반환하는 라우트로 rewrite
  2. 직접 NextResponse로 응답 반환

기타

이외에도 여러가지 처리가 가능함.

  1. 쿠키 처리

    요청, 응답 객체에 cookies 객체를 통해 편리하게 쿠키 처리 가능

  2. 헤더

    NextResponse API를 통해 응답/요청 헤더 설정 가능

  3. middleware flag

    skipMiddlewareUrlNormalizeskipTrailingSlashRedirect 두 가지 플래그를 사용해 고급 처리를 할 수 있다.

    • skipTrailingSlashRedirect 는 URL 마지막 슬래시 추가 or 제거에 대한 Next.js의 기본 리다이렉트 기능을 끄고 미들웨어 내에서 사용자 정의 처리를 할 수 있게 한다. 일부 경로에서 마지막 슬래시를 유지하고 다른 경로에선 제거할 수 있어 점진적 마이그레이션을 쉽게 할 수 있다.
    • skipMiddlewareUrlNormalize는 클라이언트 이동 및 직접 방문을 처리할 때 Next.js가 수행하는 URL 정규화를 비활성화하여 원본 URL을 사용해 전체 처리가 필요할 때 사용 (언제 쓰지?.. 감이 안잡힌다..)

Error Handling

개발환경에서 런타임 오류 발생 시 Next.js는 오류 오버레이 화면을 띄운다.(only development에서만)

라잌 디스

Untitled

Server 오류

일단 Next.js가 기본적으로 정적 페이지인 500페이지를 제공하는데 pages/500.js 파일 커스텀해서 작성하면 이걸로 뜬다.

Client 오류

클라이언트 측 오류를 처리할 때 React Error boundary를 사용하길 추천하고 있다. Error boundary쓰면 페이지가 터지지 않고 custom fallback UI를 보여주며 에러 로그도 찍을 수 있으니 꿀이라고 한다.

Error boundary 쓸려면 클래스 컴포넌트로 작성해야 한다고 한다. 그리고 만든걸로 pages/_app.js 의 페이지 컴포넌트를 감싸주자.

  • 이러면 오류 발생시 어플리케이션 상태를 회복시킬 수 있다.
  • 에러가 발생했을때 fallback UI를 보여준다.
  • 에러 정보 로그도 가능
  • 에러를 한곳에서 에러바운더리에다가 위임해서 처리할 수 있다.

Security Headers

어플리케이션 보안을 향상할려면 모든 라우트에 HTTP 응답헤더를 적용하기 위해 next.config.jsheaders 설정을 사용하라고 한다.

// next.config.js

// 뭐 이런식임
// 여기 사용할 헤더 추가
const securityHeaders = []

module.exports = {
  async headers() {
    return [
      {
	      // source가 헤더를 적용할 라우트
				// 이렇게하면 모든 route에 적용된다.
        source: '/:path*',
        headers: securityHeaders,
      },
    ]
  },
}

X-DNS-Prefetch-Control

DNS prefetching을 제어해서 브라우저가 외부링크, 이미지, CSS, JS 등에서 도메인 명 확인을 사전에 할 수 있게 한다. 백그라운드에서 수행되므로 참조된 항목이 필요할 때까지 DNS가 해결될 가능성이 높아서 링크 클릭시 지연이 줄어든다 꿀👍

{
  key: 'X-DNS-Prefetch-Control',
  value: 'on'
}

Strict-Transport-Security

HTTP 대신 HTTPS를 사용하여 접근해야 함을 브라우저에 알린다. 이렇게 하면 HTTP를 통해서만 제공될 수 있는 페이지 또는 하위 도메인에 대한 접근이 차단된다. 참고로 Vercel에 배포하면 이거 설정안해도 알아서 다해준다고 한다.

{
  key: 'Strict-Transport-Security',
  value: 'max-age=63072000; includeSubDomains; preload' // max age 2년동안 HTTPS 사용, sub domain 포함
}

X-XSS-Protection

페이지가 XSS 공격을 탐지하면 페이지 로드를 중지한다. CSP를 사용하면 인라인 자바스크립트 사용을 금지해 안써도되는데 CSP 지원안하는 웹 브라우저에 대해서 보호 기능을 제공할 수 있다고 한다.

{
  key: 'X-XSS-Protection',
  value: '1; mode=block'
}

X-Frame-Options

iframe내에서 사이트를 표시할 수 있는지 여부를 나타낸다. 클릭 재킹 공격을 방지할 수 있다고 한다. 최신 브라우저에서 더 나은 지원을 제공하는 CSP의 frame-ancestors 옵션으로 대체됐다고 한다.

Permissions-Policy

브라우저에서 사용할 수 있는 기능과 API를 제어할 수 있다. https://github.com/w3c/webappsec-permissions-policy/blob/main/features.md 여기서 permission option들을 볼 수 있다.

{
  key: 'Permissions-Policy',
  value: 'camera=(), microphone=(), geolocation=(), browsing-topics=()'
}

X-Content-Type-Options

Content-Type 헤더가 설정 안된 콘텐츠의 경우 브라우저가 추측하는것을 방지한다. 이렇게 하면 사용자가 파일을 업로드하고 공유할 수 있는 웹 사이트에 대한 XSS 공격을 방지할 수 있다. 예를 들어 사용자가 이미지를 다운로드하려는데 실행파일(악성파일일 수도 있음)같은 다른 Content-Type으로 취급되는 경우. 브라우저 확장에도 적용된다함 이 헤더에 유효값은 nosniff 밖에 없다.

{
  key: 'X-Content-Type-Options',
  value: 'nosniff'
}

Referrer-Policy

현재 웹사이트(오리진)에서 다른 사이트로 이동할 때 얼마나 많은 정보를 브라우저가 포함할 수 있는지 제어한다. https://scotthelme.co.uk/a-new-security-header-referrer-policy/ 여기서 option들을 볼 수 있다.

Content-Security-Policy

XSS, 클릭 재킹 및 기타 코드 주입 공격을 방지하는 데 도움이 된다. CSP(Content Security Policy)는 스크립트, 스타일시트, 이미지, 글꼴, 개체, 미디어(오디오, 비디오), iframe 등을 포함한 콘텐츠에 대해 허용된 오리진을 지정할 수 있다.

https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP 여기서 옵션을 살펴볼 수 있다.

// 템플릿 리터럴로 이렇게 만들어놓고
// self같은건 ''이걸로 감싸야함
const ContentSecurityPolicy = `
  default-src 'self';
  script-src 'self';
  child-src example.com;
  style-src 'self' example.com;
  font-src 'self';  
`

{
  key: 'Content-Security-Policy',
	// 요런식으로 주입하자 개행을 공백으로 바꾸어서 붙여줌
  value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim()
}
profile
FE 개발자를 꿈꾸고 있습니다 :)

3개의 댓글

comment-user-thumbnail
2023년 6월 25일

이놈이

1개의 답글