1*J5URBSRh3kOK22AnX-isqQ.png

오프라인시 웹뷰가 꺼지는 문제를 제기하고, 해결한 이야기

기술 스택: React Query + Service Worker + Workbox


문제 제기

“이거 문제 아닌가요?”

스타트업 프론트엔드 개발자로 일하며 우리 제품을 자주 테스트했습니다.

어느 날, 지하 주차장에서 앱을 켰을 때 이상함을 느꼈습니다.

네이티브 앱 vs 웹뷰

네이티브 앱으로 구현된 부분은 정상작동했고 심지어 오프라인에서 업로드 예약 기능까지 구현되어있었습니다.

같은 제품인데 왜 경험이 다를까?

원인 분석

네이티브 팀 코드를 살펴보니, 이미 오프라인 처리가 완벽하게 구현되어 있었습니다.

웹뷰는? 아무 처리도 안 되어 있었습니다.

우리팀은 앱 개발 속도를 높이고, 앱을 프론트 개발자가 병렬적으로 작업하고자 웹뷰를 채택했었습니다


💬 문제 제기: “웹뷰도 오프라인 지원이 필요합니다”

건설 현장이라는 우리 도메인의 특수성을 생각했습니다:

건설 현장의 현실:
• 지하층 작업 - 신호 약함
• 산간 지역 공사 - 네트워크 불안정
• 대형 건물 내부 - 와이파이 죽은 구간
• 이동 중 네트워크 전환 - 일시적 끊김

비즈니스적 가치로도 충분히 도입해볼만 했습니다.

네트워크 애러로 인한 이탈률을 줄 일 수 있고 로딩 대기시간을 줄여 작업자들의 효율을 높이며 , 네이티브와 동일한 UX가치를 제공해주는 것입니다 .

특히 저희 대부분의 사용자는 현장의 소장님이기에 가장 간단한 UX와 느린 인터넷을 고려해야했습니다.

( “ 3G 또는 오래된 기종을 사용하시는 작업자분도 많으시다는 도메인적 특징이 …” )

따라서 바로 열심히 문서화를 해서 테크리드에게 말씀드렸습니다!

테크리드의 결정

테크리드: "좋은 지적입니다. 하지만..."
나: "네?"
테크리드: "다른 사람들이 이해할 수 있게 충분히 문서화 할 수 있겠어요 ?
				 가능하다면 웹뷰 오프라인 기능, 당신이 맡아주세요."
나: "...네!!!"
기한: 2주
요구사항:
- 네이티브 수준의 오프라인 지원
- 기존 기능 동작 보장
- 보안 이슈 없어야 함

어쩌다보니 제품의 핵심 UX를 도맡게 되었습니다….

압박감이 상당했습니다


🏗️ 해결 설계: 어떻게 구현할까?

기술 조사

네이티브는 이미 해결했으니, 웹에서도 가능할 것입니다.

처음에는 오프라인 기능을 사용하면 리액트쿼리 캐싱과 프리펫치 적절히 하면 되지 않을까 ?

필요하다면 로컬스토리지나 인덱스db에 넣으면 어떨까 ? 라는 생각이었습니다.

허나 아래와 같은 이유로 기각했습니다

  1. ❌ LocalStorage — 용량 제한 (5MB), 동기식, 성능 이슈
  2. ❌ IndexedDB — 직접 구현하면 복잡도 높음

그러던중 PWA에 대해서 영상을 보게 되었고

서비스워커와 리액트 쿼리를 활용한 아키텍처를 구상하게 되었습니다.

  1. Service Worker + React Query — PWA 표준, 검증됨
  2. 문서화가 중요하다!!! 내가 평생 프로젝트를 가져갈 수 없기에 !

1_XJ6soYsuW5_HClEUEzmPqA.png

  • 문서화 작업만 이틀걸린것은 비밀 …

가장 중요한것은 사용자가 오래된 데이터를 보지 않으면서 적절히 캐싱이 되고 오프라인에서는 마지막 데이터가 보이도록 하는 아주 추상적인 요구사항을 만족 시키는 아키텍처입니다.


구현 과정

첫 시도

“일단 모든 걸 캐싱하면 되겠지?”

리액트 쿼리 없이 캐싱으로 모든것을 구현하는 아키텍처를 테스트 해봤습니다.

VitePWA({
  workbox: {
    runtimeCaching: [
      {
        urlPattern: /.*/,  // 모든 요청 캐싱!
        handler: "CacheFirst",
      },
    ],
  },
});

모든것을 테스트 해본 결과 로딩 속도 3.2초 → 0.8초 (75% 개선) 오프라인에서도 작동 “대박, 이미 완성인데?”

허나 슬랙에서 온 메시지와 함께 자리로 찾아오신 …

QA: "로그인이 안 됩니다"
나: "뭐가 문제지...?"

A로그인후 캐싱이되어서 B로 로그인해도 A로 로그인되는 치명적인버그!!!! 긴급 롤백 후 다른 아키텍처를 찾게 되었습니다.

❌ 모든 것을 캐싱하면 보안 사고 !!

선택적 캐싱이 필요합니다 .. 특히 Auth 인증 관련 API는 캐싱 절대 금지!!


React Query 와 SW 도입

오프라인 환경에서도 안정적으로 데이터를 제공하기 위해,

애플리케이션 레벨(React Query)네트워크 레벨(Service Worker) 을 명확히 분리했습니다.

각 계층은 서로 다른 책임을 가집니다.

1️⃣ 애플리케이션 레벨 — React Query

React Query는 화면 단에서 데이터를 관리하고,

사용자에게 “즉시 반응하는 경험”을 제공합니다.

// queryClient.ts
export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,      // 5분 동안 신선 데이터로 간주
      gcTime: 30 * 60 * 1000,        // 30분 동안 캐시 유지
      networkMode: "offlineFirst",   // 오프라인 우선 전략
      refetchOnWindowFocus: true,    // 포커스 복귀 시 자동 갱신
      retry: false,                  // 네트워크 에러 시 재시도 비활성화
    },
  },
});

React Query는 Stale-While-Revalidate(SWR) 패턴으로 작동합니다.

즉, 캐시가 stale(만료) 상태이더라도 기존 데이터를 먼저 즉시 화면에 보여주고

백그라운드에서 최신 데이터를 받아오면 자동으로 UI를 갱신합니다.

이 덕분에 사용자는 로딩 없이 즉각적인 응답을 경험하면서도

항상 최신 상태를 유지할 수 있습니다.

2️⃣ 네트워크 레벨 — Service Worker (Workbox)

Service Worker는 네트워크 요청 단에서 동작하며, 네트워크 상태에 따라 캐시 또는 실제 API를 적절히 선택합니다.

// vite.config.ts
{
  urlPattern: /\/api\/.*/,             // API 요청만 필터링
  handler: "NetworkFirst",             // 최신 데이터 우선 전략
  options: {
    cacheName: "api-cache",            // API 캐시 이름
    networkTimeoutSeconds: 3,          // 3초 내 응답 없으면 캐시 사용
    expiration: {
      maxAgeSeconds: 60 * 60 * 24,     // 캐시 유지 기간: 1일
    },
  },
}

이 설정은 NetworkFirst 전략을 사용합니다. 즉, 가능한 한 네트워크에서 최신 데이터를 먼저 가져오되,

오프라인이거나 응답이 늦을 경우에는 캐시된 데이터를 즉시 제공합니다.

이를 통해 사용자는 네트워크가 불안정해도 서비스가 멈추지 않고 부드럽게 동작하는 경험을 하게 됩니다.

문제가 존재합니다 어떻게 민감한 데이터는 캐싱하지 않으면서 작업이 가능할까요 ?

팀 피드백: “여전히 로그인 API 응답이 캐싱되고 있습니다”

처음에는 이 문제를 백엔드에서 해결해야 하나 고민했습니다.

“response 헤더에 Cache-Control: no-cache를 추가해 달라”고 요청하면 해결될 수도 있었죠.

하지만 당시 백엔드팀은 이미 다른 업무로 바빴고,

이 문제를 해결하려면 백엔드와 프론트 모두의 수정이 필요했습니다.

“프론트에서만 해결할 수 있는 방법은 없을까?”

고민 끝에, 요청(request) 단계에서 캐싱 제어 신호를 직접 보내는 방법을 떠올렸습니다.

백엔드 응답이 아니라, 프론트 요청 헤더에 “X-No-Cache”: “true”를 추가하는 겁니다.

이 신호를 감지해서 Axios가 URL에 _noCache=1을 붙이면,

Service Worker가 해당 요청을 자동으로 캐싱 예외 처리하도록 만들 수 있죠.

즉,

“백엔드의 응답을 바꾸는 대신,프론트에서 ‘이건 캐시하지 마’라는 힌트를 URL에 붙이자”

라는 아이디어로 문제를 완전히 프론트엔드 내부에서 해결한 것입니다.

⚙️ 구현 단계별 설명

① Axios 인터셉터

요청 헤더에 “X-No-Cache”: “true”가 있으면,

Axios가 자동으로 _noCache=1 파라미터를 URL에 붙여줍니다.

publicAxiosInstance.interceptors.request.use((config) => {
  if (config.headers["X-No-Cache"] === "true") {
    const separator = config.url?.includes("?") ? "&" : "?";
    config.url = `${config.url}${separator}_noCache=1`;
  }
  return config;
});

👉 이렇게 하면 개발자가 별도로 URL 수정할 필요 없이

헤더 한 줄로 캐싱 예외를 선언할 수 있습니다.


② 실제 API 호출 예시

const postLogin = async (data) => {
  return publicAxiosInstance.post("/auth/login", data, {
    headers: { "X-No-Cache": "true" }
  });
};

👉 “X-No-Cache”: “true”만 추가하면

자동으로 /auth/login?_noCache=1 요청으로 바뀌고

해당 요청은 캐싱되지 않습니다.


③ Service Worker에서 필터링

{
  urlPattern: ({ url }) => {
    if (url.searchParams.has("_noCache")) {
      return false; // 캐싱 제외
    }
    return true;
  },
}

👉 Service Worker는 _noCache 파라미터가 붙은 요청을

절대 캐시 저장하지 않도록 필터링합니다.

“안전한 선택적 캐싱 완성”

구분캐싱설명/api/projects✅일반 API, 캐싱 가능/auth/login❌인증 관련, 캐싱 금지/auth/refresh❌토큰 재발급, 캐싱 금지/payment/complete❌결제 응답, 캐싱 금지

이로써 보안 이슈 없이 오프라인 캐싱 기능 유지가 가능해졌습니다.

코드 품질 — 글로벌 에러 처리

또한 전역적으로 네트워크 오류로 페이지를 아예 보여주지 못하는 경우를 처리 했습니다.

문제: 20개 페이지에 중복된 에러 처리 코드 리액트 쿼리의 전역 옵션으로 네트워크 오류일경우에만 throw 해줘서 전역 애러 바운더리에 새로 생성한 네트워크 바운더리를 적용시켰습니다.

// 반복되는 패턴...
const GalleryPage = () => {
  const { data, error, isLoading } = useGetGallery();
if (isLoading) return <LoadingSpinner />;
  if (error) { /* 에러 처리 로직 10줄 */ }
  return <GalleryView data={data} />;
};

해결: 한 곳에서 모든 에러 처리

// queryClient.ts
{
  throwOnError: (error, query) => {
    if (!isNetworkError(error)) return false;
    const hasCache = query.state.data !== undefined;
    return !hasCache;  // 캐시 없을 때만 throw
  }
}

// GlobalErrorProvider.tsx
export const GlobalErrorProvider = ({ children }) => {
  return (
    <ErrorBoundary FallbackComponent={GlobalErrorFallback}>
      {children}
    </ErrorBoundary>
  );
};

💡 핵심 인사이트

1. 도메인 이해의 중요성

기술만 안다고 해결되지 않습니다.

일반 웹앱:
- 사무실 환경
- 안정적 네트워크
- 오프라인 지원 불필요

건설 현장 앱:
- 지하/산간 지역
- 불안정한 네트워크
- 오프라인 지원 필수 ✅

“우리 도메인의 특수성을 이해하고, 그에 맞는 기술을 선택해야 한다”


2. 비즈니스 가치로 말하기

테크리드를 설득하는 방법을 배웠습니다.

❌ “Service Worker를 써서 PWA를 만들고 싶어요”

“현재 네트워크 에러로 월 웹이 꺼진것 처럼 보입니다 다양한 시나리오를 가져왔습니다.“

오프라인 지원으로 이를 해결할 수 있습니다. 네이티브는 이미 구현되어 있고, 웹뷰만 미지원 상태입니다.”

결과: 즉시 승인 + 리소스 할당

“기술 이야기가 아니라, 비즈니스 가치를 문서로 말해야 한다”


3. 능동적 문제 제기

스타트업에서 가장 중요한 점이라고 생각합니다.

빠른 개발을 하다보면 놓치는 경우가 많기 때문입니다.

❌ “이건 제 업무가 아닌데요”

❌ “누가 시키면 하죠”

❌ “네이티브 팀이 해야죠”

✅ “이거 문제인 것 같은데, 제가 해결해볼게요”

✅ “더 나은 방법이 있을 것 같아요”

✅ “제가 리서치해보겠습니다”


🎓 기술적 성취

⚙️ networkMode: “offlineFirst”의 진실

처음에는 “offlineFirst”라는 이름 때문에

“오프라인일 때만 캐시를 사용한다”고 오해했습니다.

하지만 실제 동작 방식은 전혀 다릅니다.

React Query의 실제 동작

networkMode는 “네트워크 요청이 실패했을 때 어떻게 처리할지”를 제어하는 옵션입니다.

즉, 캐싱 전략이 아니라 에러 처리 전략에 가깝습니다.

옵션설명”online”오프라인일 때 요청이 일시 정지(paused) 상태가 되고, 온라인 복귀 시 자동 재시작”always”네트워크 상태와 관계없이 항상 요청 시도, 실패 시 즉시 에러 발생”offlineFirst”항상 네트워크 요청을 시도하지만, 실패했을 때 캐시가 있다면 에러로 처리하지 않음

즉, “offlineFirst”는 다음과 같이 동작합니다:

  1. 온라인/오프라인 여부와 상관없이 항상 네트워크 요청을 시도
  2. 요청 실패 시, 기존 캐시가 있으면 에러를 발생시키지 않음
  3. 캐시도 없을 경우에만 요청을 일시정지

🌐 VitePWA(Workbox)의 네트워크 전략과의 차이점

VitePWA(또는 Workbox)의 네트워크 전략은 이름이 비슷하지만

의미와 목적이 완전히 다릅니다.

여기서는 요청의 **“데이터 소스 우선순위”**를 결정합니다.

전략설명CacheFirst캐시가 있으면 네트워크를 건드리지 않고 캐시를 바로 반환 (정적 리소스에 적합)NetworkFirst네트워크를 먼저 시도하고, 실패 시 캐시를 사용 (API 요청에 적합)StaleWhileRevalidate캐시된 데이터를 즉시 보여주고, 백그라운드에서 최신 데이터를 받아 캐시 갱신

  • React Query의 networkMode는 “에러 처리” 중심이고,
  • Workbox의 네트워크 전략은 “데이터 로딩 방식” 중심입니다.

📘 정리

**구분React QueryWorkbox (VitePWA)**개념네트워크 실패 시 처리 방식캐시/네트워크 우선순위 결정”offlineFirst” 의미네트워크 실패 시 캐시 있으면 에러로 간주하지 않음(존재하지 않음, 혼동 금지)”NetworkFirst” 의미✖️ 없음네트워크 우선, 실패 시 캐시 사용”StaleWhileRevalidate” 대응React Query의 기본 동작 패턴명시적 전략 중 하나

React Query는 애플리케이션 레벨, Workbox는 네트워크 레벨에서 데이터를 다루는 구조입니다.

두 가지 캐싱 전략

Service Worker (네트워크 레벨):

  • 정적: CacheFirst
  • 동적: NetworkFirst

React Query (애플리케이션 레벨):

  • staleTime 이내: 즉시 반환
  • staleTime 이후: 캐시 + 백그라운드 갱신

⚠️ 에러는 여러 계층을 거친다

Service Worker → Axios → React Query → ErrorBoundary → UI

에러는 이렇게 여러 계층을 순차적으로 통과하며 전파됩니다.

이 중 하나라도 적절히 처리하지 않으면,

에러가 상위로 전달되지 않고 “조용히 사라지는” 문제가 발생합니다.

이번에 겪었던 Workbox 이슈가 바로 그 예였습니다.

Service Worker에서 발생한 no-response 에러가 Axios나 React Query로 전달되지 않아

결국 사용자 입장에서는 **“무한 로딩”**으로 보이는 현상이 발생했습니다.

💡 핵심 교훈:네트워크, 요청, 상태, UI — 어느 한 계층이라도 놓치면 에러는 눈에 보이지 않게 된다


가장 중요한 교훈

기술적 구현 능력도 중요하지만, 그보다 중요한 것은:

📖 도메인 이해 였습니다. 회사의 가치를 높이기 위해서는 우리 사용자의 환경을 아는 것이 중요합니다.

💼 비즈니스 가치 — 결국 아무리 좋은 기술이라도 가치가 되지 않으면 무용지물입니다. 기술을 가치로 번역하는 것

🔍 능동적 태도 — 문제를 미리 정의하고 해결하려는 오너십이 중요하다고 생각합니다.

“좋은 개발자는 기술을 아는 사람이고, 훌륭한 개발자는 문제를 발견하고 해결하는 사람이다”


“최고의 성능은 네트워크 요청을 하지 않는 것이고,최고의 에러 처리는 사용자가 상황을 이해하는 것이다”— Jake Archibald (Google)


🔗 참고 자료

공식 문서:

추천 읽을거리: