Skip to content

feat: 회원 API 연결#30

Merged
ccconac merged 12 commits intodevfrom
feat/25-user-api
Dec 31, 2025
Merged

feat: 회원 API 연결#30
ccconac merged 12 commits intodevfrom
feat/25-user-api

Conversation

@ccconac
Copy link
Copy Markdown
Member

@ccconac ccconac commented Dec 31, 2025

✅ 체크리스트

  • 로그인 API 연결
  • 회원 정보 API 연결
  • 회원 탈퇴 API 연결
  • 로그아웃 임시 구현

📝 작업 상세 내용

회원 관련 API 연결이 모두 완료되었습니다. 빠른 배포를 위해 급하게 작업하느라 코드가 굉장히 더럽습니다. (...) 1차 MVP 구현이 모두 완료된 이후 리팩토링 예정입니다.

🔎 후속 작업 (선택 사항)

전반적인 코드 리팩토링 필요

✅ 셀프 체크리스트

  • 브랜치 확인하기
  • 불필요한 코드가 들어가지 않았는지 재확인하기
  • issue 닫기
  • reiewers, assignees, Lables 등록 확인하기

이슈 번호: #25

Summary by CodeRabbit

  • 새 기능
    • Kakao 소셜 로그인 및 콜백 처리 페이지 추가
    • 전역 인증 컨텍스트 도입(로그인 상태 및 로그아웃 관리)
    • 네비게이션 바에 사용자 메뉴 추가(로그아웃·계정 탈퇴)
    • 사용자 정보 조회·계정 삭제 API 통합
    • 로컬 토큰 저장소 및 API 요청 시 토큰 자동 첨부 구현

✏️ Tip: You can customize this high-level summary in your review settings.

@ccconac ccconac linked an issue Dec 31, 2025 that may be closed by this pull request
4 tasks
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 31, 2025

Caution

Review failed

The pull request is closed.

📝 Walkthrough

워크스루

AuthProvider와 토큰 저장소, API 인터셉터, Kakao OAuth 교환 흐름, 계정 API 및 네비게이션 UI 통합이 추가되어 앱 전반에 인증 상태 관리와 Kakao 로그인/탈퇴 플로우가 도입되었습니다.

변경 사항

코호트 / 파일(들) 변경 사항 요약
인증 컨텍스트 및 저장소
src/contexts/useAuth.tsx, src/lib/authStorage.ts
React AuthProvider 및 useAuth 훅 추가; 로컬 스토리지 기반 토큰 관리(authStorage.getAccessToken, setTokens, clearTokens) 구현
인증 API 타입 및 엔드포인트
src/apis/auth/auth.types.ts, src/apis/auth/account.ts, src/apis/auth/kakao.ts
KakaoLoginResponse, UserInfoResponse, AccountDeleteResponse 타입 추가; postKakaoCode(code), fetchAccount(), deleteAccount() API 헬퍼 추가
HTTP 클라이언트 설정
src/config/api.ts
API_BASE_URL 상수 추가 및 axios 인스턴스에 authStorage로부터 읽은 accessToken을 Bearer Authorization 헤더로 자동 첨부하는 요청 인터셉터 추가
Kakao 콜백 처리 페이지
src/pages/Auth/KakaoCallbackPage.tsx
OAuth 콜백 수신 후 postKakaoCode 호출, 토큰 저장, 전역 사용자 상태(setUser) 갱신 및 홈으로 리다이렉트 구현
라우팅
src/routes.tsx
/auth/kakao/callback 라우트 등록
앱 루트 통합
src/App.tsx
App 컴포넌트 트리를 <AuthProvider>로 래핑하여 인증 컨텍스트 제공
네비게이션 바 및 다이얼로그
src/components/NavigationBar/NavigationBar.tsx, src/components/NavigationBar/_components/Dialog.tsx
로그인 버튼/유저 드롭다운 조건부 렌더링 추가; Kakao 로그인 리디렉션 핸들러, 계정 삭제(deleteAccount) 호출 및 로컬 토큰 정리/로그아웃 흐름 구현

시퀀스 다이어그램

sequenceDiagram
    participant User as 사용자(브라우저)
    participant App as App (AuthProvider)
    participant Callback as KakaoCallbackPage
    participant API as 백엔드 API
    participant Storage as localStorage
    participant Context as Auth Context

    rect rgb(220,240,255)
    note over User,App: 앱 초기 로드 — 인증 상태 복원
    User->>App: 페이지 로드
    App->>Storage: getAccessToken()
    alt 토큰 존재
        App->>API: fetchAccount()
        API-->>App: UserInfoResponse
        App->>Context: setUser(id, name)
    else 토큰 없음/실패
        App->>Storage: clearTokens()
        App->>Context: setUser(null)
    end
    end

    rect rgb(240,255,240)
    note over User,API: Kakao OAuth 로그인 플로우
    User->>App: "Kakao 로그인" 클릭 (네비게이션)
    App->>API: 리디렉션 -> Kakao 인증 페이지
    API->>User: Kakao 인증 페이지
    User->>API: 인증 후 콜백 (/auth/kakao/callback?code=...)
    User->>Callback: 콜백 페이지 로드
    Callback->>API: postKakaoCode(code)
    API-->>Callback: KakaoLoginResponse (accessToken, refreshToken, user)
    Callback->>Storage: setTokens(accessToken, refreshToken)
    Callback->>Context: setUser(id, name)
    Callback->>User: 홈으로 리다이렉트
    end

    rect rgb(255,240,240)
    note over User,API: 계정 삭제(탈퇴)
    User->>App: 사용자 메뉴 > 탈퇴
    App->>API: deleteAccount()
    API-->>App: AccountDeleteResponse
    App->>Storage: clearTokens()
    App->>Context: setUser(null)
    App->>User: 로그아웃 상태로 리다이렉트
    end
Loading

예상 코드 리뷰 노력

🎯 3 (Moderate) | ⏱️ ~25 minutes

관련 이슈

관련 PR

  • feat: 회원 API 연결 #30: AuthProvider, authStorage, API 인터셉터, Kakao 콜백 등 인증 관련 경로를 유사하게 수정하여 코드 레벨 상 직접적인 연관이 있습니다.
  • feat: Dialog 컴포넌트 구현 #20: NavigationBar의 Dialog 컴포넌트 변경과 겹치며, 본 PR에서 Login/Withdraw 핸들러를 확장합니다.

제안된 레이블

✨ FEAT

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목 '회원 API 연결'은 pull request의 주요 변경사항과 정확하게 일치합니다. 회원 관련 API(로그인, 회원정보, 회원탈퇴)를 연결하는 것이 이 PR의 핵심이며, 제목이 이를 명확하게 전달합니다.

📜 Recent review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ffd3f45 and ccb7d43.

📒 Files selected for processing (1)
  • src/components/NavigationBar/_components/Dialog.tsx

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

Copy link
Copy Markdown

@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: 7

🧹 Nitpick comments (8)
src/config/api.ts (1)

4-4: 환경 변수 유효성 검증이 필요합니다.

VITE_API_BASE_URL이 정의되지 않은 경우에 대한 처리가 없습니다. 런타임 에러를 방지하기 위해 유효성 검증을 추가하는 것을 권장합니다.

🔎 제안하는 수정사항
-export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
+export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
+
+if (!API_BASE_URL) {
+  throw new Error('VITE_API_BASE_URL is not defined in environment variables');
+}
src/apis/auth/auth.types.ts (1)

1-29: 타입 정의가 명확합니다.

인증 관련 응답 타입들이 잘 정의되어 있습니다.

향후 리팩토링 시 고려사항:

  • UserInfoResponsesuccess 필드를 포함하여 다른 응답 타입들과 구조가 일관되지 않습니다. 응답 구조 통일을 권장합니다.
  • Line 4의 role: 'USER'는 향후 관리자 등 다른 역할 추가 시 'USER' | 'ADMIN' 등으로 확장 필요할 수 있습니다.
src/apis/auth/account.ts (1)

4-14: API 함수 구현이 일관되고 명확합니다.

fetchAccountdeleteAccount 함수가 일관된 패턴으로 구현되어 있으며, 401 에러를 적절히 처리하고 있습니다.

향후 리팩토링 시 개선 권장사항:

  1. 에러 타입 개선: Line 9, 21의 error: anyunknown으로 변경하고 타입 가드 사용
  2. 타입 안전성: axios 에러 구조 체크를 위한 타입 가드 추가
🔎 참고: 타입 안전한 에러 처리 패턴
import { AxiosError } from 'axios';

export const fetchAccount = async (): Promise<UserInfoResponse | null> => {
  try {
    const { data } = await API.get<UserInfoResponse>('/api/accounts/me');
    return data;
  } catch (error) {
    if (error instanceof AxiosError && error.response?.status === 401) {
      return null;
    }
    throw error;
  }
};

Also applies to: 16-26

src/contexts/useAuth.tsx (1)

24-45: useEffect 정리 함수 추가 권장

컴포넌트가 마운트 해제되는 동안 비동기 작업이 진행 중인 경우, 마운트 해제된 컴포넌트에서 상태 업데이트가 시도될 수 있습니다. React 19에서는 이를 더 잘 처리하지만, 명시적인 정리 로직을 추가하면 코드 품질이 향상됩니다.

🔎 제안된 개선 사항
 useEffect(() => {
+  let isMounted = true;
   const accessToken = authStorage.getAccessToken();

   if (!accessToken) {
     setLoading(false);
     return;
   }

   (async () => {
     try {
       const response = await fetchAccount();

-      if (response) setUser({ id: response.data.id, name: response.data.nickname });
-      else authStorage.clearTokens();
+      if (!isMounted) return;
+      
+      if (response?.data?.id && response.data.nickname) {
+        setUser({ id: response.data.id, name: response.data.nickname });
+      } else {
+        authStorage.clearTokens();
+      }
     } catch (error) {
       console.error('API 호출 실패', error);
-      authStorage.clearTokens();
+      if (isMounted) authStorage.clearTokens();
     } finally {
-      setLoading(false);
+      if (isMounted) setLoading(false);
     }
   })();
+
+  return () => {
+    isMounted = false;
+  };
 }, []);
src/pages/Auth/KakaoCallbackPage.tsx (1)

52-52: useEffect 의존성 배열에 setUser 누락

Line 52의 의존성 배열에 setUser가 포함되어 있지 않습니다. 비록 컨텍스트에서 가져온 함수가 안정적일 가능성이 높지만, React의 exhaustive-deps 규칙을 준수하고 잠재적인 stale closure 문제를 방지하기 위해 포함하는 것이 좋습니다.

🔎 제안된 수정 사항
-  }, [searchParams, navigate]);
+  }, [searchParams, navigate, setUser]);
src/components/NavigationBar/NavigationBar.tsx (3)

8-12: 주석 처리된 코드 제거 권장

Lines 8-12와 67-73에 주석 처리된 코드가 남아있습니다. 필요시 git 히스토리에서 복구할 수 있으므로, 코드 가독성을 위해 제거하는 것을 권장합니다.

Also applies to: 67-73


28-50: 드롭다운 외부 클릭 처리 추가 권장

사용자가 드롭다운 메뉴 외부를 클릭해도 메뉴가 닫히지 않습니다. 일반적인 UX 패턴에 따라 외부 클릭 시 메뉴를 닫는 로직을 추가하는 것을 권장합니다.

🔎 제안된 구현 예시

NavigationBar 컴포넌트에 다음과 같은 useEffect를 추가할 수 있습니다:

useEffect(() => {
  if (!menuOpen) return;

  const handleClickOutside = (event: MouseEvent) => {
    const target = event.target as HTMLElement;
    if (!target.closest('[data-user-menu]')) {
      setMenuOpen(false);
    }
  };

  document.addEventListener('mousedown', handleClickOutside);
  return () => document.removeEventListener('mousedown', handleClickOutside);
}, [menuOpen]);

그리고 UserMenuButton과 UserDropdown을 감싸는 div에 data-user-menu 속성을 추가:

-            <div className="relative">
+            <div className="relative" data-user-menu>
               <UserMenuButton name={user.name} onClick={() => setMenuOpen((prev) => !prev)} />
               {menuOpen && <UserDropdown onLogout={handleLogout} />}
             </div>

Also applies to: 75-79


16-26: 키보드 접근성 및 ARIA 속성 추가 권장

UserMenuButton과 UserDropdown에 키보드 탐색 및 스크린 리더 지원이 부족합니다. 접근성을 개선하기 위해 다음을 고려하세요:

  • 드롭다운 메뉴에 role="menu"aria-expanded 속성 추가
  • 메뉴 아이템에 role="menuitem" 추가
  • 키보드 탐색 지원 (Arrow keys, Escape, Enter)
  • 메뉴 열릴 때 포커스 관리

더 나은 접근성을 위해 Radix UI의 DropdownMenu와 같은 검증된 라이브러리 사용을 고려할 수 있습니다.

Also applies to: 28-50

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f828747 and ffd3f45.

📒 Files selected for processing (11)
  • src/App.tsx
  • src/apis/auth/account.ts
  • src/apis/auth/auth.types.ts
  • src/apis/auth/kakao.ts
  • src/components/NavigationBar/NavigationBar.tsx
  • src/components/NavigationBar/_components/Dialog.tsx
  • src/config/api.ts
  • src/contexts/useAuth.tsx
  • src/lib/authStorage.ts
  • src/pages/Auth/KakaoCallbackPage.tsx
  • src/routes.tsx
🧰 Additional context used
🧬 Code graph analysis (8)
src/apis/auth/kakao.ts (2)
src/apis/auth/auth.types.ts (1)
  • KakaoLoginResponse (13-19)
src/config/api.ts (1)
  • API (6-9)
src/apis/auth/account.ts (2)
src/apis/auth/auth.types.ts (2)
  • UserInfoResponse (21-24)
  • AccountDeleteResponse (26-29)
src/config/api.ts (1)
  • API (6-9)
src/contexts/useAuth.tsx (2)
src/lib/authStorage.ts (1)
  • authStorage (1-11)
src/apis/auth/account.ts (1)
  • fetchAccount (4-14)
src/components/NavigationBar/NavigationBar.tsx (4)
src/components/NavigationBar/_components/Dialog.tsx (1)
  • Dialog (74-77)
src/components/Dialog/Dialog.tsx (1)
  • Dialog (6-52)
src/contexts/useAuth.tsx (1)
  • useAuth (66-72)
src/components/Logo/Logo.tsx (1)
  • Logo (34-37)
src/components/NavigationBar/_components/Dialog.tsx (3)
src/config/api.ts (1)
  • API_BASE_URL (4-4)
src/apis/auth/account.ts (1)
  • deleteAccount (16-26)
src/lib/authStorage.ts (1)
  • authStorage (1-11)
src/App.tsx (1)
src/contexts/useAuth.tsx (1)
  • AuthProvider (20-64)
src/pages/Auth/KakaoCallbackPage.tsx (3)
src/contexts/useAuth.tsx (1)
  • useAuth (66-72)
src/apis/auth/kakao.ts (1)
  • postKakaoCode (4-10)
src/lib/authStorage.ts (1)
  • authStorage (1-11)
src/config/api.ts (1)
src/lib/authStorage.ts (1)
  • authStorage (1-11)
🔇 Additional comments (5)
src/apis/auth/kakao.ts (1)

4-10: 구현이 간결하고 명확합니다.

Kakao OAuth 코드 교환 로직이 올바르게 구현되었습니다. 에러 처리는 호출자에게 위임하는 패턴으로, MVP에서는 적절합니다.

src/routes.tsx (1)

5-5: 라우트 추가가 적절합니다.

Kakao 콜백 페이지 라우트가 올바르게 등록되었습니다.

Also applies to: 13-13

src/App.tsx (1)

3-3: 인증 컨텍스트 통합이 올바릅니다.

AuthProvider로 앱을 감싸서 전역 인증 상태를 제공하는 구조가 적절합니다.

Also applies to: 7-11

src/components/NavigationBar/_components/Dialog.tsx (1)

13-15: Kakao 로그인 리다이렉트가 올바르게 구현되었습니다.

OAuth authorize 엔드포인트로의 리다이렉트가 적절합니다.

src/components/NavigationBar/NavigationBar.tsx (1)

53-59: 인증 통합 구현 확인

useAuth 훅을 사용한 인증 상태 관리와 로그아웃 핸들러 구현이 올바르게 되어 있습니다. 로그아웃 시 메뉴를 닫는 로직도 적절합니다.

Comment on lines +40 to +43
const handleWithdraw = async () => {
await deleteAccount();
authStorage.clearTokens();
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

에러 처리 및 상태 관리 개선이 필요합니다.

회원 탈퇴 로직에 다음 문제들이 있습니다:

  1. 에러 처리 누락: deleteAccount() 실패 시에도 토큰이 삭제되어 불일치 상태가 발생합니다.
  2. UI 상태 불일치: 토큰 삭제 후 UI 상태 업데이트가 없어 사용자가 여전히 로그인된 것처럼 보일 수 있습니다.
  3. 중복 요청 방지 없음: 버튼을 빠르게 여러 번 클릭하면 중복 요청이 발생할 수 있습니다.
🔎 제안하는 수정사항
+import { useAuth } from '../../../contexts/useAuth';
+import { useState } from 'react';

 const WithdrawDialog = ({ trigger }: DialogCommonProps) => {
+  const { logout } = useAuth();
+  const [isLoading, setIsLoading] = useState(false);
+
   const handleWithdraw = async () => {
+    if (isLoading) return;
+    
+    setIsLoading(true);
-    await deleteAccount();
-    authStorage.clearTokens();
+    try {
+      const result = await deleteAccount();
+      
+      if (result) {
+        logout(); // useAuth의 logout을 사용하여 전역 상태 업데이트
+        // 필요시 성공 메시지 표시
+      } else {
+        // 401 에러 등으로 result가 null인 경우
+        logout();
+      }
+    } catch (error) {
+      console.error('회원 탈퇴 실패:', error);
+      // 에러 메시지 표시
+    } finally {
+      setIsLoading(false);
+    }
   };

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/NavigationBar/_components/Dialog.tsx around lines 40 to 43,
the withdraw handler currently awaits deleteAccount() then unconditionally
clears tokens; fix it so errors are caught and tokens are only cleared on
successful deletion, update the app auth/UI state after success so the user is
shown logged-out (e.g., call your existing logout/setAuthState function or
navigate to the public route), and prevent duplicate submissions by tracking a
local "isSubmitting" boolean (disable the withdraw button while true). Wrap
deleteAccount() in try/catch, handle and surface errors (toast/log) in the catch
branch, and ensure finally resets isSubmitting where appropriate.

Comment thread src/config/api.ts
Comment on lines +11 to +23
API.interceptors.request.use(
(config) => {
const token = authStorage.getAccessToken();

if (token) {
config.headers = config.headers ?? {};
config.headers.Authorization = `Bearer ${token}`;
}

return config;
},
(error) => Promise.reject(error)
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

토큰 갱신 메커니즘 구현이 필요합니다.

현재 액세스 토큰만 요청에 첨부하고 있으며, 토큰 만료 시 자동 갱신 로직이 없습니다. 401 응답을 처리하고 리프레시 토큰으로 액세스 토큰을 갱신하는 response interceptor 추가가 필요합니다.

MVP 이후 리팩토링 시 다음 로직 추가를 권장합니다:

  • Response interceptor에서 401 에러 감지
  • 리프레시 토큰으로 새 액세스 토큰 발급
  • 실패한 요청 재시도
  • 리프레시 실패 시 로그아웃 처리
🔎 참고: 토큰 갱신 패턴 예시
API.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;
    
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      
      try {
        const refreshToken = authStorage.getRefreshToken();
        // POST /api/auth/refresh with refreshToken
        const { data } = await API.post('/api/auth/refresh', { refreshToken });
        
        authStorage.setTokens(data.accessToken, data.refreshToken);
        originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;
        
        return API(originalRequest);
      } catch (refreshError) {
        authStorage.clearTokens();
        window.location.href = '/';
        return Promise.reject(refreshError);
      }
    }
    
    return Promise.reject(error);
  }
);
🤖 Prompt for AI Agents
In src/config/api.ts around lines 11 to 23, the request interceptor attaches
only the access token but there is no response interceptor to handle 401s and
refresh tokens; add an axios response interceptor that detects 401 responses,
avoids infinite retry by marking the original request (e.g., _retry flag), reads
the refresh token from authStorage, calls the refresh endpoint to obtain new
tokens, persists them via authStorage, updates the original request
Authorization header with the new access token and retries the original request,
and if refresh fails clear tokens and force logout/redirect; ensure concurrent
401s are handled safely (single refresh in flight) and propagate errors when
refresh cannot succeed.

Comment thread src/contexts/useAuth.tsx
Comment on lines +32 to +44
(async () => {
try {
const response = await fetchAccount();

if (response) setUser({ id: response.data.id, name: response.data.nickname });
else authStorage.clearTokens();
} catch (error) {
console.error('API 호출 실패', error);
authStorage.clearTokens();
} finally {
setLoading(false);
}
})();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

response.data에 대한 null 안전성 검사 누락

Line 36에서 response.data.idresponse.data.nickname에 접근하고 있지만, response.data가 존재하는지 확인하지 않습니다. fetchAccount가 null이 아닌 응답을 반환하더라도 data 속성이 undefined일 경우 런타임 에러가 발생합니다.

🔎 제안된 수정 사항
 (async () => {
   try {
     const response = await fetchAccount();

-    if (response) setUser({ id: response.data.id, name: response.data.nickname });
+    if (response?.data?.id && response.data.nickname) {
+      setUser({ id: response.data.id, name: response.data.nickname });
+    }
     else authStorage.clearTokens();
   } catch (error) {
     console.error('API 호출 실패', error);
     authStorage.clearTokens();
   } finally {
     setLoading(false);
   }
 })();
🤖 Prompt for AI Agents
In src/contexts/useAuth.tsx around lines 32 to 44, the code accesses
response.data.id and response.data.nickname without verifying response.data
exists; update the logic to check that response and response.data are defined
(e.g., if (response && response.data && response.data.id)) or use optional
chaining (response?.data) before setting the user, and if data is missing call
authStorage.clearTokens() instead of accessing properties, so you avoid runtime
errors while keeping the existing error/clearTokens/finally behavior.

Comment thread src/lib/authStorage.ts
Comment on lines +1 to +11
export const authStorage = {
getAccessToken: () => localStorage.getItem('accessToken'),
setTokens: (accessToken: string, refreshToken: string) => {
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
},
clearTokens: () => {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
},
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

API 불일치: getRefreshToken 메서드 누락

setTokens로 리프레시 토큰을 저장하지만 이를 조회하는 getRefreshToken 메서드가 없어 API가 불완전합니다. 토큰 갱신 로직 구현 시 필요합니다.

🔎 제안하는 수정사항
 export const authStorage = {
   getAccessToken: () => localStorage.getItem('accessToken'),
+  getRefreshToken: () => localStorage.getItem('refreshToken'),
   setTokens: (accessToken: string, refreshToken: string) => {
     localStorage.setItem('accessToken', accessToken);
     localStorage.setItem('refreshToken', refreshToken);
   },
   clearTokens: () => {
     localStorage.removeItem('accessToken');
     localStorage.removeItem('refreshToken');
   },
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const authStorage = {
getAccessToken: () => localStorage.getItem('accessToken'),
setTokens: (accessToken: string, refreshToken: string) => {
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
},
clearTokens: () => {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
},
};
export const authStorage = {
getAccessToken: () => localStorage.getItem('accessToken'),
getRefreshToken: () => localStorage.getItem('refreshToken'),
setTokens: (accessToken: string, refreshToken: string) => {
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
},
clearTokens: () => {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
},
};
🤖 Prompt for AI Agents
In src/lib/authStorage.ts around lines 1 to 11, the module stores a refresh
token via setTokens but lacks a getRefreshToken method; add a getRefreshToken:
() => localStorage.getItem('refreshToken') to the exported authStorage object so
callers can read the stored refresh token (ensure the method signature and
return type match getAccessToken).

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and view its contents
find . -name "authStorage.ts" -type f 2>/dev/null | head -20

Repository: promlog/promlog-client

Length of output: 90


🌐 Web query:

JWT token storage security best practices 2025 XSS localStorage httpOnly cookie

💡 Result:

Short answer — do not store long‑lived JWTs in localStorage. Prefer HttpOnly+Secure+SameSite cookies (or a BFF) plus short access tokens, refresh-token rotation and CSRF/XSS hardening. Key recommendations and why:

  • Prefer HttpOnly, Secure, SameSite cookies (access token or refresh token stored server‑side / cookie set by server). Blocks JavaScript token theft (XSS) but still needs CSRF defenses. [1][2][3]
  • Use a Backend‑for‑Frontend (BFF) or keep tokens out of the browser when possible. BFFs keep all tokens on the server and only send minimal info to the client. [4][2]
  • If using cookies, defend against CSRF: SameSite=Lax/Strict when possible, plus double‑submit CSRF token or origin/referer checks for state‑changing requests. Don’t rely on SameSite alone for critical flows. [3][5]
  • For SPAs where cookies aren’t workable, prefer in‑memory storage (or WebWorker/private closure) for access tokens and use short lifetimes + refresh tokens handled securely. In‑memory prevents persistent theft but loses persistence across refreshes. [1][2]
  • Avoid localStorage/sessionStorage for sensitive tokens — they are readable by any script/extension on the origin and are exposed by XSS. If you must use them, make tokens very short‑lived and combine with rotation/fingerprinting (last resort). [1][6]
  • Use short‑lived access tokens (e.g., 10–30 min), rotating refresh tokens, revoke/blacklist refresh tokens on logout, and monitor/use anomaly detection. [2][7]
  • Reduce XSS attack surface: strong CSP, sanitize/escape user input, minimize third‑party scripts and use SRI where possible, and run dependency supply‑chain audits. These measures are as important as cookie flags. [6][1]
  • Use appropriate JWT best practices: validate iss/aud/exp/jti, prefer asymmetric signing (RS/ES), rotate keys, and keep payloads minimal. [7][2]

If you want a short implementation checklist (cookie headers, CSRF patterns, BFF skeleton, or SPA in‑memory + refresh flow), tell me which stack (e.g., Node/Express + React) and I’ll give concrete code.

Sources: [1] Auth0 token storage guidance; [2] OWASP JWT cheat sheet; [3] Practical cookie + SameSite guidance; [4] BFF / modern SPA guidance; [5] CSRF mitigation patterns; [6] articles on why localStorage is risky; [7] JWT token rotation/expiration best practices.


보안 취약점: localStorage 기반 토큰 저장

localStorage에 액세스 토큰을 저장하는 것은 XSS(Cross-Site Scripting) 공격에 취약합니다. 악성 스크립트가 주입되면 토큰을 탈취당할 수 있습니다.

현재 구현의 보안 위험:

  1. XSS 공격으로 인한 토큰 탈취 가능 (JavaScript에서 직접 접근 가능)
  2. 토큰이 모든 JavaScript 코드에서 접근 가능
  3. 브라우저 세션 종료 후에도 토큰이 영구 저장됨

권장하는 개선 방안:

🔒 우선순위별 보안 대안

옵션 1 (가장 권장): HTTP-only Cookie

  • 서버에서 Set-Cookie 헤더로 토큰 전달 (httpOnly, Secure, SameSite 플래그 사용)
  • 클라이언트 JavaScript에서 토큰에 직접 접근 불가
  • 브라우저가 자동으로 쿠키를 요청에 포함
  • XSS 공격으로부터 완벽하게 보호됨
  • CSRF 방어: SameSite=Strict/Lax 설정 필수

옵션 2: In-Memory Storage (SPA 최적화)

  • 토큰을 메모리에만 저장 (XSS 방어 + JavaScript 접근 차단)
  • 페이지 새로고침 시 재인증 필요
  • 짧은 만료 시간의 액세스 토큰과 함께 사용 권장

옵션 3: sessionStorage (임시방편)

  • 브라우저 탭 닫으면 자동 삭제
  • localStorage보다 개선되지만 여전히 XSS에 취약
  • 장기적 해결책은 아니며 옵션 1, 2로의 마이그레이션 권장

추가 필수 보안 조치:

  • Content Security Policy (CSP) 헤더 설정으로 XSS 공격 표면 감소
  • 짧은 액세스 토큰 만료 시간 (10-30분) + 리프레시 토큰 로테이션
  • 리프레시 토큰: HTTP-only 쿠키 또는 서버 세션 저장소 사용
  • 사용자 입력 검증 및 이스케이프, 제3자 스크립트 최소화로 XSS 방지
🤖 Prompt for AI Agents
In src/lib/authStorage.ts lines 1-11, the current implementation stores
access/refresh tokens in localStorage which is vulnerable to XSS; replace this
with a secure flow: stop persisting tokens in localStorage, switch to server-set
HTTP-only Secure SameSite cookies for auth (client should not read/write
tokens), or if immediate change needed use in-memory token storage only (cleared
on reload) while you implement cookie-based auth; update authStorage to either
be a no-op wrapper (no getAccessToken/setTokens that access localStorage) or to
hold tokens only in memory, add code to clear any existing localStorage tokens
during migration, and update related client API calls to rely on cookie-based
authentication (no direct token injection into Authorization headers).

Comment on lines +18 to +30
if (error) {
console.error('Kakao OAuth:', error, errorDescription);
navigate('/', { replace: true });

return;
}

if (!code) {
console.error('Kakao OAuth: code를 찾을 수 없습니다.');
navigate('/', { replace: true });

return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

사용자 피드백 부재

OAuth 에러 발생 시(Lines 18-23, 25-30) 또는 API 호출 실패 시(Lines 47-50) 콘솔에만 로그를 남기고 홈으로 리다이렉트합니다. 사용자는 "로그인 중입니다..." 메시지만 보다가 갑자기 홈으로 이동하게 되어 무엇이 잘못되었는지 알 수 없습니다.

토스트 알림이나 에러 상태를 추가하여 사용자에게 명확한 피드백을 제공하는 것을 권장합니다.

Also applies to: 47-50

Comment on lines +35 to +51
(async () => {
try {
const { data } = await postKakaoCode(code);

setUser({
id: data.account.id,
name: data.account.nickname,
});

authStorage.setTokens(data.accessToken, data.refreshToken);

navigate('/', { replace: true });
} catch (error) {
console.error('code가 유효하지 않습니다.', error);
navigate('/', { replace: true });
}
})();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

API 응답 구조 검증 누락

Lines 37-44에서 postKakaoCode 응답의 구조를 검증하지 않고 바로 접근하고 있습니다. data.account.id, data.account.nickname, data.accessToken, data.refreshToken이 존재한다고 가정하고 있지만, API 응답이 예상과 다를 경우 런타임 에러가 발생합니다.

TypeScript 타입이 있더라도 런타임 검증을 추가하거나, 최소한 옵셔널 체이닝과 에러 처리를 강화하는 것을 권장합니다.

🔎 제안된 개선 사항
 (async () => {
   try {
     const { data } = await postKakaoCode(code);
     
+    if (!data?.account?.id || !data?.account?.nickname || !data?.accessToken || !data?.refreshToken) {
+      console.error('Kakao OAuth: 응답 데이터가 올바르지 않습니다.', data);
+      navigate('/', { replace: true });
+      return;
+    }
+
     setUser({
       id: data.account.id,
       name: data.account.nickname,
     });

     authStorage.setTokens(data.accessToken, data.refreshToken);

     navigate('/', { replace: true });
   } catch (error) {
     console.error('code가 유효하지 않습니다.', error);
     navigate('/', { replace: true });
   }
 })();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
(async () => {
try {
const { data } = await postKakaoCode(code);
setUser({
id: data.account.id,
name: data.account.nickname,
});
authStorage.setTokens(data.accessToken, data.refreshToken);
navigate('/', { replace: true });
} catch (error) {
console.error('code가 유효하지 않습니다.', error);
navigate('/', { replace: true });
}
})();
(async () => {
try {
const { data } = await postKakaoCode(code);
if (!data?.account?.id || !data?.account?.nickname || !data?.accessToken || !data?.refreshToken) {
console.error('Kakao OAuth: 응답 데이터가 올바르지 않습니다.', data);
navigate('/', { replace: true });
return;
}
setUser({
id: data.account.id,
name: data.account.nickname,
});
authStorage.setTokens(data.accessToken, data.refreshToken);
navigate('/', { replace: true });
} catch (error) {
console.error('code가 유효하지 않습니다.', error);
navigate('/', { replace: true });
}
})();
🤖 Prompt for AI Agents
In src/pages/Auth/KakaoCallbackPage.tsx around lines 35 to 51, the code assumes
postKakaoCode response always contains data.account.id, data.account.nickname,
data.accessToken and data.refreshToken; add runtime validation before using
these properties: check that data and data.account exist and that
id/nickname/accessToken/refreshToken are non-empty, use optional chaining where
appropriate, and if validation fails throw or handle a clear error path (log the
problem, avoid destructuring undefined, and navigate/back to login). Ensure
tokens are only stored when present and user state is set only after successful
validation.

@ccconac ccconac merged commit 83cf4a4 into dev Dec 31, 2025
1 check was pending
@ccconac ccconac deleted the feat/25-user-api branch January 2, 2026 09:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 회원 API 연결

1 participant