Skip to content

Fix(client): FCM알람 커스텀 및 api instance 수정#162

Merged
jllee000 merged 2 commits intodevelopfrom
fix/#161/fcm-setting-edit
Sep 29, 2025
Merged

Fix(client): FCM알람 커스텀 및 api instance 수정#162
jllee000 merged 2 commits intodevelopfrom
fix/#161/fcm-setting-edit

Conversation

@jllee000
Copy link
Collaborator

@jllee000 jllee000 commented Sep 29, 2025

📌 Related Issues

관련된 Issue를 태그해주세요. (e.g. - close #25)

📄 Tasks

  1. FCM 푸시알람 형태 수정
  2. FCM 정적 파일은 예외 처리
  3. api instance수정 : 요청,응답 모두 토큰 재발급 로직 사용하도록

⭐ PR Point (To Reviewer)

📷 Screenshot

Summary by CodeRabbit

  • 버그 수정
    • 백그라운드 푸시 알림 처리 방식을 개선해 제목·본문 표시의 일관성과 수신 안정성을 높였습니다.
    • 클라이언트와 확장프로그램에서 요청 시 토큰 자동 갱신 및 예외 처리를 강화해 인증 오류를 줄이고, 필요 시 온보딩으로 자연스럽게 안내합니다. 로그인 유지성이 향상되어 불필요한 재로그인이 감소합니다.
  • Chores
    • firebase-messaging-sw.js가 안정적으로 제공되도록 배포 리라이트 설정을 추가했습니다.

@jllee000 jllee000 self-assigned this Sep 29, 2025
@vercel
Copy link

vercel bot commented Sep 29, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
pinback-client-client Ready Ready Preview Comment Sep 29, 2025 1:23pm
pinback-client-landing Ready Ready Preview Comment Sep 29, 2025 1:23pm

@coderabbitai
Copy link

coderabbitai bot commented Sep 29, 2025

Walkthrough

FCM service worker now uses Firebase Messaging’s onBackgroundMessage for notifications. Axios request interceptors in client and extension were reworked to conditionally fetch/refresh tokens using stored email, handle failures by clearing storage and redirecting to onboarding, and set Authorization headers accordingly. Vercel rewrite was added for the service worker path.

Changes

Cohort / File(s) Summary
FCM service worker
apps/client/public/firebase-messaging-sw.js
Initialize Firebase/Messaging; replace manual push listener with messaging.onBackgroundMessage; show notifications using payload.notification.title/body; updated activate log string.
Vercel rewrites
apps/client/vercel.json
Add rewrite for /firebase-messaging-sw.js to ensure service worker is served at root path.
Client Axios interceptor
apps/client/src/shared/apis/setting/axiosInstance.ts
Request interceptor now: reads email, attempts refreshToken(email) when needed, stores/attaches Bearer token, on failure clears storage and redirects to /onboarding; logs and throws when email/token absent.
Extension Axios interceptor
apps/extension/src/apis/axiosInstance.ts
Guard token logic behind isNoAuth; if auth required and email exists, fetch/attach token; on error clear storage, redirect to onboarding, and rethrow; throw explicit error when email missing.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User Device
  participant SW as Service Worker (client)
  participant FM as Firebase Messaging
  participant NB as Notification UI

  U->>FM: Receives push via FCM
  FM-->>SW: Background message payload
  Note over SW: onBackgroundMessage handler (new)
  SW->>NB: showNotification(title, body)
  NB-->>U: Displays notification
Loading
sequenceDiagram
  autonumber
  participant App as Client/Extension App
  participant AX as Axios Request Interceptor
  participant LS as localStorage
  participant AUTH as Auth API (refreshToken)
  participant NAV as Router/Location

  App->>AX: Outgoing request
  AX->>AX: Check isNoAuth
  alt Auth required
    AX->>LS: Get email (+possibly token)
    alt Email present
      AX->>AUTH: refreshToken(email)
      alt Success
        AUTH-->>AX: New token
        AX->>LS: Store token
        AX->>AX: Set Authorization: Bearer <token>
      else Failure
        AX->>LS: Clear token/email
        AX->>NAV: Redirect to /onboarding
        AX-->>App: Throw error
      end
    else No email
      AX-->>App: Throw "토큰이 없습니다..." and/or redirect
    end
  else No-auth path
    AX->>AX: Bypass token logic
  end
  AX-->>App: Proceed with request
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

fix, frontend, ⚙️ Setting

Suggested reviewers

  • jjangminii
  • constantly-dev

Poem

삐빅—푸시 왔나? 토끼가 먼저 뛰었지!
숲속 서버에서 메시지 톡, 알림도 착.
토큰은 싱싱, 없으면 온보딩 띵동.
길 잃은 쿠키는 툭툭 털고 재발급!
오늘도 빌드 숑—알림 한 봉지, 평화 한 줌. 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Linked Issues Check ⚠️ Warning 본 PR은 FCM 알람 커스텀을 위한 #161의 요구사항을 충족하지만 디자인 시스템의 Progress 컴포넌트 구현을 요구하는 #25의 과제는 전혀 다루지 않아 목표를 완전하게 만족하지 못합니다. 이 PR에서 #25 관련 작업을 구현하거나 해당 이슈 연결을 제거하여 관련성을 명확히 정리해주세요.
Out of Scope Changes Check ⚠️ Warning 클라이언트 및 익스텐션의 axiosInstance 관련 토큰 재발급 로직 추가는 FCM 커스텀(#161)이나 Progress 컴포넌트(#25) 구현과 무관한 별도 기능으로 PR 범위를 벗어납니다. 토큰 재발급 로직 변경은 별도 PR로 분리하거나 해당 변경을 scope에 맞게 조정해주세요.
✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed 제목은 PR의 주요 변경사항인 FCM 알람 커스텀 및 API 인스턴스 수정을 명확하게 요약하고 있어 의도에 부합하며 적절한 컨벤션을 따르고 있습니다.
Description Check ✅ Passed PR 설명은 Related Issues와 Tasks가 올바르게 작성되어 템플릿에 충실하지만 PR Point와 Screenshot 섹션이 비어있어 제거하거나 내용을 보강하는 편이 좋습니다.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/#161/fcm-setting-edit

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jllee000 jllee000 added feat 기능 개발하라 개발 달려라 달려 fix 버그 수정하라 러브버그 labels Sep 29, 2025
@github-actions
Copy link

✅ Storybook chromatic 배포 확인:
🐿️ storybook

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/extension/src/apis/axiosInstance.ts (1)

76-79: 하드코드된 이메일로 토큰 재발급 시도 — 사용자 계정 오염/오동작 위험.

응답 인터셉터에서 fetchToken('test@gmail.com')은 치명적입니다. 저장된 이메일로 재발급하도록 수정하세요. 이메일이 없으면 온보딩 유도 로직으로 위임하세요.

-      const newToken = await fetchToken('test@gmail.com');
+      // chrome.storage에서 이메일 조회
+      const email = await new Promise<string | undefined>((resolve) => {
+        chrome.storage.local.get('email', (result) => resolve(result.email));
+      });
+      if (!email) {
+        return Promise.reject(new Error('이메일 정보가 없어 토큰 재발급에 실패했습니다. 온보딩을 먼저 완료해주세요.'));
+      }
+      const newToken = await fetchToken(email);
       originalRequest.headers.Authorization = `Bearer ${newToken}`;
       return apiRequest(originalRequest);

형 안전성을 위해 _retry 속성 타입 확장도 권장합니다(별도 d.ts):

// types/axios.d.ts
import 'axios';
declare module 'axios' {
  interface InternalAxiosRequestConfig {
    _retry?: boolean;
  }
}
🧹 Nitpick comments (10)
apps/client/public/firebase-messaging-sw.js (1)

14-16: 활성화 단계에서 클라이언트 즉시 장악 권장.

새 SW가 설치되면 바로 페이지를 제어하도록 clients.claim()을 호출해 주세요. 업데이트 전파가 더 빨라집니다.

 self.addEventListener('activate', function () {
-  console.log('실행중..');
+  console.log('실행중..');
+  self.clients && self.clients.claim && self.clients.claim();
 });
apps/extension/src/apis/axiosInstance.ts (2)

34-38: 매 요청 전 저장된 토큰 로드가 불필요합니다. 즉시 재발급으로 덮어씌워집니다.

이 구간에서 읽은 token은 곧바로 fetchToken(email) 결과로 대체되어 의미가 없습니다. 제거해도 동작 동일하며 비용을 줄일 수 있습니다.

-  let token = await new Promise<string | undefined>((resolve) => {
-    chrome.storage.local.get('token', (result) => {
-      resolve(result.token);
-    });
-  });
+  let token: string | undefined;

25-29: isNoAuth 이중 체크 정리로 가독성 개선.

이미 Line 28에서 early return 하므로 Line 40의 if (!isNoAuth)는 항상 참 전제입니다. 조건 중복을 제거하면 흐름이 명확해집니다.

-  if (isNoAuth) return config;
+  if (isNoAuth) return config;
   // ...
-  if (!isNoAuth) {
+  {
     // 본문 동일
   }

Also applies to: 40-41

apps/client/vercel.json (1)

3-6: firebase-config.js·firebase_sdk 리라이트 제거 및 SW no-cache 헤더 추가
앱 디렉토리(apps/client/public)에서 해당 파일/폴더가 발견되지 않아 해당 리라이트는 불필요합니다. 대신 firebase-messaging-sw.jsCache-Control: no-cache 헤더를 설정해 SW 업데이트를 보장하세요.

apps/client/src/shared/apis/setting/axiosInstance.ts (6)

57-58: 헤더 대입 시 타입/정규화 보장 필요

Axios는 헤더 키를 정규화합니다. 안전하게 동작하도록 headers 초기화 + 대괄호 표기 사용을 권장합니다.

-    config.headers.Authorization = `Bearer ${token}`;
+    config.headers = config.headers ?? {};
+    (config.headers as any)['Authorization'] = `Bearer ${token}`;

11-32: refreshToken은 전용 axios 인스턴스를 사용하여 인터셉터 재귀를 원천 차단하세요.

기본 axios에 다른 곳에서 인터셉터가 붙으면 재귀/루프가 생길 수 있습니다. 타임아웃도 설정 권장.

예시:

const authClient = axios.create({
  baseURL: import.meta.env.VITE_BASE_URL,
  headers: { 'Content-Type': 'application/json' },
  timeout: 5000,
});

// 내부에서 axios 대신 authClient 사용
const response = await authClient.get('/api/v1/auth/token', { params: { email } });

53-55: 이메일 없음 분기에서도 일관되게 정리/리다이렉트 권장

요청 인터셉터의 실패 분기(라인 46-51)는 정리 후 리다이렉트하지만, 이 분기는 단순 throw만 합니다. UX 일관성을 위해 동일 처리 권장.

-      console.error('토큰이 없습니다. 온보딩을 먼저 완료해주세요.');
-      throw new Error('토큰이 없습니다. 온보딩을 먼저 완료해주세요.');
+      console.error('토큰/이메일이 없습니다. 온보딩을 먼저 완료해주세요.');
+      localStorage.removeItem('token');
+      localStorage.removeItem('email');
+      window.location.href = '/onboarding';
+      throw new Error('인증 정보 없음');

22-24: 보안 관점: 액세스 토큰의 localStorage 보관은 XSS 노출면이 큽니다.

장기적으로 httpOnly/secure 쿠키(또는 WebAuthn 등)로 전환을 검토하세요. 당장은 CSP 강화, DOM XSS 가드(라이브러리/린팅) 적용을 권장합니다.


36-37: noAuthNeeded 중복 선언 제거 제안

두 인터셉터에서 동일 상수를 중복 정의하고 있습니다. 모듈 상단으로 끌어올려 단일 소스로 관리하면 누락/드리프트를 방지할 수 있습니다.

const NO_AUTH_PATHS = ['/api/v1/auth/token', '/api/v1/auth/signup'];
// ...요청/응답 인터셉터에서 NO_AUTH_PATHS 사용

Also applies to: 68-71


63-105: 응답 인터셉터: _retry 타입 보강·헤더 초기화 권장

  • src/types/axios.d.ts에 아래 선언 추가해 AxiosRequestConfig를 확장하세요.
    declare module 'axios' {
      export interface AxiosRequestConfig {
        _retry?: boolean;
      }
    }
  • 오류 발생 시 originalRequest.headersundefined일 수 있어, 다음처럼 초기화 후 대괄호 표기법으로 설정하세요.
    - originalRequest.headers.Authorization = `Bearer ${newToken}`;
    + originalRequest.headers = originalRequest.headers ?? {};
    + (originalRequest.headers as any)['Authorization'] = `Bearer ${newToken}`;
  • 코드베이스 전반을 확인한 결과 이 파일 외에 전역 request/response 인터셉터가 없어 재귀 호출 위험은 없습니다.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae83b9a and 67c390a.

📒 Files selected for processing (4)
  • apps/client/public/firebase-messaging-sw.js (1 hunks)
  • apps/client/src/shared/apis/setting/axiosInstance.ts (1 hunks)
  • apps/client/vercel.json (1 hunks)
  • apps/extension/src/apis/axiosInstance.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/client/public/firebase-messaging-sw.js (1)
apps/client/src/firebase-config.ts (1)
  • firebaseConfig (1-9)
🪛 GitHub Check: lint
apps/extension/src/apis/axiosInstance.ts

[warning] 45-45:
Unexpected console statement

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: storybook
🔇 Additional comments (2)
apps/extension/src/apis/axiosInstance.ts (2)

45-50: 스토리지 정합성 및 확장 환경용 리디렉션 방식 적용 필요

  • 토큰·이메일 삭제는 localStorage가 아니라 chrome.storage.local.remove(['token','email'])로 통일하고, 로깅은 import.meta.env.DEV에서만 남기세요.
  • window.location.href 대신 확장 API(chrome.runtime.openOptionsPage() 또는 chrome.tabs.create({ url: chrome.runtime.getURL('index.html#/onboarding') }))를 사용해 리디렉션하고, 실제 라우팅 방식(내장 해시 라우터 vs 외부 온보딩 페이지 https://www.pinback.today/onboarding) 및 배포 구조에 맞춰 경로를 확정해야 합니다.

45-45: ESLint 설정에서 no-console 규칙 확인 필요
CI 환경에서 console.error가 lint 에러로 처리되는지, 사용 중인 packages/eslint-config/base.jsrules.no-console 설정을 검토하세요. 적용된 규칙에 따라 개발 모드 조건부 래핑(import.meta.env.DEV)을 도입하거나 로거 유틸로 대체해야 합니다.

Comment on lines +4 to +8
importScripts(
'./firebase_sdk/firebase-app-compat.js',
'./firebase_sdk/firebase-messaging-compat.js',
'./firebase-config.js'
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

firebase-config 로딩 방식 점검 필요 (런타임 ReferenceError 위험).

importScripts('./firebase-config.js')는 글로벌 스코프에 firebaseConfig 변수가 정의되어 있어야 합니다. 현재 레포에는 src/firebase-config.ts만 보이며, 번들 산출물이 public/firebase-config.js로 생성되지 않으면 SW에서 firebaseConfig 참조 시 즉시 실패합니다. 또한 이 파일과 firebase_sdk/*가 SPA 캐치올 리라이트의 영향을 받지 않는지 확인이 필요합니다.

다음으로 존재/형식을 확인해 주세요(전역 변수 할당 여부까지 점검):

가능하면 public/firebase-config.js는 다음과 같이 전역에 주입되도록 유지하세요.

// apps/client/public/firebase-config.js
self.firebaseConfig = { /* ...env에서 주입된 값... */ };

🏁 Script executed:

#!/bin/bash
# public/firebase-config.js 유무 및 전역 변수 정의 확인
if fd -a '^firebase-config\.js$' apps/client/public >/dev/null; then
  echo "[OK] apps/client/public/firebase-config.js 존재"
  echo "---- 파일 앞부분 ----"
  head -n 20 apps/client/public/firebase-config.js || true
  echo "--------------------"
else
  echo "[ERROR] apps/client/public/firebase-config.js 없음"
fi

# SDK 파일 존재 확인
fd -a '^firebase-app-compat\.js$' apps/client/public/firebase_sdk
fd -a '^firebase-messaging-compat\.js$' apps/client/public/firebase_sdk

Length of output: 418


firebase-config.js 및 SDK 파일 경로와 전역 변수 정의를 확인하려면 아래 스크립트를 실행해 주세요.


🏁 Script executed:

#!/bin/bash
echo "=== firebase-config.js 위치 및 내용 확인 ==="
config_path=$(fd -t f 'firebase-config.js' | head -n1)
if [ -n "$config_path" ]; then
  echo "[FOUND] $config_path"
  echo "---- 상위 20줄 ----"
  head -n 20 "$config_path"
  echo "---- 'firebaseConfig' 검색 ----"
  grep -R "firebaseConfig" "$config_path" || echo "[WARN] 'firebaseConfig' 할당 미발견"
else
  echo "[ERROR] firebase-config.js 파일을 찾을 수 없음"
fi

echo -e "\n=== firebase-app-compat.js 위치 ==="
fd -t f 'firebase-app-compat.js' || echo "[ERROR] 해당 파일 없음"

echo -e "\n=== firebase-messaging-compat.js 위치 ==="
fd -t f 'firebase-messaging-compat.js' || echo "[ERROR] 해당 파일 없음"

echo -e "\n=== firebase_sdk 디렉터리 및 내부 파일 목록 확인 ==="
sdk_dir=$(fd -t d 'firebase_sdk' | head -n1)
if [ -n "$sdk_dir" ]; then
  echo "[FOUND DIR] $sdk_dir"
  echo "---- 내부 파일 ----"
  ls "$sdk_dir" || true
else
  echo "[ERROR] firebase_sdk 디렉터리를 찾을 수 없음"
fi

Length of output: 228


apps/client/public에 firebase-config.jsfirebase-sdk 파일이 없음
importScripts에서 참조하는 firebase-config.js, firebase-app-compat.js, firebase-messaging-compat.js 파일이 존재하지 않아 서비스 워커 로딩 시 ReferenceError가 발생합니다. 빌드 프로세스가 해당 파일들을 apps/client/public에 산출하도록 설정하거나, importScripts 경로를 올바르게 조정하세요.

🤖 Prompt for AI Agents
In apps/client/public/firebase-messaging-sw.js around lines 4 to 8, the
importScripts references firebase-config.js, firebase-app-compat.js, and
firebase-messaging-compat.js which are missing and cause a ReferenceError when
the service worker loads; fix by ensuring the build step outputs those files
into apps/client/public (or copy them into that directory), or update the
importScripts paths to point to the correct existing locations (e.g., adjust to
/static/, /assets/, or correct relative path) and verify the files are served by
the dev/prod server so the service worker can successfully import them.

Comment on lines +21 to 29
messaging.onBackgroundMessage((payload) => {
console.log('Received background message ', payload);

const notificationTitle = payload.notification?.title ?? '알림이 도착했어요!';
const notificationOptions = {
body: resultData.body,
body: payload.notification?.body,
};
console.log(resultData.title, {
body: resultData.body,
});
e.waitUntil(
self.registration.showNotification(notificationTitle, notificationOptions)
);
self.registration.showNotification(notificationTitle, notificationOptions);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

onBackgroundMessage는 “data‑only” 메시지에만 호출됩니다. click 핸들러와 data fallback을 추가하세요.

payload.notification 필드가 포함된 푸시는 브라우저가 자체 표시하고 SW 콜백이 호출되지 않는 경우가 많습니다. 커스텀 UI가 필요하면 서버에서 data‑only 형태로 전송하고, SW에서는 payload.data도 처리하세요. 또한 알림 클릭 시 목적지로 이동하도록 notificationclick 핸들러를 추가하세요.

 messaging.onBackgroundMessage((payload) => {
   console.log('Received background message ', payload);

-  const notificationTitle = payload.notification?.title ?? '알림이 도착했어요!';
-  const notificationOptions = {
-    body: payload.notification?.body,
-  };
-  self.registration.showNotification(notificationTitle, notificationOptions);
+  const titleFromData = payload.data && (payload.data.title || payload.data.notificationTitle);
+  const bodyFromData = payload.data && (payload.data.body || payload.data.notificationBody);
+  const notificationTitle =
+    payload.notification?.title ?? titleFromData ?? '알림이 도착했어요!';
+  const notificationOptions = {
+    body: payload.notification?.body ?? bodyFromData ?? '',
+    data: payload.data || {},
+    // 아이콘/배지 등 필요 시 추가
+    // icon: '/icons/icon-192.png',
+    // badge: '/icons/badge-72.png',
+  };
+  self.registration.showNotification(notificationTitle, notificationOptions);
 });
+
+// 알림 클릭 시 포커스/열기
+self.addEventListener('notificationclick', (event) => {
+  event.notification.close();
+  const url = event.notification?.data?.url || event.notification?.data?.click_action || '/';
+  event.waitUntil(
+    (async () => {
+      const allClients = await clients.matchAll({ type: 'window', includeUncontrolled: true });
+      const existing = allClients.find((c) => c.url.includes(url));
+      if (existing) return existing.focus();
+      return clients.openWindow(url);
+    })()
+  );
+});

Comment on lines +40 to +51
let token = localStorage.getItem('token');
const email = localStorage.getItem('email');
if (email) {
try {
token = await refreshToken(email);
} catch (err) {
console.error('요청 인터셉터에서 토큰 재발급 실패:', err);
localStorage.removeItem('token');
localStorage.removeItem('email');
window.location.href = '/onboarding';
throw err;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

매 요청마다 토큰을 재발급하고 있어 서버 부하 및 중복 호출 위험이 큽니다.

email 존재 시 무조건 refreshToken을 호출해 모든 요청이 재발급을 트리거합니다. 동시 다발 요청 시 토큰 재발급이 중복으로 발생하고, 응답 인터셉터의 재시도와 겹쳐 지연 및 불필요 트래픽이 생깁니다. 토큰이 없거나 만료(또는 만료 임박)한 경우에만 재발급하고, 단일 진행(single-flight)으로 동시 요청을 합치는 게 안전합니다.

아래처럼 만료 검사 + 단일 진행으로 수정 제안합니다(요청 인터셉터 부분만 diff). 헬퍼는 별도 코드 블록 참고.

   if (!isNoAuth) {
-    let token = localStorage.getItem('token');
-    const email = localStorage.getItem('email');
-    if (email) {
-      try {
-        token = await refreshToken(email);
-      } catch (err) {
-        console.error('요청 인터셉터에서 토큰 재발급 실패:', err);
-        localStorage.removeItem('token');
-        localStorage.removeItem('email');
-        window.location.href = '/onboarding';
-        throw err;
-      }
-    } else {
-      console.error('토큰이 없습니다. 온보딩을 먼저 완료해주세요.');
-      throw new Error('토큰이 없습니다. 온보딩을 먼저 완료해주세요.');
-    }
-
-    config.headers.Authorization = `Bearer ${token}`;
+    let token = localStorage.getItem('token');
+    if (!token || isTokenExpired(token)) {
+      const email = localStorage.getItem('email');
+      if (!email) {
+        console.error('토큰/이메일이 없습니다. 온보딩을 먼저 완료해주세요.');
+        localStorage.removeItem('token');
+        localStorage.removeItem('email');
+        window.location.href = '/onboarding';
+        throw new Error('인증 정보 없음');
+      }
+      try {
+        if (!refreshPromise) {
+          refreshPromise = refreshToken(email).finally(() => {
+            refreshPromise = null;
+          });
+        }
+        token = await refreshPromise;
+      } catch (err) {
+        console.error('요청 인터셉터에서 토큰 재발급 실패:', err);
+        localStorage.removeItem('token');
+        localStorage.removeItem('email');
+        window.location.href = '/onboarding';
+        throw err;
+      }
+    }
+    config.headers = config.headers ?? {};
+    (config.headers as any)['Authorization'] = `Bearer ${token}`;
   }

헬퍼/전역 보조 코드(파일 상단 아무 곳에 추가):

let refreshPromise: Promise<string> | null = null;

function isTokenExpired(token: string, skewSec = 60): boolean {
  try {
    const [, payload] = token.split('.');
    const data = JSON.parse(atob(payload));
    if (!data?.exp) return false;
    const now = Math.floor(Date.now() / 1000);
    return data.exp - now <= skewSec;
  } catch {
    // 파싱 실패 시 보수적으로 만료로 간주하여 재발급 유도
    return true;
  }
}
🤖 Prompt for AI Agents
In apps/client/src/shared/apis/setting/axiosInstance.ts around lines 40-51, the
interceptor unconditionally calls refreshToken for every request which causes
redundant refreshes and server load; change it to only call refreshToken when
there is no token or the token is expired/about-to-expire using an
isTokenExpired(token, skewSec) check, and implement a single-flight refresh
using a module-level refreshPromise: if a refresh is already in progress await
that promise instead of starting a new one, on success store the new token in
localStorage and use it for the request, and on failure clear token/email, reset
refreshPromise to null, redirect to onboarding and rethrow the error; ensure
refreshPromise is cleared on both success and failure so subsequent requests can
retry.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 기능 개발하라 개발 달려라 달려 fix 버그 수정하라 러브버그

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Fix] FCM 커스텀 및 연동 수정

1 participant