Next.js app Router 기능정리

이수빈·2024년 4월 3일
1

Next.js

목록 보기
12/15

Server vs Client 컴포넌트

  • Next.js에서는 서버 컴포넌트(Server Component)와 클라이언트 컴포넌트(Client Component)를 구분해 코드 일부가 서버 혹은 클라이언트에서 출력될 수 있도록 만들 수 있습니다.

  • 기본적으로 생성하는 모든 컴포넌트는 서버 컴포넌트입니다!
    클라이언트 컴포넌트로 변경/사용하려면 다음과 같이 컴포넌트 최상단에 'use client' 선언이 필요하고, 해당 선언이 없으면 서버 컴포넌트입니다.

  • 클라이언트 컴포넌트 또한 일부 정적 요소는 서버에서 렌더링합니다.
    따라서 클라이언트 컴포넌트는 '서버 + 클라이언트'의 하이브리드 컴포넌트로 이해해야 합니다!

  • 기본적으로 컴포넌트는 서버컴포넌트로 만들고, 꼭 필요한 것들만 클라이언트 컴포넌트로 만드는 것이 좋다!!

클라이언트 컴포넌트 또한 일부 정적 요소는 서버에서 렌더링합니다.
따라서 클라이언트 컴포넌트는 '서버 + 클라이언트'의 하이브리드 컴포넌트로 이해해야 합니다!

  • 클라이언트 컴포넌트도 서버 컴포넌트를 포함하는 개념임 !!! 화면에서 보여주는 것은 서버에서 렌더링해서 클라이언트로 보내주는 형식임.

서버 컴포넌트와 클라이언트 컴포넌트는 다음과 같이 사용할 수 있는 일부 API가 다릅니다.
이런 구분을 통해, 서버 컴포넌트에서는 보안이나 캐싱, 성능, SEO 등의 이점을, 클라이언트 컴포넌트에서는 상호작용('click', 'load' 이벤트 등), 브라우저 API(window, document 등) 활용 등의 이점을 얻을 수 있습니다.

서버 컴포넌트만 사용:
cookies
headers
redirect
...

클라이언트 컴포넌트만 사용:
useState
useEffect
onClick
onChange
useRouter
useParams
...

  • 브라우저 api를 사용할때는 클라이언트 컴포넌트 사용

  • use client 선언이 없다면 ? => 기본적으로 서버 컴포넌트임, 클라이언트에서만 사용할 수 있는 hook들이 존재함..

서버 컴포넌트 => 서버에서 렌더링 됨.
정적 HTML 렌더링한거 전달한거임

https://dev.to/hryggrbyr/using-slots-in-react-pcp

  • next.js에서는 axios 대신 fetch 함수 사용 => next에서 자체적으로 mapping함 (추가기능이 존재함)
  • fetch함수로 가져온 데이터 => 캐싱가능함. 오..
  • 클라이언트에 넘겨서 처리해야 하는 부분이 있고, 서버에서 해야 하는게 있다.

  • 서버사이드에서 렌더링 할 수 있는 부분은 최대한 서버에서 활용하는게 좋다.

비동기 서버 컴포넌트

  • 서버에서 데이터처리를 하기 때문에, 데이터가 완벽하게 받아오기 전까진 클라이언트에게 UI를 전달하면 안된다. 이럴때 async 키워드 사용함.

  • 컴포넌트에 async 키워드를 붙일 수도 있다, 서버컴포넌트 => 이러면 비동기 컴포넌트가 됨 (서버 컴포넌트)

  • 클라이언트에서는 비동기 컴포넌트 사용을 권장하지 않는다. => Promise를 반환하는데에 처리하는 시간이 들기 때문!

  • 에러 컴포넌트는 클라이언트에서 렌더링이 되어야 한다.

github 공식문서 참조
https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md#why-cant-client-components-be-async-functions

type Movies = {
  Title: string
  imdbId: string
}[]

export default async function Movies() {
  await new Promise(resolve => setTimeout(resolve, 3000))
  const endPoint = 'https://omdbapi.com/?apikey=7035c60c&s=avengers'
  const response = await fetch(endPoint)
  const { Search: movies }: { Search: Movies } = await response.json()

  console.log(movies)
  return (
    <ul>
      {movies.map(movie => (
        <li key={movie.imdbId}>{movie.Title}</li>
      ))}
    </ul>
  )
}

Link태그

  • Link태그 => 로컬에서는 캐싱안함, production 환경에서만 동작함

  • intersection observer와 같이 사용자가 스크롤들 내렸을때, Link태그를 만나면 이동에 필요한 파일들을 알아서 가져옴(알아서 최적화 해준다는 의미임) => prefetch 속성을 통해 최적화 !!

  • null (default) : 정적경로일때는 전부 가져오지만, 동적경로일때는 loading.tsx가 있는 가장 가까운 세그먼트까지 가져옴(선택적으로 가져온다)
  • true : 동적경로일때도 모두 가져온다.
  • false : 미리 가져오지 않는다.

(공식문서)

prefetch
Prefetching happens when a component enters the user's viewport (initially or through scroll). Next.js prefetches and loads the linked route (denoted by the href) and its data in the background to improve the performance of client-side navigations. Prefetching is only enabled in production.

  • null (default): Prefetch behavior depends on whether the route is static or dynamic. For static routes, the full route will be prefetched (including all its data). For dynamic routes, the partial route down to the nearest segment with a loading.js boundary will be prefetched.
  • true: The full route will be prefetched for both static and dynamic routes.
  • false: Prefetching will never happen both on entering the viewport and on hover.

프로그래밍 방식의 탐색

  • Link 를 사용하지 않고 useRouter를 사용해 페이지 이동을 구현할 때

  • router.prefetch로 prefetch기능 구현가능

  useEffect(() => {
    router.prefetch('/movies')
  }, [router])

비동기 컴포넌트 스트리밍

  • SeverSide Rendering방식에서 Suspense를 함께 적용하면, 컴포넌트를 chunk 단위로 렌더링 할 수 있다.
export default async function A() {
  await new Promise(resolve => setTimeout(resolve, 5000))
  return <h2>컴포넌트 A</h2>
}
export default async function B() {
  await new Promise(resolve => setTimeout(resolve, 2000))
  return <h2>컴포넌트 B</h2>
}
import A from './A'
import B from './B'

export default async function AsyncPage() {
  await new Promise(resolve => setTimeout(resolve, 3000))
  return (
    <>
      <A /> 5초걸림
      <B /> 2초걸림 
    </>
  )
}
  • Suspense가 없다면 모든 컴포넌트가 렌더링이 완료된 후 서버에서 클라이언트로 완료된 UI를 보낸다.

  • 하지만 Suspense를 Wrapping해서 사용하면 각각의 컴포넌트가 렌더링이 완료되는 대로 서버사이드에서 클라이언트에게 완성된 UI를 보낼 수 있다.

export default async function Async() {
  await new Promise(resolve => setTimeout(resolve, 3000))

  return (
    <>
      <h1>Async Page</h1>
      <Suspense fallback={<Loading />}>
        <A />
      </Suspense>
      <Suspense fallback={<Loading />}>
        <B />
      </Suspense>
    </>
  )
}

데이터 가져오기 및 캐시

  • Next.js는 기본 fetch Web API를 확장해서, 서버에서 사용 가능하며 다양한 요청을 캐싱하거나 재검증하는 등의 추가 동작을 사용할 수 있다.

  • endpoint가 같으면, 결과가 캐싱되기 때문에 항상 같은 결과를 반환함. 캐싱을 비활성화 하려면 fetch 함수의 cache 옵션을 사용할 수 있음.

  • delay 관련 revalidate가능

Next Image 태그

  • 이미지를 캐싱해서 가져오게됨, width, height값 필수
<Image
      src={movie.Poster}
      alt={movie.Title}
      width="300"
      height="450"
    />

Next 폰트

  • Next.js는 지원하는 모든 글꼴 파일에 대한 자체 호스팅(automatic self-hosting)이 내장되어 있다.

  • 모든 Google Fonts를 자체 호스팅을 제공함, api요청을 하지 않는다.

import { Roboto, Oswald } from 'next/font/google'

const roboto = Roboto({
  subsets: ['latin'], // 사용할 폰트 집합
  weight: ['400', '700'], // 사용할 폰트 두께
  display: 'swap', // 폰트 다운로드 전까지 기본 폰트 표시(성능 최적화)
  variable: '--font-roboto' // 사용할 CSS 변수 이름
})
const oswald = Oswald({
  subsets: ['latin'],
  weight: ['500'],
  display: 'swap',
  variable: '--font-oswald'
})
export { roboto, oswald }

메타데이터

  • 정적인 메타데이터는 상관없는데, 동적인 메타데이터는 generateMeta함수를 사용해야 한다.

  • generateMeta함수는 페이지 컴포넌트의 인수를 기본적으로 params로 받는다.

  • 함수 내부적으로 data를 한번 더 호출해서 받아야한다. caching 작업이 있어서 괜찮다.

import { Metadata } from 'next'
 
// either Static metadata
export const metadata: Metadata = {
  title: '...',
}
 
// or Dynamic metadata
export async function generateMetadata({ params }) {
  return {
    title: '...',
  }
}

export async function generateMetadata({
  params,
  searchParams
}: Context) {
  const movie = await fetchMovie(params.movieId, searchParams.plot)
  return {
    title: movie.Title,
    description: movie.Plot,
    openGraph: {
      title: movie.Title,
      description: movie.Plot,
      images: movie.Poster,
      url: `https://nextjs-movie-app-steel.vercel.app/movies/${movie.imdbID}`,
      type: 'website',
      siteName: 'Nextjs Movie App',
      locale: 'ko_KR'
    }
  }
}
  • 달라지는 메타데이터에 템플릿도 제공 가능하다.
export const metadata: Metadata = {
  title: {
    template: '%s | 토스',
    default: '토스'
  },
  description: 'Generated by create next app'
}

환경변수

  • 각 컴포넌트에서 process.env.변수이름으로 접근 가능한 환경변수는 /.env.local 파일에서 관리하며, 기본적으로 서버 컴포넌트에서만 접근할 수 있습니다.

  • 만약 클라이언트 컴포넌트에서도 접근하도록 만들고 싶다면, 변수 이름에 NEXTPUBLIC을 접두사로 추가해야 합니다

  • 보안이 중요한 경우 => (api key같은 민감한 정보) 서버에서 다뤄야 한다. (요청시 노출되기 때문)

OMDB_API_KEY=7035c60c
NEXT_PUBLIC_SITE_NAME=Nextjs Movie App

경로

경로그룹

  • 소괄호( () ) 를 이용해 url경로에 영향을 주지 않는 폴더 (그룹)을 만들 수 있다.

  • 이들은 각각의 layout을 가질 수 있기 때문에 경로에 맞는 여러 레이아웃을 제공 할 수 있다.

경로 병렬처리

  • 경로 병렬 처리

  • @ 접두사의 폴더는 URL 경로에 영향을 주지 않는 페이지로, 하나의 레이아웃에서 동시에 처리(Parallel Routes)할 수 있습니다.

  • 이는 '이름을 가진 슬롯' 방식으로, page.tsx 컴포넌트가 같은 레벨 layout.tsx의 children Prop으로 전달되는 것처럼, @abc/page.tsx 컴포넌트는 layout.tsx의 abc Prop으로, @xyz/page.tsx 컴포넌트는 xyz Prop으로 전달

  • 이러한 구조의 폴더구조가 있을때

├─app
│  ├─async
│  │  ├─@abc
│  │  │  ├─loading.tsx
│  │  │  └─page.tsx
│  │  ├─@xyz
│  │  │  ├─loading.tsx
│  │  │  └─page.tsx
│  │  ├─layout.tsx
│  │  ├─loading.tsx
│  │  └─page.tsx
  • 여기서 @가 붙은 폴더들은 각각의 경로를 생성하지 않고 병렬 처리된다. 또한 아래처럼 공통 페이지 레이아웃의 prop으로 전달되어 사용 할 수 있다
export default function Layout({
  children,
  abc,
  xyz
}: {
  children: React.ReactNode
  abc: React.ReactNode
  xyz: React.ReactNode
}) {
  return (
    <>
      {children}
      {abc}
      {xyz}
    </>
  )
}

경로가로채기

  • 현재 레이아웃에서 다른 url 경로를 출력가능하다.

  • (..) 경로가로채기의 이름 규칙 => 폴더가 아닌 세그먼트 기준으로 한다. 즉 파일시스템이 아니라 라우팅이 되는 기준이다!!

  • @으로 시작한 slot들은 경로탐색기준에 포함되지 않는다.

/app/a/b/(.)x => /a/b/x 같은 레벨 세그먼트
/app/a/b/(..)x => /a/x 상위 레벨 세그먼트
/app/a/b/(...)x => /x 루트 레벨 세그먼트

  • 아래와 같은 폴더 구조를 가진 프로젝트가 있다고 할때, a/b/c경로에서 루트에 있는 x경로를 가로채려고 한다.
├─app
│  ├─a
│  │  └─b
│  │     └─c
│  │        ├─@xWrap
│  │        │  ├─(...)x
│  │        │  │  └─page.tsx
│  │        │  └─page.tsx
│  │        ├─layout.tsx
│  │        └─page.tsx
│  └─x
│    └─page.tsx
  • a/b/c 경로에 C페이지에는 아래와 같이 x경로로 갈 수 있는 Link가 존재한다.
import Link from 'next/link'

export default function CPage() {
  return (
    <>
      <h1>C Page!</h1>
      <Link href="/x">goto X</Link>
    </>
  )
}
  • 여기서 X page 링크를 누르면, /x경로로 이동하지만, (...x)폴더, 즉 경로를 가로채는 폴더가 존재하기 때문에 여기를 타고 이동하게 된다.

  • 이때 현재 레이아웃에서 가로챈 경로를 출력하기 때문에 병렬처리를 해줘야한다. (url은 이동하지만, 레이아웃은 개발자가 현재 레이아웃 + 의도한대로 조작 가능하다는 뜻 !!)

  • 가로챈 경로를 출력하려면 병렬처리가 필요하다. => 이게 @Xwrap의 역할이다.

  • 공식문서에 따르면, modal을 만들때 url을 통해 전달할 수 있고 새로고침이 되어도 context를 유지 할 수 있다.

  • 경로를 통해 모달을 처리하면, 뒤로가기 버튼을 눌렀을 때 또한 모달을 닫을 수 있다.

export default function CLayout({
  children,
  xWrap
}: {
  children: React.ReactNode
  xWrap: React.ReactNode
}) {
  return (
    <>
      {children}
      {xWrap}
    </>
  )
}

공식문서 : https://nextjs.org/docs/app/building-your-application/routing/intercepting-routes

Next server

  • api폴더아래 route라는 이름의 파일로 사용, 각각 응답 메소드명이 정해져 있다.

  • queryString, params를 받는 방법이 다름

  • 이전 버전의 API_ROUTES와 동일한 기능임.

export async function GET(request: NextRequest) {
  const queryString = request.nextUrl.searchParams
  const limit = queryString.get('limit')

  const res = await fetch('https://api.heropy.dev/v0/users')
  const { users }: ResponseValue = await res.json()

  return Response.json(users.slice(0, Number(limit)))
}

// 포로토콜+도메인/api/users?limit=3

}

export async function GET(request: NextRequest, context: Context) {
  const userId = context.params.id
  const res = await fetch('https://api.heropy.dev/v0/users')
  const { users }: ResponseValue = await res.json()

  return Response.json(users.find(user => user.id === userId))
}

// 포로토콜+도메인/api/users?/userId='xxx'


ref) 블로그 : https://www.heropy.dev/p/n7JHmI

profile
응애 나 애기 개발자

0개의 댓글