// src/server.tsx 파일import { renderRequest } from "@parcel/rsc/node";import express from "express";import { AboutPage } from "./pages/about/AboutPage";const app = express();app.get("/about", addDelay, async (req, res) => { await renderRequest(req, res, <AboutPage />, { component: AboutPage });});app.listen(3000, () => { console.log("Server listening on port http://localhost:3000");});
1.5kB, CSS 10kB)과 작은 런타임(일반 파일 5.0kB 및 페이지 전용 파일 약 65kB 등 두 개의 파일)을 수신하여 렌더링 프로세스 속도를 높입니다.
// src/pages/tasks/TasksPage.tsx 파일import { Layout } from "../../layout/Layout";import { TaskList } from "./TaskList";import { getTasks } from "./tasks";export async function TasksPage({ filter,}: { filter?: "active" | "completed" | undefined;}) { const { tasks, totalActiveTasks } = await getTasks(filter); return ( <Layout> <TaskList filter={filter} tasks={tasks} totalActiveTasks={totalActiveTasks} /> </Layout> );}<TasksPage>는 getTasks 함수를 호출하고 이를 직접 기다립니다(useEffect가 필요 없습니다). getTasks 함수는 "use server"; 지시문이 있는 파일에 선언되어 있습니다.// src/pages/tasks/tasks.ts 파일"use server";import { db } from "../../db";export const getTasks = async (filter?: string) => { const query = db .from("tasks") .select("*") .order("created_at", { ascending: false }); if (filter === "active") { query.is("completed_at", null); } else if (filter === "completed") { query.not("completed_at", "is", null); } const { data, error } = await query; const { count: totalActiveTasks, error: errorActiveTask } = await db .from("tasks") .select("id", { count: "exact", head: true }) .is("completed_at", null); if (error || errorActiveTask) { throw new Error( `Error fetching tasks: ${ error ? error.message : errorActiveTask?.message }`, ); } return { tasks: data, totalActiveTasks: totalActiveTasks ?? 0 };};<TasksPage> 컴포넌트는 선택적(optional) filter 프롭(prop)을 받으므로, 라우트는 URL에서 해당 필터를 읽어 컴포넌트에 전달합니다.// src/server.tsx 파일import { callAction, renderRequest } from "@parcel/rsc/node";import express from "express";import { TasksPage } from "./pages/tasks/TasksPage";const app = express();app.get("{/:filter}", addDelay, async (req, res) => { const filter = getFilter(req.params.filter as string | undefined); if (filter !== undefined && filter !== "active" && filter !== "completed") { res.status(404).send("Not found"); return; } await renderRequest(req, res, <TasksPage filter={filter} />, { component: TasksPage, });});const getFilter = (filter: string | undefined) => { if (filter === "active" || filter === "completed") { return filter; } return undefined;};/active 라우트에 접속하는 사용자는 필요한 작업만 로드하게 됩니다.
<TaskList>는 작업을 필터링하기 위해 몇 가지 Tailwind 클래스가 적용된 단순한 HTML 링크를 렌더링합니다.// src/pages/tasks/TaskFilter.tsx 파일const TaskFilter = ({ currentFilter, filter, children,}: { currentFilter: "active" | "completed" | undefined; filter?: "active" | "completed"; children: React.ReactNode;}) => ( <a href={`/${filter}`} data-active={currentFilter === filter ? true : undefined} className="basis-1/3 group btn btn-sm join-item data-active:btn-primary" > <span className="hidden group-data-loading:block group-data-loading:loading group-data-loading:loading-spinner group-data-loading:loading-xs" /> {children} </a>);"use client-entry";import { hydrate, fetchRSC } from "@parcel/rsc/client";let updateRoot = hydrate();async function navigate(pathname, push = false) { let root = await fetchRSC(pathname); updateRoot(root, () => { if (push) { history.pushState(null, "", pathname); } });}// 링크 클릭을 가로채서 RSC 탐색을 수행합니다.document.addEventListener("click", (e) => { let link = e.target.closest("a"); if (link) { link.dataset.loading = "true"; // 로딩 상태 표시 e.preventDefault(); navigate(link.pathname, true) .then(() => { delete link.dataset.loading; // 로딩 상태 숨김 }); }});// 사용자가 뒤로 가기 버튼을 클릭하면 RSC로 탐색합니다.window.addEventListener("popstate", (e) => { navigate(location.pathname);}data-loading 속성을 추가했습니다. 이를 통해 사용자에게 자신의 동작이 처리되고 있음을 즉시 알릴 수 있습니다.
"use server;" 지시문이 있는 파일에서 서버 함수를 가져와 직접 호출할 수 있습니다. 내부적으로는 실제로 fetch 요청이 발생하며, 이로 인해 클라이언트 코드와 서버 코드 사이의 경계가 모호해집니다.TaskList에 새 작업을 추가하는 폼(form)을 포함할 수 있습니다.// src/pages/tasks/TaskList.tsx 파일import { addTask } from "./tasks";export const TaskList = ({ filter, tasks, totalActiveTasks,}: { filter?: "active" | "completed"; tasks: Task[]; totalActiveTasks: number;}) => { const handleAddTask = async (formData: FormData) => { await addTask(formData); }; return ( <> <form action={handleAddTask}> {/* inputs */} </form> <ul> {tasks.map(task => ( <li key={task.id}>{task.description}</li> ))} </ul> <p> Total tasks: {totalActiveTasks} </p> </> );}addTask 함수는 데이터베이스에 새 작업을 삽입하는 서버 함수(Server Function)입니다.// src/pages/tasks/tasks.ts 파일"use server";import { db } from "../../db";export const addTask = async (formData: FormData) => { const description = formData.get("description"); if (!description || typeof description !== "string") { throw new Error("Invalid task description"); } const { error } = await db.from("tasks").insert([{ description }]); if (error) { throw new Error(`Error adding task: ${error.message}`); }};POST 요청)이 호출되는 동안 UI에서 그 결과를 즉시 가짜로 보여주는 것을 의미합니다. 대부분의 경우 서버 호출은 성공할 것이며, 사용자는 업데이트된 UI를 더 빨리 보게 됩니다. 서버 호출이 실패할 경우 UI 변경 사항을 롤백(Rollback)하면 됩니다.import { addTask } from "./tasks";import { getTaskFromFormData } from "./getTaskFromFormData";export const TaskList = ({ filter, tasks, totalActiveTasks,}: { filter?: "active" | "completed"; tasks: Task[]; totalActiveTasks: number;}) => {+ const initialOptimisticData = { tasks, totalActiveTasks };+ const [optimisticData, setOptimisticTasks] = useOptimistic<+ { tasks: Task[]; totalActiveTasks: number },+ Task+ >(initialOptimisticData, (currentOptimisticDataValue, newTasks) => {+ const { totalActiveTasks } = currentOptimisticDataValue;+ return {+ tasks: newTasks,+ totalActiveTasks: totalActiveTasks + 1,+ };+ }); const handleAddTask = async (formData: FormData) => {- await addTask(formData);+ setOptimisticTasks([...tasks, getTaskFromFormData(formData)]);+ startTransitionNewTask(async () => {+ await addTask(formData).catch((error) => {+ setOptimisticTasks(tasks); // 작업을 초기 값으로 롤백+ alert(error.message);+ });+ });}; return ( <> <form action={handleAddTask}> {/* inputs */} </form> <ul>- {tasks.map(task => (+ {optimisticData.tasks.map(task => ( <li key={task.id}>{task.description}</li> ))} </ul> <p>- Total tasks: {totalActiveTasks}+ Total tasks: {optimisticData.totalActiveTasks} </p> </> );}<LikeButton> 컴포넌트를 생각해 보십시오.// src/pages/tasks/TaskCheckbox.tsx 파일"use client";import { useOptimistic, useTransition } from "react";import type { Task } from "../../types";import { updateTask } from "./api";import { CircleCheckIcon, CircleIcon } from "./icons";export const TaskCheckbox = ({ task,}: { task: Task;}) => { const [isPending, startTransition] = useTransition(); const [optimisticCompleted, setOptimisticCompleted] = useOptimistic< boolean, boolean >( // 초기 값 task.completed_at ? true : false, // 낙관적 업데이트 함수 (current, next) => next ); return ( <form className="group inline-flex w-auto" data-loading={isPending ? "true" : undefined} action={(formData) => { const completed_at = task.completed_at ? "" // 작업이 완료된 상태라면 완료되지 않음으로 표시 : new Date().toISOString(); // 작업이 완료되지 않은 상태라면 완료됨으로 표시 formData.append("completed_at", completed_at); setOptimisticCompleted(!optimisticCompleted); startTransition(async () => { await updateTask(formData); }); }} > <input type="hidden" name="id" value={task.id} /> <div className="tooltip" data-tip={optimisticCompleted ? "Undo" : "Complete"} > <button type="submit" className="btn btn-square btn-ghost hover:text-primary group-data-loading:text-base-content/75" > <span className="sr-only"> {optimisticCompleted ? "Undo" : "Complete"} </span> {optimisticCompleted ? <CircleCheckIcon /> : <CircleIcon />} </button> </div> </form> );};<TaskCheckbox>를 <TaskListItem> 컴포넌트와 단일 작업을 수정하는 다른 페이지 모두에서 사용할 수 있습니다.아직 댓글이 없습니다.
첫 번째 댓글을 작성해보세요!

타입 세이프한 합성 컴포넌트 만들기
Inkyu Oh • Front-End

javascript의 try-catch가 성능에 영향을 주나요?
Inkyu Oh • Front-End

fate
Inkyu Oh • Front-End

프로그래밍 언어로서의 TypeScript 타입
Inkyu Oh • Front-End