이전 아티클에서는 Supabase와 Apollo Client의 기본적인 Setting에 대한 글을 작성하였습니다.
이번 아티클과 내용이 이어집니다!! 참고하실 분들은 아래 링크를! ⤵️
본격적으로 Apollo Client를 이용하여 query하고 mutate하는 방법을 소개하도록 하겠습니다.
이번 예제 코드는 모두 Supabase DB를 사용하고, Supabase에서 제공하는 GraphQL API를 사용하였습니다.
❗️우선 Supabase에서 Test라는 Table을 만들었고, 몇가지 Test 데이터를 추가한 상태임을 알려드려요!
❗️추가적으로 Supabase에서는 GraphQL Playground인 GraphiQL기능을 제공합니다. 여기서 쿼리들을 실험해볼 수 있기 때문에, 우선적으로 코드를 작성하고 복붙하는 방식을 취했습니다!
Supabase Dashbaord -> Project -> API Docs (사이드바) -> GraphiQL
Supabase의 GraphQL구조는 특이합니다. 이는 Relay-Style Connections을 따르는 것입니다.
쿼리를 작성하면서 나오는 -Collection, edges, node 등은 이 컨벤션을 따르는 것입니다.
1️⃣ Apollo Client의 useQuery
GraphQL을 이용해 query하는 로직을 작성해보도록 하겠습니다.
기본적으로 Apollo Client를 통해 query하는 useQuery 훅은 Tanstack Query와도 유사한데요, 기본적인 방식은 다음과 같습니다.
const { loading, error, data, refetch } = useQuery(GET_DOG_PHOTO, {
variables: { breed },
pollInterval: 500,
});
Test라는 Table을 추가하면, 기본적으로 여러 필드를 추가적으로 정의하는데요, Test들의 집합인 testCollection을 제공합니다. 내부 edges는 node들로 구성되었는데요, 바로 이 node가 Test 입니다.
export const GET_TEST = gql`
query GetTests {
testCollection {
edges {
node {
id
name
}
}
}
}
`;
또한 Codegen을 이용하여 받아온 타입을 이용해 Response 타입을 지정할 수 있습니다.
import { Query } from "./graphql";
// Client에서 Response를 지정함에 따라서 각 query에 타입설정을 다 해줘야 편하게 사용할 수 있다.
export type GetTestResponse = Pick<Query, "testCollection">;
이렇게 만들어진 query와 useQuery를 이용하여 쉽게 데이터를 받아올 수 있습니다.
import { useQuery } from "@apollo/client/index.js";
import { GET_TEST, GetTestResponse } from "src/gql/queries";
export default function Test() {
const { data, loading, error } = useQuery<GetTestResponse>(GET_TEST);
if (loading) return <div>Loading</div>;
if (error) return <div>Error</div>;
return (
<div>
{data?.testCollection?.edges.map(({ node }) => (
<div key={node.id}>{node.name}</div>
))}
</div>
);
}
2️⃣ Apollo Client의 useMutation
기본적으로 Supabase의 Resolver에서 제공하는 Mutate 스키마는 insert delete update 입니다.
공식문서에서 소개하는 useMutation은 다음과 같습니다.
const [mutateFunction, { data, loading, error }] = useMutation(INCREMENT_COUNTER);
순서대로 살펴보겠습니다.
☑️ Insert
Supabase GraphQL에서 제공하는 Insert를 통해 우선적으로 GraphQL 쿼리 구문을 만들어 줍니다.
export const INSERT_TEST = gql`
mutation InsertTest($name: String!, $createdAt: Datetime!) {
insertIntoTestCollection(objects: { name: $name, createdAt: $createdAt }) {
records {
id
name
createdAt
}
}
}
`;
Supabase에서는 Test라는 테이블을 만들면, insertIntoTestCollection 를 제공합니다.
해당 쿼리를 이용해 insert 할 수 있습니다.
import { useMutation } from "@apollo/client/index.js";
import { INSERT_TEST, InsertTestResponse } from "src/gql/queries";
// ...
const [insertTest, { data, loading, error }] = useMutation<InsertTestResponse>(INSERT_TEST);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await insertTest({
variables: { name, createdAt: new Date().toISOString() },
});
alert("Test record inserted successfully!");
} catch (err) {
console.error("Error inserting test:", err);
}
};
☑️ Delete
export const DELETE_TEST = gql`
mutation DeleteTest($id: Int!) {
deleteFromTestCollection(filter: { id: { eq: $id } }) {
affectedCount
}
}
`;
delete 로직은 Supabase에서 제공하는 deleteFromTestCollection를 사용해 작성할 수 있습니다.
affectedCount는 GraphQL 스키마의 수정된 데이터 수를 의미합니다.
//...
const [deleteTest, { data, loading }] = useMutation<DeleteTestResponse>(DELETE_TEST);
const handleDelete = async (id: number) => {
if (loading) return;
await deleteTest({ variables: { id } });
};
Insert 로직과 동일한 형태로 작성할 수 있습니다.
☑️ Update
export const UPDATE_TEST = gql`
mutation UpdateTest($id: Int!, $newName: String!) {
updateTestCollection(filter: { id: { eq: $id } }, set: { name: $newName }) {
affectedCount
records {
id
name
}
}
}
`;
update역시 Supabase에서 제공하는 updateTestCollection 를 통해서 작성할 수 있습니다.
이후 추가적 로직은 다른 mutate와 동일한 형태로 작성할 수 있습니다.
GraphQL의 사용법만 알면 단순히 fetch하는 로직과 크게 다르지 않다는 것을 알 수 있습니다.
3️⃣ Optimistic UI. 낙관적 UI
Apollo Client에서 낙관적 Update는 optimisticResponse 와 update 메서드를 통해서 구현할 수 있습니다.
해당 로직을 구현을 알아보기 위해서 Test라는 데이터를 Delete시 어떻게 낙관적 업데이트를 진행하는지 살펴보도록 하겠습니다.
우선 아까전 Delete 로직을 그대로 가져옵시다.
//...
const [deleteTest, { data, loading }] = useMutation<DeleteTestResponse>(DELETE_TEST);
const handleDelete = async (id: number) => {
if (loading) return;
await deleteTest({ variables: { id } });
};
여기서 mutate 함수는 deleteTest입니다. 해당 로직대로라면 낙관적 업데이트는 이루어지지 않습니다.
☑️ optimisticResponse
optimisticResponse 속성을 통해 useMutation의 응답값을 예상하여 반환할 수 있는데요, 이를 살펴보겠습니다.
//...
const handleDelete = async (id: number) => {
if (loading) return;
await deleteTest({
variables: { id },
optimisticResponse: {
deleteFromTestCollection: {
__typename: "TestDeleteResponse",
affectedCount: Math.min(0, (data?.testCollection?.edges.length || 0) - 1),
records: data?.testCollection?.edges.map(({ node }) => node) || [],
},
}, // optimisticResponse를 작성해준다.
});
};
optimisticResponse 값을 설정하여 useMutation의 응답값이 반환되기 이전 미리 응답받은 것 처럼 동작하게 만들 수 있습니다.
만약 별도의 쿼리를 수정하지 않는다면 이 속성 하나만으로 Optimistic UI를 구현할 수 있겠습니다.
☑️ update 메서드
위 Optimistic Response값을 설정해주었지만, Test를 Get해온 데이터에는 영향을 주지 못합니다. 만약 Test가 삭제됨에 따라 새롭게 Test를 get해오기 위해선 다음 속성을 사용할 수 있습니다.
await deleteTest({
variables: { id },
refetchQueries: [{ query: GET_TEST }],
})
하지만 refetch를 통해 별도의 API를 수정하는 것이 아닌, Mutatation 응답값에 따라서 캐시 데이터를 업데이트할 수 있습니다.
await deleteTest({
variables: { id },
update(cache) {
const existingData = cache.readQuery<GetTestResponse>({
query: GET_TEST,
});
if (existingData?.testCollection) {
const updatedEdges = existingData.testCollection.edges.filter((edge) => edge.node.id !== id);
cache.writeQuery({
query: GET_TEST,
data: {
testCollection: {
...existingData.testCollection,
edges: updatedEdges,
},
},
});
}
},
});
GET_TEST 에 해당하는 쿼리에 해당하는 캐시를 수정할 수 있습니다.
하지만 위 update메서드 하나만으론 Optimistic UI를 구현할 수 없습니다.
해당 메서드는 mutation이 완료된 이후 실행되는 메서드이기 때문입니다. 따라서 optimisticResponse 속성값과 함께 사용하여, 낙관적 응답값을 생성해줆으로써 Optimistic UI를 구현할 수 있습니다.
await deleteTest({
variables: { id },
refetchQueries: [{ query: GET_TEST }],
optimisticResponse: {
deleteFromTestCollection: {
__typename: "TestDeleteResponse",
affectedCount: Math.min(0, (data?.testCollection?.edges.length || 0) - 1),
records: data?.testCollection?.edges.map(({ node }) => node) || [],
},
},
update(cache) {
const existingData = cache.readQuery<GetTestResponse>({
query: GET_TEST,
});
if (existingData?.testCollection) {
const updatedEdges = existingData.testCollection.edges.filter((edge) => edge.node.id !== id);
cache.writeQuery({
query: GET_TEST,
data: {
testCollection: {
...existingData.testCollection,
edges: updatedEdges,
},
},
});
}
},
});
여기까지 Apollo Client를 이용하여 query를 get, insert, update, delete하는 기본적은 CRUD하는 로직들을 살펴봤습니다.
'Front End > React' 카테고리의 다른 글
Next.js Server Action와 React useActionState 알아보기 (0) | 2025.01.03 |
---|---|
Paint 이전 Macro Task가 실행될 가능성과 React의 useEffect (1) | 2024.11.28 |
[Settings] GraphQL과 Supabase, Apollo Client 설정 (0) | 2024.11.26 |
크루루 서비스의 PopOverMenu 컴포넌트 개선기(1), createPortal과 합성 이벤트 문제 해결 (1) | 2024.11.23 |
[React] useLayoutEffect와 useEffect (0) | 2024.11.22 |