Ver3.5 GraphQL API 클라이언트 응용프로그래밍 완전 구축 가이드 🚀

GraphQL API 클라이언트 응용프로그래밍 완전 구축 가이드 🚀
야 솔직히 말해서, 처음 GraphQL 들었을 때 "이게 뭐야 SQL이랑 뭔 차이임?" 했던 사람 손 들어봐 🙋
나도 그랬거든ㅋㅋㅋ 근데 한 번 제대로 파고들면 진짜 "왜 이걸 이제 알았지?" 싶은 기술임.
오늘은 GraphQL API 클라이언트 응용프로그래밍을 처음부터 끝까지 싹 다 뜯어볼 거야.
이론만 주구장창 늘어놓는 거 아니고, 실제로 코드 짜면서 어떻게 구축하는지 팩트로만 알려줄게. 가보자고! 💪
GraphQL은 2012년 Facebook(현 Meta)이 내부적으로 개발하고 2015년에 오픈소스로 공개한 API를 위한 쿼리 언어야.
단순히 데이터 가져오는 방식이 아니라, 클라이언트가 원하는 데이터를 정확하게 명시해서 요청하는 패러다임 자체가 달라진 거임.
REST API는 서버가 "이 엔드포인트에서는 이 데이터 줄게" 하고 미리 정해놓는 방식이잖아?
근데 GraphQL은 클라이언트가 "나 이것만 줘, 저것도 같이 줘" 하고 직접 요청 구조를 설계하는 거야. 완전 다른 철학임 ㄹㅇ.
위 그림 보면 딱 감 오지? REST는 엔드포인트마다 데이터 구조가 고정돼 있어서 클라이언트가 원하든 원하지 않든 정해진 데이터를 받아야 해.
근데 GraphQL은 클라이언트가 쿼리로 "나 이것만 줘" 하면 서버가 딱 그것만 줌. 완전 효율적이지 않음? 🎯
| 비교 항목 | REST API | GraphQL |
|---|---|---|
| 엔드포인트 | 리소스마다 별도 URL | 단일 엔드포인트 (/graphql) |
| 데이터 요청 | 서버가 정한 구조 그대로 | 클라이언트가 원하는 구조 지정 |
| Over-fetching | 발생 가능 | 없음 |
| Under-fetching | 발생 가능 (N+1 문제) | 한 번에 해결 |
| 타입 시스템 | 별도 문서 필요 | 스키마로 자체 문서화 |
| 버전 관리 | v1, v2... 필요 | 스키마 진화로 해결 |
| 실시간 지원 | WebSocket 별도 구현 | Subscription 내장 |
GraphQL 클라이언트 코드 짜기 전에 핵심 개념 좀 잡고 가야 해. 이거 모르면 코드 봐도 뭔 소린지 모름 ㅋㅋ
스키마는 GraphQL API의 계약서야. 서버가 어떤 데이터를 제공할 수 있는지, 어떤 타입이 있는지 전부 정의해놓은 것.
클라이언트는 이 스키마를 보고 "아 이런 데이터 요청할 수 있구나" 하고 쿼리를 작성하는 거임.
# GraphQL 스키마 정의 언어 (SDL) 예시
type User {
id: ID!
name: String!
email: String!
age: Int
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
createdAt: String!
}
type Query {
user(id: ID!): User
users: [User!]!
post(id: ID!): Post
}
type Mutation {
createUser(name: String!, email: String!): User!
updateUser(id: ID!, name: String): User
deleteUser(id: ID!): Boolean!
}
type Subscription {
userCreated: User!
postAdded: Post!
}
타입 뒤에 붙는
!는 "null이 될 수 없음(Non-null)"을 의미해.String! → 반드시 문자열 값이 있어야 함[Post!]! → 배열 자체도 null 불가, 배열 안의 요소도 null 불가
Query는 REST의 GET 요청과 비슷해. 데이터를 읽어오는 작업이야.
근데 차이점은 내가 원하는 필드만 딱 골라서 요청할 수 있다는 거!
# 기본 쿼리
query GetUser {
user(id: "1") {
id
name
email
posts {
title
createdAt
}
}
}
# 변수를 사용한 쿼리 (실무에서 이렇게 씀)
query GetUser($userId: ID!) {
user(id: $userId) {
id
name
email
}
}
# 변수 값 (별도로 전달)
{
"userId": "1"
}
Mutation은 REST의 POST, PUT, DELETE에 해당해. 데이터를 생성, 수정, 삭제하는 작업이야.
# 뮤테이션 예시
mutation CreateUser($name: String!, $email: String!) {
createUser(name: $name, email: $email) {
id
name
email
}
}
# 변수
{
"name": "김개발",
"email": "dev@example.com"
}
Subscription은 WebSocket을 통해 실시간으로 데이터 변경을 받아볼 수 있는 기능이야.
채팅앱, 실시간 알림 같은 거 만들 때 진짜 유용함 ㄹㅇ.
# 서브스크립션 예시
subscription OnUserCreated {
userCreated {
id
name
email
}
}
Fragment는 쿼리에서 반복되는 필드 묶음을 재사용할 수 있게 해주는 기능이야.
DRY(Don't Repeat Yourself) 원칙 지키는 데 딱임!
# 프래그먼트 정의
fragment UserFields on User {
id
name
email
}
# 프래그먼트 사용
query GetUsers {
users {
...UserFields
posts {
title
}
}
}
자 이제 실제로 클라이언트 코드를 짜야 하는데, 어떤 라이브러리를 쓸지 선택해야 해.
주요 라이브러리들 비교해줄게. 이거 선택 잘못하면 나중에 마이그레이션 지옥 가는 수가 있으니까 잘 봐 ㅋㅋ
처음 GraphQL 클라이언트 개발 시작한다면 Apollo Client가 제일 무난해.
문서도 제일 잘 돼있고, 커뮤니티도 크고, 튜토리얼도 많아서 막혔을 때 찾기 쉬움.
번들 크기 걱정된다면 urql도 좋은 선택이야!
자 이제 진짜 코드 짜는 시간이야! Apollo Client 기준으로 React 앱에서 GraphQL 클라이언트를 구축하는 방법을 처음부터 끝까지 알려줄게.
따라오기만 해, 어렵지 않아 ㅋㅋ 🔥
먼저 필요한 패키지들 설치해야 해.
# npm 사용시
npm install @apollo/client graphql
# yarn 사용시
yarn add @apollo/client graphql
# pnpm 사용시
pnpm add @apollo/client graphql
@apollo/client는 Apollo Client 본체고, graphql은 GraphQL 쿼리를 파싱하는 데 필요한 패키지야.
Apollo Client 인스턴스를 생성하고 앱에 연결해야 해.
// src/apollo/client.js
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
// HTTP 링크 설정 (GraphQL 서버 URL)
const httpLink = createHttpLink({
uri: 'https://api.example.com/graphql',
});
// 인증 토큰 설정 (JWT 등)
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('authToken');
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
};
});
// Apollo Client 인스턴스 생성
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
},
},
});
export default client;
React 앱의 최상위에 ApolloProvider를 감싸줘야 해. 이렇게 해야 앱 전체에서 Apollo Client를 사용할 수 있어.
// src/index.js 또는 src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ApolloProvider } from '@apollo/client';
import client from './apollo/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>
);
이제 실제 컴포넌트에서 GraphQL 쿼리를 사용해보자!
// src/components/UserList.jsx
import { useQuery, gql } from '@apollo/client';
// gql 태그로 쿼리 정의
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
posts {
id
title
}
}
}
`;
function UserList() {
// useQuery 훅으로 데이터 가져오기
const { loading, error, data, refetch } = useQuery(GET_USERS, {
fetchPolicy: 'cache-and-network', // 캐시 전략
pollInterval: 30000, // 30초마다 자동 갱신
});
if (loading) return <p>로딩 중...</p>;
if (error) return <p>에러 발생: {error.message}</p>;
return (
<div>
<button onClick={() => refetch()}>새로고침</button>
{data.users.map(user => (
<div key={user.id}>
<h3>{user.name}</h3>
<p>{user.email}</p>
<p>게시글 수: {user.posts.length}</p>
</div>
))}
</div>
);
}
export default UserList;
데이터 생성/수정/삭제는 useMutation 훅을 써야 해.
// src/components/CreateUser.jsx
import { useState } from 'react';
import { useMutation, gql } from '@apollo/client';
const CREATE_USER = gql`
mutation CreateUser($name: String!, $email: String!) {
createUser(name: $name, email: $email) {
id
name
email
}
}
`;
function CreateUser() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [createUser, { loading, error, data }] = useMutation(CREATE_USER, {
// 뮤테이션 성공 후 캐시 업데이트
update(cache, { data: { createUser } }) {
cache.modify({
fields: {
users(existingUsers = []) {
return [...existingUsers, createUser];
},
},
});
},
onCompleted: (data) => {
console.log('유저 생성 완료!', data.createUser);
setName('');
setEmail('');
},
onError: (error) => {
console.error('에러 발생:', error.message);
},
});
const handleSubmit = (e) => {
e.preventDefault();
createUser({ variables: { name, email } });
};
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={e => setName(e.target.value)} />
<input value={email} onChange={e => setEmail(e.target.value)} />
<button type="submit" disabled={loading}>
{loading ? '생성 중...' : '유저 생성'}
</button>
{error && <p>에러: {error.message}</p>}
</form>
);
}
export default CreateUser;
Apollo Client의 진짜 강점은 InMemoryCache야.
이걸 제대로 이해하고 쓰면 성능이 완전 달라짐. 근데 이게 좀 복잡해서 많은 사람들이 그냥 기본값으로만 쓰는데, 그러면 반쪽짜리임 ㅋㅋ
| Fetch Policy | 동작 방식 | 언제 쓰나 |
|---|---|---|
cache-first |
캐시 있으면 캐시 사용, 없으면 네트워크 요청 | 기본값, 자주 안 바뀌는 데이터 |
cache-and-network |
캐시 먼저 보여주고 동시에 네트워크 요청 | 빠른 응답 + 최신 데이터 둘 다 필요할 때 |
network-only |
항상 네트워크 요청, 캐시 무시 | 항상 최신 데이터가 필요할 때 |
no-cache |
네트워크 요청 + 캐시에 저장도 안 함 | 민감한 데이터, 캐시 불필요할 때 |
cache-only |
캐시만 사용, 네트워크 요청 안 함 | 오프라인 모드 |
standby |
캐시 업데이트 시에만 반응 | 비활성 쿼리 |
// 고급 캐시 설정
const cache = new InMemoryCache({
typePolicies: {
// User 타입의 캐시 키 설정
User: {
keyFields: ['id'], // 기본값이지만 명시적으로 설정
},
// 페이지네이션 처리
Query: {
fields: {
users: {
// 커서 기반 페이지네이션
keyArgs: false,
merge(existing = [], incoming) {
return [...existing, ...incoming];
},
},
},
},
},
});
const client = new ApolloClient({
link: httpLink,
cache,
});
뮤테이션 후에 관련 쿼리 캐시를 업데이트하지 않으면 UI가 최신 데이터를 반영하지 못해.
refetchQueries나 cache.modify()를 적절히 사용해야 해!
// 뮤테이션 후 관련 쿼리 자동 재요청
const [deleteUser] = useMutation(DELETE_USER, {
refetchQueries: [
{ query: GET_USERS }, // 삭제 후 유저 목록 다시 가져오기
],
// 또는 쿼리 이름으로 지정
// refetchQueries: ['GetUsers'],
awaitRefetchQueries: true, // 재요청 완료 후 뮤테이션 완료 처리
});
GraphQL Subscription은 WebSocket을 통해 서버에서 클라이언트로 실시간 데이터를 푸시하는 기능이야.
채팅, 알림, 실시간 대시보드 같은 거 만들 때 필수임! 🔔
// src/apollo/client.js (Subscription 지원 버전)
import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { getMainDefinition } from '@apollo/client/utilities';
// HTTP 링크 (Query, Mutation용)
const httpLink = new HttpLink({
uri: 'https://api.example.com/graphql',
});
// WebSocket 링크 (Subscription용)
const wsLink = new GraphQLWsLink(
createClient({
url: 'wss://api.example.com/graphql',
connectionParams: {
authToken: localStorage.getItem('authToken'),
},
})
);
// 쿼리 타입에 따라 링크 분기
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink, // Subscription → WebSocket
httpLink // Query/Mutation → HTTP
);
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
});
// src/components/RealTimeNotifications.jsx
import { useSubscription, gql } from '@apollo/client';
const USER_CREATED_SUBSCRIPTION = gql`
subscription OnUserCreated {
userCreated {
id
name
email
}
}
`;
function RealTimeNotifications() {
const { data, loading, error } = useSubscription(
USER_CREATED_SUBSCRIPTION,
{
onData: ({ data }) => {
console.log('새 유저 생성됨!', data.data.userCreated);
// 알림 표시 로직
},
}
);
if (loading) return <p>실시간 연결 중...</p>;
if (error) return <p>연결 오류: {error.message}</p>;
return (
<div>
{data && (
<div>
새 유저: {data.userCreated.name}
</div>
)}
</div>
);
}
export default RealTimeNotifications;
실제 앱에서 인증 처리는 필수잖아. GraphQL 클라이언트에서 JWT 토큰 같은 인증 정보를 어떻게 처리하는지 알아보자.
이거 제대로 안 하면 보안 구멍 뚫리는 수가 있으니까 집중해 ㅋㅋ 🔒
// src/apollo/authLink.js
import { ApolloLink, Observable } from '@apollo/client';
import { onError } from '@apollo/client/link/error';
// 토큰 갱신 함수
async function refreshAccessToken() {
const refreshToken = localStorage.getItem('refreshToken');
const response = await fetch('/api/refresh-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
const data = await response.json();
localStorage.setItem('authToken', data.accessToken);
return data.accessToken;
}
// 에러 링크 (401 에러 시 토큰 갱신)
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
for (let err of graphQLErrors) {
if (err.extensions?.code === 'UNAUTHENTICATED') {
// 토큰 만료 → 갱신 후 재요청
return new Observable(observer => {
refreshAccessToken()
.then(newToken => {
operation.setContext(({ headers = {} }) => ({
headers: {
...headers,
authorization: `Bearer ${newToken}`,
},
}));
})
.then(() => {
const subscriber = {
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer),
};
forward(operation).subscribe(subscriber);
})
.catch(observer.error.bind(observer));
});
}
}
}
});
export default errorLink;
Access Token은 메모리(변수)에 저장하고, Refresh Token은 HttpOnly 쿠키에 저장하는 게 보안상 제일 안전해.
localStorage에 Access Token 저장하면 XSS 공격에 취약할 수 있어. 실무에서는 이 부분 꼭 신경 써야 함!
GraphQL에서 에러 처리는 REST랑 좀 달라. GraphQL은 HTTP 상태 코드 200으로 응답하면서 에러 정보를 JSON 바디에 담아서 보내거든.
이 특성을 이해하고 제대로 처리해야 해! 🚨
// GraphQL 에러 응답 구조
{
"data": {
"user": null // 부분적으로 null일 수 있음
},
"errors": [
{
"message": "User not found",
"locations": [{ "line": 2, "column": 3 }],
"path": ["user"],
"extensions": {
"code": "NOT_FOUND",
"http": { "status": 404 }
}
}
]
}
// src/hooks/useGraphQLQuery.js
import { useQuery } from '@apollo/client';
import { useCallback } from 'react';
// 에러 처리가 포함된 커스텀 훅
function useGraphQLQuery(query, options = {}) {
const result = useQuery(query, {
...options,
errorPolicy: 'all', // 부분 에러도 data와 함께 반환
});
const { loading, error, data, networkStatus } = result;
// GraphQL 에러 파싱
const graphQLErrors = error?.graphQLErrors || [];
const networkError = error?.networkError;
// 에러 코드별 처리
const errorMessages = graphQLErrors.map(err => {
switch (err.extensions?.code) {
case 'UNAUTHENTICATED':
return '로그인이 필요합니다.';
case 'FORBIDDEN':
return '접근 권한이 없습니다.';
case 'NOT_FOUND':
return '데이터를 찾을 수 없습니다.';
case 'BAD_USER_INPUT':
return `입력값 오류: ${err.message}`;
default:
return err.message;
}
});
return {
...result,
isLoading: loading,
hasError: !!error,
errorMessages,
networkError,
};
}
export default useGraphQLQuery;
// src/apollo/errorLink.js
import { onError } from '@apollo/client/link/error';
const errorLink = onError(({ graphQLErrors, networkError, operation }) => {
// GraphQL 에러 처리
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path, extensions }) => {
console.error(
`[GraphQL 에러] 메시지: ${message}, ` +
`위치: ${JSON.stringify(locations)}, ` +
`경로: ${path}`
);
// Sentry 같은 에러 모니터링 서비스에 전송
// Sentry.captureException(new Error(message));
});
}
// 네트워크 에러 처리
if (networkError) {
console.error(`[네트워크 에러]: ${networkError}`);
// 오프라인 감지
if (!navigator.onLine) {
alert('인터넷 연결을 확인해주세요.');
}
}
});
export default errorLink;
GraphQL 클라이언트 앱 성능 최적화는 진짜 중요해. 잘못 쓰면 오히려 REST보다 느려질 수도 있거든 ㅋㅋ
핵심 최적화 기법들 알려줄게! ⚡
여러 개의 GraphQL 요청을 하나의 HTTP 요청으로 묶어서 보내는 기법이야.
네트워크 왕복 횟수를 줄여서 성능을 높일 수 있어.
// 배칭 링크 설정
import { BatchHttpLink } from '@apollo/client/link/batch-http';
const batchLink = new BatchHttpLink({
uri: 'https://api.example.com/graphql',
batchMax: 5, // 최대 5개 요청을 하나로 묶음
batchInterval: 20, // 20ms 내의 요청을 묶음
});
const client = new ApolloClient({
link: batchLink,
cache: new InMemoryCache(),
});
컴포넌트 마운트 시 자동으로 실행되지 않고, 특정 이벤트 시에만 실행되는 쿼리야.
검색 기능이나 버튼 클릭 시 데이터 로드할 때 유용해!
// useLazyQuery 사용
import { useLazyQuery, gql } from '@apollo/client';
const SEARCH_USERS = gql`
query SearchUsers($keyword: String!) {
searchUsers(keyword: $keyword) {
id
name
email
}
}
`;
function SearchComponent() {
const [keyword, setKeyword] = useState('');
// 컴포넌트 마운트 시 자동 실행 안 됨
const [searchUsers, { loading, data }] = useLazyQuery(SEARCH_USERS, {
fetchPolicy: 'network-only',
});
const handleSearch = () => {
if (keyword.trim()) {
searchUsers({ variables: { keyword } }); // 버튼 클릭 시 실행
}
};
return (
<div>
<input value={keyword} onChange={e => setKeyword(e.target.value)} />
<button onClick={handleSearch}>검색</button>
{loading && <p>검색 중...</p>}
{data?.searchUsers.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}
대량의 데이터를 한 번에 가져오면 성능 저하가 심해. 페이지네이션으로 나눠서 가져와야 해!
// 커서 기반 페이지네이션
const GET_USERS_PAGINATED = gql`
query GetUsers($first: Int!, $after: String) {
users(first: $first, after: $after) {
edges {
node {
id
name
email
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
function UserListPaginated() {
const { loading, data, fetchMore } = useQuery(GET_USERS_PAGINATED, {
variables: { first: 10 },
});
const loadMore = () => {
fetchMore({
variables: {
after: data.users.pageInfo.endCursor,
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
return {
users: {
...fetchMoreResult.users,
edges: [
...prev.users.edges,
...fetchMoreResult.users.edges,
],
},
};
},
});
};
return (
<div>
{data?.users.edges.map(({ node }) => (
<div key={node.id}>{node.name}</div>
))}
{data?.users.pageInfo.hasNextPage && (
<button onClick={loadMore} disabled={loading}>
더 보기
</button>
)}
</div>
);
}
서버 응답을 기다리지 않고 UI를 먼저 업데이트하는 기법이야.
사용자 경험이 훨씬 빠르게 느껴지게 해줌! 좋아요 버튼 같은 거에 딱임 ㅋㅋ
// Optimistic UI 예시 (좋아요 기능)
const LIKE_POST = gql`
mutation LikePost($postId: ID!) {
likePost(postId: $postId) {
id
likesCount
isLiked
}
}
`;
function LikeButton({ post }) {
const [likePost] = useMutation(LIKE_POST, {
// 서버 응답 전에 UI 먼저 업데이트
optimisticResponse: {
likePost: {
__typename: 'Post',
id: post.id,
likesCount: post.isLiked ? post.likesCount - 1 : post.likesCount + 1,
isLiked: !post.isLiked,
},
},
});
return (
<button onClick={() => likePost({ variables: { postId: post.id } })}>
{post.isLiked ? '❤️' : '🤍'} {post.likesCount}
</button>
);
}
TypeScript 쓰는 프로젝트에서 GraphQL 쿼리 타입을 일일이 손으로 작성하면 진짜 노가다야 ㅋㅋ
GraphQL Code Generator를 쓰면 스키마에서 TypeScript 타입을 자동으로 생성해줘! 완전 꿀임 🍯
# 패키지 설치
npm install -D @graphql-codegen/cli @graphql-codegen/client-preset
# 설정 파일 생성
npx graphql-codegen init
// codegen.ts (설정 파일)
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: 'https://api.example.com/graphql',
documents: ['src/**/*.tsx', 'src/**/*.ts'],
generates: {
'./src/gql/': {
preset: 'client',
plugins: [],
config: {
documentMode: 'string',
},
},
},
};
export default config;
// package.json scripts에 추가
{
"scripts": {
"codegen": "graphql-codegen --config codegen.ts",
"codegen:watch": "graphql-codegen --config codegen.ts --watch"
}
}
이렇게 설정하고 npm run codegen 실행하면 스키마에서 TypeScript 타입이 자동 생성돼.
그러면 이렇게 타입 안전하게 쿼리를 쓸 수 있어:
// 자동 생성된 타입을 사용한 컴포넌트
import { useQuery } from '@apollo/client';
import { graphql } from '../gql';
import type { GetUsersQuery } from '../gql/graphql';
// graphql() 함수로 쿼리 정의 (타입 자동 추론)
const GET_USERS = graphql(`
query GetUsers {
users {
id
name
email
}
}
`);
function UserList() {
// data 타입이 GetUsersQuery로 자동 추론됨!
const { data } = useQuery(GET_USERS);
// data.users[0].name → 타입 안전하게 접근 가능
return (
<div>
{data?.users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}
재능넷에는 GraphQL, TypeScript, React 등 다양한 개발 관련 재능을 가진 전문가들이 있어.
혼자 공부하다 막히면 전문가한테 직접 물어보는 것도 좋은 방법이야! 🎯
실제 프로젝트에서 GraphQL 클라이언트 코드를 어떻게 구조화하는지 알아보자.
파일 구조가 엉망이면 나중에 유지보수할 때 진짜 고통받음 ㅋㅋ 처음부터 잘 잡아야 해!
컴포넌트에서 GraphQL 로직을 직접 쓰는 것보다 커스텀 훅으로 추상화하면 훨씬 깔끔해!
// src/hooks/useUsers.ts
import { useQuery, useMutation } from '@apollo/client';
import { GET_USERS, CREATE_USER, DELETE_USER } from '../queries/userQueries';
export function useUsers() {
const { data, loading, error, refetch } = useQuery(GET_USERS);
const [createUser, { loading: creating }] = useMutation(CREATE_USER, {
refetchQueries: ['GetUsers'],
});
const [deleteUser, { loading: deleting }] = useMutation(DELETE_USER, {
refetchQueries: ['GetUsers'],
});
return {
users: data?.users ?? [],
isLoading: loading,
error,
refetch,
createUser,
isCreating: creating,
deleteUser,
isDeleting: deleting,
};
}
// 컴포넌트에서 사용
function UserList() {
const { users, isLoading, createUser, deleteUser } = useUsers();
// 컴포넌트는 UI에만 집중!
if (isLoading) return <p>로딩 중...</p>;
return (
<div>
{users.map(user => (
<div key={user.id}>
{user.name}
<button onClick={() => deleteUser({ variables: { id: user.id } })}>
삭제
</button>
</div>
))}
</div>
);
}
GraphQL 클라이언트 코드 테스트하는 방법도 알아야 해. 테스트 없는 코드는 시한폭탄이나 마찬가지임 ㅋㅋ 💣
Apollo Client는 테스트를 위한 MockedProvider를 제공해. 실제 서버 없이도 테스트할 수 있어!
// src/components/__tests__/UserList.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import UserList from '../UserList';
import { GET_USERS } from '../../queries/userQueries';
// Mock 데이터 설정
const mocks = [
{
request: {
query: GET_USERS,
},
result: {
data: {
users: [
{ id: '1', name: '김개발', email: 'dev@test.com', posts: [] },
{ id: '2', name: '이코딩', email: 'code@test.com', posts: [] },
],
},
},
},
];
describe('UserList 컴포넌트', () => {
it('유저 목록을 올바르게 렌더링해야 한다', async () => {
render(
<MockedProvider mocks={mocks} addTypename={false}>
<UserList />
</MockedProvider>
);
// 로딩 상태 확인
expect(screen.getByText('로딩 중...')).toBeInTheDocument();
// 데이터 로드 후 확인
await waitFor(() => {
expect(screen.getByText('김개발')).toBeInTheDocument();
expect(screen.getByText('이코딩')).toBeInTheDocument();
});
});
it('에러 상태를 올바르게 처리해야 한다', async () => {
const errorMock = [{
request: { query: GET_USERS },
error: new Error('서버 에러 발생'),
}];
render(
<MockedProvider mocks={errorMock} addTypename={false}>
<UserList />
</MockedProvider>
);
await waitFor(() => {
expect(screen.getByText(/에러 발생/)).toBeInTheDocument();
});
});
});
개발할 때 디버깅 도구를 잘 활용하면 생산성이 완전 달라져. GraphQL 개발에 유용한 도구들 소개해줄게!
| 도구 | 용도 | 특징 |
|---|---|---|
| Apollo DevTools | 브라우저 확장 프로그램 | 캐시 상태 확인, 쿼리 히스토리, 뮤테이션 추적 |
| GraphiQL | 브라우저 기반 IDE | 쿼리 작성/테스트, 스키마 탐색, 자동완성 |
| Apollo Studio | 클라우드 기반 플랫폼 | 스키마 관리, 성능 모니터링, 팀 협업 |
| Altair GraphQL | 데스크탑/브라우저 클라이언트 | Subscription 지원, 환경변수, 파일 업로드 테스트 |
// 개발 환경에서 DevTools 활성화
const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache(),
connectToDevTools: process.env.NODE_ENV === 'development', // 개발 환경에서만
name: 'my-app-client', // DevTools에서 표시될 이름
version: '1.0',
});
GraphQL 클라이언트 개발하면서 많이들 하는 실수들 정리해봤어. 이거 미리 알면 삽질 시간 확 줄어들 거야 ㅋㅋ 🎯
클라이언트에서 루프 안에서 쿼리를 반복 실행하면 N+1 문제가 생겨.
서버 사이드에서 DataLoader를 사용하거나, 클라이언트에서 한 번의 쿼리로 필요한 데이터를 모두 가져오도록 설계해야 해.
뮤테이션 후 캐시를 업데이트하지 않으면 UI가 최신 상태를 반영하지 못해.
refetchQueries, cache.modify(), cache.evict() 중 적절한 방법을 선택해서 사용해야 해.
gql 태그로 정의한 쿼리를 컴포넌트 함수 내부에 두면 렌더링마다 새로운 객체가 생성돼서 성능 저하가 생겨.반드시 컴포넌트 외부에 상수로 정의해야 해!
// ❌ 잘못된 방법 - 컴포넌트 내부에 쿼리 정의
function UserList() {
const GET_USERS = gql`...`; // 렌더링마다 새 객체 생성!
const { data } = useQuery(GET_USERS);
}
// ✅ 올바른 방법 - 컴포넌트 외부에 상수로 정의
const GET_USERS = gql`...`; // 한 번만 생성
function UserList() {
const { data } = useQuery(GET_USERS);
}
GraphQL은 HTTP 200으로 에러를 반환하기 때문에
try-catch만으로는 부족해.error 객체의 graphQLErrors와 networkError를 모두 처리해야 해.
GraphQL의 장점이 필요한 데이터만 요청하는 건데, 막상 쿼리에 모든 필드를 다 넣어버리면 의미가 없어.
컴포넌트에서 실제로 사용하는 필드만 요청하도록 쿼리를 최적화해야 해!
요즘 Next.js 많이 쓰잖아. Next.js에서 Apollo Client 쓸 때 SSR(서버 사이드 렌더링)이랑 같이 쓰는 방법도 알아야 해!
// src/lib/apolloClient.ts (Next.js App Router)
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
import { registerApolloClient } from '@apollo/experimental-nextjs-app-support/rsc';
// 서버 컴포넌트용 Apollo Client
export const { getClient } = registerApolloClient(() => {
return new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: 'https://api.example.com/graphql',
// 서버 사이드에서는 절대 URL 필요
fetchOptions: { cache: 'no-store' }, // 항상 최신 데이터
}),
});
});
// 서버 컴포넌트에서 사용
// src/app/users/page.tsx
import { getClient } from '@/lib/apolloClient';
import { GET_USERS } from '@/queries/userQueries';
export default async function UsersPage() {
// 서버에서 직접 데이터 페칭
const { data } = await getClient().query({
query: GET_USERS,
});
return (
<div>
{data.users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}
Next.js App Router에서 Apollo Client를 쓰려면
@apollo/experimental-nextjs-app-support 패키지가 필요해.npm install @apollo/experimental-nextjs-app-support로 설치해줘!재능넷(https://www.jaenung.net)에서 Next.js + GraphQL 관련 전문가를 찾아보면 더 심화된 내용도 배울 수 있어! 🎓
실제 프로젝트에 GraphQL 클라이언트를 도입할 때 확인해야 할 체크리스트야.
이거 하나씩 체크하면서 진행하면 빠뜨리는 거 없이 완성도 높은 앱 만들 수 있어! ✅
자 여기까지 GraphQL API 클라이언트 응용프로그래밍 구축 방법을 처음부터 끝까지 다 훑어봤어!
진짜 많은 내용이었는데 잘 따라왔지? ㅋㅋ 마지막으로 핵심만 딱 정리해줄게.
1. 라이브러리 선택
입문자는 Apollo Client, 번들 크기 중요하면 urql, 대규모 React 앱은 Relay 고려
2. 캐싱 전략
InMemoryCache를 제대로 이해하고 fetchPolicy를 상황에 맞게 설정하는 게 핵심
3. 에러 처리
GraphQL 에러(graphQLErrors)와 네트워크 에러(networkError)를 모두 처리해야 함
4. TypeScript 통합
GraphQL Code Generator로 타입 자동 생성하면 개발 생산성과 안전성이 크게 향상됨
5. 성능 최적화
Optimistic UI, 페이지네이션, 쿼리 배칭, 캐시 업데이트를 적절히 활용해야 함
6. 코드 구조
쿼리 정의 → 커스텀 훅 → 컴포넌트 순으로 관심사를 분리하면 유지보수가 쉬워짐
7. 실시간 기능
Subscription은 WebSocket 링크를 별도로 설정하고 split 함수로 HTTP/WS를 분기해야 함
GraphQL은 처음엔 진입장벽이 좀 있어 보이지만, 한 번 제대로 익히면 REST API 개발할 때보다 훨씬 효율적이고 즐거운 개발 경험을 할 수 있어.
특히 복잡한 데이터 요구사항이 있는 앱에서 GraphQL의 진가가 발휘되거든!
오늘 배운 내용 바탕으로 실제 프로젝트에 적용해보면서 경험을 쌓아가길 바라.
막히는 부분 있으면 공식 문서나 커뮤니티 활용하고, 필요하면 재능넷 같은 플랫폼에서 전문가 도움도 받아봐! 화이팅! 🚀✨
관련 키워드
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.

댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.