# next.js의 라우팅과 서버 액션

# 파일 시스템 라우팅

next.js의 라우팅은 파일 시스템 기반입니다. app 라우터 기준으로,

* `/app/page.tsx` =&gt; `https://your.site/`
    
* `/app/example/page.tsx` =&gt; `https://your.site/example`
    

이 되는 식이죠. `your.site`는 사이트의 도메인을 나타내는 예시 값입니다. 각 `page.tsx`는 `default` 내보내기로 JSX 컴포넌트를 내보내야 하고요.

## `layout.tsx`, `page.tsx`

* [**공식 문서**](https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts)
    

`page.tsx`는 한 페이지를 나타냅니다. `default` 내보내기로 JSX 컴포넌트를 내보내야 합니다.

```typescript
export default function Page() {
  return <h1>Hello, Home page!</h1>
}
```

`layout.tsx`는 특정 폴더 내의 모든 페이지에서 공유할 UI를 넣는 파일입니다. `page.tsx`와 마찬가지로 `default` 내보내기로 JSX 컴포넌트를 내보내지만, `{ children }` 를 인자로 받습니다.

```typescript
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <section>
      {/* 폴더 안의 모든 페이지에서 공통적으로 사용되는 UI */}
      <nav></nav>
 
      {children}
    </section>
  )
}
```

참고로 루트 레이아웃은 반드시 필요합니다. 또한, 루트 레이아웃 같은 경우 HTML 계층도의 최상단에 위치하기 때문에, `<html></html>` 이 필요합니다.

```typescript
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}
```

### `layout.tsx`의 중첩

* [공식 문서](https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts#nesting-layouts)
    

`layout.tsx` 파일은 렌더링될 page.tsx까지 디렉토리를 타고 내려가면서 순서대로 중첩됩니다. 상위 폴더에 있는 레이아웃 파일이 HTML 계층도에서 더 상위에 있습니다. 예시를 하나 들어보겠습니다.

```plaintext
❯ tree app
app
├── docs
│   ├── (with-search)
│   │   ├── building-your-application
│   │   │   ├── layout.tsx
│   │   │   └── routing
│   │   │       └── page.tsx
│   │   ├── layout.tsx
│   │   ├── nav.tsx
│   │   └── test
│   │       └── page.tsx
│   └── 1
│       └── page.tsx
├── favicon.ico
├── globals.css
├── layout.tsx
├── page.tsx
└── products
    └── [productId]
        └── page.tsx
```

위는 현재 폴더 구조입니다. `(with-search)`는 아래에서 설명할 예정인 라우트 그룹인데, 여기서는 그냥 폴더라고 생각하시면 됩니다.

* `/docs/building-your-application/routing` :
    
    * `/layout.tsx`, `/docs/(with-search)/layout.tsx`, `/docs/(with-search)/building-your-application/layout.tsx` 순서대로 중첩
        
* `/docs/test` :
    
    * `/layout.tsx`, `/docs/(with-search)/layout.tsx` 순서대로 중첩
        
* `/docs/1` :
    
    * `/layout.tsx` 만 사용됨
        

이런 식입니다.

## 동적 라우팅

* [공식 문서](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes)
    

그런데 url에 동적인 값이 들어가는 경우도 있죠? 그런 경우엔 파일 이름에 특수문자를 사용합니다.

* `[productId]`
    
* `[...slug]`
    
* `[[...slug]]`
    

### 단일 인자 라우팅

이런 식으로요. 위에서부터 살펴보자면, `[productId]`는 URL에서 두 개의 `/` 사이에 오는 값을 string으로 `page.tsx` 에 넘겨주라는 의미입니다.

타입스크립트로 표현하면 다음과 같습니다.

```typescript
// 파일: /app/products/[productId]/page.tsx

export default function ProductDetail({ params }: { params: { productId: string } }) {
  console.log(params.productId)
  console.log(params)
  
  return (
    <div>
      <h1>Product Detail: {params.productId}</h1>
    </div>
  );
}
```

위와 같은 파일이 있을 때, `/products/foo`로 접속하면 `params`는 `{ productId: "foo" }` 가 됩니다. 주의하셔야할 점이 하나 있는데, 그건 바로 `/products/123`처럼 변수 위치에 숫자만 있는 경우에도 `params.productId`의 타입은 문자열이라는 점입니다. 참고로 이 문법은 `productId`에 `/`가 들어가는 걸 허용하지 않기 때문에, `/products/foo/images` 로 접속하면 `/app/products/[productId]/images/pages.tsx`가 렌더링됩니다.

### 가변 인자 라우팅 (rest 문법)

`[...slug]` 같은 경우 `/`가 몇개가 있는지와 상관 없이 해당 위치에서부터 마지막까지의 pathname을 먹습니다. 위에서 `/products/[productId]/image`로 접속해도 `/app/products/[productId]/page.tsx`가 렌더링되지는 **않는다**는 얘기를 했는데요, `/app/products/[...productId]/page.tsx` 가 존재하면 이 파일이 렌더링됩니다. 이때의 `params.productId` 는 `string[]` 타입입니다.

타입스크립트로 표현하면 다음과 같습니다.

```typescript
// 파일: /app/products/[...productId]/page.tsx

export default function ProductDetail({ params }: { params: { productId: string[] } }) {
  console.log(params)
  
  return (
    <div>
      <h1>Product Detail</h1>
      <p>{params.productId.join(' !!!!! ')}</p>
    </div>
  );
}
```

`/products/foo/images` 로 접속했을 때, `params.productId`의 값은 `["foo", "images"]`입니다. 이런 걸 어디에 쓰나 싶으실 수도 있는데, 웹에서 사용할 수 있는 클라우드 스토리지 서비스처럼 폴더 개념이 있는 경우 매우 유용합니다. 단, 이 문법은 빈 문자열에는 매칭되지 않습니다. 쉽게 말해서, `/foo/bar/[...slug]/page.tsx`는 `/foo/bar`로 접속했을 때 렌더링되지 않습니다.

### 옵셔널 가변 인자 라우팅

`[[...slug]]` 같은 경우, 빈 문자열에도 매칭되는 Rest 문법입니다. `/foo/bar/[[...slug]]/page.tsx` 가 있다면, `/foo/bar`로 접속했을 때 해당 파일이 렌더링됩니다.

참고로 값을 `[]`로 감싸서 해당 값이 옵셔널임을 나타내는 건 유닉스 계열의 CLI 도구에서 흔히 볼 수 있는 일종의 약속입니다.

## 라우트 그룹

* [공식 문서](https://nextjs.org/docs/app/building-your-application/routing/route-groups)
    

라우트 그룹은, 말 그대로 일부 라우트를 그룹으로 묶은 겁니다. 근데 라우터가 파일 시스템 기반이니까 특정 폴더에 넣은 꼴이 되는 것이죠. 그런데 폴더 이름이 그대로 URL 경로가 되는 게 next.js의 라우팅 시스템이므로, 특정 폴더는 URL경로에서 제외하기 위한 문법이 필요하고, 그 문법은 `(dir-name)`입니다.

페이지들을 용도에 따라 분류하기 위해서 사용할 수도 있고, `layout.tsx`를 특정 페이지들에만 적용시키기 위해서 사용할 수도 있습니다. 저 같은 경우는 `/app/(auth)/layout.tsx`에 인증이 안 되어있으면 로그인 페이지로 보내는 로직을 넣어놓고, 인증을 해야만 접근할 수 있는 페이지는 `/app/(auth)/settings/profile/page.tsx` 같은 `(auth)` 폴더의 하위 경로에 만듭니다. 이렇게 하면 인증 여부 확인하는 로직을 단 한번만 작성한 뒤 인증이 필요한 모든 페이지에서 재사용할 수 있습니다.

## Parallel 라우트

* [공식 문서](https://nextjs.org/docs/app/building-your-application/routing/parallel-routes)
    

next.js는 한 페이지를 여러 파일로 쪼갠 뒤, 각각에 대해 `page.tsx`, `layout.tsx` 나, 이 글에서 다루지 않는 `loading.tsx`, `error.tsx`등을 사용하는 걸 지원합니다.

`foo`라는 폴더에 이름이 `@left`, `@right` 인 폴더들이 있다면, 해당 폴더들의 `page.tsx`를 렌더링한 게 `foo/layout.tsx` 에 인자로 넘어옵니다. 이를 타입스크립트로 표현하면 다음과 같습니다.

```typescript
export default function Layout(props: {
  children: React.ReactNode
  left: React.ReactNode
  right: React.ReactNode
}) {
  return (
    <div>
      <div>
          {props.left}
      </div>
      <main>
          {props.children}
      </main>
      <div>
          {props.right}
      </div>
    </div>
  )
}
```

예시 이름이 `@left`, `@right`이었기 때문에 각각 `left`, `right`가 된 것입니다.

## Intercepting 라우트

* [공식 문서](https://nextjs.org/docs/app/building-your-application/routing/intercepting-routes)
    

Intercepting 라우트는 말 그대로 특정 URL로 이동할 때 이를 가로채는 라우팅인데요, 이를 이용하면 URL을 바꾸면서도 기존 페이지의 상태는 유지하는 게 가능합니다. 그런데 URL 매칭 룰이 꽤 복잡합니다.

공식 예시인 `nextgram` 을 사용해서 설명할 건데, 폴더 구조는 다음과 같습니다.

```plaintext
❯ tree
.
├── @modal
│   ├── (.)photos
│   │   └── [id]
│   │       └── page.tsx
│   └── default.tsx
├── default.tsx
├── global.css
├── layout.tsx
├── opengraph-image.png
├── page.tsx
└── photos
    └── [id]
        └── page.tsx
```

아래 화면은 `nextgram`을 실행한 화면입니다.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699135868719/634315e2-3f3b-4b17-98df-de4105afec58.jpeg align="center")

여기서 이미지 하나를 클릭하면 화면이 아래처럼 되는데요, 주소창을 보시면 URL이 바뀌어있습니다.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699135799950/83077b41-fd55-450f-9429-cdaf2d8a4cf7.jpeg align="center")

URL이 바뀌기 때문에 즐겨찾기나 공유 기능을 자연스럽게 사용할 수 있습니다.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699135872985/a1999583-54df-4dd1-ac6d-3d337ae64d86.png align="center")

새로고침을 하거나, 즐겨찾기로 들어오거나, 공유된 링크를 이용해서 접속하면 위와 같은 화면이 나옵니다.

# 컴포넌트 렌더링 (서버/클라이언트 컴포넌트)

## RSC: React Server Component

* [공식 문서](https://nextjs.org/docs/app/building-your-application/rendering/server-components)
    

React에는 React Server Component라는 개념이 있습니다. 이름은 서버에서 렌더링하는 컴포넌트라는 뜻입니다. 일반적으로 서버쪽 코드에서 데이터 가져오는 로직은 훨씬 간단하기에 코드가 간단해지고, RSC는 서버에서 렌더링되는만큼 검색 엔진 최적화 (SEO)에도 도움이 됩니다.

```typescript
export default async function Page() {
  await wait(10000)

  return <div>My doc</div>;
}

async function wait(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
```

서버 컴포넌트는 `async/await`을 지원합니다. 위의 코드로 페이지를 만들면, 10초를 기다린 뒤 `My Doc`이 렌더링되고 페이지가 브라우저에 렌더링됩니다.

하지만 서버 컴포넌트는 클라이언트에게 클로져 같은 걸 넘길 수 없고, `useEffect`, `useState` 같은 훅들을 사용할 수도 없습니다. 서버 컴포넌트와 클라이언트 컴포넌트를 적절히 섞어서 활용해야하는 이유죠.

## 클라이언트 컴포넌트

* [공식 문서](https://nextjs.org/docs/app/building-your-application/rendering/client-components)
    

next.js에서 모든 컴포넌트는 기본값으로 서버 컴포넌트이기 때문에, 클라이언트 컴포넌트는 클라이언트 컴포넌트라고 표시를 해줘야합니다. 이는 파일 첫번째 줄에 `'use client';` 를 추가함으로써 표시합니다.

```typescript
'use client'

export default function ProductDetail({ params }: { params: { productId: string } }) {  

  return (
    <div>
      <h1>Product Detail</h1>
    </div>
  );
}
```

위의 예시는 페이지 전체를 클라이언트 컴포넌트로 선언하는 예시지만, 페이지의 일부만을 클라이언트 컴포넌트로 만들 수도 있습니다. 단, 서버 컴포넌트와 클라이언트 컴포넌트를 같은 파일에 넣을 수는 없습니다.

클라이언트 컴포넌트에서는 `async/await`을 사용할 수 없지만 `useState`나 `useEffect` 같은 훅을 사용할 수 있습니다.

### Hydration

클라이언트에서 자바스크립트로 DOM 요소들에 이벤트 핸들러 함수등을 연결하는 걸 Hydration이라고 합니다. 클라이언트 컴포넌트들에 대해서 일어납니다.

# 서버 액션

* [공식 문서](https://nextjs.org/docs/app/building-your-application/data-fetching/forms-and-mutations#server-only-forms)
    

개발을 하다보면 데이터베이스 관련 작업들처럼 서버에서 해야하는 작업들이 있는데요, 원래는 이걸 하려면 form으로 다른 페이지로 이동시키거나, REST 엔드포인트를 만들어서 `fetch` 등으로 해당 엔드포인트를 호출했어야합니다. 그 엔드포인트에서는 요청 body 비직렬화해서 작업을 수행한 뒤 처리 결과를 직렬화해서 클라이언트한테 반환해줘야했고요. 그런데 서버 액션을 쓰면 이런 작업들을 할 필요가 없어집니다.

```typescript
export default function Page() {
  async function create(formData: FormData) {
    'use server'
 
    // DB 쿼리
  }
 
  return <form action={create}>...</form>
}
```

사용자가 작성한 `create` 함수의 코드는 서버에서 실행됩니다. 클라이언트 사이드에도 `create`함수가 존재하긴 하지만, 하는 일은 전혀 다릅니다. 클라이언트 사이드에 존재하는 `create` 함수는 서버사이드의 `create` 함수를 JSON 직렬화/비직렬화를 통해 호출하고 반환 값을 직렬화해서 반환하는 함수입니다.

위 예시처럼 특정 기능에 관련된 로직을 한 파일에 모으기 위해 서버 액션을 사용할 수도 있고, `'use server';`을 사용하는 서버 액션들을 한 파일에 모을 수도 있습니다.

타입 추론을 포함한 타입스크립트 타이핑이 완벽하게 지원되기 때문에 잘 사용하면 매우 편리합니다.
