Next.js 13 -> 14 로 넘어오면서 Server-Action이라는 게 등장했다.
"use client"로 클라이언트 컴포넌트를 만드는 것은 많이 해봤지만 Server-Action은 맨날 구경만 하다가 이번에야 기록을 남긴다.
Server-Action이란?
Server-Action은 서버 단에서 실행되는 비동기 함수로서, form 제출이나 Data Mutation을 다루기 위해 Server와 Client 단 모두에서 쓰일 수 있는 Action을 뜻한다.
How To Invoke Server-Action
기본적인 사용법은 공식문서에 잘 나와있기에 다루지 않고 공부하면서 몰랐던 점들 위주로 설명하겠다.
먼저 비교를 위해서 Server-Action을 사용하지 않고 원래대로 Client 사이드에서 서버로 데이터를 전송하려면
// Client 컴포넌트
const clientSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.currentTarget;
const something = Object.fromEntries(new FormData(form));
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();
return data;
} catch (e) {
throw new Error('Client-Side에서 api로 fetch fail');
}
};
// app > api > dailyroute > route.ts
export const POST = async (req: NextRequest) => {
try {
const data = await req.json();
const docRef = await addDoc(collection(db, 'test'), data);
return Response.json(docRef);
} catch (e) {
throw new Error('fail to add a new item');
}
};
<div className="border-4 py-4 px-4">
<form onSubmit={clientSubmit}> // ----> onSubmit prop에 함수 전달
<h1>client Submit: </h1>
<input type="text" name="3" className="border"></input>
<input type="number" name="4" className="border"></input>
<button className="border">send</button>
</form>
</div>
이렇게 Clinet 컴포넌트 -> API route -> 서버 -> 다시 API route -> Client 컴포넌트 과정으로 요청이 처리된다.
그런데 Server-Action으로 하면,
우선 next.config.mjs 파일에 serverActions 설정을 true로 바꿔주고
1. Client 컴포넌트에서 form 태그의 action prop에
action.ts 파일로부터 import한 Server-Action을 import 하면 된다.
// src > app > action.ts --- Server-Action
'use server';
export const serverAdd = async (data: FormData) => {
const formData = Object.fromEntries(data);
try {
await addDoc(collection(db, 'test'), formData);
revalidatePath('/daily');
} catch (e) {
throw new Error('fail to add a new item');
}
};
// Client 컴포넌트
import { serverAdd } from ' @/app/action';
...
<div>
<form action={serverAdd}> ----> action prop에 Server-Action import
<h1>Server-action: </h1>
<input type="text" name="name" className="border"></input>
<input type="number" name="message" className="border"></input>
<button className="border">send</button>
</form>
</div>
- 여기서 질문
form 태그에 무언가를 작성하는 것은 user와의 상호작용이 필요한 것인데,
애초에 이게 어떻게 Server-Action으로 가능한가?
Server-Action은 JavaScript와 별개로 동작한다. 즉, JS가 비활성화된 상태에서도 동작한다는 것이다.
또한, form 태그에 입력하고 제출하는 것은 JS의 기본 메커니즘이 아니라 HTML form 태그의 기본 메커니즘이다.
이런 HTML 기본 메커니즘은 브라우저에 내장되어 있고, 브라우저에서 발생하는 동작이라고 해서 무조건 Client-Action이 아니다. JS의 관여가 필요한 것을 Client-Action이라고 할 수 있다.
브라우저에서 Client-Action 없이 수행되는 것들(form 제출, link 클릭, form 태그의 required 속성이 있다면 필드 값이 채워지지 않으면 제출되지 않는 등) 중에 하나가 form 제출이다.
즉, 브라우저에서 사용자와의 상호작용이 필요하다고 해서 무조건 JS가 관여하지 않고,
form 제출처럼 JS와 별개로 동작하는 기본 브라우저 동작은 Server-Action의 대상이 될 수 있다.
- 다음 질문
보통 form 태그의 onSubmit prop에 넘기는 함수는
꼭 e.preventDefault()가 필요했는데 Server-Action은 필요가 없나?
e.preventDefault()를 애초에 왜 썼는지부터 생각해봐야 한다.
페이지가 새로고침되는 form 태그의 기본동작을 막고, JS로 데이터 전송을 하기 위해서 e.preventDefault()을 쓴다.
그것까지는 알겠는데, 그럼 Server-Action에서는 그 기본동작을 막지 않아도 되는 것인가?
실제로 Server-Action에서 e.preventDefualt()를 사용하지 않아도 페이지 새로고침이 되지 않는다.
이 문제에 대해서 Github에 next.js에 직접 물어봤다ㅋㅋㅋ 놀랍게도 정말 5분? 내로 답을 받았고 완전히 이해가 되었다.
놀랍게도, 문제는 Client-Side인지, Server-Side인지가 아니었다!
onSubmit prop을 사용할지, action prop을 사용할지가 관건이었다.
onSubmit prop을 사용하면 우리가 통상 사용하듯 e.preventDefault()가 필요하다.
하지만 action prop과 관련해서는, 만약 함수로 action prop을 받으면
React-Dom에서 자체적으로 preventDefault()가 가능하도록 설계해둔 것이 이유였다.
아래 링크에서 확인 가능하다.
- https://github.com/facebook/react/blob/63d673c67656390d776bfa082c6ab49f0c636582/packages/react-dom-bindings/src/events/plugins/FormActionEventPlugin.js#L160
Wow...
+) "use server"를 사용해서 Server-Action을 정의해둔 함수에 인자로 event 객체를 받으면 콘솔에서 어련히 오류가 난다.
// Server-Action
const propServerAction = async (e: FormEvent<HTMLFormElement>) => {
'use server';
e.preventDefault();
const form = e.currentTarget;
const something = Object.fromEntries(new FormData(form));
try {
await addDoc(collection(db, 'props'), something);
} catch (error) {
throw new Error('props로 server-action Client한테 내리기 실패');
}
};
2. Client 컴포넌트에서 Server-Action을 import 하지 않고,
props로 전달받아도 가능
Server 컴포넌트에서 Server-Action을 정의한 뒤 Client 컴포넌트로 넘겨준다.
// Server 컴포넌트
const propServerAction = async (data: FormData) => {
'use server';
const formData = Object.fromEntries(data);
try {
await addDoc(collection(db, 'props'), formData);
} catch (error) {
throw new Error('props로 server-action Client한테 내리기 실패');
}
revalidatePath('/daily');
};
return (
<div className="flex flex-col gap-4">
<DailyClientComponent propServerAction={propServerAction} /> ---> Client 컴포넌트
<div>
...
)
// Client 컴포넌트
<div className="border-4 py-4 px-4">
<form action={propServerAction}>
<h1>Prop: </h1>
<input type="text" name="name" className="border"></input>
<input type="number" name="number" className="border"></input>
<button className="border">send</button>
</form>
</div>
2-1. 나는 Client로 넘기고는 싶은데,
꼭 form 태그에 넘겨야 해?
그냥 button 태그 onClick prop에 Server-Action을 넘기고 싶어
라고 할 수 있다.
그럴 때는 Server-Action의 인자로 formData 말고 다른 인자를 받고(여기서는 단순 string을 받도록 했다)
Client로 넘긴 다음,
// Server 컴포넌트
const propServerAction = async (data: string) => {
'use server';
try {
await addDoc(collection(db, 'props'), { name: data });
} catch (error) {
throw new Error('props로 server-action Client한테 내리기 실패');
}
revalidatePath('/daily');
};
return (
<div className="flex flex-col gap-8 w-full max-w-5xl mx-auto">
<DailyClientComponent propServerAction={propServerAction} />
<div className="border-4 py-4 px-4">
...
)
Client 컴포넌트에서는 Server-Action이 비동기 함수이고 인자를 받기 때문에 async, await을 걸어서 onclick prop으로 넘겨줘야 한다.
// Client 컴포넌트
const DailyClientComponent = ({ propServerAction }: { propServerAction: any }) => {
const [input, setInput] = useState('');
return (
...
<div className="border-4 py-4 px-4">
<input type="text" name="prop" className="border" onChange={handleOnChange}></input>
<button className="border" onClick={async () => await propServerAction(input)}>
Props
</button>
</div>
...
)
}
- 그럼 여기서 또 질문
보통 button 태그에 onClick 함수를 전달할 때,
인자가 있으면 그 함수 이름만 써서는 안되고 onClick={() => function(인자)} 이렇게 넘기는데,
왜 server action에서는 함수가 formData라는 인자를 받도록 정의되어 있어도
<form action={serverActionFunc}> 이렇게 인자를 안받아도 전달이 되는걸까?
이것에 대해서 안그래도 Next.js 공식문서에 나와있다.
즉, 브라우저가 폼을 제출하면 폼 필드의 데이터가 자동으로 수집되어 formData 객체가 생성되고 그게 Server-Action 함수로 전달된다는 것이다.
3. 만약 form 태그 안에 button이 두 개라면?
formAction prop을 사용하면 된다.
formAction prop은 React의 canary, 아직 실험 버전의 hook 인데,
form 태그 안에 <button>, <input type="submit">, and <input type="image"> 가 있으면
form 태그의 action prop 함수보다 선행되도록 하는 prop이다.
-> 따라서 form 태그 내에서 또 다른 action이 일어나길 원할 때(좋아요, 임시저장 등) 사용하면 좋다.
// Server 컴포넌트
<div className="border-4 py-4 px-4">
<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" formAction={beforeFormServerAction}>
like ---> formAction으로 선행될 좋아요 버튼
</button>
<button className="border" type="submit"> ---> 기본 제출버튼
send
</button>
</form>
</div>
// 좋아요 Server-Action
export const beforeFormServerAction = async () => {
try {
await addDoc(collection(db, 'like'), { name: 1 });
} catch (error) {
throw new Error('좋아요 업로드 실패');
}
};
- like 버튼만 누르면 send 버튼의 기본 폼 제출 함수가 실행되지 않고, like 버튼의 함수만 실행된다.
내용이 너무 길어질 듯 하여 다음 포스팅에서 이어 쓰겠다.
다음 포스팅에서는
- Server-Action으로 네트워크 요청한 데이터를 어떻게 바로 화면에 반영할 수 있는지
- Server-Action 중의 로딩 상태는 어떻게 처리할 수 있는지
- 유효성 검사는 어떻게 하면 좋을지
에 대해서 이야기 할 예정이다.
'TIL' 카테고리의 다른 글
2024.05.31 #Next.js #Server-Action3 #유효성검사 #ErrorHandling #낙관적_업데이트 (0) | 2024.05.31 |
---|---|
2024.05.30 #Next.js #Server-Action2 #화면반영 #로딩상태 (0) | 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 |
2024.04.26 TIL #useSuspenseQuery #useSuspenseQueries #useMemo (0) | 2024.05.17 |