Nextjs App Router (^15)

Ryan Cho·2025년 10월 1일

Nextjs App Router

Page Router은 추후에 학습한다. 기존 리액트를 다뤘다면 Nextjs에 대해 학습곡선이 낮고, 서버컴포넌트의 아름다움을 느끼게 된다.

기본 Routing 구조

폴더기반 라우팅 구조

Next에서 지원하는 특수 네이밍파일을 기준으로 폴더 기반 라우팅을 구성한다.
page.tsx, layout.tsx, not-found.tsx, error.tsx, loading.tsx, error.tsx

Nextjs directive ('use client', 'use server')

Next의 컴포넌트는 기본적으로 Server Componenet이다.

파일 최상단에 use client 선언시 Client Component로 선언됨.
use serverServer Action에 선언하여 form요소의 action에 직접 사용한다,

Server/Client Components 구분 기준

기존 React는 SPA, 즉 Client Side Rendering 방식의 Client Component만 적용했다면,
Nextjs를 사용하는 이유는 SSR의 이점을 얻기 위함이고 (보안, 렌더링속도, SEO 등), 따라서 Nextjs의 기본값은 모든 컴포넌트는 Server Component이다.

즉, SSR을 우선 원칙으로 두고, 필요시에만 outsourcing & code splitting으로 Client Component를 분리한다.

Client Components 선택 기준

  • 에러 핸들링 (error.tsx) must be 'use client'
    런타임에 발생한 에러, 서버에서 에러가 발생해도(server action / fetch) 해당 컴포넌트는 React-Error-Boundary로 이동되기 때문에 Client Component로 분류되어야함.
  • 사용자 인터렉션이 필요한 경우 (Event Handler)
    ** form 제출을 Server Action으로 수행해도, 채팅등의 스트리밍 응답 요청, 낙관적 업데이트, 백그라운드 리패칭 등 복잡한 상태관리가 필요할 때는 client 에서 수행
  • 사이드이펙트를 발생시키는 use- hooks들은 모두 Client Component에서만 호출 가능

default (Server Component)

Server Component는 비동기 컴포넌트로 만들 수 있다. 따라서 데이터 패칭도 Server Component 내에서 바로 fetch 가능
-> 이에 따른 loading, error 상태는 라우팅 단계에서 설정 가능

** 이는 라우트 레벨에서만 작동함(첫 페이지 로드). 이후 동일한 페이지 내에서 발생하는 loading 상태에 대해서는 해당 loading.tsx가 트리거 되지 않음.
따라서 세밀한 로딩 제어는 <Suspense fallback={}/> 으로 처리

고급 Routing Pattern

Dynamic Routing

디렉토리 네이밍으로 [slug] 와 같이 작성.
-> pathParams 객체를 하위 페이지에서 Props으로 받음. (params)

pathParams, searchParams 모두 비동기 처리 필요 (next ^15)

const export Page = async({params}: {params: Promise<NextParsedUrlQuery>) => {
	const {slug} = await params
    
    return ...
}

Catch-All Routing

중첩된 dynamic routing 구조를 일괄 표현하여 디렉토리 네이밍으로 [[...slug]] 와 같이 작성.
-> pathParams안에 모든 slug들이 배열로 이루어짐
depth가 깊지 않다면 중첩해서 일반 dynamic routing을 쓰는게 나을듯

Parallel Routing

** 하나의 레이아웃에 독립된 페이지들을 한 url 경로안에 보여줄 수 있다

slot을 형성한다 -> @modal, @items, @feed 등으로 디렉토리 네이밍. 이를 slot으로 칭한다.
-> 해당 slot은 경로를 참조할 때 depth가 계산되지 않는다.
-> layout에서 slot명을 props로 받아서 (ReactNode) 렌더링한다.
-> 필요에 따라 default.tsx요구
-> 동일한 url안에 동적인 페이지를 부분적으로 렌더링 가능

Intercepting Routing

이건 아무리 봐도 개쩌는 기능이다.
특정 경로의 페이지에서 사용자 인터렉션으로 라우팅 된 경우에 트리거되며 렌더링을 다르게 표현할 수 있다.
정확히 표현하자면 해당 경로 페이지의 컨텍스트를 유지한다.

예를 들자면, 어떤 상품리스트가 존재하고, 해당 상품을 클릭하면 모달로 상세화면을 볼 수 있다. (기존페이지는 모달 백그라운드에 오버레이됨) => 그리고 모달 상태의 url을 공유하면 공유받은 사용자는 해당 url로 모달이 아닌 상품의 상세페이지 뷰를 새롭게 보게할 수 있다.

Intercepting Routing의 컨벤션은 다음과 같다.

  • (.)pathName/ -> 동일 경로 참조
  • (..)pathName/ -> 한 단계 위 경로 참조
  • (..)(..)pathName/ -> 두 단계 위 경로 참조
  • (...)pathName/ -> 루트경로 (app/) 참조

컨벤션의 경로에 slot은 계산되지 않는다

컨벤션의 pathName과 실제 프로젝트 경로에 존재하는 pathName과 100% 1:1 매칭되는건 아니다.
(위의 예제는 100% 존재해야겠지만)

그리고 Parallel Routing구조, slot과 주로 같이 사용된다.

** 위 예시의 대략적 구현

# /items/page.tsx
export function Page(){
    return <Item/>
}

# /feed/@modal/(..)items/page.tsx
export function Page(){
    return <Modal onClose={}><Item/></Modal>
}

# /feed/@modal/page.tsx
export function Page(){
    return null
}

# /feed/@feed1/page.tsx
export function Page(){
    return <Link href="/item">ITEM</Link>
}

Server Action

Server Action에 깊이 알아보자.
주로 Nextjs에 백엔드로직을 직접 구현해 HTTP 통신 없이 폼 제출 및 서버 사이드 상태 업데이트 등을 효율적으로 처리하는데 최적화 되어있다. (HTTP POST Method만 공식적으로 지원한다)
하지만 외부 백엔드의 Mutation Api를 Server Action에서 사용하기도 한다.

Server Action의 장점은 클라이언트 사이드에서 호출해도 비동기 mutation 함수가 Nextjs 서버에서 HTTP 요청이 아닌 함수처럼 실행된다

이는 타입 안정성, 보안, 코드일관성, Nextjs 아키텍쳐와 통합 등에서 큰 이점이 존재한다.

주로 form 요소의 action 으로 사용된다.
폼 제출(회원가입, 장바구니, 프로필 설정 등), 단순 업데이트(좋아요, 팔로우, 북마크 등) 에 많이 사용됨.

Form 상태 관리 hooks

폼 제출에 Server Action을 사용할 때, 폼 제출의 결과 업데이트, pending 상태 등의 상태관리를 위한 hooks이 존재한다.

useFormStatus

마지막 폼 제출의 상태 정보를 제공한다.

const { pending, data, method, action } = useFormStatus();

useActionState

useFormStatus가 폼 제출 중인지를 간단히 알기 위한 용도였다면, useActionState는 서버 액션의 상태와 결과를 함께 관리하고 UI에 반영하는 좀더 고차원의 훅이다.

const [state, formAction, isPending] = useActionState(fn, initialState, permalink?);

이 훅을 사용할 경우 반환된 formAction을 form의 action attribute로서 사용한다

훅의 인자로 fn는 기존 action에 해당하고, initialState는 초기값을 지원한다.

만약 server action을 폼 요소에 적용하고, 유효성 검사로직에 통과하지 못할경우 커스텀 에러객체를 발생시킨다면
useActionState로 formAction을 사용하고 반환된 state로 error객체의 유무를 UI에 렌더링 가능하다.

Server Action은 form요소에 람다(익명함수)를 허용하지 않는다

formaction attribute는 함수의 참조만 넣을 수 있다.

그럼 formData가 아닌, 외부 인자를 요구하는 Server Action은 어떻게 대응하는가?

유일하게 bind() 를 사용한다
이를 통해 서버 액션 시스템에 직렬화가 가능하고 외부 인자를 사용 가능하다.
bind()의 첫번째 인자는 this 이고, 두번째부터 필요한 인자를 고정할 수 있다

<form action={toggleSomething.bind(null, props.id)}>
<button>
</form>

Route Handler (API Route)

Nextjs에서 지원하는 특수 파일 네이밍 중 route.ts도 존재한다.
폴더 경로 기반으로 해당 route.ts파일이 존재하는 위치를 엔드포인트로 삼고 REST API를 작성할 수 있다.

// api/post/route.ts
export async function GET() {
	await fetch() ...
}

=> fetch(https://{Nextjs FE도메인}/api/post)

API Route를 언제 쓰는가?

  • BFF 역할로 주로 사용, 외부 백엔드 API가 있을 때 클라이언트 오리진으로 프록시하여 CORS 문제 방지
  • 인증, 로깅, 요청 변환 등의 미들웨어 처리가 가능하여 API 게이트웨이 역할 수행
  • 공개 API, 웹훅, 외부 모바일 앱 통합용 RESTful 엔드포인트 구현에 적합

** API Route역시 Server Action처럼 Nextjs 서버내에 실행되지만 외부 api를 전통적인 HTTP 통신 방식으로 다룬다

Cache에 대해

빌드 후 프로덕션 기준

1) 기본적으로 Nextjs ^15는 fetch()에 대해 SSR처럼 작동한다 (cache: 'no-store')
2) 서버컴포넌트에서 직접 백엔드로직으로 db에서 데이터패칭을 할 경우에는 SSG로 작동한다.

Stale-While-Revalidate

next: {}

fetch옵션 중 cache를 직접 설정도 가능하지만 fetch 옵션 중 next: {}를 Nextjs에서 지원한다.

fetch('/...', {
	next: {
    	revalidate: seconds //해당 시간동안 캐싱, 이후는 캐시 초기화
    }
})

파일 자체에 선언

혹은 파일 자체에 선언하는 방법도 존재한다.

- export const revalidate = 5; // Nextjs에서 자동으로 revalidate를 적용
- export const dynamic = 'force-dynamic'; // Nextjs에서 cache: 'no-store' 적용 (기본값임)

revalidatePath(), revalidateTag()

Server Action이나 API Route에서 사용 가능하다
해당 함수를 적용하면 적용하고자하는 path 혹은 tag명에 캐시를 초기화한다.

  • revalidatePath('/path', 'page | layout')
    두번째 옵션 기본값은 'page'로 해당 경로에만 캐시 초기화, 'layout'일 경우는 경로의 하위 경로까지 캐시 초기화

  • revalidateTag()
    fetch옵션에 next: {tag: ['tagname']} 적용, 해당하는 tag명을 가진 데이터에 대해 캐시 초기화

서버컴포넌트에서 직접 백엔드 로직으로 db에서 데이터패칭을 할때의 revalidate

앞서 말했듯 이 경우는 SSG처럼 작동하기 때문에, 데이터 업데이트 시에 revalidatePath()를 사용하거나,
revalidateTag() 를 사용할 경우에는 백엔드 로직에 unstale_cache()를 wrapper로서 사용해서 해당 두번째 인자에 key값을, 세번째 인자에 tag명 삽입 후 사용.

결론

개발환경의 캐시와, 빌드 시점의 캐시를 어떻게 관리할지 고민해라

Metadata

일반 Metadata 설정은 layout.tsx 혹은 page.tsx에서

export const metadata: Metadata = {}

형식으로 지정하는데, 레이아웃에서 공통으로 지정 후, 페이지 단위에서 다시 Metadata를 지정하면
해당 페이지에서만 Metadata가 오버라이드된다.

Dynamic Metadata

동적 metadata적용은 위 metadata를 선언하는게 아닌,

export const generateMetadata: () => Promise<Metadata> = async(config) => {}

형식으로 생성 가능하다. 인자로 config 객체안에는 url파라미터 정보 등이 포함되어있다.

profile
From frontend to fullstack

0개의 댓글