따라 하며 이해하는 불안정한 네트워크와 프론트엔드
BE 개발자가 API를 설계할 때 FE가 네트워크 문제를 어떻게 다루는지 이해하기 위한 실습 문서입니다. 각 장의 실습에서는 실제 TanStack Query v5 코어(@tanstack/query-core)가 브라우저에서 동작하고, 네트워크(지연·유실·끊김·오프라인·망 전환)와 서버(Spring 쪽 동작)는 시뮬레이션합니다. Chrome DevTools로는 재현하기 어려운 "서버는 처리했는데 응답만 사라진" 상황도 버튼 하나로 만들 수 있습니다.
실습 화면 읽는 법
따라하기
번호 단계를 순서대로 진행합니다. 할 일에 나오는 버튼 이름 칩에 마우스를 올리면 실습 안의 그 버튼이 강조되고, 누르면 그 위치로 이동합니다. 이 단계 설정으로 맞추기를 누르면 필요한 선택·토글이 자동으로 맞춰지고, 실행 버튼은 직접 누릅니다. "예상해 보기"로 결과를 먼저 짐작해 보세요.
개선 전 / 개선 후
흔한 구현과 권장 구현(FE 설정 + BE 계약)을 한 번에 전환합니다. 개별 토글로 한쪽만 바꿔 볼 수도 있습니다.
장애 주입 버튼
점선 테두리 버튼(예: 다음 응답 유실)은 다음 요청 한 번에만 장애를 겁니다. 켜 두면 "대기 중"으로 표시됩니다.
코드 탭
FE(React + TanStack Query), BE(Kotlin + Spring Boot 4), HTTP 메시지를 보여 줍니다. 방식·토글을 바꾸면 코드도 지금 설정 그대로 바뀌고, 달라진 줄은 잠깐 파랗게 강조됩니다(다른 탭이 바뀌면 탭 이름 옆에 점). + 줄은 개선으로 추가된 부분, ! 줄은 문제 지점입니다.
추천 순서
1장부터 차례로 읽어도 되지만, 실습만 따라가려면 아래 순서가 자연스럽습니다. 한 번이라도 단계를 진행한 실습에는 ✓가 붙습니다(이 브라우저에만 저장).
예제 도메인은 가상의 쇼핑몰(products · orders · payments · cart · uploads)입니다. 시뮬레이터의 시간·지연 값은 관찰하기 쉽게 고른 것이고 실측값이 아닙니다. 라이브러리 동작(재시도·중복 제거·취소·일시정지·scope 직렬화)은 실제 @tanstack/query-core가 수행하므로 문서와 같게 움직입니다.
FE 관점에서 본 네트워크 문제 분류
"네트워크가 불안정하다"는 한 문장은 FE 코드의 서로 다른 계층에서 해결됩니다. 먼저 일상적인 상황이 어느 계층의 문제인지 골라 보고, 요청 하나가 실패하는 방식을 직접 만들어 봅니다.
상황을 고르면 관련 계층이 보입니다
정리: 계층별 문제와 해결 위치
| 계층 | 문제 | 주로 해결하는 곳 |
|---|---|---|
| Network | 지연, 대역폭, 패킷 손실, 타임아웃, 연결 리셋, 오프라인, 재연결 | 타임아웃(AbortSignal.timeout), HTTP/2·3, 재연결 감지, BFF로 RTT 줄이기 |
| Server State | 캐시, stale 데이터, 백그라운드 재검증, 중복 요청, 요청 취소, 동기화, 페이지네이션 | TanStack Query 같은 서버 상태 라이브러리 + BE의 ETag·cursor |
| Mutation | 재시도, 중복 실행, 낙관적 반영, 롤백, 순서, 오프라인 쓰기 | mutation 옵션(retry·scope·onMutate) + BE 멱등성(Idempotency-Key, PUT/DELETE, If-Match) |
| UX | 로딩, 스켈레톤, 재시도 버튼, 오프라인 배너, 부분 실패, "확인 중" 상태 | 화면 설계. 실패를 단정하지 않는 문구가 중요(응답 유실은 실패가 아닐 수 있음) |
| File | 업로드 진행률, 청크, 이어 올리기, 일시정지·재개, 다운로드 이어받기 | tus / S3 multipart(서버가 진행 상태 보관), Range(다운로드) |
FE는 "응답을 못 받았다"는 사실만으로 요청 유실과 응답 유실을 구분할 수 없습니다. 둘 다 같은 TimeoutError입니다. 그래서 쓰기 요청을 안전하게 재시도하게 만드는 일은 서버(멱등성)의 몫이고, 3장에서 직접 해결합니다.
출처: MDN AbortSignal.timeout() · RFC 9110 §9.2.2 멱등 메서드 · RFC 9110 503 · Retry-After
서버 상태 라이브러리 비교
TanStack Query, SWR, Apollo Client, RTK Query를 "어떤 네트워크 문제를 푸는가" 기준으로 비교합니다. 문제를 고르면 관련 행이 강조됩니다. 이 문서의 실습은 TanStack Query를 사용합니다.
| TanStack Query v5 | SWR | Apollo Client | RTK Query | |
|---|---|---|---|---|
| 캐시 구조 | queryKey별 문서 캐시 | 키별 문서 캐시 | 정규화 캐시 (__typename:id) | 엔드포인트+인자별 캐시(비정규화) |
| 신선도 | staleTime(기본 0) · gcTime(기본 5분) | 항상 캐시 즉시 반환 + 재검증 | fetchPolicy(cache-first 등) | keepUnusedDataFor(60초) · refetchOnMountOrArgChange |
| 중복 제거 | 같은 키 in-flight 공유 | dedupingInterval(2초) | in-flight 쿼리 공유 | 같은 캐시 키 공유 |
| 재시도 | query 3회·지수 백오프, mutation 0회 | onErrorRetry로 제어 | RetryLink 추가 구성 | retry 유틸 |
| 취소 | queryFn에 signal 자동 전달·자동 취소 | fetcher에서 직접 | 링크에 signal 연결 | 내장 AbortController, abort() |
| 폴링·재검증 | refetchInterval, 포커스·재연결 시 재검증 | refreshInterval, focus·reconnect | pollInterval | pollingInterval, refetchOnFocus/Reconnect |
| 낙관적 업데이트 | onMutate → onError 롤백 → onSettled 무효화 | optimisticData + rollbackOnError | optimisticResponse(정규화 캐시에 자동 전파) | updateQueryData + undo(), 태그 무효화 |
| 오프라인 | networkMode로 일시정지·재개, mutation 큐, scope 직렬화 | 공식 기능 없음 | 공식 기능 없음 | 공식 기능 없음 |
| 영속화 | persist-client(localStorage·IndexedDB) | 커스텀 provider | apollo3-cache-persist(커뮤니티) | redux-persist에 얹음 |
| 무한 스크롤 | useInfiniteQuery(양방향, maxPages) | useSWRInfinite | fetchMore + merge 정책 | infiniteQuery 엔드포인트 |
| SSR·RSC | dehydrate / HydrationBoundary | Next.js 통합 | RSC 통합 패키지 | Redux 특성상 제약 |
| 프레임워크 | React · Vue · Solid · Svelte · Angular | React | React 중심, Vue·Angular | Redux를 쓰는 React |
정규화 캐시 vs 문서 캐시
Apollo는 객체를 __typename:id로 평탄화해 저장하므로, 같은 상품을 다섯 개 화면이 참조해도 실체는 하나이고 하나를 고치면 모두 갱신됩니다. TanStack Query·SWR·RTK Query는 키 단위 문서 캐시라서 "이 수정이 끝나면 이 키들을 무효화"를 명시합니다(RTK Query는 태그로 선언). 정규화는 편하지만 필드 정책이 복잡해질 수 있고, 문서 캐시는 단순하지만 무효화 대상을 개발자가 알아야 합니다. BE 관점에서는 두 경우 모두 안정적인 id와 수정 후 무엇이 바뀌는지 예측 가능한 응답이 도움이 됩니다.
출처: TanStack Query Important Defaults · SWR Revalidation · Apollo Queries · RTK Query Cache Behavior
재시도와 멱등성 — 무엇을, 언제, 몇 번
세 개의 실습으로 진행합니다. ① 조회(GET)의 재시도와 백오프, ② 많은 클라이언트가 동시에 재시도할 때 서버에 생기는 일, ③ 결제(POST) 재시도가 이중 결제가 되는 과정과 Idempotency-Key로 막는 방법.
정리
재시도해 볼 가치가 있는 오류
- 네트워크 오류(연결 실패·리셋·오프라인)
- 타임아웃
429—Retry-After만큼 기다린 뒤502 · 503 · 504(500은 상황에 따라)
재시도해도 결과가 같은 오류
400 · 401 · 403 · 404 · 409 · 422- 401은 토큰 갱신 후 1회만 다시 보내는 별도 흐름
- 409는 멱등 키 "처리 중" 응답일 때만 대기 후 재시도
| 이중 결제 방지에서 | 할 수 있는 것 | 할 수 없는 것 |
|---|---|---|
| FE | 재시도 여부·횟수·간격 결정, 버튼 잠금, "확인 중" 표시, 키 발급과 재사용 | 응답 없이 서버의 처리 여부를 알아내기, 중복 실행 자체를 막기 |
| BE | 키 선점(PK), 응답 저장·재생, 처리 중 409, 본문 불일치 422, 키 없음 400, 보관 기간 | FE가 재시도마다 새 키를 만드는 실수 막기(계약 위반) |
Idempotency-Key는 IETF httpapi 워킹그룹의 Internet-Draft(draft-ietf-httpapi-idempotency-key-header-07)로, 2026년 9월 현재 RFC가 아닙니다. 초안은 키로 UUID 같은 무작위 값을 권장하고, 헤더 값을 Structured Field 문자열(따옴표 포함)로 정의합니다. Stripe 등 결제사가 같은 방식을 오래 써 왔습니다(Stripe는 응답에 Idempotent-Replayed 헤더를 붙입니다).
출처: TanStack Query Retries · Mutations (retry 기본 0, scope) · IETF Idempotency-Key draft-07 · Stripe Idempotent requests · Spring Framework 7 Resilience (@Retryable) · RFC 9110 Retry-After
오프라인 / 온라인
오프라인에서 사용자가 한 행동을 버리지 않고, 앱을 다시 켜도 남겨 두었다가, 온라인이 되면 순서대로 서버에 반영하는 흐름을 실습합니다. 그 과정에서 BE가 지켜야 할 계약(멱등 키, 재검증, 명확한 거절 이유)이 드러납니다.
정리
단순 오프라인 UX
오프라인이면 배너를 띄우고 요청을 멈춥니다(TanStack Query 기본 networkMode: 'online'). 구현 비용이 낮고 대부분의 관리 도구·대시보드에 충분합니다.
오프라인 우선 (offline-first)
로컬 데이터 사용 → 쓰기를 큐에 저장 → 온라인 시 동기화 → 충돌 해결. 노트, 현장 업무 앱, 협업 문서처럼 오프라인에서도 일이 계속돼야 할 때만 정당화되는 복잡도입니다.
navigator.onLine과 online/offline 이벤트는 신뢰도가 낮습니다(LAN만 연결돼도 online). 실제 요청 성공·실패로 보완합니다.- Background Sync API는 2026년 현재 Chromium 계열만 지원하고 Safari·Firefox는 지원하지 않습니다. 앱이 다시 열리거나 포그라운드로 돌아올 때 동기화하는 방식이 여전히 필요합니다.
- 영속화 저장소로는 용량 제한이 작고 동기 API인 localStorage보다 IndexedDB가 적합합니다.
출처: TanStack Query Network Mode · Persisting Offline mutations · persistQueryClient · MDN Navigator.onLine · caniuse Background Sync · RFC 9457 Problem Details
캐시와 stale 데이터
FE에는 캐시가 여러 층 있습니다. 실습에서는 그중 두 층, TanStack Query의 메모리 캐시(staleTime·gcTime)와 브라우저 HTTP 캐시(ETag·Cache-Control)가 요청 하나를 어떻게 나눠 처리하는지 봅니다.
정리: 네 개의 캐시 층
| 층 | 저장하는 것 | 답하는 질문 | BE가 주는 것 |
|---|---|---|---|
| 브라우저 HTTP 캐시 | HTTP 응답(바이트) | 네트워크에 갈 필요가 있나? 가면 본문을 다시 받아야 하나? | Cache-Control, ETag, Last-Modified |
| Service Worker Cache | 요청-응답 쌍(앱이 직접 관리) | 네트워크가 아예 없을 때 무엇을 줄까? | (정적 자원 버전 관리) |
| TanStack Query 캐시 | 파싱된 객체 + 상태(stale, fetching, error) | 화면에 무엇을 보여 주고, 언제 다시 물어볼까? | 안정적인 응답 구조, 변경 후 무효화할 키를 알 수 있는 API |
| IndexedDB 영속화 | 위 캐시의 스냅샷 | 앱을 다시 켰을 때 무엇을 바로 보여 줄까? | — |
staleTime vs gcTime
staleTime: 다시 물어볼 필요가 있는가(신선도). gcTime: 화면에서 사라진 캐시를 언제 지울까(보관). stale이어도 캐시 값은 즉시 보여 주고 뒤에서 재검증합니다(stale-while-revalidate, 원래 RFC 5861의 HTTP 개념).
staleTime vs max-age
staleTime은 "JS가 fetch를 다시 부를지", max-age·ETag는 "그 fetch가 서버까지 갈지, 본문을 다시 받을지"를 정합니다. API에는 보통 no-cache + ETag, 신선도 판단은 staleTime에 맡깁니다.
ResponseEntity.ok().eTag(...)로 응답하면 Spring MVC가 GET/HEAD의 If-None-Match를 비교해 자동으로 304를 돌려줍니다. 본문을 만드는 비용 자체를 아끼려면 WebRequest.checkNotModified(etag)로 먼저 확인합니다.
출처: TanStack Query Important Defaults · MDN HTTP Caching · RFC 9111 · RFC 5861 · Spring MVC HTTP Caching
낙관적 업데이트와 동시 수정
좋아요 버튼으로 낙관적 업데이트가 틀어지는 다섯 가지 상황을 재현하고, 두 관리자가 같은 상품을 고칠 때의 "잃어버린 수정"을 If-Match로 막습니다.
정리: 어디까지가 FE의 몫인가
| 상황 | FE(TanStack Query)가 하는 일 | BE가 보장해야 하는 것 |
|---|---|---|
| 빠른 연타 | 버튼 잠금 또는 scope로 직렬화 | 멱등한 상태 지정 API(PUT/DELETE) |
| 응답 순서 역전 | 중간 응답을 믿지 않고 마지막 mutation 뒤 invalidate | GET이 최종 진실을 돌려줌 |
| 요청 도착 순서 역전 | scope 직렬화 | 필요하면 클라이언트 시퀀스·버전으로 늦은 요청 무시 |
| 부분 실패 | 스냅샷 롤백(동시 mutation이 있으면 invalidate로 재동기화) | 실패한 요청이 아무것도 바꾸지 않았음을 보장(트랜잭션) |
| 다른 사용자의 동시 수정 | 412를 받으면 최신 값을 보여 주고 사용자가 결정 | ETag(버전) + If-Match → 412, JPA @Version |
| 서버 검증 실패 | 롤백 + 이유 표시 | ProblemDetail로 무엇이 왜 거절됐는지 |
출처: TanStack Query Optimistic Updates · Mutation Scopes · RFC 9110 If-Match · 412 · RFC 6585 428 Precondition Required
요청 취소
검색어 자동완성에서 "a → ap → app"이 빠르게 입력되면 응답이 뒤죽박죽 도착합니다. 취소 없이, AbortController로, TanStack Query로 각각 무엇이 보이는지 비교합니다.
정리
AbortController+fetch가 표준 취소 방법입니다. Axios의CancelToken은 deprecated이고 AbortController(signal)를 쓰는 것이 현재 권장입니다.- TanStack Query는 queryFn에
signal을 넘기고, 쿼리 키가 바뀌거나 컴포넌트가 언마운트돼 구독자가 사라지면 그 signal을 abort합니다(signal을 fetch에 넘긴 경우). - 취소는 브라우저가 응답을 기다리지 않게 할 뿐입니다. 서버 부하를 줄이려면 디바운스(FE)와 최소 검색어 길이·캐시(BE)가 필요합니다.
출처: TanStack Query Query Cancellation · Axios Cancellation · MDN AbortController
페이지네이션과 무한 스크롤
같은 피드를 offset과 cursor 두 방식으로 불러오면서 글을 추가·삭제해 봅니다. 불안정한 네트워크에서는 페이지 하나가 실패하고 다시 불러오는 일이 잦아서, 페이지 경계가 흔들리지 않는 것이 더 중요해집니다.
정리
| offset | cursor (keyset) | |
|---|---|---|
| 앞쪽 추가·삭제 | 경계가 밀려 중복·누락 | 영향 없음 |
| 실패한 페이지 재시도 | 그사이 데이터가 바뀌면 다른 결과 | 같은 커서로 같은 위치부터 |
| DB 비용 | OFFSET만큼 읽고 버림 (뒤로 갈수록 느림) | 인덱스 seek |
| 임의 페이지 이동 | 가능 (게시판 번호 페이지) | 어려움 (무한 스크롤에 적합) |
BE 설계 포인트: 커서는 정렬 키를 인코딩한 불투명 토큰으로 주고(FE가 내부 구조에 의존하지 않게), 동률을 막기 위해 (created_at, id) 같은 복합 키를 쓰며, 하나 더 읽어 hasNext/nextCursor를 명시합니다.
출처: TanStack Query Infinite Queries · RTK Query Infinite Queries
대용량 파일 업로드
48MB 영상을 단일 POST, tus, S3 multipart로 올리면서 연결 끊김·브라우저 재시작·서명 만료를 일으킵니다. 핵심 질문은 "끊겼을 때 누가 진행 상태를 기억하는가"입니다.
정리
| 단일 multipart POST | tus (Uppy + tusd) | S3 multipart (Uppy + presigned) | |
|---|---|---|---|
| 진행 상태를 기억하는 곳 | 없음 | tus 서버 (업로드 URL + Upload-Offset) | S3 (uploadId + 완료된 파트) |
| 끊긴 뒤 | 처음부터 | HEAD → 받은 지점부터 PATCH | 전송 중이던 파트만 다시 |
| 브라우저 재시작 | 처음부터 (파일 재선택) | 저장된 URL로 이어서 | 저장된 uploadId + listParts로 이어서 |
| 서버 구현 | 가장 단순 | tusd 운영 + 훅 처리 | BFF의 생성·서명·완료 API, 버킷 CORS·라이프사이클 |
| 본문이 거치는 곳 | API 서버 | tusd (스토리지는 S3 등 선택) | 브라우저 → S3 직접 |
- Uppy(현재 v5)는 프로토콜이 아니라 업로드 UI·오케스트레이션 라이브러리입니다. 실제 전송은
@uppy/tus(tus-js-client) 또는@uppy/aws-s3가 합니다. 둘 중 하나를 고릅니다. @uppy/tus의 기본chunkSize는 Infinity,retryDelays기본값은 [0, 1000, 3000, 5000]입니다.@uppy/aws-s3는 기본적으로 100MiB 초과 파일에 multipart를 씁니다.- S3 파트는 마지막을 제외하고 최소 5MiB, 최대 10,000개입니다. 버킷 CORS의 ExposeHeaders에
ETag가 없으면 브라우저가 파트 ETag를 읽지 못해 완료할 수 없습니다. - tus는 IETF에서 HTTP 재개 업로드 표준으로 논의되고 있습니다(draft-ietf-httpbis-resumable-upload).
출처: tus 1.0 프로토콜 · tusd Hooks · Uppy Tus · Uppy AWS S3 · AWS S3 Multipart Upload · IETF Resumable Uploads draft
대용량 파일 다운로드
다운로드 이어받기는 브라우저와 HTTP 표준이 대부분 처리합니다. 실습에서는 BE가 지원하지 않거나(Range 미지원), 그사이 파일이 바뀌는(If-Range 필요) 경우를 봅니다.
정리: 업로드와 다운로드가 비대칭인 이유
누가 상태를 아나
다운로드는 받은 쪽(클라이언트)이 받은 만큼만 기억하면 되고 서버는 상태가 없습니다. 업로드는 서버가 받은 만큼을 기억해 줘야 합니다.
표준화
Range·206·If-Range는 HTTP 표준이고 서버·CDN·브라우저가 기본 지원합니다. 업로드 재개는 tus 같은 별도 프로토콜이 필요합니다.
중간 상태
다운로드는 완성된 파일에서 구간을 떼어 주면 됩니다. 업로드는 "절반만 존재하는 파일"을 서버가 보관·병합·검증해야 합니다.
출처: MDN HTTP Range requests · RFC 9110 If-Range · Spring MVC 소스(AbstractMessageConverterMethodProcessor): Resource 반환 시 Range 처리, If-Range 미평가
오래 걸리는 작업
8초짜리 작업을 긴 요청, 202 + 폴링, SSE로 처리하면서 도중에 Wi-Fi → LTE 전환을 일으킵니다. "긴 연결을 유지하는 것"과 "짧은 요청 + 상태 조회"의 차이가 드러납니다.
정리
| 패턴 | 적합한 경우 | 불안정한 네트워크에서 |
|---|---|---|
| 긴 요청 | 수 초 안에 끝나고 재시도가 싼 작업 | NAT·프록시 idle timeout, 망 전환, 백그라운드 전환에 결과 유실. 재시도하면 작업 중복 |
| 202 + 폴링 | 수십 초~수 시간, 배치성 작업 | 폴링 한 번 실패해도 다음 폴링이 이어받음. 인프라 호환성 최고 |
| SSE | 진행률·토큰 스트리밍 같은 서버 → 클라이언트 단방향 | EventSource 자동 재연결 + Last-Event-ID로 이어서 |
| WebSocket | 채팅·협업 편집 같은 양방향 실시간 | 재연결 후 상태 동기화(시퀀스·체크포인트)를 앱이 직접 설계 |
| Web Push | 앱이 닫혀 있어도 알려야 할 때 | OS 푸시 인프라 경유. 폴링·SSE와 함께 사용 |
출처: Azure Asynchronous Request-Reply · WHATWG Server-sent events · RFC 9110 202 Accepted
BFF와 API 집계
홈 화면을 위해 FE가 API 여섯 개를 직접 부를지, BFF가 한 번에 모아 줄지. RTT·프로토콜·의존 관계·장애를 바꿔 가며 두 타임라인을 비교합니다.
정리
| 관점 | FE가 여러 번 호출 | BFF가 한 번에 |
|---|---|---|
| RTT | 의존 호출마다 클라이언트 RTT가 쌓임 | 클라이언트 왕복 1번, 의존 호출은 데이터센터 안에서 |
| 캐시 | 리소스별로 세밀하게 (ETag·staleTime) | 화면 단위 응답이라 일부만 바뀌어도 전체 무효화 |
| 부분 실패 | 자연스럽게 영역별 처리 | 호출별 타임아웃 + null + errors 설계가 필수 |
| 페이로드 | API마다 공통 필드 중복 가능 | 화면에 필요한 필드만 골라 줄 수 있음(모바일용 BFF) |
| 비용 | 클라이언트가 여러 계약을 앎 | BFF 계층의 개발·배포·관측 부담 |
출처: Sam Newman — Backends For Frontends · Spring Kotlin Coroutines
HTTP/2 · HTTP/3가 바꾸는 것과 바꾸지 않는 것
멀티플렉싱과 QUIC이 해결하는 문제(연결 수 제한, 전송 계층 HOL blocking, 망 전환)를 눈으로 확인하고, "HTTP/2·3면 요청을 합칠 필요가 없다"는 오해를 정리합니다.
정리
| HTTP/1.1 | HTTP/2 | HTTP/3 (QUIC) | |
|---|---|---|---|
| 동시 요청 | 출처당 연결 약 6개, 넘으면 대기 | 연결 하나에 멀티플렉싱 | 연결 하나에 독립 스트림 |
| 패킷 손실 | 그 연결의 요청이 대기 | 모든 스트림이 대기 (TCP HOL) | 손실된 스트림만 대기 |
| 망 전환 | 새 연결 + TLS | 새 연결 + TLS | connection ID로 연결 유지 (지원 시) |
| 새 연결 비용 | TCP+TLS 1.3 = 2 RTT | 2 RTT | 1 RTT (재방문 0-RTT) |
사라진 것은 연결 수 제한과 연결 수립 오버헤드입니다. 남아 있는 것: 요청 수만큼의 서버 처리(인증·조회·직렬화), 의존 호출의 RTT 누적, 요청마다 붙는 헤더, 화면 단위 캐시·부분 실패 설계. BFF의 이유 중 "연결 제한 회피"는 약해졌지만 나머지는 그대로입니다. 2026년 현재 HTTP/3는 주요 브라우저 대부분이 지원합니다.
출처: RFC 9000 QUIC (Connection Migration, disable_active_migration) · RFC 9114 HTTP/3 · caniuse HTTP/3 · RFC 7838 Alt-Svc
실전 라이브러리 조합 (2026)
프레임워크를 고르면 어떤 라이브러리가 어느 계층을 맡는지와 최소 설정 코드가 바뀝니다. 서버 상태·전송·업로드 계층은 프레임워크가 달라도 거의 같다는 점을 확인하세요.
Zustand·Redux·Pinia 같은 상태 관리는 클라이언트 상태(모달, 폼 입력, 필터)를, TanStack Query는 서버 상태(서버가 진실인 데이터)를 맡습니다. 서버 응답을 전역 store에 복사해 두면 재검증·무효화·중복 제거를 다시 구현하게 됩니다.
출처: TanStack Query (React·Vue·Solid 어댑터) · Solid useQuery · Uppy
브라우저 API vs HTTP 표준 vs 라이브러리 vs 직접 구현
가장 헷갈리기 쉬운 구분입니다. 카드마다 네 가지 중 하나를 골라 보세요. 누가 보장하는지 알아야 "라이브러리를 바꾸면 사라지는 것"과 "BE가 구현하지 않으면 어떤 라이브러리로도 안 되는 것"이 보입니다.
정리
| 구분 | 예 | 의미 |
|---|---|---|
| 브라우저 제공 | fetch, AbortController, AbortSignal.timeout, IndexedDB, Service Worker, EventSource, online 이벤트, Background Sync | 표준 API. 브라우저별 지원 차이는 확인해야 함 |
| HTTP·프로토콜 표준 | Cache-Control, ETag/304, If-Match/412, Range/206, Retry-After, Problem Details, QUIC, tus | 서버와 클라이언트의 계약. 서버가 구현하지 않으면 FE 라이브러리가 아무리 좋아도 소용없음 |
| 라이브러리 제공 | staleTime·gcTime, 재시도·백오프, 중복 제거, networkMode, mutation 큐·scope, 영속화, Uppy 복원 | 위 두 층을 조합해 자동화. 걷어내도 직접 구현은 가능 |
| 앱·BE 직접 구현 | Idempotency-Key 저장·재생, 커서 발급, 충돌 해결 UI, 되돌릴 범위, 재시도할 오류 판단 | 표준도 라이브러리도 대신해 주지 않는 설계 |
쇼핑몰 아키텍처 — 10가지 시나리오 재생
지금까지의 전략을 하나의 쇼핑몰로 묶었습니다. 시나리오를 고르고 다음 ▶으로 한 단계씩 넘기며 각 요청의 이유를 읽어 보세요. 관련 실습으로 바로 이동할 수 있습니다.
BE 개발자가 FE에서 알아야 할 것
FE 프레임워크를 배우려는 게 아니라, API를 설계할 때 FE의 네트워크 요구를 받아 줄 계약을 갖추기 위한 정리입니다. 아래 점검표로 지금 만드는 API를 체크해 보세요.
이해 수준별 정리
| 수준 | 항목 | 담당 | BE 관점에서 왜 |
|---|---|---|---|
| 반드시 이해 | Query 캐시 · stale/fresh | FE | staleTime이 곧 API 호출 빈도. 트래픽 예측의 기준 |
| 재시도 · 지수 백오프 | FE | 장애 때 받게 될 재시도 트래픽 모양. Retry-After로 유도 가능 | |
| Idempotency-Key | 계약 | BE가 강제하지 않으면 FE의 재시도 설계가 무의미 | |
| 낙관적 업데이트 · 롤백 | 계약 | 명확한 에러와 멱등 API가 있어야 롤백이 정확 | |
| mutation 순서 · scope | FE | 서버 도착 순서는 보장되지 않는다는 전제로 API 설계 | |
| 요청 취소 | FE | 취소돼도 서버는 보통 끝까지 처리 — 비싼 API는 BE도 방어 | |
| 오프라인 · 재연결 | FE | 재연결 순간 몰리는 요청(큐 재전송·재검증)을 감당해야 함 | |
| cursor 페이지네이션 | 계약 | 커서·hasNext를 BE가 줘야 무한 스크롤이 정합성을 지킴 | |
| 이어 올리기 · 진행률 | BE | 업로드 상태를 서버(tus·S3)가 보관하는 구조를 BE가 운영 | |
| 개념만 | 정규화 캐시 vs 문서 캐시 | FE | 안정적인 id가 있으면 어느 쪽이든 편하다 |
| SSE 재연결 세부 | 계약 | 이벤트 id와 재전송 범위만 맞추면 됨 | |
| Service Worker 캐시 전략 | FE | 정적 자원 버전 관리와 연결됨 | |
| HTTP/3 · QUIC | BE | 엣지(CDN·LB) 설정 문제에 가깝다 | |
| 깊이 몰라도 됨 | React 훅 문법, 리렌더 최적화 | FE | API 설계에 영향 없음 |
| Uppy·Golden Retriever 저장 형식 | FE | BE는 업로드 프로토콜 쪽만 맞추면 됨 | |
| QUIC 패킷 수준 동작 | BE | 인프라가 대신 처리 |
최종 표와 우선순위
문제별로 FE 전략·대표 라이브러리·브라우저 API·HTTP/프로토콜·BE 대응을 한 줄에 모았습니다. Tier와 담당으로 걸러 보세요. 행의 실습 버튼으로 바로 해당 실습에 갈 수 있습니다.
| 문제 | FE 전략 | 대표 라이브러리 | Browser API | HTTP / Protocol | BE 대응 | 실습 |
|---|---|---|---|---|---|---|
| 느린 응답 | 캐시 즉시 표시 + 재검증, prefetch | TanStack Query | fetch | Cache-Control, ETag | ETag·304, 응답 시간 관리, 필요 시 BFF | |
| 타임아웃 | 모든 요청에 타임아웃, 재시도와 예산 분리 | TanStack Query + fetch 래퍼 | AbortSignal.timeout / any | — | p99 처리 시간 공유, 과부하 시 빠른 503 | |
| 재시도 | 재시도할 오류만, 지수 백오프 + 지터 | TanStack Query | fetch | Retry-After (429·503) | 정확한 상태 코드, Retry-After, 레이트 리밋 | |
| 중복 요청 (결제 등) | 버튼 잠금 + 의도당 멱등 키 재사용 | useMutation | crypto.randomUUID | Idempotency-Key (IETF 초안) | 키 선점·응답 재생·409·422·400 | |
| 오래된 데이터 | staleTime·gcTime, 포커스·재연결 재검증, 수정 후 무효화 | TanStack Query / SWR | visibilitychange | ETag / If-None-Match / 304 | ETag 발급, 의도적인 Cache-Control | |
| 늦게 온 응답 (경쟁 상태) | 키 기반 쿼리 + 자동 취소, 디바운스 | TanStack Query | AbortController | — | 비싼 검색 방어(최소 길이·캐시) | |
| 낙관적 업데이트 | onMutate 스냅샷 → onError 롤백 → 마지막에 invalidate | TanStack Query | — | PUT / DELETE (멱등) | 상태 지정 API, 명확한 에러 | |
| 동시 수정 | 412 시 최신 값 보여 주고 사용자 결정 | TanStack Query | — | ETag + If-Match → 412, 428 | 버전 = ETag, JPA @Version | |
| 목록 정합성 | 커서를 그대로 전달, id로 dedupe | useInfiniteQuery | — | — | 불투명 cursor + hasNext, keyset 쿼리 | |
| 오프라인 | 일시정지 + mutation 큐 + 영속화 + scope | TanStack Query persist | IndexedDB, online 이벤트 | — | 큐 재전송 대비 멱등 키, 재검증·충돌 409 | |
| 오래 걸리는 작업 | 짧은 요청 + 폴링 / SSE | TanStack Query refetchInterval | EventSource | 202 Accepted, Location, Last-Event-ID | 작업 큐 + 상태 API + 이벤트 id | |
| 모바일 고지연 | 요청 수·의존 호출 줄이기 | — | — | HTTP/2 · HTTP/3 | BFF(호출별 타임아웃·부분 응답) | |
| 재시도 폭주 | 지터 있는 백오프 | TanStack Query retryDelay | — | Retry-After | 레이트 리밋, Retry-After 분산 | |
| 대용량 업로드 | 청크·진행률·일시정지 | Uppy | File, Blob.slice, IndexedDB | tus / S3 Multipart | tusd 또는 서명·완료 API, CORS ETag, 라이프사이클 | |
| 업로드 재개 | 저장된 URL·uploadId로 이어서 | Uppy + tus-js-client / aws-s3 | localStorage | tus HEAD/PATCH, S3 ListParts | 진행 상태 보관, 미완료 정리 | |
| 다운로드 재개 | 브라우저 기본 다운로드에 맡김 | — | 브라우저 다운로드 관리자 | Range / 206 / If-Range | Range + 강한 ETag (또는 CDN·S3 위임) | |
| 망 전환 | 재연결 후 재검증 | — | — | QUIC connection migration | HTTP/3, LB connection ID 라우팅 |
우선순위
Tier 1 · 대부분의 서비스
Query 캐시·staleTime, 재시도할 오류 구분, 타임아웃, Idempotency-Key, ETag/304, 상태 지정 API, If-Match, cursor 페이지네이션, 요청 취소, ProblemDetail
Tier 2 · 모바일·불안정 네트워크 비중이 큰 서비스
오프라인 큐와 영속화, 202 + 폴링·SSE, BFF와 부분 응답, 지터·레이트 리밋, 재연결 폭주 대비
Tier 3 · 대용량·특수 요구
tus·S3 multipart 이어 올리기, 다운로드 Range/If-Range, HTTP/3 connection migration, Background Sync, 충돌 해결 UI
이 문서는 2026년 9월 기준 공식 문서(TanStack Query, Spring Framework, Uppy, tus, AWS), IETF RFC·Internet-Draft, WHATWG·MDN을 우선해 조사했습니다. 각 장 하단에 출처가 있습니다. 시뮬레이터 수치는 관찰용 가정값입니다.