Nextjs14 업데이트
2023-11-15, (수) 20:01
Nextjs14.0.0 업데이트 관련 내용

업데이트 내용 전문

Nextjs14가 업데이트 되었다.

주요 업데이트 사항은 다음과 같다.

기타 업데이트 사항은 다음과 같다.

  • 최소 노드버전 18.17
  • next-swc 빌드에 wasm 타겟 제거
  • @next/font 지원중단, next/font 사용 권장
  • ImageResponse가 이제 next/server가 아닌 next/og에 있습니다
  • next export 명령어 지원 중단, 대신에 next.config.js설정에 output: 'export'
  • Image속성에 onLoadingComplete 지원 중단, 대신 onLoad 사용 권장
  • next.config.js설정에 images.domains 지원 중단, 대신 remotePatterns 사용 권장
  • fetch 캐싱에 좀 더 자세한 로깅 활성화 가능
  • create-next-app 크기 80% 절감
  • edge 개발시 메모리 관리 강화
Nextjs13부터 확장된 Fetch에 대해서 - (Mutations을 알아보기 전에)

Forms and Mutations 그리고 Server Actions가 Stable이 되었다. Mutations 관련된 기능은 보통 캐싱 전략에서 필요하다. 서버에 데이터 캐시를 최신으로 업데이트 하는 용도라고 보는게 가장 이해가 쉬울것이다.

그리고 이 기능들을 설명하기 위해서는 nextjs app 라우터에서 확장한 fetch에 대해서 이해할 필요가 있다. Nextjs Documents에서도 이것과 관련해서는 한 그룹으로 묶어서 설명하고 있다.

우선 Nextjs가 확장한 fetch에 대해서 조금 살펴보도록 하자. 이 기능을 위해서는 기존의 fetch의 cache에 대한 기본적인 이해가 필요하다.

javascript
// 'force-cache'가 Next.js 에서 기본값, 생략 가능
fetch('https://...', { cache: 'force-cache' })

이런 형식으로 fetch에는 cache의 속성을 지정해 요청할 수 있으며, 캐시 속성에 대한것은 이곳을 참조하자.

이렇게 캐시된 데이터는 재검증이 이뤄지기 전까지 저장된 캐시데이터를 활용하게 된다. 여기서 최신 데이터로 업데이트 하려면, 시간 검증 및 주문형 재검증을 통한 작업이 필요하다.

A. 시간 검증

시간검증은 revalidate를 활용하여, 캐시의 수명을 직접 설정하는 방법이다.

1. fetch를 활용한 시간검증.

{ next: { revalidate: 3600 } }) Nextjs가 확장한 옵션 revalidate 이용한다.

javascript
fetch('https://...', { next: { revalidate: 3600 } })

fetch내에서 revalidate를 통해 캐시 수명을 설정할 수 있다.

2. fetch를 사용하지 못하거나, 다른 라이브러리를 통한 시간검증

(1) react18에서의 cache 활용

javascript
import { cache } from 'react'
 
export const revalidate = 3600 // revalidate the data at most every hour
 
export const getItem = cache(async (id: string) => {
  const item = await db.item.findUnique({ id })
  return item
})

(2) next/cache를 활용 (unstable)

javascript
import { getUser } from './data';
import { unstable_cache } from 'next/cache';
 
const getCachedUser = unstable_cache(
  async (id) => getUser(id),
  ['my-app-user']
);
 
export default async function Component({ userID }) {
  const user = await getCachedUser(userID);
  ...
}

// const data = unstable_cache(fetchData, keyParts, options)() 의 형태를 가지며
// options에 tags와 revalidate를 설정할 수 있다.
// 23년 11월 기준) unstable이라서 변경되거나 활용되지 않을 가능성이 있다.

B. 주문형 검증

주문형 검증은 캐싱 재검증을 원하는 때에 요청할 수 있다. 방법은 여기서 다시 두가지로 나뉘는데, 하나는 tag를 미리 설정하여 태그를 요청하는 것, 또 하나는 요청한 path route를 이용하는 것이다.

1. tag를 활용

app/page.tsx
export default async function Page() {
  const res = await fetch('https://...', { next: { tags: ['collection'] } })
  const data = await res.json()
  // ...
}

{ next: { tags: ['collection'] } Nextjs가 확장한 옵션 tags를 이용한다음, revalidateTag를 요청하여, 재검증을 요청한다.

app/actions.ts
'use server'
 
import { revalidateTag } from 'next/cache'
 
export default async function action() {
  revalidateTag('collection')
}

2. Path를 활용

path를 활용하는 방법은 revalidatePath의 인자로 경로를 직접 넣어주면 된다. 이렇게하면 해당 페이지에서 요청하여 캐시된 데이터 전체가 적용된다.

javascript
import { revalidatePath } from 'next/cache'
revalidatePath('/blog/post-1')

해당경로는 파일 시스템 경로 /product/[slug]/page 도 가능하며, /product/123 형태로 slug를 직접 지정 해서도 가능하다. (1024자 미만)

revalidatePath의 형태는 다음과 같은데, type옵션이 app라우터의 page와 layout을 지정해 줄 수 있다.

javascript
revalidatePath(path: string, type?: 'page' | 'layout'): void;
Forms and Mutations과 Server Actions

Nextjs 문서에서 Forms and Mutations를 확인해보면, 이 기능은 사실 [email protected] 또는 [email protected]의 새로운 것들이 적용된 것이다.

우선 이 버전에서의 form이 기존 HTML에서 동작하던 방식과는 달라졌는데, 기존에는 action이 URL로 처리되기 때문에 본래의 이벤트가 동작하지 않게 별도의 처리를 하지 않아도 된며, 첫번째 인수가 FormData로 넘어온다.

typescript
<form action=((formData:FormData)=>{
           // do something
           }>

그리고 이 form과 관련해서 몇가지 훅들이 추가 되었는데,

이 문서 예제에서 대표적인 훅들로는 다음과 같다

javascript
const [state, formAction] = useFormState(fn, initialState);
javascript
const { pending, data, method, action } = useFormStatus();
javascript
  const [optimisticState, addOptimistic] = useOptimistic(state, updateFn);

이런 react의 최신 훅을 소개한 것과 더불어 Nextjs에서는 서버사이드 상에서 Server Actions를 활용해서 Form의 작업을 서버상에서 안전하게 처리할 수 있도록 했다. 이때 캐시 재검증(revalidate)를 요청해서 데이터를 최신화 할 수 있다.

결국 이걸 이해하기 위해서는 다시 Server Actions를 먼저 살펴봐야 하는데, 사용방법은 서버컴포넌트와 클라이언트 컴포넌트의 경우가 각각 다르다.

  • 서버 컴포넌트에서 Server Actions사용

app/page.js
export default function ServerComponent() {
  async function myAction() {
    'use server'
    // ...
  }
}
  • 클라이언트 컴포넌트에서 Server Actions사용

app/actions.js
"use server"
 
export async function myAction() {
  // ...
}

클라이언트 컴포넌트에서는 페이지 내에 'use client'가 선언되어 있기때문에, 별도로 다른 파일로 관리해주어야 하는게 차이점이다.

그리고 Server Actions의 인수와 리턴값은 Serializable 해야한다. 직렬화Serialization는 전송에 적합한 형태로 변환하는 프로세스이며, 여기서는 문자열로 변환 가능한 형식(스트링, 배열, 객체)만 허용한다는 뜻

예제에서는 주로 form을 활용하여서 보여주고 있는데, 다른 이벤트에서도 물론 가능하다. 다만 Dom에 바인딘됭 이벤트 함수의 event인수는 Serializable 하지 않기 때문에 그대로 전달할 수 없다.

app/actions.ts
'use server'
export async function callSomething() {
  console.log('something')
}

이런 함수가 있다고 했을때, 아래의 코드 예시와 같이 직렬화 되지 않은 인수를 전달하거나, 혹은 리턴하게 되면 에러가 난다.

app/page.tsx
"use client";

import { callSomething } from "./actions";

export default function ClientTest() {
  return (
    <div>
    <button onClick={callSomething}>call something</button> // Error, 직렬화 되지않은 event 인수가  Server에 전달되려고 시도해서 에러를 일으킨다.
      <button onClick={() => callSomething()}>call something</button> // Ok, 동작 가능
    </div>
  );
}

지금까지의 내용을 모두 종합하면 다음처럼 사용할 수 있다.

Server Actions 활용
app/actions.tsx
'use server'

import { revalidatePath } from 'next/cache'
import { z } from 'zod'


export async function createTodo(prevState: any, formData: FormData) {
  const schema = z.object({
    todo: z.string().min(1), // 스키마 검증
  })
  // ... 기타 작업
    revalidatePath('/') // 경로 재검증
    return { message: `Added todo ${data.todo}` } // 결과 리턴
  } catch (e) {
    return { message: 'Failed to create todo' } // 결과 리턴
  }
}
forms and mutations 활용
app/page.tsx

const initialState = {
  message: null,
}

function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button type="submit" aria-disabled={pending}>
      Add
    </button>
  )
}

export function AddForm() {
  const [state, formAction] = useFormState(createTodo, initialState)

  return (
    <form action={formAction}>
      <label htmlFor="todo">Enter Task</label>
      <input type="text" id="todo" name="todo" required />
      <SubmitButton />
      <p aria-live="polite" className="sr-only" role="status">
        {state?.message}
      </p>
    </form>
  )
}
@tanstack/react-query 5 업데이트, 변경사항
Markdown 페이지 설정 - 블로그 3