[ Data Fetching ] Server Actions

차차·2023년 5월 19일
2

Next Docs

목록 보기
22/34
post-thumbnail

Server Actions은 Next.js의 알파 기능으로, React Actions을 기반으로 구축되었다. 이 기능을 사용하면 서버 측 데이터 변경, 클라이언트 측 JavaScript 감소 및 점진적으로 향상된 폼을 구현할 수 있다.

import { cookies } from 'next/headers';
 
export default function AddToCart({ productId }) {
  async function addItem(data) {
    'use server';
 
    const cartId = cookies().get('cartId')?.value;
    await saveToDb({ cartId, data });
  }
 
  return (
    <form action={addItem}>
      <button type="submit">Add to Cart</button>
    </form>
  );
}


Server Action 규칙

Next.js 프로젝트에서 Server Actions을 사용하려면 experimental serverActions 플래그를 활성화해야 한다.

// next.config.js

module.exports = {
  experimental: {
    serverActions: true,
  },
};

생성

"use server" 지시문이 함수 본문의 맨 위에 있는 비동기 함수를 정의함으로써 Server Action을 생성할 수 있다. 이 함수는 직렬화 가능한 인수와 React Server Components 프로토콜을 기반으로 하는 직렬화 가능한 반환 값이 있어야 한다.

// app/components/component.js

async function myAction() {
  'use server';
  // ...
}

또한 파일의 최상위에 "use server" 지시문을 사용할 수도 있다. 이는 여러 개의 Server Action을 내보내는 단일 파일이 있는 경우 유용하며, Client Component에서 Server Action을 가져오는 경우에는 필수적이다.

// app/actions.js

'use server';
 
export async function myAction() {
  // ...
}

최상위에 "use server" 지시문을 사용할 경우, 모든 내보내기(export)는 Server Action으로 간주된다.


호출

다음 방법을 사용하여 Server Actions를 호출할 수 있다.

  • action 사용 : React의 action 속성을 사용하여 <form> 요소에서 Server Action을 호출할 수 있다.

  • formAction 사용 : React의 formAction 속성을 사용하여 <button>, <input type="submit"><input type="image"> 요소를 <form>에서 처리할 수 있다.

  • startTransition을 사용한 사용자 정의 호출 : action이나 formAction을 사용하지 않고 startTransition을 사용하여 Server Actions를 호출할 수 있다. 이 방법은 Progressive Enhancement를 비활성화한다.

1. action

React의 action 속성을 사용하여 <form> 요소에서 Server Action을 호출할 수 있다. action 속성으로 전달된 Server Actions는 사용자 상호작용에 대한 비동기적인 부작용(asynchronous side effects)으로 작동한다.


action은 HTML의 기본 action과 유사하다.

export default function AddToCart({ productId }) {
  async function addItem(data) {
    'use server';
 
    const cartId = cookies().get('cartId')?.value;
    await saveToDb({ cartId, data });
  }
 
  return (
    <form action={addItem}>
      <button type="submit">Add to Cart</button>
    </form>
  );
}

2. startTransition 을 사용한 사용자 지정 호출

action이나 formAction을 사용하지 않고도 Server Actions를 호출할 수도 있다. 이를 위해 useTransition 훅이 제공하는 startTransition을 사용할 수 있으며, 이는 form, button 또는 input 외부에서 Server Actions를 사용하고자 할 때 유용하다.


startTransition을 사용하면 Progressive Enhancement 기능이 기본적으로 비활성화된다.

// app/components/example-client-component.js

'use client';
 
import { useTransition } from 'react';
import { addItem } from '../actions';
 
function ExampleClientComponent({ id }) {
  let [isPending, startTransition] = useTransition();
 
  return (
    <button onClick={() => startTransition(() => addItem(id))}>
      Add To Cart
    </button>
  );
}
// app/actions.js

'use server';
 
export async function addItem(id) {
  await addItemToDb(id);
  revalidatePath(`/product/${id}`);
}

3. startTransition 없이 사용자 지정 호출

Server Mutations을 수행하지 않는 경우, 다른 함수와 마찬가지로 함수를 직접 prop으로 전달할 수 있다.

// app/posts/[id]/page.tsx

import kv from '@vercel/kv';
import LikeButton from './like-button';
 
export default function Page({ params }: { params: { id: string } }) {
  async function increment() {
    'use server';
    await kv.incr(`post:id:${params.id}`);
  }
 
  return <LikeButton increment={increment} />;
}
// app/post/[id]/like-button.tsx

'use client';
 
export default function LikeButton({
  increment,
}: {
  increment: () => Promise<void>;
}) {
  return (
    <button
      onClick={async () => {
        await increment();
      }}
    >
      Like
    </button>
  );
}

개선사항

1. 실험적인 useOptimistic

실험적인 useOptimistic 훅은 애플리케이션에서 낙관적 업데이트를 구현하는 방법을 제공한다. 낙관적 업데이트는 앱이 더 반응적으로 보이도록하여 사용자 경험을 향상시키는 기술이다.

Server Action이 호출되면 응답을 기다리지 않고 예상 결과를 반영하여 UI가 즉시 업데이트된다.

'use client';
 
import { experimental_useOptimistic as useOptimistic } from 'react';
import { send } from './actions.js';
 
export function Thread({ messages }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [...state, { message: newMessage, sending: true }],
  );
  const formRef = useRef();
 
  return (
    <div>
      {optimisticMessages.map((m) => (
        <div>
          {m.message}
          {m.sending ? 'Sending...' : ''}
        </div>
      ))}
      <form
        action={async (formData) => {
          const message = formData.get('message');
          formRef.current.reset();
          addOptimisticMessage(message);
          await send(message);
        }}
        ref={formRef}
      >
        <input type="text" name="message" />
      </form>
    </div>
  );
}

2. 실험적인 useFormStatus

실험적인 useFormStatus 훅은 Form Action 내에서 사용할 수 있으며, pending 속성을 제공한다.

import { experimental_useFormStatus as useFormStatus } from 'react-dom';
 
function Submit() {
  const { pending } = useFormStatus();
 
  return (
    <input
      type="submit"
      className={pending ? 'button-pending' : 'button-normal'}
      disabled={pending}
    >
      Submit
    </input>
  );
}

3. 점진적 향상 ( Progressive Enhancement )

Progressive Enhancement(점진적 향상)은 JavaScript가 비활성화되었거나 JavaScript로드에 실패한 경우에도 <form>이 제대로 작동하도록 하는 기능이다. 이를 통해 JavaScript를 사용하지 않아도 사용자가 양식과 상호작용하고 데이터를 제출할 수 있다.


Server Form Actions 및 Client Form Actions 모두 Progressive Enhancement를 지원하며, 다음 두 가지 전략 중 하나를 사용한다.

  • Server Action을 직접 <form>에 전달하면 JavaScript가 비활성화되어도 양식이 상호작용 가능하다.

  • Client Action<form>에 전달하면 양식은 여전히 상호작용 가능하지만, 액션은 양식이 하이드레이션될 때까지 대기열에 추가된다. <form>은 선택적 하이드레이션으로 우선순위가 부여되므로 빠르게 진행된다.


'use client';
 
import { useState } from 'react';
import { handleSubmit } from './actions.js';
 
export default function ExampleClientComponent({ myAction }) {
  const [input, setInput] = useState();
 
  return (
    <form action={handleSubmit} onChange={(e) => setInput(e.target.value)}>
      {/* ... */}
    </form>
  );
}

두 경우 모두 하이드레이션 이전에 양식은 상호작용할 수 있다. Server Action은 클라이언트 JavaScript에 의존하지 않는 추가 이점이 있지만, 상호작용성을 희생하지 않고 필요한 경우에는 여전히 Client Action과 함께 추가 동작을 구성할 수 있다.



예시

클라이언트 컴포넌트와 함께 사용

Import

Server Actions는 Client Components 내에서 정의할 수 없지만, 가져올 수는 있다. Client Components에서 Server Actions를 사용하려면, 최상위에 "use server" 지시문이 포함된 파일에서 액션을 가져와야 한다.

// app/actions.js

'use server';
 
export async function addItem() {
  // ...
}
// app/components/example-client-component.js

'use client';
 
import { useTransition } from 'react';
import { addItem } from '../actions';
 
function ExampleClientComponent({ id }) {
  let [isPending, startTransition] = useTransition();
 
  return (
    <button onClick={() => startTransition(() => addItem(id))}>
      Add To Cart
    </button>
  );
}

Props

Server Actions을 가져오는 것이 권장되지만, 경우에 따라 Server Action을 프롭(prop)으로 Client Component에 전달하고 싶을 수도 있다.

예를 들어, 액션 내에서 동적으로 생성된 값을 사용하고자 할 때, Server Action을 프롭으로 전달하는 것이 적절한 해결책일 수 있다.

// app/components/example-server-component.js

import { ExampleClientComponent } from './components/example-client-component.js';
 
function ExampleServerComponent({ id }) {
  async function updateItem(data) {
    'use server';
    modifyItem({ id, data });
  }
 
  return <ExampleClientComponent updateItem={updateItem} />;
}
// app/components/example-client-component.js

'use client';
 
function ExampleClientComponent({ updateItem }) {
  async function action(formData: FormData) {
    await updateItem(formData);
  }
 
  return (
    <form action={action}>
      <input type="text" name="name" />
      <button type="submit">Update Item</button>
    </form>
  );
}

온디맨드 재검증(revalidation)

Server Action에 전달되는 데이터는 액션을 호출하기 전에 유효성 검사 또는 필터링될 수 있다.

예를 들어, 액션을 인자로 받아들이는 래퍼 함수를 생성하고, 유효하다면 해당 액션을 호출하는 함수를 반환하는 방식으로 구현할 수 있다.

// app/actions.js

'use server';
 
import { withValidate } from 'lib/form-validation';
 
export const action = withValidate((data) => {
  // ...
});
// lib/form-validation

export function withValidate(action) {
  return (formData: FormData) => {
    'use server';
 
    const isValidData = verifyData(formData);
 
    if (!isValidData) {
      throw new Error('Invalid input.');
    }
 
    const data = process(formData);
    return action(data);
  };
}

헤더 사용

서버 액션 내에서는 쿠키헤더와 같은 수신 요청 헤더를 읽을 수 있다. 이를 통해 서버 액션 로직 내에서 요청 헤더에서 정보를 검색하고 활용할 수 있다.

import { cookies } from 'next/headers';
 
async function addItem(data) {
  'use server';
 
  const cartId = cookies().get('cartId')?.value;
 
  await saveToDb({ cartId, data });
}

또한 서버 액션 내에서 쿠키를 수정할 수 있다.

import { cookies } from 'next/headers';
 
async function create(data) {
  'use server';
 
  const cart = await createCart():
  cookies().set('cartId', cart.id)
  // or
  cookies().set({
    name: 'cartId',
    value: cart.id,
    httpOnly: true,
    path: '/'
  })
}


용어 설명

Actions

사용자 상호작용에 대한 비동기적인 부수효과를 수행하며, 오류 처리와 낙관적 업데이트에 대한 내장된 솔루션을 제공한다. HTML 기본 동작과 유사하다.

Form Actions

웹 표준 <form> API에 통합된 액션으로, 기본적인 점진적 향상과 로딩 상태를 지원한다. HTML 기본 formaction과 유사하다.

Server Functions

서버에서 실행되지만 클라이언트에서 호출할 수 있는 함수다.

Server Actions

액션으로 호출되는 서버 함수다.

Server Mutations

데이터를 변경하고 redirect, revalidatePath 또는 revalidateTag를 호출하는 서버 액션이다.



[ 출처 ]
https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions

profile
나는야 프린이

0개의 댓글