Articlestack.convex.dev·2026년 7월 7일·0

How hard is it to migrate AWAY from Convex?

Quick Summary

저자는 Convex에서 벗어나는 난이도를 직접 검증하기 위해 간단한 Tanstack Start 예제를 기준으로 함수, 클라이언트 호출, 반응형 쿼리, 데이터베이스 이전을 차례로 Tanstack Start와 Drizzle/Postgres로 옮겨 보며 실제 작업량과 복잡도 변화를 살펴본다.

How hard is it to migrate AWAY from Convex? 관련 대표 이미지

🖼️ 인포그래픽

How hard is it to migrate AWAY from Convex? 내용을 설명하는 본문 이미지

🖼️ 4컷 인포그래픽

How hard is it to migrate AWAY from Convex? 내용을 설명하는 본문 이미지

💡 한 줄 요약

저자는 Convex에서 벗어나는 난이도를 직접 검증하기 위해 간단한 Tanstack Start 예제를 기준으로 함수, 클라이언트 호출, 반응형 쿼리, 데이터베이스 이전을 차례로 Tanstack Start와 Drizzle/Postgres로 옮겨 보며 실제 작업량과 복잡도 변화를 살펴본다.

📌 핵심 요약

  • 글의 출발점은 Convex의 클라우드 락인 우려다. 저자는 Convex가 오픈소스라 전체 백엔드와 대시보드를 자체 호스팅할 수 있고, 이것이 가장 빠르고 쉬운 탈출 경로일 수 있다고 설명한다.
  • 하지만 저자는 단순히 Convex Cloud만 벗어나는 것이 아니라, 데이터베이스와 기술 스택 자체를 다른 것으로 바꾸는 경우를 가정한다. 이를 위해 Convex의 간단한 숫자 추가 예제를 Tanstack Start 기반 서버 함수로 옮겨 본다.
  • Convex의 query, mutation, action은 Tanstack Start의 createServerFn과 Zod 검증으로 비교적 비슷하게 재구성된다. 서버 함수 사이에서 직접 호출하고 await하는 흐름도 Convex action이 query와 mutation을 호출하는 구조와 유사하게 구현된다.
  • 클라이언트 쪽에서는 Convex의 useQuery와 useMutation을 React Query 방식으로 바꿔야 한다. 기능은 구현되지만 Convex의 live query처럼 자동 반응형 갱신이 되지 않기 때문에 mutation 성공 후 queryClient.invalidateQueries를 수동으로 호출해야 한다.
  • 마지막으로 메모리 배열 대신 로컬 Postgres와 DrizzleORM을 붙여 데이터를 영속화한다. 이 과정에서 Convex가 자동으로 제공하던 _id와 createdAt 같은 필드를 직접 정의하고, 마이그레이션과 쿼리 작성도 직접 관리해야 하므로 코드가 점점 Convex와 달라지고 작업량이 늘어날 수 있음을 확인한다.

🧩 주요 포인트

  1. 글의 출발점은 Convex의 클라우드 락인 우려다. 저자는 Convex가 오픈소스라 전체 백엔드와 대시보드를 자체 호스팅할 수 있고, 이것이 가장 빠르고 쉬운 탈출 경로일 수 있다고 설명한다.
  2. 하지만 저자는 단순히 Convex Cloud만 벗어나는 것이 아니라, 데이터베이스와 기술 스택 자체를 다른 것으로 바꾸는 경우를 가정한다. 이를 위해 Convex의 간단한 숫자 추가 예제를 Tanstack Start 기반 서버 함수로 옮겨 본다.
  3. Convex의 query, mutation, action은 Tanstack Start의 createServerFn과 Zod 검증으로 비교적 비슷하게 재구성된다. 서버 함수 사이에서 직접 호출하고 await하는 흐름도 Convex action이 query와 mutation을 호출하는 구조와 유사하게 구현된다.
  4. 클라이언트 쪽에서는 Convex의 useQuery와 useMutation을 React Query 방식으로 바꿔야 한다. 기능은 구현되지만 Convex의 live query처럼 자동 반응형 갱신이 되지 않기 때문에 mutation 성공 후 queryClient.invalidateQueries를 수동으로 호출해야 한다.
  5. 마지막으로 메모리 배열 대신 로컬 Postgres와 DrizzleORM을 붙여 데이터를 영속화한다. 이 과정에서 Convex가 자동으로 제공하던 _id와 createdAt 같은 필드를 직접 정의하고, 마이그레이션과 쿼리 작성도 직접 관리해야 하므로 코드가 점점 Convex와 달라지고 작업량이 늘어날 수 있음을 확인한다.

🧠 상세 정리

1. 락인 우려에서 시작된 실험

글은 Convex에서 벗어나는 것이 실제로 얼마나 어려운지 확인하려는 문제의식에서 시작한다. 저자는 Convex에 대한 최근 영상 댓글을 계기로, 개발자들이 클라우드 서비스 락인을 걱정한다는 점에 공감했다고 말한다. 그래서 단순한 이론적 비교가 아니라, 직접 이틀을 들여 Convex에서 다른 기술 조합으로 옮겨 보는 실험을 진행했다. 글의 목표는 Convex 자체를 비판하기보다, 이동 가능성이 실제 코드 수준에서 어느 정도인지 보여주는 데 있다.

2. 가장 쉬운 탈출구로서의 자체 호스팅

저자는 먼저 Convex가 오픈소스이며 백엔드와 대시보드 전체를 자체 호스팅할 수 있다는 점을 언급한다. GitHub에는 관련 문서도 제공되어 있어, Convex Cloud에 대한 락인만 피하려는 목적이라면 자체 호스팅이 가장 빠르고 쉬운 방법일 수 있다고 본다. 다만 그는 이것이 많은 사람들에게는 문제의 핵심을 비껴갈 수 있다고 지적한다. 락인을 걱정하는 사용자는 Convex Cloud만이 아니라 Convex의 데이터베이스와 기술 체계 자체에서 벗어나고 싶어 할 가능성이 크기 때문이다.

3. Convex 예제의 구조 파악

실험의 출발점은 npm create convex@latest로 만든 Tanstack Start 템플릿이다. 이 예제는 버튼을 누르면 숫자가 배열에 추가되고 화면에 표시되는 단순한 앱으로, 스키마와 쿼리, 뮤테이션을 보여주기에 적합하다. Convex 스키마는 numbers라는 테이블 하나와 value라는 숫자 필드 하나만 가진다. 함수도 listNumbers, addNumber, myAction으로 구성되어 있어, Convex의 핵심 함수 모델을 작게 재현하는 데 충분한 사례가 된다.

4. Convex 함수 모델을 Tanstack Start 서버 함수로 옮기기

Convex에는 query, mutation, action 세 종류의 함수가 있으며, query와 mutation은 데이터베이스에 직접 접근하고 트랜잭션 성격을 가진다. 저자는 이 구조를 Tanstack Start의 createServerFn으로 옮기고, Convex의 검증 방식 대신 Zod를 사용해 입력을 검증한다. 처음에는 실제 데이터베이스를 붙이지 않고 numbers라는 메모리 배열을 임시 저장소로 둔다. listNumbers는 배열에서 숫자를 읽고, addNumber는 배열에 값을 추가하며, myAction은 listNumbers와 addNumber를 직접 호출하는 방식으로 Convex action과 유사한 흐름을 만든다.

5. 클라이언트 호출 방식의 변화

Convex에서는 useQuery에 api.myFunctions.listNumbers와 인자를 넘기고, useMutation으로 addNumber를 가져와 호출하는 방식이 간단하게 동작한다. Tanstack Start로 옮긴 뒤에는 React Query를 사용해 queryKey와 queryFn을 직접 구성해야 한다. mutation 역시 useMutation에 mutationFn을 넘기고, 값은 mutate 호출을 통해 전달한다. 저자는 이 변화가 여전히 Convex 코드와 꽤 비슷해 보인다고 평가하지만, 추상화 수준은 Convex보다 조금 낮아지고 작성해야 할 코드도 늘어난다.

6. Convex의 반응형 쿼리와 수동 무효화의 차이

Convex의 중요한 장점으로 저자는 useQuery가 live query처럼 동작한다는 점을 든다. Convex에서는 addNumber mutation을 실행하면 관련 쿼리가 자동으로 갱신되어 화면도 자연스럽게 바뀐다. 반면 Tanstack Start와 React Query 조합에서는 같은 반응형 갱신이 자동으로 제공되지 않는다. 그래서 mutation 성공 시 queryClient.invalidateQueries를 호출해 listNumbers 쿼리를 수동으로 무효화하고 다시 실행되도록 만들어야 한다. 기능적으로는 같은 결과를 얻지만, Convex가 감춰 주던 갱신 로직을 개발자가 직접 명시해야 한다.

7. 메모리 저장소에서 Postgres와 DrizzleORM으로 이동

메모리 배열을 쓰는 초기 이전은 서버 프로세스를 재시작하면 숫자가 사라지는 문제가 있다. 이를 해결하기 위해 저자는 다른 클라우드 데이터베이스로 이동하는 대신, 로컬에서 실행하는 일반적인 Postgres 데이터베이스를 선택한다. raw SQL을 직접 쓰지는 않고 Convex와 비슷한 API 감각을 얻기 위해 DrizzleORM을 사용한다. Drizzle 스키마에서는 numbers 테이블에 _id, createdAt, value를 직접 정의해야 하며, Convex가 자동으로 제공하던 식별자와 생성 시간 필드도 개발자가 명시적으로 관리해야 한다.

8. 데이터베이스 이전 후 드러나는 실제 작업량

Drizzle 설정 후에는 npx drizzle-kit push로 마이그레이션을 반영하고, 서버 함수도 실제 데이터베이스를 사용하도록 수정한다. listNumbers는 numbersTable에서 createdAt 기준 내림차순으로 데이터를 가져와 제한된 개수만 반환하고, addNumber는 전달받은 값을 데이터베이스에 삽입한다. 숫자를 문자열로 변환하는 부분에 대해서는 Postgres numeric과 JavaScript number의 정밀도 문제와 관련된 것으로 추정된다고 저자는 설명한다. 결과적으로 서버를 재시작하고 페이지를 새로고침해도 데이터가 유지되지만, 이 시점부터 코드는 점점 Convex의 간결한 형태에서 멀어지고 실제 마이그레이션이 큰 코드베이스에서는 시간이 걸릴 수 있음을 보여준다.

🧾 핵심 주장 / 시사점

  • Convex에서 벗어나는 가장 쉬운 방법은 자체 호스팅이지만, 이것은 Convex 생태계 자체를 떠나는 것과는 다른 문제다.
  • 함수와 기본 데이터 흐름은 Tanstack Start, React Query, DrizzleORM, Postgres 조합으로 재현할 수 있지만 Convex가 자동 제공하던 반응형 갱신과 기본 필드, 데이터베이스 관리 편의성은 직접 구현해야 한다.
  • 작은 예제에서는 이전이 비교적 단순해 보이지만, Convex 코드가 많아질수록 수동 무효화, 스키마 관리, 마이그레이션, ORM 래핑 같은 작업이 누적되어 실제 비용이 커질 수 있다.

✅ 액션 아이템

  • 컨텍스트를 Convex Cloud 이탈과 데이터베이스·기술 스택 전환으로 분리해, 단계별 이전 난이도와 구현 폭을 정량적으로 비교한다.
  • Tanstack Start에서 query·mutation·action을 createServerFn과 Zod로 재구성한 뒤 서버 함수 간 await 호출 흐름을 기존 구조와 동일성 중심으로 점검한다.
  • 클라이언트는 React Query로 전환해 mutation 직후 queryClient.invalidateQueries 호출 시점을 고정하고, live query 부재 구간의 갱신 누락 가능성을 점검한다.

❓ 열린 질문

  • Convex Cloud 이탈 경로와 DB·스택 전환 경로는 어떤 기준에서 비용 대비 효율이 갈리는가?
  • React Query 기반의 수동 갱신 방식은 어떤 화면·시나리오에서 실시간 갱신 체감 저하로 나타날 가능성이 있는가?
  • Postgres/Drizzle로 옮길 때 _id·createdAt 직접 정의와 마이그레이션/쿼리 관리 부담은 어느 지점에서 기존 대비 임계치가 되는가?

관련 문서

공통 태그와 주제 흐름을 기준으로 같이 보면 좋은 문서를 이어서 제안합니다.