프론트앤드 애플리케이션의 미래, PESPA 아키텍처에는 전역 상태 관리 솔루션이 필요 없을 것입니다.
js가 없어도 기본 기능이 동작해야 하기 때문이죠.
그런데 로그인 정보, 테마 정보 같은건 전역 상태로 어디선가 가져와야 하지 않을까요?
그런 정보는 어디서 가져오나요?
PESPA의 구현체인 remix를 통해 해답을 찾아봅니다.
PESPA의 상태 관리 솔루션
1. 웹 표준 : Form
리액트 라우터 6.4 이후 리액트 라우터에 Remix의 철학이 많이 녹아들어왔습니다.
React 자체의 여러 상태관리 솔루션(useState 등)의 사용보다,
바닐라 JS와 브라우저 API, 웹 표준의 사용을 지향합니다.(#useThePlatform)
아래는 SPA에서 사용하는 솔루션인 리액트 라우터 코드지만, Remix와 별반 차이가 없습니다.
다른 점이 있다면, Remix의 경우는 서버에서 html을 리턴해 주기에 js가 필요없다는 것이죠
출처 : https://reactrouter.com/en/main/start/tutorial#adding-search-spinner
export default function Root() {
const { contacts, q } = useLoaderData();
const navigation = useNavigation();
const submit = useSubmit();
const searching =
navigation.location &&
new URLSearchParams(navigation.location.search).has(
"q"
);
useEffect(() => {
document.getElementById("q").value = q;
}, [q]);
return (
<>
<div id="sidebar">
<h1>React Router Contacts</h1>
<div>
<Form id="search-form" role="search">
<input
id="q"
className={searching ? "loading" : ""}
// existing code
/>
<div
id="search-spinner"
aria-hidden
hidden={!searching}
/>
{/* existing code */}
</Form>
{/* existing code */}
</div>
{/* existing code */}
</div>
{/* existing code */}
</>
);
}
다른 상태 관리 솔루션이 왜 필요없는지 아직 납득이 안된다면 아래 게시물도 읽어보세요.
https://itchallenger.tistory.com/736
2. 소프트웨어 공학 : Colocation(aka 응집도)
전역 상태가 필요한 이유는, 보통 필요한 상태가 다른 컴포넌트에 있기 때문입니다.
하지만 필요한 상태가 함께 있으면 전역 상태관리 솔루션이 필요없죠.
Remix는 route에 필요한 기능을 하나의 파일에 모아 위의 문제를 극복합니다.
https://remix.run/docs/en/v1/tutorials/jokes#jokes-app-tutorial
import type { Joke } from "@prisma/client";
import type {
ActionFunction,
LoaderFunction,
MetaFunction,
} from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { useCatch, useLoaderData, useParams } from "@remix-run/react";
import { JokeDisplay } from "~/components/joke";
import { db } from "~/utils/db.server";
import { getUserId, requireUserId } from "~/utils/session.server";
// SEO
export const meta: MetaFunction = ({
data,
}: {
data: LoaderData | undefined;
}) => {
if (!data) {
return {
title: "No joke",
description: "No joke found",
};
}
return {
title: `"${data.joke.name}" joke`,
description: `Enjoy the "${data.joke.name}" joke and much more`,
};
};
type LoaderData = { joke: Joke; isOwner: boolean };
// 페이지 갱신 시 마다 화면 만들기 위한 데이터 처리
export const loader: LoaderFunction = async ({ request, params }) => {
const userId = await getUserId(request);
const joke = await db.joke.findUnique({
where: { id: params.jokeId },
});
if (!joke) {
throw new Response("What a joke! Not found.", {
status: 404,
});
}
const data: LoaderData = {
joke,
isOwner: userId === joke.jokesterId,
};
return json(data);
};
// post관련 액션 처리
export const action: ActionFunction = async ({ request, params }) => {
const form = await request.formData();
if (form.get("_method") !== "delete") {
throw new Response(`The _method ${form.get("_method")} is not supported`, {
status: 400,
});
}
const userId = await requireUserId(request);
const joke = await db.joke.findUnique({
where: { id: params.jokeId },
});
if (!joke) {
throw new Response("Can't delete what does not exist", {
status: 404,
});
}
if (joke.jokesterId !== userId) {
throw new Response("Pssh, nice try. That's not your joke", {
status: 401,
});
}
await db.joke.delete({ where: { id: params.jokeId } });
return redirect("/jokes");
};
export default function JokeRoute() {
const data = useLoaderData<LoaderData>();
return <JokeDisplay joke={data.joke} isOwner={data.isOwner} />;
}
// 400번대 에러처리
export function CatchBoundary() {
const caught = useCatch();
const params = useParams();
switch (caught.status) {
case 400: {
return (
<div className="error-container">
What you're trying to do is not allowed.
</div>
);
}
case 404: {
return (
<div className="error-container">
Huh? What the heck is {params.jokeId}?
</div>
);
}
case 401: {
return (
<div className="error-container">
Sorry, but {params.jokeId} is not your joke.
</div>
);
}
default: {
throw new Error(`Unhandled error: ${caught.status}`);
}
}
}
// 500번대 오류 처리
export function ErrorBoundary({ error }: { error: Error }) {
console.error(error);
const { jokeId } = useParams();
return (
<div className="error-container">{`There was an error loading joke by the id ${jokeId}. Sorry.`}</div>
);
}
상태 관리와 렌더링 위치의 직교를 극복하는 방법은 아래 게시글을 참고하세요
https://itchallenger.tistory.com/758
3. 웹 표준 : Session + cookie
서두에서 던진 질문에 대한 대답입니다.
PESPA의 솔루션은 MPA 앱 개발 방법론과 SPA의 장점을 결합하는 것입니다.
클라이언트 측에서 필요하면서 사용자에게 노출해도 괜찮은 정보는 cookie로 넘겨주면 되고,
전역적으로 필요하면서 사용자에게 노출하면 안되는 정보는 session에서 처리하면 됩니다.
세션은 특히 서버 측 양식 유효성 검사와 관련하여 또는 JavaScript가 페이지에 없을 때
서버가 동일한 사람의 요청을 식별할 수 있도록 하는 웹 사이트의 중요한 부분입니다.
세션은 소셜, 전자 상거래, 비즈니스 및 교육 웹 사이트를 포함하여
사용자가 "로그인"할 수 있는 많은 사이트의 기본 빌딩 블록입니다.
쿠키는 특정 클라이언트에 서버가 HTTP 응답 보내는 작은 정보로,
브라우저가 후속 요청에서 서버에 다시 보낼 것으로 기대할 수 있습니다.
이 기술은 인증(세션), 장바구니, 사용자 기본 설정 및 "로그인"한 사람을 기억해야 하는 기타 여러 기능을 구축할 수 있도록
상태를 추가하는 많은 대화형 웹사이트의 기본 빌딩 블록입니다.
4. Progressive Enhancement : useTransition
그렇다면 Pending(보류 중임을 나타내는) UI는 어떻게 보여줄까요?
Optimistic UI를 구현하려면 성공, 실패를 위한 플래그가 필요할텐데요.
이런 사용사례가 JS를 사용하여 사용자 경험을 강화하기에 좋은 곳입니다.
해당 상태를 나타내는 useTransition 훅을 사용하여, ui를 위한 상태(@ 클라이언트)와 UI 업데이트 로직(서버의 action)을 분리합니다.
- 전역 로딩 스피너
- 클릭한 링크의 스피너
- 변형이 일어나는 동안 form 비활성화
- submit 버튼에 스피너 추가
- 데이터가 서버에서 생성되는 동안 사용자가 생성한 레코드를 낙관적으로 표시
- 업데이트되는 동안 레코드를 업데이트 한 상태를 낙관적으로 표시
import { useTransition } from "@remix-run/react";
function SomeComponent() {
const transition = useTransition();
transition.state;
transition.type;
transition.submission;
transition.location;
}
결론 : Progressive Enhancement & #useThePlatform
제가 오픈소스 커뮤니티 문서에 번역으로 처음 기여한 글인데요,
해당 글은 SPA에 관한 내용이지만, Remix에도 적용되는 개념이 있습니다.
https://itchallenger.tistory.com/719?category=1104363
URL세그먼트, 레이아웃, 데이터는 좋은 짝꿍입니다.
loader는 리액트 쿼리 등 데이터 가져오기를 url 단위로 캡슐화 해주는 요소입니다.
action은 폼 제출(submit)을 에뮬레이트 합니다.
앱이 라우터에 non-get submit("post", "put", "patch", "delete")을 보낼 때마다 action이 호출됩니다.
Remix는 중첩 경로와 Outlet, 데이터 로더, 액션 개념을 이용해 colocation(응집도)라는 소프트웨어 공학 개념의 힘을 보여주었고,
웹 표준 기술의 장점을 솔루션에 잘 녹여냈습니다.
그리고 예전과 다르게, 브라우저 자체도 엄청난 속도로 진화하고 있습니다.
리액트와 같은 프레임워크에 대한 지식 없이 브라우저 플랫폼만의 힘으로도 쉽게 개발할 수 있는 시대가 올 수도 있습니다.
(리액트 팀과 크롬 팀이 상당 기간 협업해 온 것은 잘 알려져 있죠)
추가로 상황에 따라 js를 활용할 수 없는 사용자들에게 웹을 돌려주기 위한 Progressive Enhancement는,
요즘 제로 번들 사이즈 운동과도 합이 잘 맞습니다.
웹 개발의 복잡성을 플랫폼과 표준 기술을 활용하여 해결하는 Remix의 솔루션은, 기본의 중요성이 무엇인지 보여주는 것 같습니다.
우리는 Remix API를 최소한으로 유지하고 대신 웹 표준을 사용하려고 노력합니다.
예를 들어, 자체 req/res API를 발명하거나 Node의 API를 사용하는 대신
Remix(및 Remix 앱)는 Web Fetch API객체와 함께 작동합니다.
즉, Remix에 능숙해지면 Request, Response, URLSearchParams 및 URL과 같은 웹 표준에도 능숙해집니다.
이 모든 것은 이미 브라우저에 있으며 배포 위치에 관계없이 Remix 서버에도 있습니다.
참고 :
https://remix.run/docs/en/v1/pages/philosophy
https://itchallenger.tistory.com/736
'FrontEnd' 카테고리의 다른 글
의존성 역전 원칙과 NestJS(Dependency Inversion Principle with NestJS) (0) | 2022.10.22 |
---|---|
타입스크립트 데코레이터 완벽 가이드[A Complete Guide to TypeScript Decorators] (0) | 2022.10.22 |
리액트를 위한 이벤트 버스🚌 [Event Bus for React] (0) | 2022.10.20 |
리액트 use, 새로 등장한 훅을 알아보자 (React use) (0) | 2022.10.20 |
리액트 디자인 패턴 : uncontrolled compound components(제어없는 컴파운드 컴포넌트) (0) | 2022.10.19 |