Next.js의 App Directory 관련 기능을 뜯어보자

Next.js의 App Directory 관련 기능을 뜯어보자

Tag
React
NextJS
프론트엔드 개발자라면, 뭐 그렇지 않더라도 nextjs의 v13은 익히 들었을 법 하다.
app directory 를 사용하고, Server Component가 도입되었다는 둥 여러가지 이야기를 들었지만 최근에 아쉽게도 nextjs를 사용할 일이 없었다.
하지만 이번에 진행하는 프로젝트에서 nextjs를 도입하게 되어, v13의 내용을 정리하려 한다.
appDir이 stable이 되어버려서 안 쓸 핑계가 없어…

App Directory

13 이전 버전의 next를 사용하였다면, pages 내부의 폴더 / 파일 위치에 따라 라우팅 구성이 자동으로 되었다는 사실은 알고 있을 것이다.
appDir도 마찬가지이다. 하지만 pages가 아닌 app 폴더에 위치하면 된다.
또한 index.{jsx, tsx} 형식이 아닌, page.{jsx, tsx} 의 이름으로 파일을 생성해야 한다.
 
예를들어 app/dashboard/settings/page.tsx 는
{domain}/dashboard/settings 로 라우팅 구성이 되는 것이다.

File Convention

  • page.js: 접근 가능한 경로를 만들며, 해당 라우트에 페이지를 생성
    • route.js: 해당 위치의 라우트에 server-side API 를 생성 ( 서버리스 함수…! )
  • layout.js: 레이아웃 형태의 컴포넌트를 제작. 해당 레이아웃은 페이지 / 하위의 세그먼트를 래핑한다.
    • template.js: layout.js 과 비슷하지만, navigate시 컴포넌트가 새로 마운트 된다는 추가 기능이 있다고 생각하면 된다. 필요없다면 layout.js를 사용하면 된다.
      • 라우트 될 때마다의 CSS / animation library가 존재하는 경우
  • loading.js: loading.js는 자동으로 페이지 또는 중첩된 세그먼트를 Suspense Boundary로 래핑해주며, Suspense Boundary 내부에서 페이지 혹은 컴포넌트 단위에 fallback UI를 보여준다.
  • error.js: loading.js와 마찬가지로 자동으로 Error Boundary로 래핑한다. fallback으로 표시된다.
    • global-error.js: error.js와 같지만, 루트의 layout.js의 에러를 캐치해준다.
  • not-found.js: 경로와 매칭되는 페이지가 없을 경우 뜨는 페이지 UI.
 

Server Component

서버 컴포넌트는 SSR과 혼동될 수 있는 개념이나,
  1. 번들에 추가되지 않는다는 점. ( 서버사이드단에서 브라우저 단으로 json 형식을 던져준다 )
  1. SSR은 페이지 단에서만 적용이 되지만, Server Component는 컴포넌트 단에서 가능하다.
 
useState, useReducer와 같이 interaction이 필요하지 않은 컴포넌트 단의 경우 서버컴포넌트를 사용한다고 생각하면 되겠다. 보통 Leaf Component에 Client Component를 사용하면 좋을 것이다.
Client Component는 Server Component를 부르지 못하거든..
 
📌
만약 Client Component에서 Server Component를 호출하고 싶은 경우가 생길 때는,
layout.js 의 방식처럼 children으로 받으면 상관이 없다!
 
Next.js 13에서 모든 컴포넌트들은 Server Component를 기본적으로 사용하고 있다.
Client Component를 사용하고 싶다면 하기와 같이 ‘use client’를 사용하면 된다.
'use client'; import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div> ); }
 
추가로 서버에서만 사용하고 싶은 코드, 즉 클라이언트단에서 사용하는 것을 방지하고 싶은 경우,
아래와 같이 사용하면 된다.
yarn add 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(); }
 
What do you need to do?
Server Component
Client Component
데이터 fetch
O
X
backend 자원을 직접적으로 접근
O
X
access tokens, API keys와 같은 민감한 정보를 서버에 저장
O
X
서버에 큰 의존성 / client 단의 javascript 크기를 줄임
O
X
상호작용 및 리스너 적용
X
O
State / LifeCycle 적용
X
O
Browser-only API 사용
X
O
state, effects, browser-only APIs를 가진 Custom hook 사용
X
O
Class Component 사용
X
O
 

nested 계층

위에서 설명한 내용을 컴포넌트 계층적으로 표현하자면 하기의 코드와 같다.
notion image
 
위에서 예를 들었던 app/dashboard/settings 의 경우는 이를 중첩으로 표현하고 있다. 라고 생각하면 되겠다.
notion image
 
📌
추가로 꼭 위의 File Convention에 해당되는 파일이 아니더라도 위치시킬 수 있으니 알고있쟈 Ex) Button.tsx, Navbar.tsx..
 

URL의 영향을 받지 않는 폴더 구성

(folderName) 이용
⇒ app/ 단에 layout.js 를 만들지 않았다면, 상단 레이아웃의 영향을 받지 않고 레이아웃을 제작 가능하다.
notion image
 
 

서버 중심의 라우팅과 Client-side의 Navigation

Next js의 App Router는 서버 중심의 라우팅을 통해 Server Component서버에서의 데이터 페칭을 조율한다. 하지만 우리가 웹페이지에서 Link와 같이 navigate를 하기 위해, router는 client-side navigation을 사용한다 ( SPA처럼 )
추가로 라우터는 Server Component의 payload를 클라이언트단 메모리의 cache로 저장하고 있다. 이는 캐시를 재사용해서 성능을 향상시킬 수 있음을 의미한다
 

<head> 조작

메타데이터를 조작하는 방법은 layout.js / page.js 파일에 아래와 같은 방법을 사용하면 된다.

1. metadata 객체 export

// app/page.tsx import { Metadata } from 'next'; export const metadata: Metadata = { title: 'Next.js', }; export default function Page() { return '...'; }

2. generateMetadata 함수

아래와 같은 방법은 페이지의 데이터에 따라 메타데이터가 변경되는 경우 사용하면 좋을 것 같다.
// app/page.tsx export const generateMetadata = async ({ params }: any): Promise<Metadata> => { const data = await getData(params.id) return { title: data.title, description: params.slug, } }
 

Dynamic Routes

[folderName] 과 같이 폴더의 이름을 대괄호를 감싸면 된다. ex) [id], [slug]
params props으로 layout, page, route, generateMetadata 함수에서 받을 수 있다.
 

generateStaticParams

next 13 이전의 getStaticPaths 의 기능을 한다고 생각하면 되겠다.
 

Route Handlers

Next.js에서 API 엔드포인트를 정의한다고 생각하면 된다.
비즈니스 로직 / 데이터베이스 조작과 같은 작업을 처리할 때 사용한다.
 

Static Rendering ( Default )

모든 렌더링 작업을 완료해놓고 CDN을 통해 제공되기 때문에 성능이 높다.
 
정적 데이터 fetching의 경우 fetch() 함수를 사용하면 되며, ISR 방식이 필요한 경우 revalidate 와 같은 옵션을 추가할 수 있다.
 

동적 렌더링

말그대로 dynamic function / dynamic fetch() 를 만나게 되면 next는 매 요청마다 동적으로 렌더링한다는 것이다.
  • cookies() / headers() 를 사용하면 요청 시 전체 경로가 동적 렌더링으로 선택
  • Client Component에서 useSearchParams() 사용 시 정적 렌더링을 skip하고 모든 Client Component는 클라이언트단의 가장 가까운 Suspense boundary에 렌더링
    • <Suspense />boundary에서 useSearchParams 사용하는 것을 추천한다.
 

Data Fetching

Server Component 에서의 async await

걍 아래와 같이 쓰면 된다…ㅋ
async function getData() { const res = await fetch('https://api.example.com/...'); // The return value is *not* serialized // You can return Date, Map, Set, etc. // Recommendation: handle errors if (!res.ok) { // This will activate the closest `error.js` Error Boundary throw new Error('Failed to fetch data'); } return res.json(); } export default async function Page() { const data = await getData(); return <main></main>; }

Client Component에서의 use

Promise를 await와 같이 사용할 수 있는 방법인 function use 이다.
이는 components, hooks, Suspense와 함께 쓸 수 있는 방법인데, 현재 fetchuse와 함께 쓰는 것은 리렌더를 유발하기 때문에 SWR / React Query 쓰는 것을 추천하고 있다.

Data Fetching

// Static Data Fetching // SSG fetch("https://...") // cache: force-cache => 무제한 캐시! // ISR fetch('https://...', { next: { revalidate: 10 } }); // revalidate second 마다 캐시 초기화 // SSR fetch("https://...", { cache: 'no-store' }); // 매 요청시마다 새로

Data Fetching Patterns

Parallel / Sequential 방식의 fetching 패턴이 적혀있으니, 이 부분은 공식 독스에서 확인하면 되겠다.
 

fetch를 쓰기 싫지만 caching, revalidating은 하고싶어…!

fetch를 쓰지 않아도 캐싱하는 것에 영향을 주지 않으며, 라우트의 세그먼트에 따라 static / dynamic이 정해진다.
말그대로 세그먼트가 static 이라면 언제나 캐싱되어있으며, revalidate 옵션을 넣었을 경우 적용이 되는 것이고, dynamic이라면, 요청 시마다 refetch 하는 것이다.
📌
cookies()headers() 가 라우트 세그먼트를 dynamic하게 만들어 주는 것을 기억하자 꼼수로 쓸 수 있을거야…!
 
추가로 revalidate 옵션을 넣고 싶은데, fetch를 쓰지 않는 경우는
export const revalidate = 1000;
과 같이 revalidate 를 export 해주면 된다.
 

Data Caching

request 마다 Caching

  • fetch()
    • 위에서 설명한 내용처럼, cache, revalidate 등의 옵션으로 세팅이 가능하다.
  • React cache()
    • API Fething의 메모이제이션 기능이라고 생각하면 되겠다.
    • fetch는 요청에 대한 cache를 자동적으로 지원하고 있기 때문에 cache를 쓸 필요는 없다..
    • server data fetching의 경우 server-only 기능을 이용하자..
  • Preload pattern with cache()
공식 독스에서 Data Fetching Patterns 파트를 읽었다면, preload pattern 을 보았을 것이다.
// @components/User import { getUser } from '@utils/getUser'; export const preload = (id: string) => { void getUser(id); }; export default async function User({ id }: { id: string }) { const result = await getUser(id); // ... } // app/user/[id]/page.tsx import User, { preload } from '@components/User'; export default async function Page({ params: { id }, }: { params: { id: string }; }) { preload(id); // starting loading the user data now const condition = await fetchCondition(); return condition ? <User id={id} /> : null; }
위의 상황에서 cacheserver-only 를 사용하게 되면 아래와 같이 만들 수 있다.
import { cache } from 'react'; import 'server-only'; export const preload = (id: string) => { void getUser(id); }; export const getUser = cache(async (id: string) => { // ... });
데이터를 우선적으로 fetch하고, 응답을 캐싱하며, 서버에서만 데이터 페칭이 이루어진 다는 것을 보장하는 코드가 되는 것이다.

Revalidating

fetch의 revalidate 옵션 / revalidate 변수 export 와 같이 revalidate를 설정할 수 있지만,
이렇게 되면 설정한 초만큼 지나야 해당 캐시는 stale 상태가 될 것이다.
아래는 필요할 경우 revalidate를 하는 방법이다.
Route Handler에 위치시켜야 한다
 
  1. revalidatePath: path 기반
// app/api/revalidate/route.ts import { NextRequest, NextResponse } from 'next/server'; import { revalidatePath } from 'next/cache'; export async function GET(request: NextRequest) { const path = request.nextUrl.searchParams.get('path') || '/'; revalidatePath(path); return NextResponse.json({ revalidated: true, now: Date.now() }); }
  1. revalidateTag: cache tag 기반
// app/page.tsx export default async function Page() { const res = await fetch('https://...', { next: { tags: ['collection'] } }); const data = await res.json(); // ... }
// app/api/revalidate/route.ts import { NextRequest, NextResponse } from 'next/server'; import { revalidateTag } from 'next/cache'; export async function GET(request: NextRequest) { const tag = request.nextUrl.searchParams.get('tag'); revalidateTag(tag); return NextResponse.json({ revalidated: true, now: Date.now() }); }
두 방법 모두 이후에 아래와 같은 방식으로 revalidate 하면 될 것이다.
fetch('/api/revalidate?path={path}') fetch('/api/revalidate?tag={tag}')
 

유용해 보이지만 당장 필요없는 기능들

Parallel Routes

모달을 띄우는 방식에서는 쓰일만 할것으로 생각되지만, 기획의 내용에 따라 달라질 것으로 생각된다.
 

Intercepting Routes

위와 마찬가지.
 

useSelectedLayoutSegment / useSelectedLayoutSegments

Client Component hook 으로서, Layout 내부의 라우트 세그먼트를 가져올 수 있다.
  • useSelectedLayoutSegments는 보통 Client Component에서 호출하여 Layout으로 import 해오는 방식으로 사용한다.
breadcrumb 만들 때 아주 야물딱지겠구만…
Layout
Visited URL
Returned Segments
app/layout.js
/
[]
app/layout.js
/dashboard
['dashboard']
app/layout.js
/dashboard/settings
['dashboard', 'settings']
app/dashboard/layout.js
/dashboard
[]
app/dashboard/layout.js
/dashboard/settings
['settings’]
 

Server Actions

Server Action은 말그대로 서버에서 하는 액션이라고 생각하면 편할 듯 하다.
서버사이드에서 데이터를 mutate할 때 쓰는 방식이다.
또한 'server-only’ 패키지와는 다르게 use server 를 사용하면 된다.
 
추가로 독스를 읽으면서, ReactQuery의 stale-while-revalidate 방식이
어떻게 ConcurrentMode와 Suspense에서도 구현이 되는 것인지 이해를 해버렸다..
아래의 첨부하는 포스팅 글을 읽고 참고하길 바란다.