Server Actions와 TanStack Query

Jessie·2026년 7월 12일
post-thumbnail

“Next.js에서 Server Actions를 사용할 수 있는데 왜 TanStack Query가 필요한가요?”

현재 진행중인 회사 프로젝트의 초기 설계 단계에서 TanStack Query의 도입을 제안했더니 사수님께서 물어보신 질문이었다. TanStack Query를 사용해 본 경험은 있었지만, 프로젝트에 왜 필요한지 명확하게 설명하려니 선뜻 답이 나오지 않았다.

“사용해 보니 편해서”, “다양한 기능을 제공해서”라는 대답을 할 수는 없어 머뭇거리자, 사수님께서는 Server Actions와 TanStack Query가 담당하는 역할을 비교해 본 뒤 다음 회의에서 다시 이야기해 보자고 하셨다.

Server Actions를 사용하면 서버에서 데이터를 조회하거나 변경할 수 있고, 변경 이후 Next.js의 캐시를 재검증해 화면에 최신 데이터를 반영할 수도 있다. 실제로 단순한 폼 제출이나 CRUD 화면은 Server Components와 Server Actions만으로 충분히 구현할 수 있다.

그렇다면 우리 프로젝트에서도 TanStack Query 없이 Server Actions만 사용하면 되는 것은 아닐까?

이 질문에 답하기 위해 먼저 Server Actions와 TanStack Query의 역할을 비교해 보았다.

Server Actions와 TanStack Query의 역할은 다르다

Server Actions와 TanStack Query 둘 다 "mutation 후 최신 데이터 반영"이라는 작업을 실행할 수 있다.

사용자 액션
→ 서버 데이터 변경
→ 기존 데이터 무효화
→ 최신 UI 표시

하지만 이는 일부 기능이 겹친 것일 뿐, 실제로는 서로 다른 캐시를 다룬다. 이 둘은 담당 계층과 동작 모델이 근본적으로 다르다.

Server Action은 클라이언트에서 서버 함수를 호출해 서버 측 명령을 수행한다. 반면 TanStack Query는 서버에서 수행한 작업을 클라이언트에서 어떻게 관리하고 화면에 반영할지를 담당한다.

즉, 이 둘은 대체 관계가 아니다. 프로젝트의 성격에 따라 둘 중 하나를 선택할 수도 있고, 이 둘을 함께 사용할 수도 있다.

TanStack Query 도입을 검토한 이유

그렇다면 다음에 해야 할 것은 우리 프로젝트에 TanStack Query를 도입하는데 드는 비용과 그것으로 얻을 수 있는 이득을 비교하는 것이다.

보통 단순한 CRUD 페이지는 TanStack Query의 도입 비용이 이득보다 클 가능성이 높다.

  • 목록 조회가 페이지 진입이나 검색 조건 변경 시에만 발생
  • 등록·수정·삭제 후 화면을 다시 렌더링하면 충분
  • 동일 데이터를 여러 컴포넌트가 복잡하게 공유하지 않음

이런 경우는 Server Action 실행 시 변경된 UI와 데이터를 한 번의 서버 왕복으로 반환할 수 있고, revalidatePath, revalidateTag, updateTag 등을 통해 간단하게 관련 데이터를 갱신할 수 있다. 클라이언트 캐시와 서버 캐시를 분리해 관리해야하고, 초기 설정과 query 함수, query key를 정의하는 비용을 굳이 지출할 필요가 없다.

반면 우리 프로젝트에서는 이런 요소들을 고려해야한다.

1. 사용자별 데이터가 중심인 어드민 서비스

우리 프로젝트는 사용자마다 접근 가능한 상점과 데이터가 다른 어드민 서비스이다. 이러한 데이터를 사용자 세션 동안 반복적으로 조회하고 갱신해야 한다면, 브라우저의 클라이언트 캐시에서 관리하는 방식이 자연스러울 수 있다.

서버 캐시는 여러 사용자의 요청이 동일한 캐시 공간을 조회할 수 있기 때문에, 사용자마다 반환 결과가 다른 데이터를 캐싱할 때는 데이터의 종류뿐 아니라 사용자, 상점, 권한 등 결과에 영향을 주는 범위까지 캐시 키나 캐시 경계에 반영해야 한다. 이 중 하나라도 누락하면 다른 사용자의 요청에 잘못된 캐시가 사용될 수 있으므로 키 설계에 더 많은 주의가 필요하다.

반면 TanStack Query의 캐시는 브라우저의 QueryClient 단위로 먼저 분리된다. 현재 사용자가 이미 캐시의 상위 경계로 격리되어 있기 때문에, query key는 보통 shopId, 리소스, 필터처럼 현재 클라이언트 안에서 조회 결과를 구분하는 값에 집중할 수 있다. 따라서 사용자별로 반환 결과가 달라지는 서비스에서는 서버 공용 캐시보다 캐시 키 설계 부담이 상대적으로 적다.

2. 데이터 갱신 범위와 서버 작업 비용

App Router의 갱신은 전체 페이지를 새로고침하는 방식과 다르다. 서버가 갱신된 Server Component 결과를 RSC payload로 전달하면 클라이언트는 이를 기존 화면에 병합한다. 다만 갱신 과정에서 서버 렌더링, 필요한 데이터의 재조회, RSC payload 생성과 전송 등의 작업이 발생할 수 있다.

예를 들어 목록의 한 행만 변경한 뒤 revalidatePath('/products')로 경로를 무효화하면, 다음 갱신 과정에서 해당 경로의 Server Component 결과가 다시 생성될 수 있다. 태그를 사용해 캐시 무효화 범위를 세밀하게 설계할 수 있지만, 클라이언트의 특정 데이터만 직접 변경하는 방식과는 동작 모델이 다르다.

반면 TanStack Query는 setQueryData로 목록 캐시를 직접 수정하거나 invalidateQueries로 특정 query만 다시 조회할 수 있다. 이 경우 별도의 Server Component 갱신 없이 클라이언트의 query cache를 기준으로 화면을 업데이트할 수 있다.

3. loading, fetching, pending, error 상태 관리

Next.js도 useActionState, useFormStatus, useTransition 등을 통해 Server Action의 실행 상태를 관리할 수 있다. 하지만 상태의 범위가 제한적이고, 여러 액션의 상태를 통합된 형태로 관리해 주지는 않는다.

예를 들어 회원 정보 페이지에 정보 저장, 이메일 중복 확인, 인증번호 전송, 인증번호 확인 버튼이 각각 존재한다면 각 액션마다 별도의 useActionStateuseTransition을 선언해야 한다.

const [isCheckingEmail, startCheckEmail] = useTransition();
const [isSendingCode, startSendCode] = useTransition();
const [isVerifyingCode, startVerifyCode] = useTransition();

또한 성공 여부, 에러, 반환 데이터 등의 상태도 액션별로 직접 설계하고 관리해야 한다.

useFormStatus는 가장 가까운 부모 form의 제출 상태를 제공하기 때문에, 하나의 form 안에 여러 액션이 있는 경우 어떤 버튼의 요청이 진행 중인지 추가로 구분해야 한다.

반면 TanStack Query의 useMutation은 각 요청마다 다음과 같이 다양한 상태를 동일한 인터페이스로 제공한다.

const checkEmailMutation = useMutation({ mutationFn: checkEmail });

checkEmailMutation.isPending;
checkEmailMutation.isError;
checkEmailMutation.error;
checkEmailMutation.isSuccess;
checkEmailMutation.data;

우리 프로젝트처럼 한 화면에서 여러 조회와 mutation이 동시에 발생하고, 각 버튼의 로딩·비활성화·에러 상태를 개별적으로 보여줘야 하는 경우에는 이러한 상태 관리 방식의 차이가 반복적인 구현 비용으로 이어질 수 있다.

4. Optimistic update

Next.js에서 useOptimistic()을 사용하면 optimistic UI를 구현할 수 있다. 하지만 이건 해당 데이터를 소유한 컴포넌트 트리의 optimistic 표현이다. 즉, 보여지는 화면만 수정할 뿐 애플리케이션 전체의 서버 데이터 캐시를 자동으로 수정하지는 않는다. 따라서 같은 데이터를 사용하는 다른 화면이나 컴포넌트에는 변경 내용이 자동으로 반영되지 않는다.

반면 TanStack Query에서는 setQueryData 등을 통해 query cache를 직접 변경할 수 있다. 같은 QueryClient에서 동일한 query key를 구독하는 컴포넌트들은 변경된 캐시를 함께 바라보게 된다.

특정 데이터가 한 화면에서만 사용된다면 큰 문제가 없지만, 여러 화면이나 컴포넌트에서 공유되는 데이터를 optimistic update해야 한다면 TanStack Query의 캐시 기반 방식이 데이터 일관성을 관리하기에 더 적합할 수 있다.

마무리하며

정리해보면, Server Actions는 클라이언트에서 서버의 작업을 호출하고 데이터 변경 이후 서버 캐시를 갱신하는 역할을 담당한다. TanStack Query는 브라우저에서 서버 상태를 캐싱하고, 조회·갱신·무효화·에러와 로딩 상태 등의 생명주기를 관리한다.

정리한 내용을 다음 회의에서 공유했고, 팀에서도 필요성에 공감해 프로젝트에 TanStack Query를 도입하게 되었다. 모든 화면에 일괄적으로 적용하기보다는, 클라이언트에서 서버 상태를 반복적으로 조회하거나 여러 mutation의 상태를 관리해야 하는 화면을 중심으로 우선 적용하며 진행했다.

이 내용은 올해 초 프로젝트를 설계하면서 찾아보고 노션에 간단히 정리해 두었던 기록에서 시작했다. 최근 Next.js 캐싱에 관한 블로그 글을 읽다가 당시의 고민이 떠올라 다시 정리하게 되었다. 글을 작성하면서 관련 문서를 다시 확인하고 개념을 구체화하는 과정도 도움이 되었지만, 프로젝트 초기에 팀원들과 함께 공부하며 설계 방향을 고민했던 시간이 떠올라 더욱 즐거웠다.

사수님은 이후에도 새로운 컨벤션이나 라이브러리 도입을 제안할 때마다 이런저런 질문을 던져주신다. 그 질문을 통해 익숙하게 사용해 온 기술도 프로젝트에 어떤 가치를 주는지 다시 생각하게 되었다. 덕분에 지금은 새로운 기술을 제안하기 전에 프로젝트의 요구사항과 도입 비용을 먼저 살펴보고, 선택의 근거를 설명할 수 있도록 공부하려고 노력하고 있다. 😚


참고 자료

profile
주니어 프론트엔드 개발자입니다 😎

0개의 댓글