Remix Framework 다중 Form의 action 함수 처리하기
Remix Framework multiple form, 리믹스 프레임워크 다중 form의 action 함수 처리하기
안녕하세요?
지난 시간에 리믹스의 서버사이드 처리 함수인 action 함수에 대해 자세히 살펴봤는데요.
오늘은 한 페이지에서 action 함수로 전달해야 할 데이터가 많을 경우에는 어떻게 하는지 알아보겠습니다.
리믹스에서는 서버사이드 함수로 데이터를 보낼 때 action 함수를 지정해서 사용한다고 했고, 그 방식은 PHP 시절 쓰던 form-submit 방식인데, 만약 보내야 할 데이터가 여러 form에 걸쳐 있을 경우 action 함수에서 어떻게 처리하는지 쉽게 생각이 나지 않는데요.
지난 시간에 배웠던 Post 생성과 관련하여 오늘은 Note라는 DB를 새로 만들어서 알아보겠습니다.
먼저, schema.prisma 파일에 아래와 같이 Note 모델을 추가합시다.
model Note {
id String @id @default(uuid())
title String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Prisma 새 모델을 만들었으면 db push를 새로 해야 합니다.
npx prisma db push
npx prisma studio를 실행해서 Note 모델이 잘 적용되어 있는지 체크해 보시길 바랍니다.
일단 기본적은 DB 부분은 마무리했습니다.
그럼 app/routes/notes/index.tsx 파일을 만들어서 notes를 보여주는 코드를 만들어 보겠습니다.
import { LoaderFunction, useLoaderData } from "remix";
import { db } from "~/utils/db.server";
export const loader: LoaderFunction = async ({ request }) => {
const notes = await db.note.findMany({
select: { id: true, title: true },
orderBy: { createdAt: "desc" },
});
return notes;
};
export default function NotesIndex() {
const notes = useLoaderData();
// console.log(notes);
return (
<div className="ml-4">
<div>NotesIndex</div>
<br />
{notes.length === 0 ? (
<p>No notes yet</p>
) : (
<ul>
{notes.map((note) => (
<li key={note.id}>{note.title}</li>
))}
</ul>
)}
</div>
);
}
이제 준비 작업은 끝났는데요.
그럼 이 index.tsx 파일에 note를 추가하는 로직과 개별 노트를 삭제하는 코드를 추가해 보겠습니다.
먼저, 노트 추가 로직입니다.
UI 부분도 TailwindCSS를 이용해서 조금만 손 봤습니다.
import { Form, LoaderFunction, useLoaderData } from "remix";
import { db } from "~/utils/db.server";
// loader 함수는 기존 코드와 동일
export default function NotesIndex() {
const notes = useLoaderData();
return (
<div className="ml-4">
<h1 className="text-4xl font-bold py-2">Notes</h1>
<Form method="post">
<input className="border rounded py-2 px-4" name="title" />
<button
className="bg-cyan-200 rounded py-2 px-4"
type="submit"
>
Save Note
</button>
</Form>
<br />
{notes.length === 0 ? (
<p>No notes yet</p>
) : (
<ul>
{notes.map((note) => (
<li key={note.id}>{note.title}</li>
))}
</ul>
)}
</div>
);
}
기존에는 form 엘러먼트를 썼었는데요.
위 코드에서 보시면 Form이라고 리믹스에서 제공해주는 컴포넌트를 썼습니다.
리믹스 다큐먼트를 보면 되도록이면 Form을 쓰라고 하는데 Nested Routing일 경우에 form을 쓰면 꼬일 수가 있다고 합니다.
그래서 Form을 썼습니다.
위 코드의 Form은 method="post" 이기 때문에 리믹스의 action 함수에서 처리해줘야 합니다.
export const action: ActionFunction = async ({ request }) => {
const formData = await request.formData();
const title = formData.get("title");
const note = await db.note.create({
data: { title: title },
});
console.log(note);
return redirect("/notes");
};
위의 action 함수를 보시면 request 변수에서 formData를 가져와서 그 formData의 title 부분을 title에 저장하고, db.note.create 함수를 이용해서 prisma DB에 저장합니다.
그리고 action 함수는 return을 해야 하는데 보통 DB 작업을 했으면 리프레쉬해야 하기 때문에 동일 라우팅인 /notes로 지정했습니다.
참고로, redirect 함수는 리믹스가 제공하는 라우팅 이동 함수입니다.
테스트해볼까요?
아주 잘 되고 있네요.
테스트해야 되니까 몇 개 더 만들어도 됩니다.
그런데 아래처럼 뭔가 이상한 브라우저 행동이 보이는데요.
Form 컴포넌트를 쓰면 위 그림처럼 입력한 값이 그대로 남아 있습니다.
이럴 경우를 위해서 리믹스는 Form의 파라미터 "reloadDocument"를 제공해 주는데요.
아래와 같이 코드를 바꿔주시면 위와 같은 현상은 사라질 겁니다.
<Form reloadDocument method="post">
이제 노트 삭제 로직을 작성해 볼까요?
먼저, UL > LI 엘러먼트 옆에 Delete 버튼을 추가하겠습니다.
물론, useState를 이용해서 리액트 방식으로 클라이언트 상에서 db.note.delete 함수를 호출할 수 있는데요.
우리가 리믹스 프레임워크를 이용하고 있고 브라우저 본연의 작동방식을 이용해서 코드를 작성하려고 하기 때문에 리믹스 방식(Form)으로 코드 작성을 진행하겠습니다.
{notes.length === 0 ? (
<p>No notes yet</p>
) : (
<ul>
{notes.map((note) => (
<li key={note.id}>
{note.title}{" "}
<Form method="post" className="inline">
<button
type="submit"
className="ml-4 border rounded py-2 px-4 bg-red-100"
>
Delete
</button>
</Form>
</li>
))}
</ul>
)}
Delete 버튼도 Form을 이용했습니다.
자 그러면 여기서 Delete 버튼을 눌러 Form이 Submit 되었다면 분명 action 함수에서 처리해야 하는데요.
여기서 뭔가 코드가 꼬이는 게 바로 뭘 눌렸냐입니다.
즉, 어떤 노트를 삭제해야 되는지 전혀 알 방법이 없습니다.
useState로 리액트 방식으로 작성하게 되면 그냥 note.id를 전달해 주면 되는데 리믹스의 Form 방식은 어떻게 해야 할까요?
정답은 input 엘러먼트의 hidden 방식에 있습니다.
Form 관련 코드를 아래와 같이 바꾸시면 됩니다.
<Form method="post" className="inline">
<input type="hidden" name="noteId" value={note.id} />
<button
type="submit"
className="ml-4 border rounded py-2 px-4 bg-red-100"
>
Delete
</button>
</Form>
input 태그에 type을 hidden으로 지정하면 화면에는 보이지 않지만 input 값의 name과 value 값이 같이 form submit 됩니다.
이런 방식으로 value={note.id} 정보를 action 함수로 전달하게 됩니다.
이제 action함수를 고쳐볼까요?
그런데 action 함수를 고치려고 보니까 기존에 Note Save 버튼으로 작성한 db.note.create 함수 로직이 있습니다.
도대체 유저가 입력한 button-submit 한 후의 form 액션은 어디에서 오는지 전혀 알 수가 없습니다.
Note Save인지, 아니면 Note Delete인지 알 수 있는 방법이 없습니다.
그래서 리믹스에서는 보통 다음과 같이 코드를 작성합니다.
바로 button 태그에 name과 value 프라퍼티를 지정해서 사용하는 방식입니다.
먼저, Delete 버튼의 name과 value를 다음과 같이 수정합시다.
<button
type="submit"
name="_action"
value="delete-note"
className="ml-4 border rounded py-2 px-4 bg-red-100"
>
Delete
</button>;
HTML의 button 태그는 name과 value 프로퍼티가 있습니다.
name과 value가 한쌍으로 움직이는데요.
const actionType = formData.get("_action")을 실행하면 actionType 값에 위에서 지정한 "delete-note"란 값이 저장됩니다.
우리는 이 방식을 이용할 건데요.
먼저, 맨 위의 Save Note 부분에서도 다음과 같이 button 태그의 name과 value 부분을 아래와 같이 고쳐보도록 하겠습니다.
<button
className="bg-cyan-200 rounded py-2 px-4"
type="submit"
name="_action"
value="create-note"
>
Save Note
</button>;
Save Note의 경우 create-note가 식별자가 되는 겁니다.
이제 action 함수를 고쳐볼까요?
export const action: ActionFunction = async ({ request }) => {
const formData = await request.formData();
const actionType = formData.get("_action");
switch (actionType) {
case "delete-note": {
const noteId = formData.get("noteId");
const result = await db.note.delete({
where: { id: noteId },
});
console.log("Delete item ===> ", result);
if (!result) console.log("Error Delete");
return redirect("/notes");
}
case "create-note": {
const title = formData.get("title");
const note = await db.note.create({
data: { title: title },
});
console.log("Create item ===> ", note);
if (!note) console.log("Error Delete");
return redirect("/notes");
}
}
};
위 코드를 보시면 actionType에 따라서 DB 작업을 다르게 하고 있습니다.
어떤가요?
다중 Form일 경우에도 우리는 button 태그의 name과 value쌍을 이용해서 여러 가지 form 작업을 처리할 수 있습니다.
물론, input type="hidden"을 이용해서 noteId 값도 전달할 수 있고요.
참고로 아래 그림은 크롬 DEV 화면인데요.
보시면 생성할 때는 페이로드에 title값과 _action 값이 우리가 의도한 목적에 맞게 전달되고 있습니다.
아래 그림은 Delete 버튼을 눌렀을 경우인데요.
이 경우에도 페이로드에 noteId 값과 _action 값이 제대로 지정되어 있습니다.
지금까지 리믹스를 공부하면서 느낀 점은 HTML의 전통적인 방식으로도 충분히 반응형 웹앱을 만들 수 있지 않을까 하는 생각이 드네요.
그럼.