프로젝트를 진행하다 보면 레거시 시스템과의 연동이나 마이크로서비스 아키텍처(MSA) 도입 등의 이유로, 하나의 클라이언트 앱에서 두 개 이상의 서로 다른 GraphQL 엔드포인트를 바라봐야 하는 상황이 발생합니다.
최근 진행한 프로젝트에서도 메인 비즈니스 로직을 담당하는 API와 실시간 채팅 기능을 담당하는 API가 분리되어 있었습니다. 이 글에서는 Next.js(App Router) 환경에서 두 개의 독립적인 Apollo Client를 안전하고 효율적으로 설정한 경험을 공유합니다.
1. 개요: 왜 두 개의 Client인가?
일반적인 Apollo Client 사용법은 앱 최상단에 하나의 ApolloProvider를 감싸는 것입니다.
하지만 엔드포인트가 다르다는 것은 스키마(Schema)가 다르다는 뜻이고,
이는 타입 시스템과 캐시 전략이 완전히 분리되어야 함을 의미합니다.
우리는 다음과 같은 전략을 세웠습니다.
- Main API: 전역적으로 사용되므로 기본 ApolloProvider에 주입.
- Sub API (Chat): 특정 도메인에서만 사용되므로, 필요한 곳에서 명시적으로 주입.
- Codegen: 두 스키마의 타입이 섞이지 않도록 격리하여 생성.
2. GraphQL Codegen 설정: 스키마 격리하기
가장 먼저 해결해야 할 문제는 타입 충돌 방지입니다.
두 API가 동일한 이름의 Type(예: User, Message)을 가질 수 있기 때문에,
Codegen 설정 단계에서부터 파일 생성 위치를 물리적으로 분리했습니다.
codegen.ts 설정 파일에서 generates 항목을 두 개의 섹션으로 나누어 구성했습니다.
// codegen.ts 예시
const config: CodegenConfig = {
// ...공통 설정
generates: {
// 1. 메인 API용 타입 생성
"src/lib/graphql/main/gqlGenerated.ts": {
schema: MAIN_API_SCHEMA_URL,
documents: ["src/lib/graphql/main/**/*.{ts,tsx,graphql}"], // 경로 제한
plugins: ["typescript-react-apollo", /* ... */],
// ...
},
// 2. 서브 API용 타입 생성
"src/lib/graphql/sub/gqlGenerated.ts": {
schema: SUB_API_SCHEMA_URL,
documents: ["src/lib/graphql/sub/**/*.{ts,tsx,graphql}"], // 경로 제한
plugins: ["typescript-react-apollo", /* ... */],
// ...
},
},
};
- Documents 경로 제한: 각 Codegen 설정이 서로 다른 폴더의 쿼리 파일만 바라보게 하여,
메인 API용 훅 생성 시 서브 API 쿼리가 포함되지 않도록 했습니다. - 출력 파일 분리: 각각 별도의 폴더에 생성된 훅(useQuery 등)을 import 하여 사용합니다.
3. Apollo Client 인스턴스 구성
두 개의 클라이언트는 엔드포인트만 다를 뿐, 인증(Auth) 로직이나 에러 핸들링 로직은 유사한 경우가 많습니다. 이를 위해 Link 설정은 재사용하되, 클라이언트 인스턴스 생성은 분리했습니다.
팩토리 패턴을 활용한 클라이언트 생성
// clientFactory.ts (개념적 코드)
// 공통으로 사용할 Auth Link, Error Link
const authLink = createAuthLink();
const errorLink = createErrorLink();
// 1. 메인 클라이언트 (기본)
export const getMainClient = () => {
return new ApolloClient({
link: from([authLink, errorLink, createHttpLink({ uri: MAIN_API_URL })]),
cache: new InMemoryCache(), // 독립된 캐시
});
};
// 2. 서브 클라이언트 (채팅 등)
let subClientInstance: ApolloClient<any> | null = null;
export const getSubClient = () => {
// 싱글톤 패턴으로 인스턴스 재사용
if (!subClientInstance) {
subClientInstance = new ApolloClient({
link: from([authLink, errorLink, createHttpLink({ uri: SUB_API_URL })]),
cache: new InMemoryCache(), // 독립된 캐시 필수!
});
}
return subClientInstance;
};
- InMemoryCache는 공유하면 안 됩니다. 스키마 구조가 다르기 때문에 캐시 키 충돌이 발생할 수 있습니다.
각 클라이언트마다 new InMemoryCache()를 따로 생성해야 합니다.
4. 실제 사용: Context vs Explicit Injection
이제 컴포넌트에서 어떻게 사용하는지 살펴보겠습니다. 이 부분이 다중 클라이언트 전략의 핵심입니다.
1) 메인 API 사용 (기본 방식)
메인 클라이언트는 ApolloProvider를 통해 주입되므로, Codegen으로 생성된 훅을 평소처럼 사용하면 됩니다.
// 일반적인 사용 (Main Client 자동 연결)
import { useUserQuery } from "@/lib/graphql/main/gqlGenerated";
export default function UserProfile() {
const { data } = useUserQuery(); // Provider의 client 사용
return <div>{data?.me.name}</div>;
}
2) 서브 API 사용 (명시적 주입)
서브 API를 사용하는 훅은 client 옵션을 명시적으로 전달해야 합니다.
Codegen이 생성해 주는 React Hook들은 QueryHookOptions를 받는데, 여기에 client 프로퍼티가 포함되어 있습니다.
// 서브 API 사용 (Client 명시)
import { useChatMessagesQuery } from "@/lib/graphql/sub/gqlGenerated";
import { getSubClient } from "@/lib/graphql/sub/client";
export default function ChatRoom() {
// ✅ 여기서 client를 직접 넣어줍니다.
const { data } = useChatMessagesQuery({
variables: { roomId: "123" },
client: getSubClient(),
});
return <div>...</div>;
}
만약 client 옵션을 넣지 않으면, Apollo는 상위 Provider에 있는 메인 클라이언트를 사용하여 요청을 보냅니다.
메인 API에는 해당 쿼리에 맞는 스키마가 없으므로 "Field not found" 에러가 발생하게 됩니다.
5. 마무리 및 트러블 슈팅 팁
다중 클라이언트 구조를 운영하면서 겪을 수 있는 실수들과 해결 팁을 정리합니다.
- Import 경로 혼동:
- IDE의 자동 완성 기능을 사용하다 보면 메인 API용 쿼리 훅을 서브 API 컴포넌트에서 잘못 import 하는 경우가 생깁니다. 폴더 구조를 명확히 나누고(예: domain-a/, domain-b/), 팀 내 컨벤션을 통해 이를 방지해야 합니다.
- 캐시 업데이트 (Mutation):
- 서브 API에 Mutation을 날릴 때도 client: getSubClient()를 꼭 명시해야 합니다.
- update 함수 내에서 cache.modify 등을 사용할 때, 해당 cache 객체는 자동으로 올바른 클라이언트의 캐시를 참조하므로 걱정하지 않아도 됩니다.
- Client Component vs Server Component:
- Next.js App Router 환경에서는 서버 컴포넌트용 클라이언트 함수(getServerApolloClient)와 클라이언트 컴포넌트용 래퍼를 구분해서 작성해야 합니다. 이 원칙은 다중 클라이언트 환경에서도 동일하게 적용됩니다.
이러한 구조를 통해 서로 다른 두 개의 백엔드 서비스를 하나의 프론트엔드 프로젝트에서 타입 안전성을 잃지 않고 깔끔하게 통합할 수 있었습니다. 특수한 상황이지만, 비슷한 고민을 하시는 분들에게 도움이 되기를 바랍니다.
'GraphQL-Apollo_Client' 카테고리의 다른 글
| [Apollo Client] 실제 API를 사용하는 것처럼 데이터 Mocking 하기 (0) | 2025.11.09 |
|---|---|
| [Apollo Client] Fragment를 어떻게 사용해야 할까 -InMemoryCache와 Fragment 관계 관점에서의 캐시 히트율 (0) | 2025.10.05 |
