TIL

2024.05.30 #Next.js #Server-Action2 #화면반영 #로딩상태

inz1234 2024. 5. 31. 17:50

저번 포스팅에서는 Server-Action의 사용법에 대해서 3가지 방법을 다뤘다.
이번 포스팅에서는 
1. Server-Action으로 네트워크 요청한 데이터를 어떻게 바로 화면에 반영할 수 있는지
2. Server-Action 중의 로딩 상태는 어떻게 처리할 수 있는지
3. 유효성 검사는 어떻게 하면 좋을지
4. Server-Action으로 낙관적 업데이트는 어떻게 하면 좋을지 + useOptimistic의 아직까지의 한계
의 내용을 다룰 예정이다.


 

1. Server-Action으로 네트워크 요청한 데이터를
어떻게 바로 화면에 반영할 수 있을까?

 

1-1. revalidatePath()

첫 번째 방법은 revalidatePath()을 사용하는 방법이다. 

// Server 컴포넌트
"use server"

const DailyPage = async () => {
  const serverSnapShot = await getDocs(collection(db, 'test'));
  const testList: any = [];
  serverSnapShot.forEach((doc) => {
    testList.push({ id: doc.id, ...doc.data() });
  });

const serverAdd = async (data: FormData) => {
  const formData = Object.fromEntries(data);
  try {
    await addDoc(collection(db, 'test'), formData);
    revalidatePath('/daily');  -----> 데이터 추가 Server-Action 후 revalidatePath
  } catch (e) {
    throw new Error('fail to add a new item');
  }
};


 return (
 ...
        <form action={serverAdd}>
          <h1>Server-action: </h1>
          <input type="text" name="name" className="border"></input>
          <input type="number" name="message" className="border"></input>
          <button className="border" type="submit">
            send
          </button>
        </form>

// 업데이트된 데이터 보여주기
       {testList.map((t: any) => (
        <div key={t.id}>
          {t.name} : {t.message}
        </div>
      ))}
...
)}

새로고침을 하지 않아도 바로 반영된다.


- 여기서 질문

revalidatePath("/daily") 가 페이지를 새로고침해서 업데이트 된 내용을 반영해주는 거라면 
input 창에 내용을 쓰고 제출을 누르면 
새로고침이 되면서 input 창이 clear되어야 하는 거 아닌가?

DB에는 잘 추가 되었지만, revalidatePath() 해 줬음에도 input창이 clear 되지 않는 걸 볼 수 있다.

revaildatePath() 함수는 특정 경로를 다시 유효화 할 때, 전체 페이지를 새로고침 하지 않고
해당 경로 페이지의 특정 부분만 갱신하기 때문이다.


- 또 다른 질문

제출 후 input 창을 clear 하는 방법은?

두 가지 방법이 있는데
Client 에서 데이터 통신 로직을 실행하는 경우와 Server-Action을 사용하는 경우로 나뉜다.

(1) Client에서 직접 데이터 통신 로직을 실행하는 경우, 인자로 formEvent를 받게 되고 
그 event target을 reset() 해주면 된다.

const anotherSubmit = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const form = e.currentTarget;
    const something = Object.fromEntries(new FormData(form));
    form.reset();                        ----> FormEvent의 target을 reset 해주기 
    try {
      const response = await fetch('/api/dailyroute', {
        method: 'POST',
        body: JSON.stringify(something),
        headers: {
          'Content-Type': 'application/json'
        }
      });
      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.message);
      }
      const data = await response.json();
      console.log('응답결과', data);
      return data;
    } catch (e) {
      throw new Error('api로 fetch fail');
    }
  };


(2) Server-Action을 사용하는 경우, 인자로 formData 객체를 받게 되는데
form 태그에 ref 값을 부여하고
Server-Action 함수를 다시 Client 함수로 감싼 뒤 해당 ref를 reset() 해주면 된다.

// Client 컴포넌트
const DailyClientComponent = () => {
    const formRef = useRef<HTMLFormElement>(null);
    
  const serverActionInputClear = async (data: FormData) => {
    const result = await serverAdd(data);             ------> Server-Action
    if (result?.error) {
      setValidationErr(result.error);
    } else {
      setValidationErr(null);
      formRef.current?.reset();                     --------> 참조한 form 태그 reset()
    }
  };
  
  return ( 
  ...
      <div className="border-4 py-4 px-4">
        <form action={serverActionInputClear} ref={formRef} className="flex flex-col gap-2">
          <h1>server-action Imported with Zod: </h1>
          <input type="text" name="name" className="border"></input>
          <input type="number" name="message" className="border"></input>
          <button className="border">send</button>
        </form>
      </div>
  ...
  )}

1-2. useFormState(updateFn, initialState)

- React의 Canary 버전의 react-hook이다

// app > action.ts
"use server"

export const serverActionWithUseFormState = async (state: any, formData: FormData) => {
  const data = Object.fromEntries(formData);
  try {
    await addDoc(collection(db, 'formAction'), data);
    revalidatePath('/daily');
  } catch (e) {
    throw new Error('fail to add a new item');
  }
};
  // Client 컴포넌트
  const DailyClientComponent = ({ propServerAction }: { propServerAction: any }) => {
  
  const [state, formAction] = useFormState(serverActionWithUseFormState, null);
  
  return (
  ...
        <div className="border-4 py-4 px-4">
        <form action={formAction} ref={formActionRef} className="flex flex-col gap-2">
        // ------> action prop에 useFormState에서 제공하는 formAction 연결
          <h1>server-action with useFormState: </h1>
          <input type="text" name="name" className="border"></input>
          <input type="number" name="message" className="border"></input>
    	  <button className="border">send</button>
        </form>
        <p>{state ? JSON.stringify(state, null, 2) : null}</p>  ---> 변경된 state를 바로 확인가능
      </div>
...
)}

+) 물론 useEffect()와 useState()로 useEffect 내에서 Server-Action을 실행하고, 그 결과를 setState() 한 뒤 렌더링 해주는 방법도 있다.


 

2. Server-Action 중의 로딩 상태는 어떻게 처리할 수 있는지?

 

이것 또한 두 가지 방법이 있다.

2-1. useTransition()

'use client'

const DailyClientComponent = () => {
  const [isPending, startTransition] = useTransition();
  
  return (
  ...
       <div className="border-4 py-4 px-4">
        <button
          className={`border ${isPending ? 'opacity-30' : null}`}
          disabled={isPending}
          onClick={() => startTransition(() => transitionAdd('트랜지션?'))}
        >
          Transition
        </button>
      </div>
 ... 
  )}
//app > action.ts
"use server"

export const transitionAdd = async (name: string) => {
// 시간 차를 두고자 일부러 timeOut을 설정함
  await new Promise((_) => setTimeout(_, 1000));

try {
    await addDoc(collection(db, 'transition'), { name });
  } catch (error) {
    throw new Error('transition 사용해서 server-action 실패');
  }
};

 

- 여기서 질문

useTransition은 클라이언트 측 hook인데 왜 server action까지 기다려주는 걸까?

useTransition()의 startTransition 함수는 클라이언트 측에서 비동기 작업을 시작하고 그 완료를 기다리는 역할을 하는데
비동기 작업 자체는 Server-Action으로 서버에서 실행되지만,
그 비동기 작업의 시작과 끝, 즉 호출과 응답은 클라이언트 측에서 비동기적으로 처리되기 때문이다.


2.2 useFormStatus()

useFormStatus 또한 React의 Canary hook 이다.
이 훅을 사용할 때는 주의할 점이 있다.

useFormState() 훅을 사용하는 컴포넌트는 반드시 form 태그의 Child여야 한다는 것이다.

'use client';

import { useFormStatus } from 'react-dom';

const SubmitBtn = () => {
  const { pending } = useFormStatus();

  return (
    <>
      <button disabled={pending}>Submit</button>
    </>
  );
};

export default SubmitBtn;
    // Server 컴포넌트
    
    <div className="border-4 py-4 px-4">
        <form action={formAction} ref={formActionRef} className="flex flex-col gap-2">
          <h1>server-action with useFormStatus: </h1>
          <input type="text" name="name" className="border"></input>
          <input type="number" name="message" className="border"></input>
          <SubmitBtn /> ------------------> form 태그의 child
        </form>
        <p>{state ? JSON.stringify(state, null, 2) : null}</p>
      </div>

내용이 또 길어져서 
- 유효성 검사
- useOptimistic() 관련 내용은 다음 포스팅에 이어서...