Remix framework의 useTransition을 이용한 UI 개선하기
Remix framework useTransition, 리믹스 프레임워크 useTransition UI 개선
안녕하세요?
지난 시간에 작성한 다중 Form을 이용한 action 함수 사용하기 글에서 아쉬웠던 게 계속 생각이 나서 바로 다음 글을 쓸려고 합니다.
바로 UI 부분인데요.
노트를 생성하고 노트를 지울 때 화면에 아무것도 표시가 되지 않아 지금 제대로 작동하고 있는지 아니면 에러가 났는지 도저히 모르는 상태인데요.
리믹스에서는 useTransition 훅을 제공하고 있어 이 부분을 해결하는데 도움을 주고 있습니다.
먼저, 지난 시간에 잠깐 언급했던게 있는데 바로 아래 코드입니다.
<Form reloadDocument method="post">
지난 시간의 코드처럼 Form에 reloadDocument를 지정하면 input 창이 깨끗하게 비워지게 되는데요.
그런데 Form에 reloadDocument를 지정하면 브라우저에 강제로 submit 하게 되어 우리가 원하는 useTransition 기능을 사용할 수가 없습니다.
그래서 꼼수를 쓸 예정인데요.
바로 useLocation 훅을 이용한 방법입니다.
useLocation 훅운 React-Router-V6에서 제공하는 훅입니다.
useLocation은 현재 위치를 나타내 주는데 여기 항목 중에 key 항목이 있는데, 매번 새로운 항목으로 갱신됩니다.
이걸 이용하는 방법인데요.
다음과 같이 수정하시면 됩니다.
const location = useLocation();
return (
<div className="ml-4">
<h1 className="text-4xl font-bold py-2">Notes</h1>
<Form method="post" replace key={location.key}>
...
...
...
</Form>
Form 태그에 replace라고 보이는데 이것도 설명드리겠습니다.
만약 이게 없으면 브라우저는 매번 새로운 form-submit으로 인해 브라우저 히스토리가 아주 많이 생기게 됩니다.
만약 replace라고 Form 태그에 추가하면 단순 form-submit으로 인한 브라우저 히스토리가 무시되는 효과가 있습니다.
되도록이면 replace를 추가하도록 합시다.
이제 useTransition 훅을 사용할 차례인데요.
먼저 코드를 보겠습니다.
const transition = useTransition();
const location = useLocation();
return (
<div className="ml-4">
<h1 className="text-4xl font-bold py-2">Notes</h1>
<Form method="post" replace key={location.key}>
<input className="border rounded py-2 px-4" name="title" />
<button
className="bg-cyan-200 rounded py-2 px-4"
type="submit"
name="_action"
value="create-note"
>
{transition.submission?.formData.get("_action") === "create-note"
? "Saving...."
: "Save Note"}
</button>
먼저, Save Note 관련 Form 코드입니다.
transition 은 location, state, submission, type 총 4가지 속성을 가지는데요.
우리가 사용할 속성은 바로 submission입니다.
왜냐하면 form을 submit 했기 때문에 이와 관련된 속성이기 때문입니다.
그래서 form을 submit 했으면 이때 transition.submission 속성이 생기고 거기에서 formData.get("_action") 값이 "create-note"가 되면, 즉, Save Note 버튼을 눌렀다면 UI 문구를 "Saving...."으로 바꾸게 됩니다.
위 스크린숏처럼 말이죠.
위 크롬 DEV 창에서 네트워크 항목에서 제한 없음을 아래와 같이 슬로 3G라고 변경하시면, Transition 효과를 더 정확하게 인식할 수 있을 겁니다.
다음으로는 Delete Note 부분을 손봐야 하는데요.
일단 Save Note 부분과 같은 방식으로 코드를 작성해 보겠습니다.
<Form method="post" className="inline">
<input type="hidden" name="noteId" value={note.id} />
<button
type="submit"
name="_action"
value="delete-note"
className="ml-4 border rounded py-2 px-4 bg-red-100"
>
{transition.submission?.formData.get("_action") === "delete-note"
? "Deleting...."
: "Delete Note"}
</button>
</Form>;
이제 테스트해 볼까요?
모든 항목이 다 Deleting.... 상태로 변했습니다.
왜 그런 걸까요?
당연히 transition.submission?. formData.get("_action")의 값이 모두 "delete-note"이기 때문입니다.
이럴 경우 어떻게 해야 하냐면 note-id를 이용해서 구별해 줘야 하는데요.
코드를 다음과 같이 바꿔 보도록 하겠습니다.
<Form method="post" className="inline">
<input type="hidden" name="noteId" value={note.id} />
<button
type="submit"
name="_action"
value="delete-note"
className="ml-4 border rounded py-2 px-4 bg-red-100"
>
{transition.submission?.formData.get("noteId") === String(note.id)
? "Deleting...."
: "Delete Note"}
</button>
</Form>;
UI를 바꿀 대상을 note.id와 비교토록 했습니다.
이렇게 하면 실제는 다음과 같이 제대로 작동하게 됩니다.
위 그림을 보시면 위에서 3번째만 Deleting...로 변했습니다.
바로 제가 Delete 버튼을 누른 항목이거든요.
useActionData 훅
이번 시간에는 useActionData 훅까지 함께 살펴보겠습니다.
리믹스의 데이터 흐름을 보면 UI 부분의 form에서 데이터가 이동하면 action 함수에서 DB 관련 처리를 하고 다시 redirect해서 라우팅하고 최종적으로 라우팅이 완료되면 페이지가 새로 로딩되기 때문에 loader함수에서 DB관련 데이터를 새로 불러오고 그 데이터를 UI에 뿌려 주게 되는데요.
그러면 form에서 submit 했을 때 action 함수에서 다시 데이터를 UI 부분으로 전달할 수 없을까요?
리믹스에서는 당연히 이렇게 할 수 있습니다.
이럴 경우는 어떤 경우냐 하면요. 바로 form validation입니다.
뭔가 잘못 입력했을 때 바로 다른 페이지로 라우팅 되지 않고 그냥 다시 현재 페이지로 넘어오는 방법인데요.
이때 쓰이는 훅이 바로 useActionData 훅입니다.
위에서 만든 note 앱 관련해서 Save 버튼이 눌려졌을 때 만약 해당 내용이 없을 경우 error를 보내고 그 error를 UI 부분에서 나타내 주는 코드를 작성해 보겠습니다.
먼저, action 함수 부분입니다.
case "create-note": {
const errors = {};
const title = formData.get("title");
if (typeof title !== "string" || title.length === 0) {
errors.title = "No Content!, Please Type some Notes";
}
// return data if we have errors
if (Object.keys(errors).length) {
return json(errors, { status: 422 });
}
// otherwise create the note and redirect
const note = await db.note.create({
data: { title: title },
});
// console.log("Create item ===> ", note);
if (!note) console.log("Error Delete");
return redirect("/notes");
}
기존 create_note 케이스일 때의 코드에서 title 부분을 체크해서 에러를 지정해 주고 있습니다.
그리고 errors 가 있을 경우 리믹스의 json 함수를 이용해서 리턴해 주고 있습니다.
만약 에러가 없다면 기존과 같은 코드이고요.
json 함수로 리턴된 객체는 바로 UI 부분에서 useActionData 훅으로 참조할 수 있습니다.
이제 이 부분을 추가해 보겠습니다.
const errors = useActionData();
return (
<div className="ml-4">
<h1 className="text-4xl font-bold py-2">Notes</h1>
{errors?.title ? <span>{errors.title}</span> : null}
<Form method="post" replace key={location.key}>
<input className="border rounded py-2 px-4" name="title" />
<button
className="bg-cyan-200 rounded py-2 px-4"
type="submit"
name="_action"
value="create-note"
>
{transition.submission?.formData.get("_action") === "create-note"
? "Saving...."
: "Save Note"}
</button>
</Form>
단순하게 h1 태그와 Form 태그 사이에 errors 부분을 추가했습니다.
테스트 결과를 볼까요?
에러 코드가 잘 나오고 있습니다.
그리고 화면을 새로고침 하면 위 에러 코드는 지워지게 됩니다.
useActionData 훅을 이용하면 Form에서 action 함수로 그리고 다시 UI 부분으로 데이터가 흐를 수 있고 이때 loader 함수 없이 데이터가 흐를 수 있다는 점이 아주 중요합니다.
그럼.