
Error Handling(재작성 예정 입니다...)
읽어주신다면 감사하겠지만, 이 포스팅은 React만의 ErrorBoundary / Suspense / Tanstack Query 에 관한 아티클로 재작성 할 예정 입니다 …
(svelte:boundary, tanstack-query가 제대로 호환되지 않아, 결과적으로 완벽한 에러핸들링을 못했기 때문입니다.. (ㅠ.ㅠ))
이번에는 Error Handling을 했던 경험에 대해 공유해보려 합니다.
Svelte 5에 React의 ErrorBoundary와 유사한 <svelte:boundary>
가 도입되면서, 이를 활용해 선언적이고 중앙 집중화된 에러 처리 구조를 만들 수 있을 것이라 기대했어요.
이 글은 Tanstack Query와 <svelte:boundary>
를 함께 사용하는 과정에서 마주한 기술적 한계, 그리고 최종적으로 도달한 가장 안정적인 에러 핸들링 패턴에 대한 내용이에요.
. Svelte 5와 Tanstack Query (v5)를 사용하는 환경을 기반으로 작성했습니다.
복잡한 로딩 / 에러 처리
기존에는 개발할 때 로딩 / 에러처리를 명령형 방식으로 처리했고, 여러 곳에 흩어진 try/catch로 인해 컴포넌트 하나가 너무 많은 역할을 수행하고 있었어요.
<script>
let data = $state(null);
let error = $state(null);
let loading = $state(true);
$effect(() => {
try {
data = await fetchData();
} catch {
error = e;
} finally {
loading = false;
}
});
</script>
{#if loading}
<Loading />
{:else if error}
<ErrorView {error} />
{:else}
<DataComponent {data} />
{/if}
- Tanstack Query / boundary 등을 적용시키지 않은 코드입니다.
API 호출부 개선
데이터 페칭 부분의 가독성이 떨어지고, 반복되는 코드가 많았기 때문에 Fetcher
라는 공통 모듈로 추상화했어요. (인증 헤더, 에러 포맷, HTTP 메서드별 처리 포함)
async function baseFetcher<T>(
method: string,
endpoint: string,
payload?: unknown
): Promise<T> {
// ... (fetch 로직 생략) ...
}
export const Fetcher = {
get: <T>(endpoint: string): Promise<T> => baseFetcher<T>('GET', endpoint),
post: <T>(endpoint: string, payload: unknown): Promise<T> =>
baseFetcher<T>('POST', endpoint, payload),
put: <T>(endpoint: string, payload: unknown): Promise<T> =>
baseFetcher<T>('PUT', endpoint, payload),
delete: <T>(endpoint: string, payload?: unknown): Promise<T> =>
baseFetcher<T>('DELETE', endpoint, payload),
};
이제 API 호출은 일관된 방식으로 처리되고, Tanstack Query의 훅으로 감싸서 재사용성을 높였어요.
// hooks/useDataQuery.ts
export const useDataQuery = () => {
return createQuery({
queryKey: ['data'],
queryFn: async () => {
// queryFn은 모듈화한 Fetcher를 보여주기 위해 이곳에 직접 작성했어요.
const response = await Fetcher.get<ApiResponse>(ENDPOINT.DATA);
return response.result;
},
});
};
이를 통해 컴포넌트 내 데이터 페칭 로직은 이전보다 간결해졌어요.
<script lang="ts">
import { useDataQuery } from './hooks/useDataQuery';
const dataQuery = useDataQuery();
</script>
{#if $dataQuery.isPending}
<Loading />
{:else if $dataQuery.error}
<ErrorFallback error={$dataQuery.error} />
{:else if $dataQuery.isSuccess}
<DataComponent data={$dataQuery.data} />
{/if}
그러나 여전히 {#if}
분기문으로 Loading / Error 처리를 반복해야 했어요. 이 구조를 개선하고 완벽히 선언적인 코드를 작성하기 위해, Svelte 5의 기능인 <svelte:boundary>
가 이상적인 해결책처럼 보였어요.
컴포넌트는 오직 성공 상태만 렌더링하고, 로딩과 에러는 상위 바운더리에 위임하는 구조를 목표로 삼고 본격적인 리팩터링을 시작했어요.
에러 / 로딩 처리 구조 개선
0. QueryClient 설정
queryClient에서는 options들을 세팅할 수 있어요. throwOnError 옵션을 true로 설정해주면 발생한 에러를 상위로 전파하게 되는데,
queries(createQuery) 에서 발생한 에러는 throw 하여 상위 boundary가 처리하게 하고, mutation(createMutation) 에서 발생한 에러는 전파되지 않도록 한 뒤, toast 메시지만 띄우는 방식으로 진행했어요.
import { QueryClient, QueryCache } from '@tanstack/svelte-query';
import { goto } from '$app/navigation';
import { HttpError } from '$lib/api/errors';
import { removeSessionToken } from '$lib/utils/sessions';
import { popupState } from '$lib/stores/popupStore.svelte';
import * as toast from '$lib/stores/toast';
// 쿼리 실패 시 전역으로 처리할 에러(인증/권한)만 담당
const handleGlobalQueryError = async (error: unknown) => {
if (error instanceof HttpError) {
switch (error.status) {
case 401:
await popupState?.openFn(
'안내',
'인증 정보가 만료되었습니다.',
'로그인 화면으로 이동합니다.'
);
removeSessionToken();
goto('/login');
break;
case 403:
await popupState?.openFn(
'안내',
'접근 권한이 없습니다.',
'초기 화면으로 이동합니다.'
);
goto('/');
break;
}
}
};
// 뮤테이션 실패 시 Toast UI를 보여주는 핸들러
const handleMutationErrorWithToast = (error: unknown) => {
let errorMessage = '예기치 못한 문제가 발생했습니다.';
if (error instanceof HttpError) {
errorMessage = `[${error.status}] ${error.message}`;
} else if (error instanceof Error) {
errorMessage = error.message;
}
toast.error(errorMessage);
};
export const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: handleGlobalQueryError,
}),
defaultOptions: {
queries: {
throwOnError: true,
// ... 기타 옵션
},
mutations: {
onError: handleMutationErrorWithToast,
throwOnError: false,
},
},
});
1. Svelte Boundary: 기대와 현실의 간극
Svelte 5의 <svelte:boundary>
는 로딩과 에러 상태를 컴포넌트 외부로 위임하여 코드를 선언적으로 만들 수 있는 기능이에요. 해당 기능을 사용해 DataComponent를 감싸서, 로딩과 에러 처리를 완전히 위임했어요.
DataComponent
가 스스로 데이터를 가져오고, 로딩 중에는 상위 Boundary가pending
상태를, 에러 발생 시에는failed
상태를 처리.
// 초기 구상
<GlobalBoundary> // 전역 에러를 처리하는 Boundary (FetchBoundary가 처리하지 못했거나, 그 외의 모든 에러를 최종적으로 처리)
<FetchBoundary> // 지역 에러를 처리하는 Boundary
<DataComponent>
</FetchBoundary>
</GlobalBoundary>
하지만 이 구조는 Tanstack Query와 함께 사용할 때 예상처럼 동작하지 않았어요. throwOnError: true
옵션을 설정했음에도 불구하고, <svelte:boundary>
는 queryFn
에서 발생한 에러나 로딩 상태를 전혀 감지하지 못했어요.
이유를 파악하기 위해 조사한 결과, React와 Svelte의 렌더링 및 에러 처리 아키텍처가 다르다는 것을 알게 되었어요.
왜 <svelte:boundary>
는 Tanstack Query의 상태를 감지하지 못할까?
Svelte 5의 템플릿 레벨 await는 <svelte:boundary>
와 잘 동작해요.
<script>
function delayed(value, milliseconds = 1000) {
return new Promise((resolve) => {
setTimeout(() => resolve(value), milliseconds);
});
}
</script>
<svelte:boundary>
<p>{await delayed('안녕하세요!')}</p>
{#snippet pending()}
<p>로딩 중 입니다...</p>
{/snippet}
{#snippet failed(error, reset)}
<button onclick={reset}>다시 시도 하기</button>
{/snippet}
</svelte:boundary>
위 코드가 동작하는 이유는 Svelte 컴파일러가 템플릿의 {await}
구문을 직접 바라보고, 해당 Promise의 상태 변화에 따라 pending 또는 failed 스니펫을 렌더링하는 코드를 생성하기 때문이에요.
하지만 Tanstack Query의 createQuery는 Promise가 아닌 Svelte Store를 반환합니다. 비동기 로직은 라이브러리 내부에서 캡슐화되어 실행되므로, Svelte 컴파일러는 템플릿만 보고는 비동기 작업의 존재를 알 수 없어요.
결과적으로 Tanstack Query가 던진 에러는 Svelte의 렌더링 생명주기 외부에서 발생한 처리되지 않은 Promise 거부(unhandledrejection)일 뿐, <svelte:boundary>
가 감지할 수 있는 ‘렌더링 에러’가 아니게 되요.
React 에러처리 방식 / Svelte 에러처리 방식 차이
React 방식 (Suspense & ErrorBoundary):
React는 Suspense를 통해 비동기 작업(데이터 로딩) 자체를 렌더링 과정의 일부로 취급해요. Tanstack Query는 이 메커니즘을 활용하는데요. 데이터가 없으면 Promise를 throw
하고, React는 이를 감지해 렌더링을 ‘일시 중지’하고 Suspense
의 fallback을 보여줘요. 만약 이 Promise가 실패(reject)하면, React는 이를 렌더링 단계의 에러로 간주하여 ErrorBoundary
가 잡을 수 있도록 위로 전파시켜요.
Svelte 방식 (Store & Boundary):
Svelte에서 createQuery
는 Svelte Store 규약을 다르는 객체를 반환해요. 비동기 작업인 queryFn
은 Tanstack Query 라이브러리가 백그라운드에서 독립적으로 실행해요. Svelte의 렌더링 엔진은 이 과정을 직접 알지 못하며, 단지 Store의 상태 (isPending
, isError
등)가 변경되었음을 통지받고 화면을 업데이트 할 뿐입니다.
결론적으로, Tanstack Query가 던진 에러는 Svelte의 렌더링 생명주기 외부에서 발생한 처리되지 않은 Promise 거부(Uncaought Promise Rejection / unhandledrejection)일 뿐, <svelte:boundary>
가 감지할 수 있는 ‘렌더링 에러’가 아니에요. 로딩 상태 역시 템플릿에 명시적인 {await}
블록이 없으므로 Boundary가 인식하지 못해요.
그럼 Tanstack Query 훅을 직접 await
할 수는 없을까요?
<!-- 동작 하지 않는 코드 -->
<svelte:boundary>
<p>{await useDataQuery()}</p>
{#snippet pending()}
<p>로딩 중 입니다...</p>
{/snippet}
{#snippet failed(error, reset)}
<button onclick={reset}>다시 시도 하기</button>
{/snippet}
</svelte:boundary>
useDataQuery()
훅이 Promise
를 반환하지 않아요. 이 훅은 createQuery
를 통해 Svelte Store 객체를 반환해요. 이 Store는 isPending
, data
, error
같은 상태를 담고 있는 컨테이너 일 뿐, await
가 기대하는 Promise
가 아니에요. 타입 자체가 맞지 않기 때문에 직접 await 할 순 없어요.
마찬가지로, 비동기 호출을 하는 <DataComponent />
를 바운더리 안에 넣어도 동작하지 않는 이유도 동일해요. Svelte 컴파일러는 <DataComponent />
라는 컴포넌트 태그만 볼 뿐, 그 컴포넌트의 <script>
내부에서 Tanstack Query가 Promise를 어떻게 관리하는지는 전혀 알지 못해요. 비동기 로직이 캡슐화되어 템플릿에 노출되지 않기 때문에, 바운더리는 반응할 수 없어요.
($effect.pre
를 사용해 에러를 강제로 렌더링 주기 안에서 다시 던져서 어떻게든 boundary를 사용하는 방법도 있지만, 이는 결과적으로 선언적인 패턴을 깨는 명령형 코드이므로 근본적인 해결책이라 보기 어렵다 판단했어요.)
2. React ErrorBoundary: 선언적 에러 / 로딩 처리의 완성
Svelte에서 겪었던 어려움과 대조적으로, React에서는 Suspense
와 ErrorBoundary
를 통해 Tanstack Query와 함께 선언적인 로딩 / 에러 처리 패턴을 구현할 수 있었어요.
이것이 가능한 이뉴는 React가 Suspense for Data Fetching 아키텍처를 통해 비동기 작업을 렌더링 과정의 일부로 통합하고, Tanstack Query가 이를 위한 전용 훅인 useSuspenseQuery
를 제공하기 때문이에요.
// 1. ErrorBoundary.jsx - 에러를 처리하는 경계 컴포넌트
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
// 에러 발생 시 보여줄 UI
return <ErrorFallback error={this.state.error} />;
}
return this.props.children;
}
}
// 2. DataComponent.jsx - 데이터를 가져와 보여주는 컴포넌트
import { useSuspenseQuery } from '@tanstack/react-query';
function DataComponent() {
// useSuspenseQuery는 로딩/에러 상태를 반환하지 않고,
// 오직 성공했을 때의 데이터만 반환해요.
const { data } = useSuspenseQuery({
queryKey: ['data'],
queryFn: fetchData, // 실패 시 에러를 throw하는 비동기 함수
});
// 이 컴포넌트는 오직 성공 케이스의 UI 렌더링만 책임집니다.
return <SuccessComponent data={data} />;
}
// 3. App.jsx - 컴포넌트 조립
import { Suspense } from 'react';
function App() {
return (
<ErrorBoundary>
<Suspense fallback={<Loading />}>
<DataComponent />
</Suspense>
</ErrorBoundary>
);
}
이 구조가 Svelte에서의 초기 목표였던 ‘선언적 에러핸들링’을 달성하는 방법이에요.
-
관심사 분리:
- DataComponent는 if/else 분기 없이 오직 성공 상태의 UI 렌더링에만 집중합니다.
- 로딩 상태는
<Suspense>
컴포넌트가 fallback prop을 통해 전적으로 책임집니다. - 에러 상태는
<ErrorBoundary>
컴포넌트가 전적으로 책임집니다.
-
선언적 위임:
useSuspenseQuery
는 로딩 중일 때 Promise를 throw하여 React에게 “아직 준비되지 않았음”을 알려요. 이 신호는<Suspense>
에게 전달되요- 에러 발생 시, useSuspenseQuery는 Error를 throw합니다. React는 이를 렌더링 에러로 간주하여
<ErrorBoundary>
에게 처리를 위임해요. - 이처럼 React의 아키텍처는 비동기 상태를 렌더링 시스템 자체에 통합하여, 상위 컴포넌트에서 선언적으로 로딩/에러를 처리하는 패턴을 가능하게 해요.
3. 최종 해결책: Tanstack Query의 상태를 직접 활용하기
위와 같은 이유로, <svelte:boundary>
를 추상화하는 대신 Tanstack Query가 제공하는 상태 플래그(isPending
, isError
등)를 컴포넌트 내에서 직접 활용하는 것이 가장 안정적이고 확실한 방법이라는 결론을 내렸어요.

- 이 방식은 Svelte-Query 공식 문서에서 권장하는, 라이브러리가 제공하는 상태(isPending, isError 등)를 직접 활용하여 조건부 렌더링을 하는 표준 패턴이에요.
// DataComponent.svelte
<script lang="ts">
import { useDataQuery } from '$lib/hooks/useDataQuery';
const query = useDataQuery();
</script>
{#if $query.isPending}
<Loading />
{:else if $query.isError}
<ErrorFallback
error={$query.error}
reset={$query.refetch}
/>
{:else if $query.isSuccess}
<DataCard data={$query.data}>
{/if}
비록 {#if}
분기문이 다시 등장했지만, 로직이 컴포넌트 내부에 명확하게 존재하여 예측 가능성은 있고, throwOnError
옵션 없이 전역 에러 핸들러와 지역 에러 UI 처리를 모두 달성할 수 있었어요.
에러 상태 분류
효과적인 에러 처리는 모든 에러를 동일하게 취급하지 않는 것에서 시작한다고 생각했고, 몇 가지 핵심 목표가 있는걸로 알고 있어요.
사용자 경험 보호: 사용자는 시스템 내부의 복잡한 사정을 알 필요가 없어요. 사용자에게 혼란을 주는 대신 명확한 상황 인지와 다음 행동을 안내해야 해요.
-
이해하기 쉬운 언어: “Request failed with status code 500”와 같은 기술적인 메시지 대신 “죄송합니다, 정보를 가져오는 데 실패했습니다.” 와 같이 사용자에게 맞는 메시지를 보여줘야해요.
-
실행 가능한 안내: “새로 고침을 해보시거나, 잠시 후 다시 시도해주세요” or “입력하신 이메일 주소를 다시 확인해주세요.” 처럼 사용자가 다음에 무엇을 할지 구체적으로 안내해야 해요.
-
점진적 기능 저하: 앱 전체가 멈추는 대신, 에러가 발생한 특정 부분만 비활성화하여 다른 기능은 계속 사용할 수 있도록 해야해요. 사용자의 작업 흐름을 최대한 방해하지 않는 것이죠.
시스템의 안정성 및 신뢰성 확보: 잘못된 에러 처리는 작은 문제 하나가 시스템 전체가 깨지는 현상을 유발할 수 있어요. 시스템을 안정적으로 유지하고 회복하게끔 해야해요.
-
에러 전파 제어: 특정 컴포넌트의 에러가 서비스 전체를 중단 시키지 않도록 svelte:boundary / ErrorBoundary 등을 통해 영향 범위를 명확히 격리해요.
-
상태 일관성 유지: 에러가 발생하더라도 데이터의 상태가 비정상적으로 남지 않도록 보장해요. (ex: 라이선스 등록 프로세스 중 에러 발생 시, ‘등록 중…’ 상태로 무한정 머무는 것을 방지)
-
자동 회복 메커니즘: 네트워크 일시 장애와 같이 재시도를 통해 해결될 수 있는 문제에 대해서는 자동으로 재시도(Retry) 로직을 수행하여 시스템이 스스로 문제를 해결하도록 해요.
개발자 경험 및 유지보수성 향상: 에러는 시스템의 상태를 알려주는 가장 중요한 신호에요. 개발자가 이 신호를 빠르고 정확하게 분석하여 문제를 해결할 수 있도록 해야해요.
-
의미 있는 로깅: 언제, 어디서, 어떤 데이터로, 왜 에러가 발생했는지 충분한 컨텍스트를 담은 로그를 기록해요. 단순한 에러 메시지뿐만 아니라 관련 변수, 사용자 ID, 요청 정보 등이 포함되어야 디버깅이 용이해요.
-
에러 모니터링 및 알림: Sentry, Datadog 같은 도구를 사용하여 에러를 중앙에서 수집, 분류하고, 심각한 문제가 발생했을 때 개발자에게 알림을 보내요. 이는 문제 발생을 인지하고 대응하는 시간을 단축시켜요.
-
일관된 처리 패턴: 예측 가능한 에러(ex: 폼 유효성 검사)는 지역적으로 처리하고, 예측 불가능한 서버 에러나 인증 문제는 전역적으로 처리하는 등 명확하고 일관된 규칙을 수립하여 코드의 예측 가능성과 유지보수성을 높여요.
이러한 핵심 목표를 100% 달성하진 못했지만 고민하고, 백엔드와 소통해서 최대한 목표에 근접하게 에러처리를 하려 노력했어요. 그래서 다음과 같이 에러의 원인과 성격에 따라 사용자에게 다른 경험을 제공하고, 크게 네 가지로 분류했어요.
1. 인증/권한 에러 (Global-level Error)
사용자가 직접 해결할 수 없으며, 로그인이나 페이지 이동 등 전역적인 조치가 필요한 에러예요.
-
401:
Unauthorized
: 인증 정보가 없거나 유효하지 않아요. 세션을 제거하고 로그인 페이지로 보내야 해요. -
403:
Forbidden
: 인증은 되었지만, 해당 리소스에 접근할 권한이 없어요. 접근할 수 없음을 알리고 메인 페이지 등으로 보내야 해요.
처리 전략:
이 에러들은 queryClient의 전역 onError
핸들러에서 처리해요. 이 핸들러는 개별 컴포넌트의 렌더링 로직보다 먼저 실행되어, 팝업 안내 후 페이지를 redirect 시키는 전역적인 사이드 이펙트를 담당해요.

2. 재시도 가능한 에러 (Local-level Error)
일시적인 서버 문제이거나 사용자의 잘못된 요청으로, 사용자가 현재 컨텍스트 내에서 재시도할 수 있는 에러예요.
-
400:
Bad Request
: 요청 정보가 잘못되었어요. (예: 필수 필드 누락) -
404:
Not Found
: 요청한 리소스를 찾을 수 없어요. -
500:
Internal Server Error
: 서버 내부에서 예기치 못한 에러가 발생했어요. -
503:
Service Unavailable
: 서버가 과부하 또는 점검으로 인해 일시적으로 응답할 수 없어요.
처리 전략:
데이터를 사용하는 컴포넌트 내부의 {#if $query.isError}
블록이 처리해요. 사용자에게는 에러가 발생한 특정 영역에만 지역적인 fallback UI가 보이며, “재시도” 버튼을 통해 query.refetch()
를 호출하여 해당 쿼리만 다시 시도할 수 있어요. 전역 팝업은 띄우지 않아 다른 부분의 사용자 경험을 해치지 않아요.
3. 사용자 상호작용 에러 (Mutation Errors)
사용자가 생성/수정/삭제 버튼을 누르는 등의 상호작용(POST, PUT, DELETE) 과정에서 발생하는 에러예요.
처리 전략:
이 에러들은 queryClient의 mutations onError 핸들러에서 처리합니다. throwOnError: false
로 설정하여 에러 전파를 막고, 대신 Toast 메시지를 띄워 사용자에게 작업이 실패했음을 간결하게 알립니다. 사용자는 현재 보던 화면을 그대로 유지한 채 피드백을 받게 됩니다.

4. 네트워크 및 기타 에러 (Catch-all Errors)
사용자의 인터넷 연결이 끊겼거나, CORS 문제, 또는 예측하지 못한 그 외의 모든 에러예요.
- -1:
HttpError status
:Fetcher
가 fetch 자체를 실패했을 때 부여하는 코드예요.
처리 전략:
‘재시도 가능한 에러’로 간주하여 컴포넌트 내부의 {#if $query.isError}
블록에서 처리하여, 사용자에게 네트워크 상태를 확인하고 재시도 안내를 해요.
에러 메시지 중앙 관리
에러 분류를 코드로 구현하기 위해, 모든 에러 메시지를 한 곳에서 관리하는 유틸리티 함수를 만들었어요.
import { HttpError } from '$lib/api/errors';
// 백엔드와 합의된 에러 메시지를 중앙에서 관리
const ERROR_DEFINITIONS: Record<number, string> = {
400: '요청 정보가 잘못되었어요. (예: 필수 필드 누락)',
404: '요청한 리소스를 찾을 수 없어요.',
500: '서버 내부에서 예기치 못한 에러가 발생했어요.',
503: '서버가 과부하 또는 점검으로 인해 일시적으로 응답할 수 없어요.',
[-1]: '네트워크 연결을 확인해주세요.', // Fetcher에서 생성한 네트워크 에러
};
export function getStandardErrorMessage(error: unknown): string {
if (error instanceof HttpError) {
// 정의된 메시지가 있으면 그것을 사용하고, 없으면 서버가 준 메시지를 그대로 사용
return (
ERROR_DEFINITIONS[error.status] ||
error.message ||
'알 수 없는 오류가 발생했어요.'
);
}
// HttpError가 아닌 경우
if (error instanceof Error) {
return error.message;
}
return '알 수 없는 오류가 발생했어요.';
}
마무리하며
이번 작업을 통해 선언적인 UI에 대한 이상적인 목표를 가지고 <svelte:boundary>
를 도입하려던 시도가, 프레임워크의 깊은 동작 원리를 탐구하는 예상치 못한 작업으로 이어졌어요. 이 과정에서 발견한 핵심적인 교훈을 다시 한번 정리하면 다음과 같아요.
-
아키텍처 차이 이해: React의 렌더링 일시중지 (Suspense)와 Svelte의 반응형 상태 업데이트(Reactivity)는 비슷한 목표를 가지지만, 근본적인 동작 방식이 달라요. 이 차이를 이해하는 것이 외부 라이브러리와의 통합에서 매우 중요하다는 것을 배웠어요.
-
라이브러리의 의도 존중: 때로는 프레임워크의 최신 기능을 활용하는 것보다, 라이브러리가 제공하는 표준적인 패턴을 따르는 것이 더 안정적이고 예측 가능한 코드를 만들어요.
-
명확한 역할 분담의 중요성: 최종적으로 queryClient는 인증/권한처럼 서버스의 흐름을 제어해야 하는 ‘전역 관심사’를 책임지고, 각 컴포넌트는 Tanstack Query의 상태를 통해 자신의 데이터에 따른 지역적인 로딩/에러/성공 UI를 온전히 책임지는 구조가 가장 효과적이었어요.
이번 리팩터링 덕분에 선언적이고 명확한 코드가 가져오는 가독성 향상과 유지보수성을 체감할 수 있었고, 앞으로도 이런 선언적 패턴을 적극적으로 활용해 더 나은 개발 경험을 만들어가려 해요.
또한 이번에 설계한 구조를 바탕으로, 좀 더 세부적인 개선점들을 찾아 지속적으로 발전시켜 나갈 계획이에요.