[nextjs] server action

Jungmin Ji·2024년 4월 8일
0

Nextjs

목록 보기
9/9

Nextjs Server Action실습을 통해 알아보기

Next.js에서 Server Action은 서버에서 직접 실행되는 함수를 의미한다. 이 함수들은 클라이언트에서 직접 호출할 수 없고, 서버에서만 실행된다. Server Action은 주로 데이터베이스 작업, API 호출, 인증 처리 등 보안이 중요한 작업을 수행하는 데 사용된다.

Next.js 13 버전부터는 app 디렉토리와 함께 서버 컴포넌트(Server Components)를 도입하면서 서버에서 실행되는 코드와 클라이언트에서 실행되는 코드를 명확하게 분리할 수 있게 되었다. 이에 따라 Server Action을 더 효율적으로 사용할 수 있게 되었다.

Server Action을 사용하는 방법

  • 서버 파일에 함수 정의: 서버에서 실행될 함수를 정의합니다. 이 함수는 서버에서만 접근 가능해야 합니다.
  • 클라이언트에서 서버 액션 호출: 클라이언트 측에서 서버 액션을 호출하여 필요한 작업을 수행합니다. 이는 보통 API 엔드포인트를 호출하는 방식으로 이루어집니다.

프로젝트 생성

nextjs-sever-action 폴더생성 후 터미널에서 npx create-next-app@latest ./

next.config.mjs

/** @type {import('next').NextConfig} */
const nextConfig = {
    experimental: {
        serverActions: true // beta이기때문에 이렇게 설정
    }
};

export default nextConfig;

Server Action?

서버컴포넌트에서 사용할수있고
재사용을 위해 분리된 파일에서 사용할 수도 있다.

사용법

server component

함수 본문 상단에 "use server"지시문을 사용하여 비동기 함수를 정의하여 서버작업을 만든다. "use server"는 이 기능이 서버에서만 실행되도록 한다.

export default function ServerCompoenet() {
	async function myAction() {
    	"use server";
        // ...
    }
    return <div></div>
}

client component

파일 상단에 "use server"지시문을 사용하여 별도의 파일에 서버 작업을 만든다.(재사용가능) 그런 다음 서버작업을 클라이언트 구성요소로 가져온다.

src/lib/actions.ts

'use server';

export async function myAction() {
    //.. 
}

src/components/ClientComponents.tsx

import { myAction } from '@/lib/actions'
import React from 'react'

const ClientComponents = () => {
  return (
    <form action={myAction}>
      <button type="submit">Add to Cart</button>
    </form>
  )
}

export default ClientComponents

어떻게 호출하는가?

  • action을 사용: form에 action속성에 넣으면 호출 할수있음
    form앨리먼트에서 리액트의 action prop은 Server Action을 부를 수 있음
  • form안의 button, input(type=submit), input(type=image)는formAction prop을 사용
  • startTransition과 함께 커스텀 호출

todo앱으로 실습

data생성, 서버

src/db.json

{
    "todos": [
        {
            "userId": 1,
            "title": "밥먹기",
            "completed": false,
            "id": 3
        },
        {
            "userId": 2,
            "title": "운동하기",
            "completed": false,
            "id": 4
        },
        {
            "userId": 3,
            "title": "공부하기",
            "completed": false,
            "id": 5
        }
    ]
}

json서버돌리기
npm install -g json-server
json-server --watch db.json -p 3001

컴포넌트

components/Form.tsx

import { addTodo } from '@/lib/actions'
import React from 'react'

const Form = () => {
  return (
    <form action={addTodo} className="flex items-center gap-2">
      <input
        type="text"
        name="title"
        className="flex-grow w-full p-1 text-xl border border-gray-400 rounded-lg"
        placeholder="새로운 할일을 생성하세요."
        autoFocus
      />
      <button type="submit" className="border border-gray-400 p-2 rounded-lg">
        Submit
      </button>
    </form>
  );
}

export default Form

TodoItem.tsx

import React from 'react'
import { Todo } from './TodoList'
import Link from 'next/link'
import { deleteTodo } from '@/lib/actions'
import Checkbox from './Checkbox'

const TodoItem = (todo: Todo) => {
  return (
    <form className="my-4 flex justify-between items-center">
      <div className="flex items-center gap-4">
        <Checkbox />
        <label htmlFor="completed" className="text-2xl hover:underline">
          <Link href={`/edit/${todo.id}`}>{todo.title}</Link>
        </label>
      </div>
        <button>
          X
        </button>
    </form>
  );
}

export default TodoItem

TodoList.tsx

import React from 'react'
import TodoItem from './TodoItem';

interface Todo {
    userId: number;
    id: number;
    title: string;
    completed: boolean;
}

async function fetchTodos() {
    try {
        const res = await fetch("http://localhost:3001/todos");
        const todos: Todo[] = await res.json();
        return todos;
    } catch(error) {
        if (error instanceof Error) {
            console.log(error.stack);
        }
    }
}
const TodoList = async() => {
    const todos = await fetchTodos();

    let content;
    if(!todos || todos.length === 0) {
        content = <p>Todo 리스트가 없습니다. </p>
    } else {
        const sortedTodos = todos.reverse();
        content = (
            <>
                {sortedTodos.map((todo) => (
                    <TodoItem key={todo.id} {...todo} />
                ))}
            </>
        )
    }
  return content;
}

export default TodoList

fetchTodos() 에서 데이터 받고
TodoList에서 정렬후 map으로 item뿌려줌

추가기능

actions.ts

export async function addTodo(data: FormData) {
    const title = data.get('title');
    await fetch('http://localhost:3001/todos',{
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            userId: 1, title, completed: false
        })
    })

    // 데이터 추가시 바로 보여주기
    // 캐시된 데이터를 purge함 
    revalidatePath('/');
}


javascript없이 form이 작동. (POST)
javascript disable 해도 잘 되는것을 확인할 수 있다.

추가 후 input value하는 방법은.. ?

삭제기능

이건 formAction속성을 이용하는 방법으로 사용해본다.
<form>안의 <button>, <input type="submit">, <input type="image"> 엘리멘트에 사용할 수 있다.

TodoItem.tsx

        <button
          formAction={async () => {
            "use server";
            await deleteTodo(todo)  
          }}
        >X</button>

actions.ts

export async function deleteTodo(todo: Todo) {
    const res = await fetch(`http://localhost:3001/todos/${todo.id}`,{
        method: 'DELETE',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            id: todo.id
        })
    })

    await res.json();
    revalidatePath('/');
}

완료기능

세번째방법인 startTransition을 사용한다.

Checkbox.tsx

'use client';
import React, { useTransition } from 'react'
import { Todo } from './TodoList';
import { updateTodo } from '@/lib/actions';

const Checkbox = ({todo}: {todo: Todo}) => {
    const [isPending, startTransition] = useTransition();
    // isPending은 데이터를 수정중에 true가 됨

  return (
    <input 
        type='checkbox'
        checked={todo.completed}
        id="completed"
        name="completed"
        disabled={isPending}
        onChange={() => startTransition(() => updateTodo(todo))}
        className='min-w-[2rem] min-h-[2rem]'
    />
  )
}

export default Checkbox

체크박스 컴포넌트를 생성해주고 TodoItem 컴포넌트에 넣어준다.
actions.ts

export async function updateTodo(todo: Todo) {
    const res = await fetch(`http://localhost:3001/todos/${todo.id}`, {
        method: 'PUT',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            ...todo, completed: !todo.completed
        })
    })

    await res.json();
    await sleep(2000); // 2초 멈춘 후 revalidate하기 위해 지연시킴
    revalidatePath('/');
}

lib/sleep.ts

// 비동기요청을 할때 지연이 되게 함
export default function sleep(ms: number) {
    return new Promise((r) => setTimeout(r, ms))
}

2초 멈춘 후 revalidate하기 위해 지연시킴

이렇게 데이터가 업데이트 되는 동안(백엔드 처리중) disable되는게 너무 보이니까!

실제로 요청이 완료되기까지 300ms가 걸린다.

useOptimistic 사용

useOptimistic을 이용하면 response를 기다리지 않고 다끝나기전에 변화된걸 보여줄 수 있다.

Checkbox.tsx

'use client';
import React, { useTransition, useOptimistic } from 'react'
import { Todo } from './TodoList';
import { updateTodo } from '@/lib/actions';

const Checkbox = ({todo}: {todo: Todo}) => {
    const [optimisticTodo, addOptimisticTodo] = useOptimistic(todo, 
        (state: Todo, completed: boolean) => ({ ...state, completed})
    )

    // const [isPending, startTransition] = useTransition();
    // isPending은 데이터를 수정중에 true가 됨
    
  return (
    <input 
        type='checkbox'
        checked={optimisticTodo.completed}
        id="completed"
        name="completed"
        // disabled={isPending}
        onChange={async () => {
            addOptimisticTodo(!todo.completed)
            await updateTodo(todo)
            // startTransition(() => updateTodo(todo));
        }}
        className='min-w-[2rem] min-h-[2rem]'
    />
  )
}

export default Checkbox

isPending을 사용하지 X
작업이 끝나기전에 UI가 변화하는것을 확인할 수 있다.

profile
FE DEV/디블리셔

0개의 댓글

관련 채용 정보