반응형
반응형

📝분석 (모니터링)

/** _components/web-vitals.tsx **/
'use client'
import { useReportWebVitals } from 'next/web-vitals'

export function WebVitals() {
    useReportWebVitals((metric) => {
        console.log(metric)
    })
}

/** /app/layout.tsx **/
import { WebVitals } from './_components/web-vitals'
 
export default function Layout({ children }) {
  return (
    <html>
      <body>
        <WebVitals />
        {children}
      </body>
    </html>
  )
}

Next.js에는 성능 지표 측정 및 보고 기능이 내장되어 있습니다

 

  1. Time to First Byte (TTFB)
    • 웹 페이지 요청 후 서버가 첫 번째 바이트를 브라우저에게 전송하는 데 걸리는 시간입니다 클라이언트가 서버로부터 첫 데이터를 받기까지의 대기 시간을 측정하여 네트워크 및 서버 성능을 평가하는 데 사용됩니다.
  2. First Contentful Paint (FCP)
    • 사용자가 웹 페이지를 열고 첫 번째 콘텐츠가 화면에 나타나기 시작하는 시점입니다. 일반적으로 텍스트나 이미지와 같은 가장 먼저 렌더링되는 콘텐츠를 말합니다.
  3. Largest Contentful Paint (LCP)
    • 웹 페이지에서 가장 큰 콘텐츠 요소가 화면에 나타나는 데 걸리는 시간입니다. 주로 이미지나 텍스트 블록과 같은 큰 콘텐츠를 측정하여 사용자 경험을 평가하는 데 사용됩니다.
  4. First Input Delay (FID)
    • 사용자가 페이지와 상호 작용(클릭, 터치 등)하고 브라우저가 실제로 이벤트에 응답하기까지 걸리는 시간입니다. 주로 사용자와의 상호 작용에 대한 웹 페이지의 반응성을 측정하는 데 사용됩니다.
  5. Cumulative Layout Shift (CLS)
    • 페이지가 로드되는 동안 레이아웃이 얼마나 불안정한지를 측정하는 지표입니다. 주로 이미지나 광고와 같은 동적으로 로드되는 콘텐츠가 페이지의 레이아웃을 어떻게 변경하는지를 평가하여 사용자가 의도치 않게 클릭하거나 다른 동작을 할 때의 불편함을 측정합니다
  6. Interaction to Next Paint (INP)
    • 사용자 상호 작용 후 다음 렌더링이 시작되기까지의 시간을 나타냅니다. 사용자가 웹 페이지와 상호 작용한 후 얼마나 빨리 새로운 콘텐츠나 업데이트가 화면에 나타나는지를 측정하여 사용자 경험을 평가합니다.

 

결과를 모든 엔드포인트로 보내 사이트의 실제 사용자 성과를 측정하고 추적할 수 있습니다

 

 

📝Typescript

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    typedRoutes: true,
  },
}
 
module.exports = nextConfig

next/link에서 오타 방지 기능 추가 가능

 

📝ESLint

Next.js는 ESLint 플러그인(@next/eslint-plugin-next)을 제공하는데 기본적으로 이러한 기능들이 적용 되어있다

 

📝환경 변수

/** .env **/
DB_HOST=localhost
DB_USER=myuser
DB_PASS=mypassword

TWITTER_USER=nextjs
TWITTER_URL=https://twitter.com/$TWITTER_USER

/** API.tsx **/
export async function GET() {
  const db = await myDB.connect({
    host: process.env.DB_HOST,
    username: process.env.DB_USER,
    password: process.env.DB_PASS,
  })
  // ...
}
  • Next.js에는 .env.local에서 process.env로 환경 변수를 로드하는 기능이 내장되어 있습니다
  • env.local을 통해 로드 된 모든 환경 변수는 Node.js에서만 사용할 수 있는데 브라우저에 변수를 사용하려면 변수 앞에 NEXT_PUBLIC_을 붙여야합니다
  • .env.test 파일에서도 동일한 작업을 수행할 수 있습니다 테스트 목적으로만 특정 환경 변수를 설정해야 하는 jest 또는 cypress와 같은 도구를 사용하여 테스트를 실행할 때 유용합니다

 

📝상대경로 , 절대경로, 경로 별칭

상대경로, 절대경로, 경로 별칭도 사용이 가능하다

 

📝MDX

마크다운을 처리할 수 있는 노드 패키지도 지원한다

반응형
반응형

📝Lazy Loading

렌더링하는 데 필요한 JavaScript의 양을 줄여 애플리케이션의 초기 로딩 성능을 향상시키는 데 도움이 됩니다 → 해당 라이브러리가 필요할 때만 클라이언트 번들에 포함할 수 있습니다 [청크 분리]

 

Next.js에서 지연 로딩 구현하는 방법은 두가지이다

  1. next/dynamic → React.lazy() + Suspense가 합쳐진 기능
  2. React.lazy() [코드스플리팅 + 지연로딩] + Suspense [로딩 기능] → 리액트에서 사용하는 방식

 

예제 코드

/**  page.tsx **/
'use client'

import dynamic from "next/dynamic";

import {useState} from "react";

export default function Home() {

  const ComponentA = dynamic(() => import('../components/A'),{loading: () => <div>A Is Loading</div>})
  const ComponentB = dynamic(() => import('../components/B'))
  const ComponentC = dynamic(() => import('../components/C'), { ssr: false, loading: () =><div>C Is Loading</div> })

  const [showMore, setShowMore] = useState(false)

  return (
      <div>
        <ComponentA />

        {showMore && <ComponentB />}
        <button onClick={() => setShowMore(!showMore)}>Toggle</button>

        <ComponentC />
      </div>
  )
}

/**  A.tsx **/
export default function A() {

    return (
        <div> A </div>
    );
}

/**  B.tsx **/
export default function B() {

    return (
        <div> B </div>
    );
}

/**  C.tsx **/
export default function C() {

    return (
        <div> C </div>
    );
}

Lazy Loading을 사용하는 경우 클라이언트 구성요소는 기본적으로 사전 렌더링(SSR)됩니다

 

ssr:false 옵션으로 비활성이 가능합니다 → 좀 이상한 점이 처음에 렌더링 될 때 C는 C Is Loading이 나오고 A는 서버에서 렌더링 되어 A Is Loading이 안나오지만 Toogle을 클릭하면 둘다 Is Loading이 나오게 된다 Toogle은 훅으로 조작하고 A랑 C는 바뀐 점이 없기 때문에 가상돔 차이가 있는 B만 렌더링되어야하는데 A랑 C도 왜 그런지 모르겠다

 

 

Toggle 클릭 전 (A, C 청크 분리 확인)

 

Toggle 클릭 후 (B 청크 Lazy Loading 확인)

반응형
반응형

📝정적 데이터

/public 폴더에 정적 파일들을 넣는다

 

📝이미지

 

로컬 이미지

import Image from 'next/image'
import profilePic from './me.png'
 
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
    />
  )

width 및 height가 정해져있기 때문에 따로 지정 안 해도 에러가 나지 않는다

 

원격 이미지

import Image from 'next/image'
 
export default function Page() {
  return (
    
  )
}

/** next.config.js에서 허용 필요 **/
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 's3.amazonaws.com',
        port: '',
        pathname: '/my-bucket/**',
      },
    ],
  },
}

이미지를 외부에서 받아와야하기 때문에 width와 height를 지정 안 하면 안 된다

 

우선순위

import Image from 'next/image'
import profilePic from '../public/me.png'
 
export default function Page() {
  return <Image src={profilePic} alt="Picture of the author" priority />
}

priority를 이용해 로딩 우선순위를 정할 수 있습니다

 

상위 컨테이너에 꽉 채우기

<div style={{width: '200px', height: '200px', position:'relative'}}>
    <Image src={"https://previews.123rf.com/images/alexblacksea/alexblacksea1705/alexblacksea170500042/78337223-%EB%8F%85%EC%88%98%EB%A6%AC-%EA%B7%B8%EB%A6%AC%EA%B8%B0.jpg"}
           alt={""} fill={true}/>
</div>

 

fill을 사용하면 상위 크기만큼 이미지를 다 채울 수 있습니다 → position:relative 상위 div 필수

 

 

📝폰트

next/font모든 글꼴 파일 에 대한 자동 자체 호스팅이 내장되어 있습니다 → CSS 및 글꼴 파일은 빌드 시 다운로드되며 나머지 정적 자산과 함께 자체 호스팅됩니다. 즉, 브라우저는 요청을 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',
})

/** 가변 글꼴을 사용할 수 없는 경우 가중치 지정 필수 **/
//const roboto = Roboto({
//  weight: '400',
//  subsets: ['latin'],
//  display: 'swap',
//})
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" className={inter.className}>
      <body>{children}</body>
    </html>
  )
}

  • weight
    • 가변글꼴에는 줄 필요 없고 나머지는 주는데 브라우저상에서 파악해 어떤 weight를 줄지 알아서 판단한다 만약 style을 따로 줄 경우 style이 우선순위가 높음
  • subsets
    • 하위 집합 중 미리 로드할 하위 집합을 정의합니다 → latin이 우선순위로 preload되고 나머지언어는 후순위 느낌인데 테스트 해봐도 잘 모르겠다
    • latin이라고 적혀있으면 latin만 적용시키는게 아닌 다른 언어가 있으면 알아서 파악해서 가져온다
  • display
    • block
      • 폰트가 사용 가능하게 되면, 브라우저는 기다리지 않고 즉시 해당 폰트를 사용하여 페이지를 렌더링합니다.
    • swap
      • 폰트가 다운로드되면, 브라우저는 기존에 사용된 폰트와 즉시 교체하여 더 나은 사용자 경험을 제공합니다.

 

로컬 글꼴의 경우 next/font/local에 저장한 이후에 사용이 가능하다

 

 

📝스크립트

import Script from 'next/script'
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    
      {children}
    </html>
  )
}

타사 스크립트를 로드하려면 next/script 스크립트를 루트 레이아웃에 직접 가져와 포함하세요

Next.js는 사용자가 여러 페이지 사이를 이동하더라도 스크립트가 한 번만 로드되도록 보장합니다

 

→ 타사 스크립트 사용시 추가적으로 보면 좋을 것 같다

https://nextjs.org/docs/app/building-your-application/optimizing/scripts

 

 

📝정적 메타데이터

export const metadata: Metadata = {
  title: 'Create Next App!!', // 탭 Title
  description: 'Generated by create next app', // 설명
}

layout.js와 page.js에서 설정 가능서버 컴포넌트에서만 지원

 

📝동적 메타데이터

동적 값이 필요한 메타데이터 generateMetadata에 함수를 사용해 메타데이터를 생성할 수 있습니다서버 컴포넌트에서만 지원

 

📝파일 기반 메타데이터

opengrapqh, seo 허용, seo guid, favicon설정 가능

 

📝동적 이미지 생성

import { ImageResponse } from 'next/og'
 
export const runtime = 'edge'
 
export async function GET() {
  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 128,
          background: 'white',
          width: '100%',
          height: '100%',
          display: 'flex',
          textAlign: 'center',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        Hello world!
      </div>
    ),
    {
      width: 1200,
      height: 600,
    }
  )
}

ImageResponse를 이용해 오픈 그래프 이미지, 트위터 카드 등과 같은 소셜 미디어 이미지를 만드는 데 유용합니다

 

📝정적 Assets

import Image from 'next/image'
 
export function Avatar() {
  return <Image src="/me.png" alt="me" width="64" height="64" />
}

public → 기본 URL 부터 시작하는 코드로 참조될 수 있습니다 (/me.png)

 

반응형
반응형

📝외부 라이브러리 종류 (공식)

server-only라는 패키지를 이용해 서버에서만 실행시킬 수 있게 할 수 있다

import 'server-only'
 
export async function getData() {
  const res = await fetch('<https://external-service.com/data>', {
    headers: {
      authorization: process.env.API_KEY,
    },
  })
 
  return res.json()
}

 

📝외부 라이브러리 사용시 주의점

/** carousel.tsx **/
'use client'
 
import { Carousel } from 'acme-carousel'
 
export default Carousel

/** page.tsx **/
import Carousel from './carousel'
 
export default function Page() {
  return (
    <div>
      <p>View pictures</p>
 
      {/*  Works, since Carousel is a Client Component */}
      <Carousel />
    </div>
  )
}

클라이언트 사이드 렌더링이 필요한 라이브러리(Carousel은 외부 라이브러리 컴포넌트)일 경우 이런식으로 처리가 가능하다 → 이렇게 사용하면 서버 컴포넌트 내에서 사용이 가능하다

 

클라이언트 구성 요소 내에서 사용할 가능성이 높으므로 대부분의 타사 구성 요소를 래핑할 필요가 없을 것으로 예상되지만 컨텍스트 제공자(리덕스, ThemeContext 따위)의 경우 어쩔 수 없이 /app/layout.tsx에서 ‘use client’를 명시해야한다여기에 use client명시한다고 하위 페이지들이 전부 client side render가 되는 건 아니다

 

 

📝런타임

  • Edge 런타임
    • Edge Runtime의 속도는 최소한의 리소스 사용으로 인해 발생하지만 많은 시나리오에서 제한될 수 있습니다
    • Vercel의 Edge 런타임에서 실행되는 코드는 1MB에서 4MB 사이를 초과할 수 없습니다., 이 제한에는 가져온 패키지, 글꼴 및 파일이 포함되며 배포 인프라에 따라 달라집니다.
  • Node.js 런타임
    • Node.js API와 이에 의존하는 모든 npm 패키지에 액세스할 수 있습니다. 그러나 Edge 런타임을 사용하는 만큼 시작하는 것이 빠르지는 않습니다.
    • Next.js 애플리케이션을 Node.js 서버에 배포하려면 인프라를 관리, 확장 및 구성해야 합니다
  • 서버리스 Node.js
    • Vercel의 서버리스 기능을 사용하면 전체 코드 크기는 50MB입니다 가져온 패키지, 글꼴 및 파일을 포함합니다 Vercel에서 지원한 거이기 때문에 가장 이상적이라고 이야기한다

 

어떤 런타임을 고르냐에 따라 성능 및 지원하는게 다르다

 

📝Style

Tailwind 사용을 권장하며 다양한 적용 방법들이 존재한다

  • Global CSS
    • app/global.css에 작성
  • CSS Modules
  • Tailwind CSS
  • Sass
  • CSS-in-JS
반응형
반응형

📝Server Component (SSR)

서버에서 자바스크립트 파일을 읽어 HTML을 만든 후 클라이언트에게 전달한다 Next에서는 기본적으로 App Router에서 서버 사이드 렌더링을 적용시키고 있다 만약 명시적으로 표현하려면 “use server”를 상단에 적어주면 된다

 

📝동작과정

  1. 서버에서 자바스크립트를 읽고 HTML 생성
  2. 클라이언트에게 보내 Javascript(이벤트 핸들러)와 Hydration한다
    • 이부분이 좀 이상한 게 있는데 server component를 사용하면 useState, onClick등과 같은 클라이언트에서 사용하는 이벤트 핸들러 따위를 사용할 수가 없다 고로 Hydration이 불가능해보이는데 잘 모르겠다

 

📝이점

  • Data Fetch
    • 렌더링에 필요한 데이터를 가져오는데 걸리는 시간과 클라이언트가 수행하는 요청 양을 줄일 수 있다
  • 보안
    • 네트워크에 잡히지 않기 때문에 어떤 URL로 HTTP 통신했는지 보이지 않습니다
  • 캐싱 (deafult 캐싱 이용)
    • 서버에서 렌더링하면 결과를 캐시하기 때문에 비용이 절감 될 수 있습니다.
await fetch('<http://localhost:8080/next>', {cache: 'force-cache'});
(캐싱전) → [1] interval 51, [2] interval 57
(캐싱후) → [1] interval 2, [2] interval 2

 

  • 번들 크기
    • 서버에서 그려서 주기 때문에 클라이언트가 서버 구성 요소용 JavaScript를 다운로드, 구문 분석 및 실행할 필요가 없기 때문에 인터넷 속도가 느리거나 성능이 떨어지는 장치를 사용하는 사용자에게 유용
  • 스트리밍

 

📝서버 렌더링

정적, 동적, 스트리밍의 세 가지 하위 집합이 존재한다

 

  • 정적 렌더링
    • 정적 렌더링을 사용하면 경로는 빌드 시 렌더링되거나 데이터 재검증 후 백그라운드에서 렌더링
    • 정적 렌더링은 경로에 사용자에게 개인화되지 않은 데이터가 있고 정적 블로그 게시물이나 제품 페이지와 같이 빌드 시 알 수 있는 데이터가 있는 경우 유용하다
  • 동적 렌더링
    • 동적 렌더링은 경로에 사용자에게 맞춤화된 데이터가 있거나 쿠키나 URL의 검색 매개변수와 같이 요청 시에만 알 수 있는 정보가 있는 경우 유용하다
  • 스트리밍
    • 서버에서 알아서 청크사이즈로 분리 후에 병렬로 처리해준다

 

Next.js가 사용된 기능과 API를 기반으로 각 경로에 가장 적합한 렌더링 전략을 자동으로 선택하므로 정적 렌더링과 동적 렌더링 중에서 선택할 필요가 없습니다

 

📝Client Component (CSR)

클라이언트에서 받은 Javascript를 해석해 HTML을 렌더링하는 방식으로 “use client” 지시어를 명시해야 사용이 가능하다 해당 지시어를 사용해야 useState같은 훅과 이벤트 핸들링이 사용 가능하다

 

📝동작과정

  1. 서버에서 HTML을 렌더링할 수 있게 자바스크립트 번들링을 보낸다
  2. 자바스크립트를 읽어 클라이언트에서 웹 브라우저가 그리기 시작한다
    • use client라고 해도 React처럼 아예 안 그리는 수준이 아니라 어느정도 그릴 순 있는 건 다 그려서 나온다 (개발자 도구에서 네트워크의 HTML을 확인해보면 알 수 있다)
  3. 이벤트 핸들링따위와 연결 할 수 있게 Hydration을 한다

 

📝SSR / CSR 주의사항

  • page.tsx가 use client면 포함되어있는 컴포넌트들도 client side rendering을 한다 → console.log 사용시 서버에 찍히는지 콘솔창에 찍히는지로 판단 가능
    • page.tsx가 use client일 때 하위 컴포넌트 use clinet 선언 안 해도 Hook 사용 가능 (알아서 지정해주는 것 같음)
    • use client만 아니면 A컴포넌트는 서버렌더링 B컴포넌트는 클라이언트 렌더링 나눌 수 있다하지만 page가 use client의 경우 하위 컴포넌트가 서버 렌더링하면 왜인지 모르겠다만 엄청나게 data fetch를 요청한다
  • SSR과 CSR 어떤게 동작한 지 궁금할 시 개발자도구 네트워크를 보면 SSR의 경우 서버에서 데이터 Fetch를 하기 때문에 잡히지 않고 CSR의 경우 잡힌다
  • 번들 크기를 줄이기 위해서는 클라이언트와 서버쪽에서 부담을 잘 분배해야한다
  • layout은 서버 컴포넌트로 두고 사용자의 이벤트 핸들링 따위가 들어간 컴포넌트의 경우는 따로 클라이언트 컴포넌트로 정의한다
  • 서버 구성요소를 클라이언트 요소로 가져오기는 지원되지 않지만 꼭 되게 하려면 {children}으로 받을 수 있다
/** ClientComponent **/
'use client'
 
// You cannot import a Server Component into a Client Component.
import ServerComponent from './Server-Component'
 
export default function ClientComponent({
  children,
}: {
  children: React.ReactNode
}) {
  const [count, setCount] = useState(0)
 
  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>
 
      <ServerComponent />
    </>
  )
}

/** ClientComponent **/
'use client'
 
import { useState } from 'react'
 
export default function ClientComponent({
  children,
}: {
  children: React.ReactNode
}) {
  const [count, setCount] = useState(0)
 
  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      {children}
    </>
  )
}

/** Page.tsx **/

import ClientComponent from './client-component'
import ServerComponent from './server-component'
 
// Pages in Next.js are Server Components by default
export default function Page() {
  return (
    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
  )
}
  • 어떤 구성 요소를 사용해야할까? (서버 구성요소 / 클라이언트 구성요소)

반응형
반응형

 

📝미들웨어

미들웨어를 사용하면 요청이 완료되기 전에 코드를 실행 할 수 있습니다.

그런 다음 들어오는 요청에 따라 요청 또는 응답 헤더를 다시 작성, 리디렉션, 수정하거나 직접 응답하여 응답을 수정할 수 있습니다.

미들웨어를 정의하려면 프로젝트 루트에 있는 파일 middleware.ts(또는 )을 사용한다

 

middleware.ts 예제

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
  return NextResponse.redirect(new URL('/home', request.url))
}
 
// See "Matching Paths" below to learn more
export const config = {
  matcher: '/about/:path*',
}

 

📝특정 경로만 허용

 

matcher를 이용한 특정 경로만 middleware 인정

export const config = {
  matcher: '/about/:path*',
}

/** ----------------------- **/
export const config = {
  matcher: ['/about/:path*', '/dashboard/:path*'],
}

/** ----------------------- **/
export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
}

 

조건문을 이용한 특정 경로만 middleware 인정

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
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))
  }
}

 

📝쿠키 사용

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  // Assume a "Cookie:nextjs=fast" header to be present on the incoming request
  // Getting cookies from the request using the `RequestCookies` API
  let cookie = request.cookies.get('nextjs')
  console.log(cookie) // => { name: 'nextjs', value: 'fast', Path: '/' }
  const allCookies = request.cookies.getAll()
  console.log(allCookies) // => [{ name: 'nextjs', value: 'fast' }]
 
  request.cookies.has('nextjs') // => true
  request.cookies.delete('nextjs')
  request.cookies.has('nextjs') // => false
 
  // Setting cookies on the response using the `ResponseCookies` API
  const response = NextResponse.next()
  response.cookies.set('vercel', 'fast')
  response.cookies.set({
    name: 'vercel',
    value: 'fast',
    path: '/',
  })
  cookie = response.cookies.get('vercel')
  console.log(cookie) // => { name: 'vercel', value: 'fast', Path: '/' }
  // The outgoing response will have a `Set-Cookie:vercel=fast;path=/test` header.
 
  return response
}

손쉽게 쿠키 조작 가능

 

📝헤더 설정

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  // Clone the request headers and set a new header `x-hello-from-middleware1`
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-hello-from-middleware1', 'hello')
 
  // You can also set request headers in NextResponse.rewrite
  const response = NextResponse.next({
    request: {
      // New request headers
      headers: requestHeaders,
    },
  })
 
  // Set a new response header `x-hello-from-middleware2`
  response.headers.set('x-hello-from-middleware2', 'hello')
  return response
}

손쉽게 헤더 설정 가능

 

📝응답 설정

import { NextRequest } from 'next/server'
import { isAuthenticated } from '@lib/auth'
 
// Limit the middleware to paths starting with `/api/`
export const config = {
  matcher: '/api/:function*',
}
 
export function middleware(request: NextRequest) {
  // Call our authentication function to check the request
  if (!isAuthenticated(request)) {
    // Respond with JSON indicating an error message
    return Response.json(
      { success: false, message: 'authentication failed' },
      { status: 401 }
    )
  }
}

손쉽게 응답 설정 가능

반응형
반응형

📝로딩 (Loading)

콘텐츠가 로드되는 동안 서버에서 즉시 로드 상태를 표시할 수 있습니다. 렌더링이 완료되면 새 콘텐츠가 자동으로 교체됩니다

 

📝즉각 로딩 상태

라우팅 폴더에 loading.tsx를 넣을 경우 하위에서 로딩이 일어났을 때 해당 tsx파일을 보여줍니다 로딩의 경우 스켈레톤 및 스피너와 같은 걸 많이 사용합니다 (참고로 로딩이 일어난 건 Fetch한 데이터를 화면에 노출시킬 때만 해당 됩니다)

 

📝서스펜스 로딩 (Suspense)

Suspense 컴포넌트를 이용할 수 있습니다 fallback에는 로딩 시킬 컴포넌트를 적어주고 하위 컴포넌트를 감싸주면 됩니다

 

Suspense다양한 이점이 있는데 SSR의 경우 위의 사진 처럼 모든 데이터를 가져온 후에 서버가 페이지의 HTML을 렌더링 할 수 있습니다 그 후 클라이언트에서 구성요소에 대한 인터렉티브한 이벤트 요소의 코드가 다운 되어야 UI의 기능을 사용할 수 있습니다(Hydration) 이럴 경우 Blokcing 처리로 지연이 될 수 있습니다

 

 

분리전 청크 사이즈                                                                         분리 후 청크 사이즈

 

 

 

Suspense를 이용하면 페이지의 각 컴포넌트를 청크를 나눠서 서버에서 클라이언트로 보낼 수 있습니다 그렇게 되면 NonBlokcking처럼 병렬처리가 되어서 더 빠른 페이지를 제공 및 더 빠른 Hydrating이 가능해 사용자가 페이지를 빨리 이용할 수 있습니다 (스트리밍)

 

SSR에서는 스트리밍을 기본적으로 지원하기 때문에 적용 안 시킨 버전하고 어떤 차이가 있는지는 테스트 못 함 하지만 병렬로 요청이 들어가는 것은 확인했고 Suspense를 사용하면 각각 Loading 보여주다가 먼저 들어오는 것부터 보여준다 아마 이렇게 사용해야 청크를 나눠서 하는 것 같다 마치 CSR처럼 데이터 패치 이후에 그려지는 것처럼 보여진다 → Suspense 필수로 적용

 

물론 Suspense는 Client에서도 사용이 가능하고 청크를 나누진 않기 때문에 다이나믹으로 청크로 나눠서 Lazy Load를 시키면 로딩 성능을 개선 시킬 수 있다 → 서버에서는 자동으로 청크를 나누기 때문에 필요 없다고 한다

 

프론트 코드 (SSR로 테스트)

'use server';

/** /test/page.tsx **/
export default function Page() {
	return (
		<div>
        <Suspense fallback={<div className="text-amber-900">Loading</div>}>
            <Results num={1}/>
        </Suspense>
        <Suspense fallback={<div className="text-amber-900">Loading</div>}>
            <Results2 num={2}/>
        </Suspense>
		</div>
	)
}

/** Results.tsx **/
export default async function Results({num}: { num: number }) {

    const now = new Date();

    let year = now.getFullYear();
    let month = String(now.getMonth() + 1).padStart(2, '0');
    let day = String(now.getDate()).padStart(2, '0');

    let hours = String(now.getHours()).padStart(2, '0');
    let minutes = String(now.getMinutes()).padStart(2, '0');
    let seconds = String(now.getSeconds()).padStart(2, '0');
    let milliseconds = String(now.getMilliseconds()).padStart(3, '0');

    const startDateTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
    console.log(`[${num}] ` + startDateTime);

    const getData = await fetch('<http://localhost:8080/next>', {cache: 'force-cache'});
    let result = await getData.json();

    const end = new Date();

    year = end.getFullYear();
    month = String(end.getMonth() + 1).padStart(2, '0');
    day = String(end.getDate()).padStart(2, '0');

    hours = String(end.getHours()).padStart(2, '0');
    minutes = String(end.getMinutes()).padStart(2, '0');
    seconds = String(end.getSeconds()).padStart(2, '0');
    milliseconds = String(end.getMilliseconds()).padStart(3, '0');

    const endDateTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
    console.log(`[${num} end] ` + endDateTime);
    console.log(`[${num}] interval ` + (end.getTime() - now.getTime()));

    return <div>
        {result.id}
    </div>
}

/** Results2.tsx **/
export default async function Results2({num}: { num: number }) {

    const now = new Date();

    let year = now.getFullYear();
    let month = String(now.getMonth() + 1).padStart(2, '0');
    let day = String(now.getDate()).padStart(2, '0');

    let hours = String(now.getHours()).padStart(2, '0');
    let minutes = String(now.getMinutes()).padStart(2, '0');
    let seconds = String(now.getSeconds()).padStart(2, '0');
    let milliseconds = String(now.getMilliseconds()).padStart(3, '0');

    const startDateTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
    console.log(`[${num}] ` + startDateTime);

    const getData = await fetch('<http://localhost:8080/next2>', {cache: 'force-cache'});
    let result = await getData.json();

    const end = new Date();

    year = end.getFullYear();
    month = String(end.getMonth() + 1).padStart(2, '0');
    day = String(end.getDate()).padStart(2, '0');

    hours = String(end.getHours()).padStart(2, '0');
    minutes = String(end.getMinutes()).padStart(2, '0');
    seconds = String(end.getSeconds()).padStart(2, '0');
    milliseconds = String(end.getMilliseconds()).padStart(3, '0');

    const endDateTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
    console.log(`[${num} end] ` + endDateTime);
    console.log(`[${num}] interval ` + (end.getTime() - now.getTime()));

    return <div>
        {result.id}
    </div>
}

 

 

백엔드 코드

@CrossOrigin(origins = "*")
@GetMapping("/next")
public @ResponseBody HashMap<String, Object> next() throws InterruptedException {

	System.out.println("next call!!");
	HashMap<String, Object> map = new HashMap<String, Object>();
	map.put("id", "Next 테스트 1");
	
	Thread.sleep(6000);

	
	return map;
}

@CrossOrigin(origins = "*")
@GetMapping("/next2")
public @ResponseBody HashMap<String, Object> next2() throws InterruptedException {

	System.out.println("next2 call!!");
	HashMap<String, Object> map = new HashMap<String, Object>();
	map.put("id", "Next 테스트 2");
	
	Thread.sleep(3000);

	
	return map;
}

결과 → /next2를 호출한 곳이 먼저 그려지고 3초 후에 /next를 호출한 곳이 그려진다

 

📝스트리밍

스트리밍을 사용하면 서버에서 UI를 점진적으로 렌더링할 수 있습니다

작업은 여러 단위로 분할되어 준비가 되면 클라이언트로 스트리밍됩니다 이를 통해 사용자는 전체 콘텐츠의 렌더링이 완료되기 전에 페이지의 일부를 즉시 볼 수 있습니다

 

스트리밍은 기본적으로 Next.js 앱 라우터에 내장되어 있습니다 이는 초기 페이지 로딩 성능뿐만 아니라 전체 경로 렌더링을 차단하는 느린 데이터 가져오기에 의존하는 UI를 모두 개선하는 데 도움이 됩니다

 

사용 예제 코드

import { Suspense } from 'react'
import { PostFeed, Weather } from './Components'
 
export default function Posts() {
  return (
    <section>
      <Suspense fallback={<p>Loading feed...</p>}>
        <PostFeed />
      </Suspense>
      <Suspense fallback={<p>Loading weather...</p>}>
        <Weather />
      </Suspense>
    </section>
  )
}

 

 

 

 

반응형
반응형

📝Next.js란

Next.js는 React 기반의 웹 애플리케이션을 쉽게 구축할 수 있게 해주는 JavaScript 프레임워크입니다

웹 개발자들이 React를 사용하여 서버 사이드 렌더링(SSR) 및 정적 생성(Static Site Generation, SSG)과 같은 기능을 구현할 수 있도록 도와줍니다

React의 경우 Client Side Rendering으로 인해 봇이 사이트에 접속해서 바로 수집할 경우 아무것도 없기 때문에 검색엔진 최적화에 문제가 있었다 Next.js는 이부분을 해결해줍니다 → SEO 최적화

 

📝용어

세그먼트(Segment) → 세그먼트는 URL에서 '/'을 기준으로 나누어지는 URL 조각

 


 

📝최상위 폴더

app 앱 라우터 → 14버전 권장
pages 페이지 라우터
public 정적 데이터

 

 

📝최상위 파일

next.config.js Next.js 구성 파일
package.json 프로젝트 종속성 관리 및 스크립트
middleware.ts Next.js 요청 미들웨어 (Request 조작 후 원하는 Response 제공) → redirect, rewrite 등…
.env 환경 변수
.env local 로컬 환경 변수
.env.production 프로덕션 환경 변수
.eslintrc.json ESLint용 구성 파일
tsconfig.json TypeScript용 구성 파일 (설정)

 

📝Route 파일 규칙

layout 최상위 호출 파일
page 페이지
loading UI로딩
not-found not found 페이지
error Error 페이지
global-error 전역 Error 페이지
route API 엔드포인트 (서버 API 역할)
template layout하고 무슨 차인지 잘 모르겠음

 

📝Route 방법 (폴더)

folder 라우팅 세그먼트
folder/folder 하위 라우팅 세그먼트

 

📝동적 Route

[folder]  동적 라우팅 세그먼트 [아래에서 상세 설명]
[…folder] 동적 포괄 세그먼트 [아래에서 상세 설명]
app/shop/[...slug]/page.js → /shop/a, shop/a/b, shop/a/b/c [해당 URL 요청도 감지]
[[…folder]] 선택적 포괄 세그먼트 [아래에서 상세 설명]
app/shop/[[...slug]]/page.js → /shop, /shop/a, shop/a/b, shop/a/b/c [해당 URL 요청도 감지]

 

📝라우팅 그룹 및 개인 폴더

(folder) 라우팅에 영향 주지않는 그룹 경로 → 라우팅 경로는 주고 싶지 않지만 폴더 구분을 하고 싶은 경우 (같은 라우팅 경로이지만 서로 다른 레이아웃을 줄 수 있다) [아래에서 상세 설명]
_folder 개인 폴더로 모든 라우팅에서 제외되는 폴더

 

📝병렬 및 경로 가로채기

@folder 페이지를 Include하는 것처럼 각 병렬 폴더에 있는 page.tsx를 layout에서 import 가능 [다음장에서 상세 설명]
(.)folder 동일한 수준의 세그먼트 일치 [다음장에서 상세 설명]
(..)folder 한 수준 위의 세그먼트 일치
(..)(..)folder 두 수준 위의 세그먼트 일치
(…)folder 루트 app 디렉토리의 세그먼트 일치

 

📝아이콘

favicon 웹 페이지 탭에 표시 시킬 아이콘 (/app route segment에 존재) [default]
icon 웹 페이지 탭에 표시 시킬 아이콘 (코드로 생성 및 세밀한 조정 가능)
apple-icon apple기기에서 보여질 아이콘 (코드로 생성도 가능)
opengraph-image 링크 공유하기 할 때 어떤형식으로 보여줄지에 대한 프로토콜로 (아이콘 등에 대한 설정 가능) → 카카오톡은 이런식으로 등… [코드로 생성 가능]
twitter-image 트위터에서 링크 공유할 때 보여줄 형식 (코드로 생성 가능)

 

📝SEO

sitemap 크롤링 봇이 어떤 경로로 수집할 수 있게 알려주는 가이드라인 (코드로도 생성 가능)
robots 크롤링 봇에 대한 여부 및 다양한 설정

 


 

📝라우팅

경로를 찾으려면 해당 폴더에 page.js가 존재해야합니다 (/dashboard/settings로 접근시 page.tsx가 열린다)

 

📝루트 레이아웃 (app/layout.tsx)

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}
  • 반드시 필요한 레이아웃으로 가장 처음에 실행된다
  • html과 body에 대한 정의가 꼭 들어가야한다

 

📝중첩 레이아웃

레이아웃은 중첩해서 사용이 가능하다

위에 예제를 보면 /dashbaord로 URL 접근시 app/layout.tsx의 최상단 레이아웃 안에 children에 dashboard/layout.tsx가 그려지게 되고 page.tsx가 존재할 시 dashboard/layout.tsx의 children에 dashboard/page.tsx가 그려지게 된다

 

 

📝Link (페이지 연결)

import Link from 'next/link'
 
export default function PostList({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link href={`/blog/${post.slug}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  )
}

<a> 제공하는 내장구성요소로 Next.js에서 경로 간을 탐색하는 기본 방법이다

 

'use client'
 
import { usePathname } from 'next/navigation'
import Link from 'next/link'
 
export function Links() {
  const pathname = usePathname()
 
  return (
    <nav>
      <ul>
        <li>
          <Link className={`link ${pathname === '/' ? 'active' : ''}`} href="/">
            Home
          </Link>
        </li>
        <li>
          <Link
            className={`link ${pathname === '/about' ? 'active' : ''}`}
            href="/about"
          >
            About
          </Link>
        </li>
      </ul>
    </nav>
  )
}

usePathname을 통해 현재 경로 URL을 가져와서 동적 처리가 가능

<Link href="/dashboard#settings">Settings</Link>
 
// Output
<a href="/dashboard#settings">Settings</a>

페이지 이동시 스크롤 위치를 #을 통해 지정할 수 있다

// next/link
<Link href="/dashboard" scroll={false}>
  Dashboard
</Link>

페이지 이동후 스크롤 비활성화 가능

 

📝useRouter (페이지 연결)

'use client'
 
import { useRouter } from 'next/navigation'
 
export default function Page() {
  const router = useRouter()
 
  return (
    <button type="button" onClick={() => router.push('/dashboard')}>
      Dashboard
    </button>
  )

클라이언트 구성 요소 내에서만 사용할 수 있다 → use client 선언 필요

 

📝페이지 연결 작동 방식

서버에서 애플리케이션 코드는 경로 세그먼트 별로 자동으로 코드 분할되고 해당 경로를 미리 가져오고 캐시를 한다 즉, 페이지를 다시 로드하지 않고 변경된 세그먼트만 다시 렌더링하여 성능을 향상시킨다

 

사용자는 경로 방문 전에 백그라운드에서 경로를 미리 로드한다 → 예를 들면 네이버 메인페이지에 다양한 링크들이 있다 메인페이지에 들어간 후 오프라인 설정한 다음 메인페이지에 있는 링크에 들어가면 로드가 된다 (전부가 로드 되는 건 아님 캐싱에 있는 애들이나 정적으로 만들어진 애들을 로드)

 

📝라우팅 및 탐색 작동 방식

서버에서 애플리케이션 코드는 경로 세그먼트 별로 자동으로 코드 분할되고 해당 경로를 미리 가져오고 캐시를 한다 즉, 페이지를 다시 로드하지 않고 변경된 세그먼트만 다시 렌더링하여 성능을 향상시킨다

 

프리패칭 (Prefetching)

사용자는 경로 방문 전에 백그라운드에서 경로를 미리 로드한다 → 예를 들면 네이버 메인페이지에 다양한 링크들이 있다 메인페이지에 들어간 후 오프라인 설정한 다음 메인페이지에 있는 링크에 들어가면 로드가 된다 (전부가 로드 되는 건 아님 캐싱에 있는 애들이나 정적으로 만들어진 애들을 로드)

 

Next에서 경로를 미리 가져오는 방법은 두가지가 있다

  1. <Link> 컴포넌트 → 뷰포트에 표시되면 자동으로 미리 가져온다
    • 정적 경로일 경우
      • prefetch의 true가 기본값이고 전체 경로가 프리패치가 되며 캐시도 된다
    • 동적 경로일 경우
      • prefetch의 true가 기본값이고 loading.tsx가 먼저 프리패치가 된다 → 그 이후에 패치작업을 미리하거나 하는 건 아닌 거 같음
  2. router.prefetch → useRouter 기능을 이용한 프리패치 방식
    • <button onClick={()=>{ router.prefetch('/어쩌구') }}>버튼</button> router.prefetch('/')

 

무작정 사용하는 건 위험할 수도 있다현재 페이지와 관련이 없는 링크를 포함하는 경우, 불필요한 리소스를 사전 로드하지 않아 초기 페이지 로딩 속도를 개선시키는 방법도 존재

 

캐싱

Next.js에는 라우터 캐시(Router Cache) 라는 메모리 내 클라이언트 측 캐시가 있습니다

사용자가 앱을 탐색할 때 미리 가져온 경로 세그먼트와 방문한 경로의 React Server 구성 요소 페이로드가 캐시에 저장

 

부분 렌더링

변경되는 경로의 세그먼트만 변경되며 나머지 세그먼트는 그대로 보존된다

 

스크롤 유지

이동한 뒤에 뒤로가기로 이동해도 이동하기 전의 스크롤을 유지시킨다

 

 

📝Route Group (라우트 그룹)

URL 경로에 매핑되지 않게 경로 그룹을 표시할 수 있다 (folderName)으로 작성

공통된 레이아웃을 가지지만 URL 경로를 추가하고 싶지 않은 것들 끼리 같은 그룹으로 묶어서 layout.tsx의 중복을 막을 수도 있다 → shop영역으로 묶은 account와 cart는 layout.tsx를 공통으로 가질 수 있다

 

📝여러 루트 레이아웃 생성

생성하려면 최상위 layout.tsx 파일을 제거하고 각 경로 그룹내에 파일을 추가 물론 각 레이아웃 파일은 <html> <body>가 존재해야한다

 

📝동적경로 (Dynamic Route)

export default function Page({ params }: { params: { slug: string } }) {
  return <div>My Post: {params.slug}</div>
}

대괄호를 묶어서 생성한다 [foldername] → 예) [id]

 

예제

app/blog/[slug]/page.js /blog/a { slug: 'a' }
app/blog/[slug]/page.js /blog/b { slug: 'b' }
app/blog/[slug]/page.js /blog/c { slug: 'c' }

 

📝포괄 세그먼트 (Catch-all Segments)

 

예제

app/shop/[...slug]/page.js /shop/a { slug: ['a'] }
app/shop/[...slug]/page.js /shop/a/b { slug: ['a', 'b'] }
app/shop/[...slug]/page.js /shop/a/b/c { slug: ['a', 'b', 'c'] }

대괄호 안에 줄임표를 이용해 모든 후속 세그먼트로 확장 가능

 

예제

app/shop/[[...slug]]/page.js /shop {}
app/shop/[[...slug]]/page.js /shop/a { slug: ['a'] }
app/shop/[[...slug]]/page.js /shop/a/b { slug: ['a', 'b'] }
app/shop/[[...slug]]/page.js /shop/a/b/c { slug: ['a', 'b', 'c'] }

이중 대괄호를 이용해 기본 URL도 동적 생성이 가능하다

 

📝정적 매개변수 생성 (SSG)

export function generateStaticParams() {
    return [{ slug: '1' }, { slug: '2' }, { slug: '3' }]
}

export default function Page({ params }: { params: { slug: string } }) {
    const { slug } = params

    console.log(params);

    return <div>
        {slug}
    </div>
}
// ----- 또 다른 예제 방식 ----- //
export async function generateStaticParams() {
  const posts = await fetch('https://.../posts').then((res) => res.json())
 
  return posts.map((post) => ({
    slug: post.slug,
  }))
}

1.html, 2.html, 3.html이 생성이 된다 → 이미 만들어져 있기 때문에 속도가 빠르다

 

📝에러 핸들링 (Error Handling)

라우팅할 폴더안에 error.tsx를 만들면 해당 라우팅에서 에러가 날때 error.tsx를 보여준다 (ErrorBoundray라는 컴포넌트가 자동적으로 생성됨)

 

error.tsx

'use client'
 
export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={() => reset()}>Try again</button>
    </div>
  )
}

Error 컴포넌트는 이렇게 만들며 reset이라는 기능도 지원하는데 에러가 발생했을 때 어떤 핸들링을 할지에 대한 것이다 (참고로 템플릿 및 레이아웃의 오류는 포착하지 않습니다)

 

루트 레이아웃 오류 처리의 경우는 /app/global-error.tsx를 이용해 처리가 가능합니다 (전체적인 Error에 대한 핸들링)

 

📝병렬 경로 (Parallel Routes)

병렬 라우팅을 사용하면 각각의 컴포넌트들이 독립적으로 렌더링이 됩니다 폴더에 @을 붙여서 병렬 처리 가능한 상태로 변경합니다 [서스펜스 로딩과 비슷한 느낌이지만 컴포넌트가 아닌 페이지 단위인 것 같다] → 병렬처리가 되는지 실제로 테스트가 필요하긴 함

 

export default function Layout(props: {
  children: React.ReactNode
  analytics: React.ReactNode
  team: React.ReactNode
}) {
  return (
    <>
      {props.children}
      {props.team}
      {props.analytics}
    </>
  )
}

@team/members → /members로 접근 가능하고 props에 여러개 적는 이름은 @에 적은 이름과 동일해야한다

 

default.js

├── @analytics
│   └── page.tsx
├── @team
│   └── settings
│       └── page.tsx
├── layout.tsx
└── page.tsx

위와 같은 트리 구조에서 “/”를 호출하면 404가 렌더링이 되는데 settings폴더 안에 page.tsx가 있기 때문이다 [@team 폴더 안에 page.tsx가 있어야 한다] 이럴 경우 @team안에 default.tsx를 만들어주면된다 (@을 슬롯이라고 부른다)

 

├── @analytics
│   └── page.tsx
├── @team
│   ├── default.tsx ✅ 추가
│   └── settings
│       └── page.tsx
├── layout.tsx
└── page.tsx

/**------------------------------------------**/
export default function Default() {
  return null;
}

이렇게 할 경우 @analytics에 해당하는 page 부분만 보이고 일치하지 않는 부분은 404를 렌더링 하지 않도록 한다

 

만약 “/settings”를 호출하면 404가 렌더링 되는데 settings와 같은 수준의 page.tsx가 존재하지 않기 때문입니다

(@analytics와 layout.tsx에서 정의한 children도 존재하지 않음)

 

'use client'
 
import { useSelectedLayoutSegment } from 'next/navigation'
 
export default async function Layout(props: {
  //...
  auth: React.ReactNode
}) {
  const loginSegments = useSelectedLayoutSegment('auth')
  // ...
}

useSelectedLayoutSegment(s) → layout의 URL 경로를 가져온다 (layout.tsx에서만 사용 가능하며 use clinet 사용 필수)

 

 

📝경로 가로채기 (Intercepting Routes)

경로를 가로채면 현재 레이아웃 내 애플리케이션의 다른 부분에서 경로를 로드할 수 있습니다

 

예를 들면 피드에서 사진을 클릭하면 피드에 오버레이되어 사진을 모달로 표시할 수 있습니다. 이 경우 Next.js는 /photo/123경로를 가로채서 URL을 마스크하고 이를 오버레이합니다

 

  • (.)
    • 동일한 수준 의 세그먼트를 일치시킬 시
  • (..)
    • 한 수준 위의 세그먼트와 일치시킬 시
  • (..)(..)
    • 두 수준 위의 세그먼트와 일치시킬 시
  • (...)루트 app
    • 디렉터리 의 세그먼트를 일치시킬 시

layout에서 /feed에서 Link로 photo/2로 클릭시 접근할 때 (..)photo/[id]/page로 연결 시킨다

(..)인 이유는 feed위로 올라가야 photo랑 동등한 수준이 되기 때문에 그렇다

 

예제 홈페이지 → https://nextgram.vercel.app/

예제 소스 → https://github.com/vercel/nextgram.git

 

병렬 경로와 경로 가로채기를 이용해 위의 예제 소스를 통해 쉽게 모달을 구현할 수 있다고 한다

 

 

 

🔗 참고 및 출처

https://velog.io/@ckstn0777/Next.js-13-Parallel-Routes-Intercepting-Routes

https://velog.io/@jay/Parallel-Routes#layoutjs-에서-parallel-route를-사용해-modal-띄우기

https://velog.io/@dhwnatjr678/Next.js-useRouter%EC%9D%98-%EB%8B%A4%EC%96%91%ED%95%9C-%EA%B8%B0%EB%8A%A5%EB%93%A4prefetch

반응형