Skip to content

State & Data Fetching

Sonne edited this page Jan 16, 2026 · 1 revision

이 문서는 세모(Delta-front) Web의 상태 관리와 데이터 패칭 구조를 정리합니다.
“UI 상태는 로컬로”, “서버 상태는 TanStack Query로”를 기본 원칙으로 합니다.

1) 상태 분류 원칙

✅ UI / Local State

탭 선택, 모달 열림, 입력값 등 화면 단위 상태는 로컬 상태로 관리합니다.

  • useState, useReducer, useMemo
  • 페이지(src/app/**) 또는 컴포넌트(src/shared/components/**) 내부에서 관리

✅ Server State

서버에서 받아오는 데이터는 캐싱/동기화/리패치가 필요하므로 TanStack Query로 관리합니다.

  • useQuery / useMutation
  • 캐시 키(QueryKey) 기반 데이터 일관성 유지

2) shared/apis 폴더 구조 (모듈별 분리)

src/shared/apis는 도메인 단위로 아래 구조를 유지합니다.

shared/apis
├─ api.ts # axios instance + interceptors(토큰/재발급/에러)
├─ api-types.ts # ApiResponse / ApiResponseError / unwrap
├─ api-error.ts # ApiError class
├─ error-codes.ts # 서버 에러 코드 상수
├─ token-storage.ts # access/refresh token 저장/조회/삭제
├─ constants
│ ├─ api-paths.ts # API_PATHS (endpoint 단일 소스)
│ └─ api-headers.ts # API_HEADERS (헤더 키 단일 소스)
├─ auth
│ ├─ auth-api.ts # auth 관련 API 호출 함수
│ ├─ auth-keys.ts # auth QueryKey factory
│ ├─ auth-events.ts # emitAuthLogout 등 이벤트
│ ├─ kakao-oauth.ts # 카카오 OAuth 유틸(state/redirectUri 등)
│ └─ hooks/ # useKakaoLoginMutation 등
├─ user
│ ├─ user-api.ts
│ ├─ user-keys.ts
│ └─ hooks/
└─ storage
├─ storage-api.ts
├─ storage-keys.ts
└─ hooks/

규칙: “API 호출 함수는 *-api.ts”, “QueryKey는 *-keys.ts”, “훅은 hooks/”로 고정합니다.

3) Axios 인스턴스 설계 (api.ts)

shared/apis/api.ts에서 axios instance를 단일로 운영합니다.

3-1) 요청 인터셉터: Access Token 자동 주입

  • tokenStorage.getTokens()로 access token을 읽고
  • Authorization 헤더에 Bearer <token>을 자동으로 붙입니다.
  • 이미 헤더가 존재하면 중복 주입하지 않습니다.

효과

  • 각 API 함수에서 토큰을 일일이 넣지 않아도 됨
  • “인증 헤더 정책”을 한 곳에서 통제 가능

3-2) 응답 인터셉터: 토큰 동기화

  • 응답 헤더에서
    • Authorization (access)
    • Refresh-Token(프로젝트에서 정의한 헤더) 를 읽어 tokenStorage.setTokens()로 갱신합니다.

효과

  • 서버가 토큰을 재발급/갱신해도 클라이언트가 자동 반영

3-3) 에러 처리: ApiError로 정규화 + 401 재시도(reissue)

서버 에러 응답이 ApiResponseError 형태인지 검사(isApiResponseError) 후,

  • ApiError.fromPayload(payload, traceId)표준 에러 객체로 변환합니다.

401 처리 정책 (핵심)

  • TOKEN_REQUIRED(토큰 없음/만료 등) → 즉시 로그아웃 처리

    • tokenStorage.clear()
    • emitAuthLogout()
  • AUTHENTICATION_FAILED(access 만료/검증 실패) → refresh로 재발급 후 1회 재시도

    • _retry 플래그로 무한 루프 방지
    • runReissueOnce()동시성 제어(reissuePromise 공유)로 중복 재발급 방지
    • 재발급 성공 후 원래 요청을 재실행(instance(config))

config._skipAuthRefresh 또는 reissue 자체 요청(API_PATHS.AUTH.REISSUE)은 재발급 로직에서 제외합니다.

효과

  • “access 만료”는 사용자 경험을 끊지 않고 자동 복구
  • “refresh 없음/불일치”는 보안적으로 즉시 종료(로그아웃)

4) Response 타입 규격 (api-types.ts)

서버 응답은 다음을 기본으로 합니다.

  • 성공: ApiResponse<T> = { status, code, data, message }
  • 실패: ApiResponseError = { status, code, data: null, message }

unwrapApiResponse

unwrapApiResponse(res)data만 꺼내는 유틸을 사용합니다.

효과

  • 각 API에서 반환 형태가 단순해져서 Query 훅/컴포넌트 사용이 쉬워짐

5) QueryKey Factory (추천 패턴)

각 도메인별 *-keys.ts는 QueryKey 생성을 담당합니다.

예시(권장 형태):

  • authKeys.me()
  • userKeys.me()
  • storageKeys.presignedGet(params)

효과

  • invalidate/refetch 시 키를 문자열로 하드코딩하지 않음
  • 키 구조가 일관되어 캐시 관리가 쉬움

6) TanStack Query 훅 작성 규칙

Query 훅

  • 이름: use + 행위 + 대상 + Query
  • 예: useGetMyProfileQuery

Mutation 훅

  • 이름: use + 행위 + 대상 + Mutation
  • 예: useKakaoLoginMutation

훅에서의 책임 범위

  • API 호출 함수(*-api.ts)를 사용해 데이터 패칭
  • 필요한 경우 select로 데이터 가공(unwrapApiResponse 등)
  • 성공/실패 후 invalidate 등 캐시 제어

공통 컴포넌트는 원칙적으로 훅을 직접 호출하기보다, 페이지에서 데이터를 가져와 props로 주입하는 방식을 우선합니다.

7) Auth 이벤트(emitAuthLogout)와 UI 반응

emitAuthLogout()은 “토큰 만료/인증 불능” 상황을 앱 전역에 알립니다.

권장 흐름

  • 전역(또는 페이지)에서 logout 이벤트를 구독
  • 로그인 페이지로 이동 / 토스트 표시 / 캐시 정리 등 수행

이 방식으로 API 레이어가 라우터를 직접 의존하지 않고도, “로그아웃 이후 UI 처리”를 분리할 수 있습니다.

8) (WebView 대비) Storage/쿠키 정책 주의

향후 apps/app에서 RN WebView로 apps/web을 감쌀 예정이므로,

  • sessionStorage / localStorage 사용은 “환경에 따라 실패 가능”을 고려합니다.
  • 토큰 전략이 WebView에서 달라질 수 있으므로, 필요한 시점에
    • 네이티브 브릿지(postMessage) 기반 토큰 주입
    • secure storage 연동 같은 방식으로 확장합니다.

9) 체크리스트

  • API_PATHS, API_HEADERS는 단일 소스로만 관리한다
  • 모든 에러는 ApiError로 정규화되어 호출부가 처리한다
  • 401 재발급은 runReissueOnce()로 동시성 제어한다
  • QueryKey는 Factory로 만들고 하드코딩하지 않는다
  • UI 상태는 로컬, 서버 상태는 TanStack Query로 분리한다

image

🏁 시작하기

🏗️ 프로젝트 구조

🎨 UI · 디자인 시스템

🧠 기능 · 도메인

⚙️ 운영 · 프로세스

📝DOCS

Clone this wiki locally