use와 같은 현대적인 Async React 기능을 사용하여 동시 렌더링(Concurrent rendering)을 지원하고 원활한 사용자 경험을 제공합니다.export const PostView = view<Post>()({ author: UserView, content: true, id: true, title: true,});export const PostCard = ({ post: postRef }: { post: ViewRef<'Post'> }) => { const post = useView(PostView, postRef); return ( <Card> <h2>{post.title}</h2> <p>{post.content}</p> <UserCard user={post.author} /> </Card> );};npx giget@latest gh:nkzw-tech/fate-templatepnpx giget@latest gh:nkzw-tech/fate-templateyarn dlx giget@latest gh:nkzw-tech/fate-templatefate-template은 간단한 tRPC 백엔드와 fate를 사용하는 React 프론트엔드를 포함하고 있습니다. 매우 빠른 개발 경험을 제공하기 위한 현대적인 도구들을 갖추고 있습니다. 시작하려면 해당 README.md를 따르세요.react-fate를 설치해야 합니다:npm add react-fatepnpm add react-fateyarn add react-fate@nkzw/fate를 설치하세요:npm add @nkzw/fatepnpm add @nkzw/fateyarn add @nkzw/fate경고 fate는 현재 알파 단계이며 프로덕션 준비가 되지 않았습니다. 작동하지 않는 부분이 있다면 풀 리퀘스트(Pull request)를 열어주세요.
참고 fate의 뷰는 GraphQL의 프래그먼트와 같은 역할을 합니다.
Post 컴포넌트를 위한 간단한 뷰를 정의하는 것부터 시작해 보겠습니다. fate는 컴포넌트에서 사용할 각 필드를 명시적으로 "선택"할 것을 요구합니다. title과 content 필드를 가진 Post 엔티티에 대한 뷰를 정의하는 방법은 다음과 같습니다:import { view } from 'react-fate';type Post = { content: string; id: string; title: string;};export const PostView = view<Post>()({ content: true, id: true, title: true,});true로 설정하여 선택됩니다. 이는 fate에게 해당 필드들을 서버에서 가져와 이 뷰를 사용하는 컴포넌트에서 사용할 수 있도록 해야 함을 알려줍니다.참고
위의 Post 타입은 예시입니다. 실제 애플리케이션에서 이 타입은 서버에서 정의되어 클라이언트 코드로 임포트됩니다.useView로 뷰 해석하기PostCard React 컴포넌트에서 사용하여 개별 Post의 참조(Reference)에 대해 데이터를 해석할 수 있습니다:import { useView, ViewRef } from 'react-fate';export const PostCard = ({ post: postRef }: { post: ViewRef<'Post'> }) => { const post = useView(PostView, postRef); return ( <Card> <h2>{post.title}</h2> <p>{post.content}</p> </Card> );};ViewRef는 특정 타입의 구체적인 객체에 대한 참조입니다. 예를 들어 ID가 7인 Post입니다. 여기에는 객체의 고유 ID, 타입 이름(__typename으로 표시), 그리고 fate 전용 메타데이터가 포함됩니다. fate는 이러한 참조를 생성하고 관리하며, 여러분은 필요에 따라 컴포넌트 간에 이를 전달할 수 있습니다.useView를 사용하는 컴포넌트는 선택된 모든 필드의 변경 사항을 감지합니다. 데이터가 변경되면 fate는 해당 데이터에 의존하는 모든 필드를 다시 렌더링합니다. 예를 들어 Post의 title이 변경되면 PostCard 컴포넌트는 새로운 데이터로 다시 렌더링됩니다. 그러나 PostView에서 선택되지 않은 likes와 같은 다른 필드가 변경되면 PostCard 컴포넌트는 다시 렌더링되지 않습니다.useRequest로 데이터 페칭하기useRequest 훅을 사용하여 서버에서 데이터를 가져옵니다. 이 훅을 사용하면 특정 화면이나 컴포넌트 트리에 필요한 데이터를 선언할 수 있습니다. 앱의 루트에서 다음과 같이 포스트 리스트를 요청할 수 있습니다:import { useRequest } from 'react-fate';import { PostCard, PostView } from './PostCard.tsx';export function App() { const { posts } = useRequest({ posts: { list: PostView } }); return posts.map((post) => <PostCard key={post.id} post={post} />);}Post를 위한 단일 뷰를 정의했습니다. fate의 핵심 강점 중 하나는 뷰 구성입니다. 포스트와 함께 작성자의 이름을 표시하고 싶다고 가정해 보겠습니다. 이를 수행하는 간단한 방법은 PostView에 구체적인 선택 사항과 함께 author 필드를 추가하는 것입니다:import { Suspense } from 'react';import { useView, ViewRef } from 'react-fate';export const PostView = view<Post>()({ author: { id: true, name: true, }, content: true, id: true, title: true,});const PostCard = ({ postRef }: { postRef: ViewRef<'Post'> }) => { const post = useView(PostView, postRef); return ( <Card> <h2>{post.title}</h2> <p>by {post.author.name}</p> <p>{post.content}</p> </Card> );};PostCard 컴포넌트에서 사용할 수 있게 합니다. 그러나 이 접근 방식에는 몇 가지 단점이 있습니다:author 선택이 PostView에 밀접하게 결합됩니다. 다른 컴포넌트에서 작성자 데이터를 사용하고 싶다면 필드 선택을 중복해서 작성해야 합니다.author에 다른 컴포넌트에서 사용하고 싶은 필드가 더 많아지면 이를 PostView에 추가해야 하므로 오버페칭으로 이어집니다.author 필드 선택을 다른 뷰나 컴포넌트에서 재사용할 수 없습니다.UserView를 정의하고 다음과 같이 PostView에 구성할 수 있습니다:import type { Post, User } from '@your-org/server/trpc/views';import { view } from 'react-fate';export const UserView = view<User>()({ id: true, name: true, profilePicture: true,});export const PostView = view<Post>()({ author: UserView, content: true, id: true, title: true,});UserView를 사용하는 별도의 UserCard 컴포넌트를 만들 수 있습니다:import { useView, ViewRef } from 'react-fate';export const UserCard = ({ user: userRef }: { user: ViewRef<'User'> }) => { const user = useView(UserView, userRef); return ( <div> <img src={user.profilePicture} alt={user.name} /> <p>{user.name}</p> </div> );};PostCard가 UserCard 컴포넌트를 사용하도록 업데이트합니다:import { UserCard } from './UserCard.tsx';export const PostCard = ({ post: postRef }: { post: ViewRef<'Post'> }) => { const post = useView(PostView, postRef); return ( <Card> <h2>{post.title}</h2> <UserCard user={post.author} /> <p>{post.content}</p> </Card> );};PostCard에서 작성자의 바이오(Bio)와 같은 추가 정보를 가져와 표시하고 싶다고 가정해 보겠습니다. author 필드에 UserView를 직접 할당하는 대신, 이를 스프레드하고 bio 필드를 추가할 수 있습니다:export const PostView = view<Post>()({ author: { ...UserView, bio: true, }, content: true, id: true, title: true,});PostCard 컴포넌트는 작성자의 bio 필드에 접근할 수 있습니다:export const PostCard = ({ post: postRef }: { post: ViewRef<'Post'> }) => { const post = useView(PostView, postRef); return ( <Card> <h2>{post.title}</h2> <UserCard author={post.author} /> {/* bio 필드에 접근 */} <p>{post.author.bio}</p> <p>{post.content}</p> </Card> );};UserStatsView라는 다른 뷰가 있다면, 다음과 같이 PostView에 포함할 수 있습니다:export const UserStatsView = view<User>()({ followerCount: true, postCount: true,});export const PostView = view<Post>()({ author: { ...UserView, ...UserStatsView, bio: true, }, content: true, id: true, title: true,});useView와 SuspenseuseRequest가 서버에서 데이터를 가져오는 역할을 하고, useView가 캐시에서 데이터를 읽는 데 사용된다는 것을 배웠습니다. 어떤 상황에서는 캐시에 데이터가 없을 수 있으며, useView는 누락된 데이터만 가져오기 위해 컴포넌트를 일시 중단(Suspend)해야 할 수도 있습니다. 해당 데이터를 가져와 캐시에 기록하면 컴포넌트는 렌더링을 재개합니다.Post의 content 선택을 잊었습니다. 그 결과, 타입 체크가 실패하고 런타임 중에 content 필드는 undefined가 됩니다:const PostView = view<Post>()({ id: true, title: true, // `content: true`가 누락됨.});const PostCard = ({ post: postRef }: { post: ViewRef<'Post'> }) => { const post = useView(PostView, postRef); return ( <Card> <h2>{post.title}</h2> {/* 여기서 TypeScript 에러가 발생하며, 런타임 시 post.content는 undefined입니다. */} <p>{post.content}</p> </Card> );};const PostDetailView = view<Post>()({ content: true,});const AnotherPostView = view<Post>()({ content: true,});const PostView = view<Post>()({ id: true, title: true, ...AnotherPostView,});const PostCard = ({ post: postRef }: { post: ViewRef<'Post'> }) => { const post = useView(PostView, postRef); return <PostDetail post={post} />;};const PostDetail = ({ post: postRef }: { post: ViewRef<'Post'> }) => { // 이 컴포넌트에 전달된 포스트 참조가 `PostDetailView`가 아닌 // `AnotherPostView` 타입이므로 에러를 던집니다. const post = useView(PostDetailView, postRef);};useView는 참조에 필요한 뷰가 포함되어 있지 않으면 에러를 던집니다.useRequest 훅은 특정 화면이나 컴포넌트 트리에 대한 데이터 요구 사항을 선언하는 데 사용될 수 있습니다. 앱의 루트에서 다음과 같이 포스트 리스트를 요청할 수 있습니다:import { useRequest } from 'react-fate';import { PostCard, PostView } from './PostCard.tsx';export function App() { const { posts } = useRequest({ posts: { list: PostView } }); return posts.map((post) => <PostCard key={post.id} post={post} />);}ErrorBoundary와 Suspense 컴포넌트로 감싸서 에러 및 로딩 상태를 표시하세요:<ErrorBoundary FallbackComponent={ErrorComponent}> <Suspense fallback={<div>Loading…</div>}> <App /> </Suspense></ErrorBoundary>참고
useRequest는 여러 요청을 발행할 수 있으며, 이는 tRPC의 HTTP Batch Link에 의해 자동으로 단일 네트워크 요청으로 묶입니다.id와 관련 view를 지정할 수 있습니다:const { post } = useRequest({ post: { id: '12', view: PostView },});ids 필드를 사용할 수 있습니다:const { posts } = useRequest({ posts: { ids: ['6', '7'], view: PostView },});type과 view만 전달하세요:const { viewer } = useRequest({ viewer: { view: UserView },});useRequest 호출 시 인자를 전달할 수 있습니다. 이는 페이지네이션, 필터링 또는 정렬에 유용합니다. 예를 들어 처음 10개의 포스트를 가져오려면 다음과 같이 할 수 있습니다:const { posts } = useRequest({ posts: { args: { first: 10 }, list: PostView, },});useRequest는 캐싱과 데이터 최신성을 제어하기 위해 다양한 요청 모드를 지원합니다. 사용 가능한 모드는 다음과 같습니다:cache-first (기본값): 캐시에 데이터가 있으면 캐시에서 반환하고, 없으면 네트워크에서 가져옵니다.stale-while-revalidate: 캐시에서 데이터를 반환하는 동시에 네트워크에서 신선한 데이터를 가져옵니다.network-only: 캐시를 무시하고 항상 네트워크에서 데이터를 가져옵니다.useRequest의 옵션으로 전달할 수 있습니다:const { posts } = useRequest( { posts: { list: PostView }, }, { mode: 'stale-while-revalidate', },);useListView를 이용한 페이지네이션useListView를 사용하여 참조 리스트를 감싸면 페이지네이션을 지원하는 커넥션 스타일 리스트를 활성화할 수 있습니다.CommentView를 정의하고 이를 CommentConnectionView 내부에서 재사용할 수 있습니다:import { useListView, ViewRef } from 'react-fate';const CommentView = view<Comment>()({ content: true, id: true,});const CommentConnectionView = { args: { first: 10 }, items: { node: CommentView, },};const PostView = view<Post>()({ comments: CommentConnectionView,});PostCard 컴포넌트 내부에서 useListView 훅을 적용하여 댓글 리스트를 읽고 필요할 때 더 많은 댓글을 로드할 수 있습니다:export function PostCard({ detail, post: postRef,}: { detail?: boolean; post: ViewRef<'Post'>;}) { const post = useView(PostView, postRef); const [comments, loadNext] = useListView( CommentConnectionView, post.comments, ); return ( <div> {comments.map(({ node }) => ( <CommentCard comment={node} key={node.id} post={post} /> ))} {loadNext ? ( <Button onClick={loadNext} variant="ghost"> Load more comments </Button> ) : null} </div> );}loadNext가 undefined라면 더 이상 로드할 댓글이 없음을 의미합니다. 대신 이전 댓글을 로드하고 싶다면 useListView가 반환하는 세 번째 인자인 loadPrevious를 사용할 수 있습니다. 마찬가지로 로드할 이전 댓글이 없으면 loadPrevious는 undefined가 됩니다.useActionState 및 React Actions와 함께 사용하기 위한 fate.actions.fate.mutations.Post 엔티티에 포스트를 추천하는 post.like라는 tRPC 뮤테이션이 있다고 가정해 보겠습니다. fate Actions와 비동기 컴포넌트 라이브러리를 사용하는 LikeButton 컴포넌트는 다음과 같은 모습일 것입니다:import { useActionState } from 'react';import { useFateClient } from 'react-fate';const LikeButton = ({ post }: { post: { id: string; likes: number } }) => { const fate = useFateClient(); const [result, like] = useActionState(fate.actions.post.like, null); return ( <Button action={() => like({ input: { id: post.id } })}> {result?.error ? 'Oops!' : 'Like'} </Button> );};useTransition을 사용하여 트랜지션 내에서 액션을 시작할 수 있습니다:const LikeButton = ({ post }: { post: { id: string; likes: number } }) => { const fate = useFateClient(); const [, startTransition] = useTransition(); const [result, like, isPending] = useActionState( fate.actions.post.like, null, ); return ( <button disabled={isPending} onClick={() => { startTransition(() => like({ input: { id: post.id }, }), ); }} > {result?.error ? 'Oops!' : 'Like'} </button> );};useActionState를 사용함으로써 fate Actions는 Suspense 및 동시 렌더링과 통합됩니다.optimistic 객체를 전달할 수 있습니다. 이는 즉시 캐시를 새로운 추천 수로 업데이트하고 likes 필드를 선택하는 모든 뷰를 다시 렌더링합니다:like({ input: { id: post.id }, optimistic: { likes: post.likes + 1 },});likes 필드를 선택하는 뷰만 다시 렌더링됩니다. 뷰가 title 필드만 선택한다면 likes 필드가 변경되어도 다시 렌더링되지 않습니다.const content = 'New Comment text';addComment({ input: { content, postId: post.id }, optimistic: { author: { id: user.id, name: user.name }, content, id: `optimistic:${Date.now().toString(36)}`, post: { commentCount: post.commentCount + 1, id: post.id }, },});view를 액션에 제공할 수 있습니다:addComment({ input: { content: 'New Comment text', postId: post.id }, view: view<Comment>()({ ...CommentView, post: { commentCount: true }, }),});const [result, addComment] = useActionState(fate.actions.comment.add, null);const newComment = result?.result;if (newComment) { // 뷰에서 선택된 모든 필드를 `newComment`에서 사용할 수 있습니다: console.log(newComment.post.commentCount);}useActionState처럼 이전 액션이 끝나기를 기다리지 않고 호출하고 싶은 경우가 있습니다. 이러한 경우 fate.mutations를 사용하여 명령형으로 뮤테이션을 호출할 수 있습니다:const result = await fate.mutations.comment.add({ input: { content, postId: post.id },});postRouter에서 like 뮤테이션을 구현한 예시입니다.import { z } from 'zod';import { connectionArgs, createResolver } from '@nkzw/fate/server';import { procedure, router } from '../init.ts';import { postDataView, PostItem } from '../views.ts';export const postRouter = router({ like: procedure .input( z.object({ args: connectionArgs, id: z.string().min(1, 'Post id is required.'), select: z.array(z.string()), }), ) .mutation(async ({ ctx, input }) => { const { resolve, select } = createResolver({ ...input, ctx, view: postDataView, }); return resolve( await ctx.prisma.post.update({ data: { likes: { increment: 1, }, }, select, where: { id: input.id }, } as PostUpdateArgs), ); }),});404와 함께 NOT_FOUND 에러를 반환하면, 액션이나 뮤테이션의 결과에 호출 지점에서 처리할 수 있는 에러 객체가 포함됩니다:const [result] = useActionState(fate.actions.post.delete, null);if (result?.error) { if (result.error.code === 'NOT_FOUND') { // 호출 지점에서 not found 에러 처리. } else { // 다른 *예상된* 에러 처리. }}500과 함께 INTERNAL_SERVER_ERROR 에러가 발생하면, 이는 에러를 던지며 가장 가까운 React 에러 바운더리에서 포착될 수 있습니다:<ErrorBoundary FallbackComponent={ErrorComponent}> <Suspense fallback={<div>Loading…</div>}> <PostPage postId={postId} /> </Suspense></ErrorBoundary>delete: true 플래그를 전달할 수 있습니다. 이 플래그는 캐시에서 객체를 제거하고 삭제된 데이터에 의존하는 모든 뷰를 다시 렌더링합니다:const [result, deleteAction] = useActionState(fate.actions.post.delete, null);deleteAction({ input: { id: post.id }, delete: true,});useActionState를 사용할 때, 액션의 결과는 해당 액션을 사용하는 컴포넌트가 언마운트될 때까지 캐시됩니다. 뮤테이션이 에러와 함께 실패했을 때, 액션을 다시 호출하지 않고 에러 상태만 지우고 싶을 수 있습니다. fate Actions는 액션 상태를 초기화하기 위해 'reset' 토큰을 받습니다:const [result, like] = useActionState(fate.actions.post.like, null);useEffect(() => { if (result?.error) { // 3초 후 액션 상태 초기화. const timeout = setTimeout( () => startTransition(() => like('reset')), 3000, ); return () => clearTimeout(timeout); }}, [like, result]);insert 옵션에 before, after, 또는 none 값을 제공하여 이 동작을 커스터마이징하고 새 객체가 리스트의 어디에 삽입될지 지정할 수 있습니다:addComment({ input: { content: 'New Comment text', postId: post.id }, insert: 'before', // 리스트의 시작 부분에 새 댓글 삽입.});none 옵션을 사용하세요:addComment({ input: { content: 'New Comment text', postId: post.id }, insert: 'none', // 어떤 리스트에도 새 댓글을 삽입하지 않음.});id)로 개별 객체를 가져오기 위한 각 데이터 타입별 byId 쿼리.list 쿼리.__typename, 예: Post, User)으로 식별되며, 클라이언트 캐시에는 __typename:id(예: "Post:123") 형식으로 저장됩니다. fate는 백엔드 프로시저와 인자에서 파생된 안정적인 키 아래에 리스트 순서를 유지합니다. 관계(Relations)는 ID로 저장되며 컴포넌트에는 ViewRef 토큰으로 반환됩니다.참고 기존 프로시저와 함께 이러한 쿼리들을 추가함으로써, 기존 스키마를 변경하지 않고도 기존 tRPC 코드베이스에 fate를 점진적으로 도입할 수 있습니다.
byId 쿼리와 포스트 리스트를 가져오기 위한 루트 list 쿼리를 노출하는 tRPC 라우터가 포함된 post.ts 파일이 있다고 가정해 보겠습니다.@nkzw/fate/server의 dataView 함수와 fate 데이터 뷰를 사용하여 동일하게 할 수 있습니다.views.ts 파일을 만듭니다. Prisma의 User 모델에 대한 User 데이터 뷰를 정의하는 방법은 다음과 같습니다:import { dataView, type Entity } from '@nkzw/fate/server';import type { User as PrismaUser } from '../prisma/prisma-client/client.ts';export const userDataView = dataView<PrismaUser>('User')({ id: true, name: true, username: true,});export type User = Entity<typeof userDataView, 'User'>;createResolver를 사용하여 클라이언트의 선택 사항을 해석할 수 있습니다. 다음은 id로 여러 사용자를 가져올 수 있는 User 타입의 byId 쿼리 구현 예시입니다:import { byIdInput, createResolver } from '@nkzw/fate/server';import { z } from 'zod';import type { UserFindManyArgs } from '../../prisma/prisma-client/models.ts';import { procedure, router } from '../init.ts';import { userDataView } from '../views.ts';export const userRouter = router({ byId: procedure.input(byIdInput).query(async ({ ctx, input }) => { const { resolveMany, select } = createResolver({ ...input, ctx, view: userDataView, }); const users = await ctx.prisma.user.findMany({ select: select, where: { id: { in: input.ids } }, } as UserFindManyArgs); return await resolveMany(users); }),});byId 쿼리에 userDataView를 적용했으므로, 서버는 선택 사항을 데이터 뷰에 정의된 필드로 제한하여 비공개 필드를 클라이언트로부터 숨기고 클라이언트 뷰에 대한 타입 안전성을 제공합니다:const UserData = view<User>()({ // 타입 에러 발생 및 런타임 중 무시됨. password: true,});list 쿼리를 구현하기 위해 fate의 createConnectionProcedure 헬퍼를 사용할 수 있습니다. 이 헬퍼는 페이지네이션 구현을 단순화합니다. 다음은 list 쿼리가 포함된 postRouter 구현 예시입니다:import { createResolver } from '@nkzw/fate/server';import type { PostFindManyArgs } from '../../prisma/prisma-client/models.ts';import { createConnectionProcedure } from '../connection.ts';import { router } from '../init.ts';import { postDataView } from '../views.ts';export const postRouter = router({ list: createConnectionProcedure({ query: async ({ ctx, cursor, direction, input, skip, take }) => { const { resolveMany, select } = createResolver({ ...input, ctx, view: postDataView, }); const findOptions: PostFindManyArgs = { orderBy: { createdAt: 'desc' }, select, take: direction === 'forward' ? take : -take, }; if (cursor) { findOptions.cursor = { id: cursor }; findOptions.skip = skip; } const items = await ctx.prisma.post.findMany(findOptions); return resolveMany(direction === 'forward' ? items : items.reverse()); }, }),});export const postDataView = dataView<PostItem>('Post')({ author: userDataView, content: true, id: true, title: true,});list 헬퍼를 사용하세요:import { list } from '@nkzw/fate/server';export const commentDataView = dataView<CommentItem>('Comment')({ content: true, id: true,});export const postDataView = dataView<PostItem>('Post')({ author: userDataView, comments: list(commentDataView),});views.ts 파일에서 Root 객체를 내보냄으로써 추가적인 루트 수준 리스트와 쿼리를 정의할 수 있습니다:export const Root = { categories: list(categoryDataView), commentSearch: { procedure: 'search', view: list(commentDataView) }, events: list(eventDataView), posts: list(postDataView), viewer: userDataView,};list(...)로 뷰를 감싼 항목은 리스트 리졸버(Resolver)로 취급되며, 해당 라우터 프로시저를 호출할 때 procedure 이름을 사용합니다(기본값은 list). list(...)를 생략하면 fate는 해당 항목을 표준 쿼리로 취급하고 뷰 타입 이름을 사용하여 라우터 이름을 추론합니다.Root 정의에 대해 useRequest를 사용하여 다음과 같은 요청을 할 수 있습니다:const query = 'Apple';const { posts, categories, viewer } = useRequest({ // 명시적 Root 쿼리: categories: { list: categoryView }, commentSearch: { args: { query }, list: commentView }, events: { list: eventView }, posts: { list: postView }, viewer: { view: userView }, // 해당 엔티티에 `byId` 쿼리가 정의된 경우 ID로 쿼리: post: { id: '12', view: postView }, comment: { ids: ['6', '7'], view: commentView },});Post 데이터 뷰에 commentCount 필드를 추가하고 싶다면, resolve 함수와 함께 데이터베이스 쿼리를 위한 Prisma 선택 사항을 정의하는 resolver 헬퍼를 사용할 수 있습니다:export const postDataView = dataView<PostItem>('Post')({ author: userDataView, commentCount: resolver<PostItem, number>({ resolve: ({ _count }) => _count?.comments ?? 0, select: () => ({ _count: { select: { comments: true } }, }), }), comments: list(commentDataView), id: true,});commentCount 필드를 클라이언트 측 뷰에서 사용할 수 있게 해줍니다.authorize 함수를 추가하여 이를 수행할 수 있습니다:export const userDataView = dataView<UserItem>('User')({ email: resolver<UserItem, string | null, { sessionUser: string }>({ authorize: ({ id }, context) => context?.sessionUserId === id, resolve: ({ email }) => email, }), id: true,});router.ts 파일이 appRouter 객체, AppRouter 타입, 그리고 정의한 모든 뷰를 내보내는지 확인하세요:import { router } from './init.ts';import { postRouter } from './routers/post.ts';import { userRouter } from './routers/user.ts';export const appRouter = router({ post: postRouter, user: userRouter,});export type AppRouter = typeof appRouter;export * from './views.ts';pnpm fate generate @your-org/server/trpc/router.ts client/src/fate.tsFateClient 컨텍스트 프로바이더를 사용하여 React 앱에서 사용하는 것입니다:import { httpBatchLink } from '@trpc/client';import { FateClient } from 'react-fate';import { createFateClient } from './fate.ts';export function App() { const fate = useMemo( () => createFateClient({ links: [ httpBatchLink({ fetch: (input, init) => fetch(input, { ...init, credentials: 'include', }), url: `${env('SERVER_URL')}/trpc`, }), ], }), [], ); return <FateClient client={fate}>{/* 컴포넌트가 여기에 위치합니다 */}</FateClient>;}참고 fate 코드의 80%는 OpenAI의 Codex가 작성했습니다. 각 작업당 네 가지 버전을 생성하고 인간이 신중하게 큐레이팅했습니다. 나머지 20%는가 작성했습니다. 어느 부분이 좋은 부분인지는 여러분이 결정하세요! 문서는 100% 인간이 작성했습니다. fate에 기여하려면 AI 도구 사용 여부를 공개해야 합니다.
useLiveView 및 SSE를 통한 라이브 뷰(Live views) 및 실시간 업데이트 지원아직 댓글이 없습니다.
첫 번째 댓글을 작성해보세요!

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

Parcel을 이용한 React Server Components
Inkyu Oh • Front-End

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

at:// 프로토콜을 아시나요?
Inkyu Oh • Back-End