react는 csr방식
을 사용합니다. 때문에 페이지에서 보여지는 데이터를 서버에서 주로 json형태로 불러온 다음 화면을 그립니다. 우리가 특정 페이지로 라우팅 될 때, 서버와 통신할 함수를 미리 만들어 놓고, 자동으로 데이터를 가져온다면 얼마나 편할까요?
react-router는 다음과 같은 모듈을 사용해 해당 기능을 구현했습니다.
loader
useLoaderData
두 모듈은 로더함수만들고 이를 경로에 연결합니다. 이게 무슨말이냐, 특정 URL “localhost:3000/a/b/c“
와 같은 경로에 만들어둔 loader 함수를 연결
하면 해당 URL에 방문할 때 자동으로 함수를 실행시키고 데이터를 불러옵니다.
개발자는 불러온데이터로 화면만 그리면 됩니다.
다음 코드를 살펴봅시다.
<Root/>컴포넌트
가 연결되어있는 “/”
url에 lodaer함수
를 연결하도록 하겠습니다.
먼저 loader함수
를 만들어 봅시다.
src/routes/root.jsx
import { Outlet, Link } from "react-router-dom";
import { getContacts } from "../contacts";
export async function loader() { //데이터를 불러오는 함수
const contacts = await getContacts(); //비동기 통신
return { contacts }; // 객체로 반환하는것은 불러오는데이터를 명확하게 하기 위함 => 나중에 객체 분해하려고
}
src/index.js
import Root, { loader as rootLoader } from "./routes/root"; //생성한 로더함수 가져오기
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
loader: rootLoader, //해당 경로로 진입할 때 로더함수 실행됨
children: [
{
path: "contacts/:contactId",
element: <Contact />,
},
],
},
]);
src/routes/root.jsx
import {
Outlet,
Link,
useLoaderData,
} from "react-router-dom";
import { getContacts } from "../contacts";
/* other code */
export default function Root() {
const { contacts } = useLoaderData(); // 해당 훅을 사용해 loader함수를 통해 서버로 부터 가져온 데이터 사용
return (
<>
<div id="sidebar">
<h1>React Router Contacts</h1>
{/* other code */}
<nav> {/* 가져온 데이터 화면에 그리기! */}
{contacts.length ? (
<ul>
{contacts.map((contact) => (
<li key={contact.id}>
<Link to={`contacts/${contact.id}`}>
{contact.first || contact.last ? (
<>
{contact.first} {contact.last}
</>
) : (
<i>No Name</i>
)}{" "}
{contact.favorite && <span>★</span>}
</Link>
</li>
))}
</ul>
) : (
<p>
<i>No contacts</i>
</p>
)}
</nav>
{/* other code */}
</div>
</>
);
}
전체적인 흐름을 봅시다.
특정 url 라우팅 ⇒ loader 함수 실행 ⇒ useLoaderDate() 로 데이터 가져오기 ⇒ 화면 그리기
이게 React router는 해당 데이터를 UI와 자동으로 동기화 합니다. 아직은 데이터가 없기 때문에 다음과 같은 빈목록이 표시됩니다.
목록은 비어있지만 해당 페이지로 라우팅 될 때 loader에 있는 함수가 자동으로 실행됩니다.
HTML에서 Form을 사용하면 입력한 데이터를 서버로 보내는 동작을 수행합니다. 이를 폼전송(Form Submisstion)
이라고 합니다. 즉 사용자가 품을 제출(submit)
하면 입력한 데이터화 함께 서버로 요청(request)
이 보내지는 것입니다.
폼을 제출하는 순간 브라우저는 내부적으로 서버로 요청을 보내기 위해 브라우저 창의 URL을 변경
합니다. 이러한 변경은 브라우저의 주소 표시줄의 URL을 새로운 주소로 업데이트 하는 것
을 의미합니다.
즉 사용자가 form을 제출하면 브라우저가 새로운 페이지로 이동하는것 처럼 보이게 되며 이때 URL이 변경됩니다. 이런동작을 네비게이션을 유발
한다고 표현합니다.
새로고침이 된것처럼 보이지만 실제로 페이지 전체를 새로 로드하는것은 아니고 서버로 데이터를 전송하는것입니다.
라우트 액션은 라우팅 과정에서 특정 동작을 의미합니다. Form에 의해 URL이 변경되면 해당 URL에 대한 라우트 액션(action)이 발생합니다.
라우트 액션을 사용하기 위해 <Form/>
을 만들어 봅시다.
src/routes/root.jsx
import {
Outlet,
Link,
useLoaderData,
Form,
} from "react-router-dom";
import { getContacts, createContact } from "../contacts";
export async function action() {
const contact = await createContact();
return { contact };
}
/* other code */
export default function Root() {
const { contacts } = useLoaderData();
return (
<>
<div id="sidebar">
<h1>React Router Contacts</h1>
<div>
{/* other code */}
<Form method="post">
<button type="submit">New</button>
</Form>
</div>
{/* other code */}
</div>
</>
);
}
src/index.jsx
/* other imports */
import Root, {
loader as rootLoader,
action as rootAction,
} from "./routes/root";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
loader: rootLoader,
action: rootAction,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
},
],
},
]);
흐름
컴포넌트를 submit(제출) ⇒ action에 연결되어있는 함수 동작 ⇒ 컴포넌트를 리랜더링하면서 loader로 데이터 다시 불러옴
<Form/>
을 사용할 때와 <form/>
을 사용할때 차이점이 있습니다. 위에서 설명했듯이 은 페이지를 새로고침하지 않는 클라이언트 라우팅을 이용한 동작입니다.
지금은 제대로된 경로로 요청을 보내는것이 아니기 때문에 에러가 발생하는데 어떤 차이가 있는지 확인해 봅시다.
<Form/>
을 이용할 때
기본 <form/>
을 이용할 때
이처럼 라우팅을 이용한다는 특징 덕분에 잘못된 요청을 발생시켰을 때, 우리는 router
에서 세팅했던 ErrorPage
를 그대로 사용할 수 있습니다.
createContact()
를 action
에 연결시켜 주면 다음과 같이 NoName 리스트가 생성됩니다.
아까 만들었던 path
를 살펴봅시다.
[
{
path: "contacts/:contactId",
element: <Contact />,
},
];
:contactId
는 동적 세그먼트로 변환하는 특별한 의미를 가집니다. 이것을 URL Params
또는 params
라고 말합니다.
페이지를 동적으로 로딩할 때 많이 사용하는데, 이 값을 페이지 초기값을 로딩할 때 사용하는 loader
에서 사용할 수는 없을까요?
src/routes/contact.jsx
import { Form, useLoaderData } from "react-router-dom";
import { getContact } from "../contacts";
export async function loader({ params }) { // 구조분해 해서 가져오기
const contact = await getContact(params.contactId);
return { contact };
}
export default function Contact() {
const { contact } = useLoaderData();
// existing code
}
src/index.jsx
/* existing code */
import Contact, {
loader as contactLoader,
} from "./routes/contact";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
loader: rootLoader,
action: rootAction,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
loader: contactLoader,
},
],
},
]);
/* existing code */
loader에서 구조분해 한 뒤 파라미터값을 가져올 수 있습니다!
이제 을 활용할 때가 왔습니다.
다음과 같은 컴포넌트를 생성해 줍니다.
src/routes/edit.jsx
import { Form, useLoaderData } from "react-router-dom";
export default function EditContact() {
const { contact } = useLoaderData();
return (
<Form method="post" id="contact-form">
<p>
<span>Name</span>
<input
placeholder="First"
aria-label="First name"
type="text"
name="first"
defaultValue={contact.first}
/>
<input
placeholder="Last"
aria-label="Last name"
type="text"
name="last"
defaultValue={contact.last}
/>
</p>
<label>
<span>Twitter</span>
<input
type="text"
name="twitter"
placeholder="@jack"
defaultValue={contact.twitter}
/>
</label>
<label>
<span>Avatar URL</span>
<input
placeholder="https://example.com/avatar.jpg"
aria-label="Avatar URL"
type="text"
name="avatar"
defaultValue={contact.avatar}
/>
</label>
<label>
<span>Notes</span>
<textarea
name="notes"
defaultValue={contact.notes}
rows={6}
/>
</label>
<p>
<button type="submit">Save</button>
<button type="button">Cancel</button>
</p>
</Form>
);
}
src/index.jsx
/* existing code */
import EditContact from "./routes/edit";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
loader: rootLoader,
action: rootAction,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
loader: contactLoader,
},
{
path: "contacts/:contactId/edit",
element: <EditContact />,
loader: contactLoader,
},
],
},
]);
/* existing code */
코드를 보면 contactLoader
를 재사용했음을 알 수 있습니다.
이제 edit을 누르면 다음과 같은 화면이 나오게 됩니다.
방금 만든 경로로 만든 양식을 랜더링 합니다. 해당 <Form/>
을 action
에 연결해서 데이터를 업데이트 해봅시다.
src/routes/edit.jsx
import {
Form,
useLoaderData,
redirect,
} from "react-router-dom";
import { updateContact } from "../contacts";
export async function action({ request, params }) {
const formData = await request.formData(); //Form에서 데이터 추출
const updates = Object.fromEntries(formData); //데이터 객체로 변환
await updateContact(params.contactId, updates);
return redirect(`/contacts/${params.contactId}`);
}
/* existing code */
src/index.jsx
/* existing code */
import EditContact, {
action as editAction,
} from "./routes/edit";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
loader: rootLoader,
action: rootAction,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
loader: contactLoader,
},
{
path: "contacts/:contactId/edit",
element: <EditContact />,
loader: contactLoader,
action: editAction, //Form에 연결할 action 추가하기
},
],
},
]);
/* existing code */
edit한 내용이 잘 반영되고 있습니다!
이게 어떻게 가능한 걸까요?
<input
placeholder="First"
aria-label="First name"
type="text"
name="first"
defaultValue={contact.first}
/>
해당 코드에서 value의 이름은 first가 됩니다.
이러한 input이 담긴 form은 FormData 형식으로 작성 됩니다.
export async function action({ request, params }) {
const formData = await request.formData();
const firstName = formData.get("first");
const lastName = formData.get("last");
// ...
}
그래서 request(요청)을 열어보면 .get()으로 보낼 데이터의 이름을 매개변수로 입력해 꺼내볼 수가 있습니다.
또 추가적으로 Object.formEntries()
를 사용해서 formdata
를 객체로 표현할 수 있습니다.
const formData = await request.formData(); // Form데이터 받아오기
const updates = Object.fromEntries(formData); // 데이터를 객체로 변환하기
await updateContact(params.contactId, updates); // 요청 보내기
여기서 말하는 request
, request.formData
, Object.formEntries
는 웹플랫폼에서 제공하는 API 입니다. react-router와는 상관없습니다.
데이터 처리를 마친후
return redirect(`/contacts/${params.contactId}`);
react-router가 제공하는 redirect() 으로 클라이언트 사이드로 페이지를 이동합니다. 근데 이상한 점이 하나있습니다. 이동한 페이지는 contacts/:contactId
인데 사이드바까지 모두 리랜더링 된다는 점입니다.
페이지를 redirect()
하면서 사이드바까지 업데이트 되었습니다.
원래 클라이언트 측 라우팅이 없으면 post요청시 서버가 리디렉션 되면서 새로운 페이지를 다시가져오기
때문에 페이지 전체가 새로고침
되었습니다. react-router는 이 과정을 예뮬레이트하고 작업후에 페이지의 데이터를 자동으로 검증해서 마치 페이지를 새로 불러온것처럼 랜더링해버립니다
.
새 연락처를 만드는 작업을 업데이트하고 편집 페이지로 리디렉션 해보겠습니다.
src/routes/root.jsx
import {
Outlet,
Link,
useLoaderData,
Form,
redirect,
} from "react-router-dom";
import { getContacts, createContact } from "../contacts";
export async function action() {
const contact = await createContact();
return redirect(`/contacts/${contact.id}/edit`);
}
이제 new
버튼을 누르면 페이지가 redirect
가 되서 새로만든 후 편집 페이지로 이동합니다.