[React] Deep Dive 모던 리액트(30) Next.js 13 & 리액트 18
FrontEnd/Deep Dive

[React] Deep Dive 모던 리액트(30) Next.js 13 & 리액트 18

728x90

 

 

터보팩(Turbopack)

 

요즘 새롭게 뜨는 라이브러리인 Rome,SWC,esbuild의 공통점은 기존에 JS로 만들어지고 제공되던 기능을 Rust,Go와 같은 언어를 사용하여 뛰어난 성능을 보여준다는 공통점이 있다.

특히 SWC의 경우 Next.js를 만든 Vercel에서 제공하는 도구로 많은 프로젝트에서 바벨을 대신하여 사용되고 있다.

 

Next.js13에서는 웹팩의 뒤를 자처하는 터보팩이 출시되었다. 웹팩대비 700배빠른데 이는 Rust를 기반으로 작성되었기 때문이라고 한다. 물론 현재는 개발 모드에서만 제한적으로 사용되기에 아직은 시간이 조금 걸릴것으로 보인다.

 

 

서버 액션(alpha)

이 기능은 API를 굳이 생성하지 않더라도 함수 수준에서 서버에 직접 접근하여 데이터 요청을 수행하는 기능이다. 서버컴포넌트와는 조금 다르게 함수 실행 그자체를 서버에서 수행할 수 있다. 

 

서버 액션을 활성화 하려면 next.config.js에서 실험 기능을 활성화해야 한다.

 

/** type {import('next').NecxtConfig} */
const nextConfig = {
	experimental : {
    	serverActions : true,
    },
}

module.exports = nextConfig

 

 

서버 액션을 만들려면 파일 상단에 'use server'지시자를 선언해야 하며 함수는 async 함수여야 한다.

 

async function serverAction() {
	"use server";
    //서버에 바로 접근하는 코드 
}

 

위 방식대로 서버 액션을 만들 수 있다.

 

 

form action

<from/>은 action props를 추가하여 데이터를 처리할 URI를 넘겨줄 수 있다. 아래 예제를 보자.

 

export default function Page() {
  async function handleSubmit() {
    "use server";

    console.log("해당 작업은 서버에서 수행하기에 CORS 이슈가 없음");

    const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
      method: "POST",
      body: JSON.stringify({
        title: "foo",
        body: "bar",
        userId: 1,
      }),
      headers: {
        "Content-Type": "application/json; charset=UTF-8",
      },
    });

    const result = await response.json();
    console.log(result);
  }

  return (
    <form action={handleSubmit}>
      <button type="submit">form 요청 보내보기</button>
    </form>
  )
}

 

 

위 코드를 실행하면 아래와 같이 post요청이 아닌 ACTION_ID라는 액션 구분자만 있다.

 

요청또한 브라우저가 아닌 서버에 찍힌다.

 

 

그럼 해당 요청은 어떻게 되어있을까? 이를 처리하는 서버를 보면 내용들이 미리 빌드되어 있다.

 

즉, 서버액션을 실행하면 클라이언트에서는 현재 라우트 주소와 ACTION_ID를 보내고 이를 바탕으로 실행해야 할 내용을 서버에서 직접 실행한다. 즉 'use server'로 선언되어 있는 내용은 빌드 시점에서 클라이언트와 분리되며 클라이언트 번들링 결과물에는 포함되지 않게 된다.

 

이는 폼과 실제 노출하는 데이터가 연동되어 있는 경우 더욱 효과적으로 사용할 수 있다.

 

import kv from "@vercel/kv";
import { revalidatePath } from "next/cache";

interface Data {
  name: string;
  age: number;
}

export default async function Page({ params }: { params: { id: string } }) {
  const key = `test:${params.id}`;
  const data = await kv.get<Data>(key);

  async function handleSubmit(formData: FormData) {
    "use server";

    const name = formData.get("name");
    const age = formData.get("age");

    await kv.set(key, {
      name,
      age,
    });

    revalidatePath(`/server-action/form/${params.id}`);
  }

  return (
    <>
      <h1>form with data</h1>
      <h2>
        서버에 저장된 정보 : {data?.name} {data?.age}
      </h2>

      <form action={handleSubmit}>
        <label htmlFor="name">이름 : </label>
        <input
          type="text"
          id="name"
          name="name"
          defaultValue={data?.name}
          placeholder="이름을 입력해 주세요"
        />

        <label htmlFor="age">나이 :</label>
        <input
          type="number"
          id="age"
          name="age"
          defaultValue={data?.age}
          placeholder="나이를 입력해 주세요"
        />

        <button type="submit">submit</button>
      </form>
    </>
  );
}

 

 

위 예제는 서버에서만 접근할 수 있는 Redis스토리지인 @vercel/kv를 기반으로 양식 데이터를 다루는 방법을 알려준다.

 

Page컴포넌트는 서버 컴포넌트로 직접 서버 요청을 수행하여 JSX를 렌더링한다. 이후 form 태그에 서버 액션인 handleSubmit을 추가하여 formData를 기반으로 데이터를 가져와 DB인 kv를 업데이트한다.

 

업데이타 성공적으로 마무리 되면 마지막으로 revalidatePath를 통하여 캐시 데이터를 갱신하는 과정을 겪는다.

 

 

앞선 예제와 비슷하게 서버에 ACTION_ID와 실행에 필요한 데이터만 보내고 직접적인 업데이틑 수행하지 않는다. 이 내용들은 전통적인 서버 기반 웹 애플리케이션과 다를게 없어보이지만 이 과정들이 페이지 새로고침 없이 수행된다는 큰 차이점이 있다.

 

revalidatePath또한 주목해야 한다! 인수로 넘겨받은 경로의 캐시를 초기화해서 해당 URL에서 새로운 데이터를 불러오는 역할을 하며 이를 server mutation이라고 한다. 

 

 

이처럼 form을 서버 액션과 함께 사용하면 form을 기반으로 한 데이터 추가 및 수정 요청을 좀 더 자연스럽게 수행할 수 있다.

 

 

서버 액션 사용시 주의점

 

1. 서버 액션은 클라이언트 컴포넌트 내에서 정의될 수 없다.

2. 서버액션을 import하는 것 뿐 아니라 props형태로 클라이언트 컴포넌트에 넘기는 것은 가능하다. 

3. 정리하자면 서버에서만 실행될 수 있는 자원은 꼭 분리해야한다.

 

 

 

Next.js 13코드 맛보기

지금까지 3개의 포스트에 거쳐서 Next.js의 변화점에 알아보았다! 이번에는 간단한 예제를 실행해보자.

 

https://github.com/wikibook/react-deep-dive-example

 

GitHub - wikibook/react-deep-dive-example: 《모던 리액트 Deep Dive》 예제 코드

《모던 리액트 Deep Dive》 예제 코드. Contribute to wikibook/react-deep-dive-example development by creating an account on GitHub.

github.com

 

 

프로젝트는 책의 GitHub에서 볼 수 있다.

 

 

getStaticProps와 비슷한 정적인 페이지 렌더링 구현

13버전 이전에서는 getStaticProps나 getStaticPaths를 이용하여 경로들을 모아둔 다음 props를 미리 빌드하는 형식으로 구성되어 있었다. 이와 유사하게 13버전에서도 fetch의 cashe를 이용하여 구현할 수 있다.

//app/ssg/[id]/page.tsx
import { fetchPostById } from '#services/server'

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

export default async function Page({ params }: { params: { id: string } }) {
  const data = await fetchPostById(params.id)

  return (
    <div className="space-y-4">
      <h1 className="text-2xl font-medium text-gray-100">{data.title}</h1>
      <p className="font-medium text-gray-400">{data.body}</p>
    </div>
  )
}

 

잘 보면 id로 사용할 수 있는 값들을 미리 객체배열로 모아두었으며 이에 대한 동작을 미리 정해두었다.

 

또, fetchPostById에 별다른 캐시 옵션을 주지 않았는데 이는 모든 cache 값을 사용한 것과 같다!

 

 

빌드된 내용을 보면 해당 id에 대항하는 페이지들이 미리 만들어져 있는 것을 확인할 수 있다.

 

정적으로 미리 빌드해두는 것 뿐 아니라 캐시를 활용하는 것 또한 가능하며 이를 활용하면 일정 시간이 지나면 새롭게 데이터를 불러오는 방식으로 페이지를 구성할 수 있다.

 

 

//app/ssg/[id]/page.tsx
export const revalidate = 60;
import { fetchPostById } from '#services/server'

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

export default async function Page({ params }: { params: { id: string } }) {
  const data = await fetchPostById(params.id)

  return (
    <div className="space-y-4">
      <h1 className="text-2xl font-medium text-gray-100">{data.title}</h1>
      <p className="font-medium text-gray-400">{data.body}</p>
    </div>
  )
}

 

 

최상단에 revalidate값을 추가하여 시간이 지나면 페이지를 다시 생성하는 것을 확인할 수 있다.

 

 

로딩, 스트리밍, 서스펜스

Next.js13 에서 스트리밍과 서스펜스를 활용하여 컴포넌트가 렌더링 중이라는 것을 나타낼 수 있다.

 

import { Suspense } from 'react'

import { PostByUserId, Users } from './components'

export default async function Page({ params }: { params: { id: string } }) {
  return (
    <div className="space-y-8 lg:space-y-14">
      <Suspense fallback={<div>유저 목록을 로딩중입니다.</div>}>
        {/* 타입스크립트에서 Promise 컴포넌트에 대해 에러를 내기 때문에 임시 처리 */}
        {/* @ts-expect-error Async Server Component */}
        <Users />
      </Suspense>

      <Suspense
        fallback={<div>유저 {params.id}의 작성 글을 로딩중입니다.</div>}
      >
        {/* @ts-expect-error Async Server Component */}
        <PostByUserId userId={params.id} />
      </Suspense>
    </div>
  )
}

 

위처럼 Suspense를 활용하면 로딩 중일때 컴포넌트가 로딩중이라는 것을 보여줄 수 있다.

 

 

지금까지 리액트 18에서 새롭게 등장한 기능등과 Next.js13의 기능들을 알아보았다.

 

책의 저자는 이를 create-react-app수준의 프레임워크 없이 새롭게 제공하는 기능을 사용하긴 어려울 것이라고 전했다. 애초에 이를 잘 사용하게 하기 위해 Next.js와 같은 프레임워크 팀과 리액트팀은 협업중이다. 따라서 리액트 개발자들은 이러한 리액트의 기초 개념을 익히고 기능을 사용하는 방식에 대해 섭렵하는데 초점을 맞추는 것이 중요할 것이다.

 

728x90