저번 포스팅에서는 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() 관련 내용은 다음 포스팅에 이어서...
'TIL' 카테고리의 다른 글
2024.06.10 drag_라이브러리_없이_구현_중_트러블슈팅 1 (1) | 2024.06.12 |
---|---|
2024.05.31 #Next.js #Server-Action3 #유효성검사 #ErrorHandling #낙관적_업데이트 (0) | 2024.05.31 |
2024.05.29 #Next.js #Server-Action1 (1) | 2024.05.31 |
2024.05.28 #Next.js #How are Client Components Rendered? #추론 (0) | 2024.05.29 |
2024.05.25 TIL #Next.js_app_router #Suspense #pre-render #팀회의 (0) | 2024.05.25 |