React Query 버전업 과정에서 발견한 API 관리 구조의 결함

강한 결합이 낳은 생산성의 함정: useRemote의 도입과 디커플링 전략
초기 선택: 빠른 생산성을 위한 강한 결합
개발 초기 단계에서 저희 팀은 빠른 생산성을 최우선 목표로 설정하고, 특정 라이브러리와 강하게 결합된 API 관리 구조를 채택했습니다. 이 구조의 중심에는 OpenAPI Generator를 활용한 코드 자동 생성이 있었습니다.
백엔드에서 Swagger를 통해 OpenAPI Specification(이하 OAS) 명세 파일을 노출하면, 프론트엔드에서는 oag-cli를 사용해 이 명세로부터 TypeScript 타입과 Axios 기반의 호출부인 Fetcher를 자동으로 생성하는 방식이었습니다. OpenAPI Generator는 OAS 문서를 해석해 프레임워크 요구사항에 맞춰 데이터를 가공한 뒤, Mustache 템플릿을 거쳐 최종 코드를 만들어냅니다. 저희는 이 커스텀 템플릿 인터페이스를 활용해 필요한 코드를 손쉽게 출력할 수 있었습니다.
useRemote: 과도한 추상화의 시작
서비스 규모가 커지며 관리해야 할 API가 5,000개를 넘어서자, 쿼리 키 생성이나 페치 함수 호출 같은 반복 작업을 줄여야 할 필요성이 생겼습니다. 이를 해결하기 위해 React Query 훅까지 한 번에 생성해 주는 useRemote를 도입했습니다.

이 훅은 여러 가지 관심사를 하나로 묶어 추상화했습니다. 파라미터를 조합해 고유한 쿼리 키를 만드는 로직, Axios 인스턴스를 주입해 Fetcher를 실행하는 로직, 그리고 실제 useQuery를 호출해 옵션을 전달하는 과정까지 모두 포함했습니다. 덕분에 개발자는 내부의 복잡한 동작 방식을 몰라도 API를 쉽게 호출할 수 있었고, 초기 개발 속도는 비약적으로 상승했습니다.
하지만 지나친 추상화는 곧 제어권 상실이라는 비용으로 돌아왔습니다. 쿼리 키의 생성 원리를 알 수 없으니 캐시 무효화나 키 간의 관계 설정이 어려워졌고, Axios 주입 방식이 감춰져 있어 인터셉터 설정 같은 세부 제어도 까다로워졌습니다. 무엇보다 라이브러리 훅이 코드 생성 단계부터 포함되면서, 향후 기술 변화에 대응할 유연성이 심각하게 부족해졌습니다.
파괴적 변경의 파급: React Query v5 업데이트
이러한 강한 결합 구조의 부작용은 React Query v5로 버전을 올리는 과정에서 명확히 드러 났습니다. v5에서는 서스펜스 지원이 안정화되면서 기존 useQuery 내부의 서스펜스 옵션이 제거되고, 대신 useSuspenseQuery라는 전용 훅이 도입되는 큰 변화가 있었습니다.
버전업의 어려움과 파급 효과
useRemote가 코드 생성 시점부터 React Query에 직접 의존하고 있었기에, 이러한 라이브러리의 파괴적 변경 사항은 전체 시스템으로 그대로 전파되었습니다.
이 변화를 수용하려면 생성 템플릿을 수정해 5,000개가 넘는 API 코드를 모두 다시 생성해야 했고, 이를 사용하는 모든 사용처에서 대규모 코드 마이그레이션이 필요했습니다. 결합도가 높았기 때문에, 단일 라이브러리의 버전업이 전체 시스템의 대대적인 수정을 요구하는 결과를 낳은 것입니다.

디커플링 전략: 의존성 해체 및 격리
저희는 이 문제를 해결하고 시스템을 더 안전하고 확장 가능하게 만들기 위해 디커플링 전략을 세웠습니다. 핵심 원칙은 관심사 분리를 통해 각 요소의 예측 가능성을 높이고, 성급한 추상화를 피해 변경 유연성을 확보하는 것이었습니다.
개선 방향 1: 코드 생성 단계의 의존성 제거
먼저 코드 자동 생성 도구가 더 이상 React Query에 의존하지 않도록 역할을 한정했습니다. 이제 생성 도구는 순수하게 Fetcher와 모델만 만들어냅니다. 대신 쿼리 훅을 호출하는 시점은 실제 코드를 사용하는 쪽에서 담당하게 했습니다. 개발자가 직접 Fetcher를 활용해 쿼리를 실행함으로써 온전한 제어권을 되찾게 된 것입니다.
이로써 코드 생성 단계는 라이브러리 버전에 영향받지 않게 되었고, 위 그림처럼 각 마이크로 앱은 상황에 맞는 React Query 버전을 독립적으로 선택할 수 있는 환경이 마련되었습니다.


개선 방향 2: 어댑터 패턴을 통한 호환성 확보
그렇다고 기존의 편의성을 완전히 포기할 수는 없었습니다. 저희는 어댑터 레이어를 도입해 라이브러리와의 호환성을 유지했습니다.
어댑터는 API 호출 로직을 전달받아 특정 라이브러리에 필요한 옵션값만 반환하는 역할을 수행합니다. 이를 통해 특정한 환경이나 라이브러리에 대한 의존을 격리하면서도, 사용처에서 기술 선택의 자율성을 확보했습니다.


개선된 코드 (TO-BE)

마치며,
저희는 빠른 구현이 최우선 순위였던 초기 구조에서 탈피하여, 운영 효율과 유연한 확장을 중시하는 안정적인 아키텍처로 성공적으로 전환했습니다. 핵심은 자동으로 생성되는 API 호출 로직의 라이브러리 의존성을 제거하고, Adapter 패턴을 통해 결합도를 낮춰 호환성을 확보하는 것입니다. 이를 통해 개발자가 상황에 맞춰 React Query Hook을 직접 제어할 수 있는 설계적 주도권을 확보했습니다.
처음부터 완벽한 구조를 만드는 것은 매우 어렵거나, 어쩌면 불가능에 가깝습니다. 중요한 것은 현재 우리가 처한 상황을 정확히 인지하고, 지속적으로 개선해 나갈 수 있는 발전 가능한 선택을 내리는 것입니다.
플렉스팀 Product Engineer (frontend) 합류하러가기
