-
Notifications
You must be signed in to change notification settings - Fork 2
Feat: 쿠키 기반의 인증 로직, 미들웨어, 인터셉터 개선 #78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
📦 번들 분석 결과📊 번들 크기 요약
🔍 주요 청크 파일 (크기순)🤖 자동 생성된 번들 분석 리포트 |
⚡ Lighthouse 성능 분석 결과📊 전체 평균 점수
📈 측정 현황
📄 페이지별 상세 분석🏠 커뮤니티 페이지:
|
| 지표 | 점수 |
|---|---|
| 🚀 Performance | 67점 |
| ♿ Accessibility | 78점 |
| ✅ Best Practices | 100점 |
| 🔍 SEO | 100점 |
📊 상세 분석 보기
👥 창업자 페이지: /main/founder
| 지표 | 점수 |
|---|---|
| 🚀 Performance | 75점 |
| ♿ Accessibility | 87점 |
| ✅ Best Practices | 100점 |
| 🔍 SEO | 100점 |
📊 상세 분석 보기
🏡 홈 페이지: /main/home
| 지표 | 점수 |
|---|---|
| 🚀 Performance | 74점 |
| ♿ Accessibility | 91점 |
| ✅ Best Practices | 100점 |
| 🔍 SEO | 100점 |
📊 상세 분석 보기
🗺️ 지도 페이지: /main/maps
| 지표 | 점수 |
|---|---|
| 🚀 Performance | 75점 |
| ♿ Accessibility | 87점 |
| ✅ Best Practices | 100점 |
| 🔍 SEO | 100점 |
📊 상세 분석 보기
👤 프로필 페이지: /main/profile
| 지표 | 점수 |
|---|---|
| 🚀 Performance | 75점 |
| ♿ Accessibility | 88점 |
| ✅ Best Practices | 100점 |
| 🔍 SEO | 100점 |
📊 상세 분석 보기
🔗 전체 상세 분석 결과
📄 측정된 페이지
- /main/community
- /main/founder
- /main/home
- /main/maps
- /main/profile
모든 페이지에서 성능 측정이 완료되었습니다.
🤖 자동 생성된 Lighthouse 성능 리포트
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
이 PR은 Body 기반 토큰 인증에서 HttpOnly Cookie 기반 인증으로 완전히 마이그레이션하는 대규모 아키텍처 개선 작업입니다. XSS 공격 방어, SSR 인증 지원, 큐 기반 토큰 갱신 등 보안과 사용자 경험을 크게 향상시키는 변경사항들이 포함되어 있습니다.
주요 개선사항:
- 보안 강화: HttpOnly 쿠키로 XSS 공격 차단, JavaScript에서 토큰 접근 불가
- SSR 인증: 서버 사이드에서 인증 상태 확인 및 페이지 보호, 초기 로드 속도 향상
- 큐 기반 토큰 갱신: 동시 401 에러 발생 시 중복 요청 방지, 한 번만 재발급
- 전역 에러 핸들링: React Query Cache 레벨에서 네트워크/서버/인증 에러 일관성 있게 처리
- 선택적 프록시: HTTP(빠른 개발) / HTTPS(완전한 기능) 모드 지원
중요한 개선이 필요한 부분:
- 토큰 갱신 로직의 동시성 제어에 일부 버그 존재 (critical)
- 서버 컴포넌트에서 쿠키 전달 누락 (moderate)
- 프로덕션 환경에서 과도한 로깅으로 인한 성능 저하 우려 (moderate)
Reviewed changes
Copilot reviewed 18 out of 23 changed files in this pull request and generated 34 comments.
Show a summary per file
| File | Description |
|---|---|
apps/web/src/lib/api-client.ts |
큐 기반 토큰 갱신 로직 추가, 프록시 경로 인터셉터 구현 (일부 버그 존재) |
apps/web/src/lib/api-error.ts |
ApiError 클래스 분리하여 재사용성 향상 |
apps/web/src/lib/server-api-client.ts |
서버 컴포넌트 전용 API 클라이언트 추가 (쿠키 전달 개선 필요) |
apps/web/src/middleware.ts |
인증 미들웨어 추가로 SSR 페이지 보호 구현 (쿠키 전달 및 하드코딩 개선 필요) |
apps/web/src/providers/AuthHydrationProvider.tsx |
SSR Prefetch로 초기 로드 최적화 |
apps/web/src/providers/queryProvider.tsx |
전역 에러 핸들링 추가 (useToast 클로저 문제 존재) |
apps/web/src/hooks/useAuth.ts |
Body 기반에서 쿠키 기반 인증으로 리팩토링, SSR 최적화 |
apps/web/src/hooks/useLogout.ts |
로그아웃 훅 신규 추가, 캐시 초기화 처리 |
apps/web/src/hooks/useKakaoAuth.ts |
쿠키 기반 인증으로 마이그레이션, authStore 의존성 제거 |
apps/web/src/app/layout.tsx |
AuthProvider를 AuthHydrationProvider로 교체, viewport 분리 |
apps/web/src/app/page.tsx |
useAuthStore에서 useAuth로 마이그레이션 |
apps/web/server.mjs |
HTTPS 로컬 개발 서버 추가 (인증서 경로 유연성 필요) |
apps/web/next.config.js |
인증 엔드포인트 프록시 설정 추가 |
apps/web/package.json |
dev:https 스크립트 추가 |
apps/web/.eslintrc.cjs |
server.mjs ignorePatterns 추가 |
apps/web/.eslintignore |
ESLint ignore 규칙 문서화 |
apps/web/src/generated/* |
API 문서 업데이트, 중복 export 제거 필요 |
apps/web/src/providers/AuthProvider.tsx |
기존 AuthProvider 제거 (AuthHydrationProvider로 대체) |
apps/web/src/stores/userStore.ts |
빈 파일 (삭제 필요) |
apps/web/src/utils/query.ts |
api-error 모듈로 import 경로 변경 |
| throw error; | ||
| } | ||
| }, | ||
| staleTime: 13 * 60 * 1000, // 13분 |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
staleTime이 13분으로 하드코딩되어 있습니다. 일관성을 위해 상수로 정의하는 것이 좋습니다.
해결 방법:
// 파일 상단에 상수 정의
const USER_CACHE_STALE_TIME = 13 * 60 * 1000; // 13분
// 사용
const query = useQuery({
queryKey: getGetCurrentUserQueryKey(),
queryFn: async () => { /* ... */ },
staleTime: USER_CACHE_STALE_TIME,
throwOnError: false,
});| onError: (error) => { | ||
| console.error('[useLogout] 로그아웃 API 실패:', error); | ||
| queryClient.clear(); | ||
| queryClient.setQueryData(getGetCurrentUserQueryKey(), null); | ||
| }, |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
로그아웃 실패 시에도 클라이언트 상태를 초기화하는 것은 좋지만, 사용자에게 알림을 표시하지 않아 문제를 인지하지 못할 수 있습니다. 최소한 개발 환경에서는 명확한 에러 메시지를 표시하는 것이 좋습니다.
제안:
onError: (error) => {
console.error('[useLogout] 로그아웃 API 실패:', error);
// 개발 환경에서는 toast 표시
if (process.env.NODE_ENV === 'development') {
// toast 사용 필요
}
// 클라이언트 상태는 여전히 초기화
queryClient.clear();
queryClient.setQueryData(getGetCurrentUserQueryKey(), null);
},| // 모든 쿠키 확인 | ||
| const allCookies = request.cookies.getAll(); | ||
| console.log( | ||
| `[Middleware] 📦 전체 쿠키 개수: ${allCookies.length}`, | ||
| allCookies.map((c) => c.name), | ||
| ); |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
모든 요청마다 전체 쿠키를 로깅하면 프로덕션 환경에서 성능 저하 및 로그 과부하가 발생할 수 있습니다.
해결 방법:
if (process.env.NODE_ENV === 'development') {
const allCookies = request.cookies.getAll();
console.log(
`[Middleware] 📦 전체 쿠키 개수: ${allCookies.length}`,
allCookies.map((c) => c.name),
);
}| // 모든 쿠키 확인 | |
| const allCookies = request.cookies.getAll(); | |
| console.log( | |
| `[Middleware] 📦 전체 쿠키 개수: ${allCookies.length}`, | |
| allCookies.map((c) => c.name), | |
| ); | |
| // 모든 쿠키 확인 (개발 환경에서만 로깅) | |
| const allCookies = request.cookies.getAll(); | |
| if (process.env.NODE_ENV === 'development') { | |
| console.log( | |
| `[Middleware] 📦 전체 쿠키 개수: ${allCookies.length}`, | |
| allCookies.map((c) => c.name), | |
| ); | |
| } |
| } | ||
|
|
||
| export function QueryProvider({ children }: QueryProviderProps) { | ||
| const toast = useToast(); |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useToast 훅을 컴포넌트 함수 본문 내에서 호출하고 있지만, useState의 initializer 함수 내부에서 사용되는 QueryClient 생성 시점에 참조하려고 하면 클로저 문제가 발생할 수 있습니다. useState의 initializer는 첫 렌더링에서만 실행되므로 toast 함수의 최신 참조를 보장할 수 없습니다.
해결 방법:
export function QueryProvider({ children }: QueryProviderProps) {
const toastStore = useToastStore();
const [queryClient] = useState(() => {
return new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
// toastStore를 클로저로 캡처하거나 useToastStore.getState() 사용
const toast = (msg: string, type: ToastType) => {
useToastStore.getState().addToast({
id: Date.now(),
message: msg,
type,
});
};
// ...
},
}),
});
});
}| console.log('[Middleware] 인증 필요:', pathname); | ||
| return redirectToLogin(request, pathname); | ||
| } |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
프로덕션 환경에서도 로깅하면 성능 저하가 발생합니다.
해결 방법:
if (PROTECTED_ROUTES.some((route) => pathname.startsWith(route))) {
if (!hasAuth) {
if (process.env.NODE_ENV === 'development') {
console.log('[Middleware] 인증 필요:', pathname);
}
return redirectToLogin(request, pathname);
}
}| isRefreshing = false; | ||
| return AXIOS_INSTANCE(originalRequest); | ||
| } catch (refreshError) { | ||
| console.error('[Auth] ❌ 토큰 갱신 실패:', refreshError); | ||
| // 대기 중인 모든 요청 실패 처리 | ||
| processQueue(refreshError); | ||
| isRefreshing = false; | ||
| return Promise.reject(refreshError); |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
토큰 갱신 중 에러 발생 시 isRefreshing 플래그가 false로 리셋되지 않는 경우가 있습니다. processQueue 호출 후에도 에러가 발생하면 isRefreshing이 true 상태로 남아 이후 모든 갱신 시도가 큐에 추가되어 무한 대기 상태에 빠질 수 있습니다.
해결 방법:
try {
await refreshToken();
processQueue();
return AXIOS_INSTANCE(originalRequest);
} catch (refreshError) {
console.error('[Auth] ❌ 토큰 갱신 실패:', refreshError);
processQueue(refreshError);
return Promise.reject(refreshError);
} finally {
isRefreshing = false; // finally 블록으로 이동
}| isRefreshing = false; | |
| return AXIOS_INSTANCE(originalRequest); | |
| } catch (refreshError) { | |
| console.error('[Auth] ❌ 토큰 갱신 실패:', refreshError); | |
| // 대기 중인 모든 요청 실패 처리 | |
| processQueue(refreshError); | |
| isRefreshing = false; | |
| return Promise.reject(refreshError); | |
| return AXIOS_INSTANCE(originalRequest); | |
| } catch (refreshError) { | |
| console.error('[Auth] ❌ 토큰 갱신 실패:', refreshError); | |
| // 대기 중인 모든 요청 실패 처리 | |
| processQueue(refreshError); | |
| return Promise.reject(refreshError); | |
| } finally { | |
| isRefreshing = false; |
| console.log( | ||
| '[ServerAPI] accessToken:', | ||
| accessToken ? '있음' : '없음', | ||
| ); |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
프로덕션 환경에서 로깅이 계속 실행되면 성능 저하 및 로그 과부하가 발생할 수 있습니다.
해결 방법:
if (process.env.NODE_ENV === 'development') {
console.log(
'[ServerAPI] accessToken:',
accessToken ? '있음' : '없음',
);
}| originalRequest && // 원래 요청이 존재 | ||
| !originalRequest._retry // 무한 루프 방지 | ||
| ) { | ||
| //이미 갱신 중이면 큐에 추가 |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
주석 앞에 공백이 누락되었습니다.
| //이미 갱신 중이면 큐에 추가 | |
| // 이미 갱신 중이면 큐에 추가 |
| console.log(`[API Client] 🔄 프록시 사용: ${url} → /api${url}`); | ||
| } else { | ||
| console.log(`[API Client] 📡 직접 호출: ${url}`); | ||
| } |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
프로덕션 환경에서 모든 API 요청에 대해 로깅하면 성능 저하가 발생할 수 있습니다.
해결 방법:
if (process.env.NODE_ENV === 'development') {
if (needsCookie) {
console.log(`[API Client] 🔄 프록시 사용: ${url} → /api${url}`);
} else {
console.log(`[API Client] 📡 직접 호출: ${url}`);
}
}| console.log( | ||
| '[Middleware] 🚫 프록시 비활성화 - 미들웨어 인증 스킵 (CSR 전용 모드)', | ||
| ); |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
프로덕션 환경에서도 모든 요청마다 로깅하면 성능 저하가 발생합니다.
해결 방법:
if (!proxyEnabled) {
if (process.env.NODE_ENV === 'development') {
console.log(
'[Middleware] 🚫 프록시 비활성화 - 미들웨어 인증 스킵 (CSR 전용 모드)',
);
}
return NextResponse.next();
}| console.log( | |
| '[Middleware] 🚫 프록시 비활성화 - 미들웨어 인증 스킵 (CSR 전용 모드)', | |
| ); | |
| if (process.env.NODE_ENV === 'development') { | |
| console.log( | |
| '[Middleware] 🚫 프록시 비활성화 - 미들웨어 인증 스킵 (CSR 전용 모드)', | |
| ); | |
| } |
📋 개요 (Summary)
Body 기반 토큰 인증에서 HttpOnly Cookie 기반 인증으로 완전히 마이그레이션했습니다.
핵심 개선사항
🔧 주요 변경사항
1. 큐 기반 토큰 갱신 (Queue-based Token Refresh)
파일:
apps/web/src/lib/api-client.ts핵심 근거:
/auth/refresh를 호출하면 중복 요청 발생구현:
2. 전역 에러 핸들링 (Global Error Handling)
파일:
apps/web/src/providers/queryProvider.tsx핵심 근거:
구현:
3. 미들웨어 인증 (Middleware Authentication)
파일:
apps/web/src/middleware.ts핵심 근거:
흐름:
/main접근accessToken쿠키 확인/login으로 리다이렉트/api/users/me호출하여 토큰 유효성 검증/api/auth/refresh호출하여 토큰 갱신 시도/login으로 리다이렉트4. useAuth() 훅 마이그레이션
파일:
apps/web/src/hooks/useAuth.ts변경 내용:
5. SSR Prefetch (Server-Side Data Fetching)
파일:
apps/web/src/providers/AuthHydrationProvider.tsx핵심 근거:
흐름:
cookies()로accessToken확인/api/users/me호출하여 유저 정보 prefetchdehydrate()로 직렬화하여 클라이언트에 전달HydrationBoundary가 캐시 복원useAuth()가 즉시 캐시된 데이터 반환 (로딩 없음)6. HTTPS 로컬 개발 서버
파일:
apps/web/server.mjs,apps/web/package.json핵심 근거:
Secure플래그가 필요함Secure쿠키는 HTTPS 환경에서만 전송됨구현:
인증서 생성 (mkcert):
brew install mkcert mkcert -install mkdir -p apps/web/certs cd apps/web/certs mkcert localhost7. Next.js Rewrites 프록시
파일:
apps/web/next.config.js핵심 근거:
localhost:3000soso.dreampaste.com/api/*경로를 백엔드로 프록시하여 도메인 통일localhost:3000에서 온 것처럼 보임동작 방식:
구현:
8. 선택적 프록시 (Conditional Proxy)
파일:
apps/web/next.config.jsapps/web/src/lib/api-client.tsapps/web/src/lib/server-api-client.tsapps/web/src/middleware.tsapps/web/src/providers/AuthHydrationProvider.tsx핵심 근거:
구현:
각 파일에서 체크:
📖 인증 시나리오 (Authentication Scenarios)
시나리오 1: 로그인 (Login)
HTTPS 모드 (프록시 활성화)
/login페이지에서 카카오 로그인 버튼 클릭useKakaoAuth()훅이 카카오 OAuth 인가 코드 받음POST /api/auth/login호출 (프록시 사용)Set-Cookie: accessToken; refreshToken; HttpOnly; Secure; SameSite=Lax응답router.replace('/main')실행/main접근 시 미들웨어가 쿠키 확인 → 통과useAuth()가 캐시된 데이터 즉시 반환 (isAuth: true)HTTP 모드 (프록시 비활성화)
/login페이지에서 카카오 로그인 버튼 클릭useKakaoAuth()훅이 카카오 OAuth 인가 코드 받음POST https://soso.dreampaste.com/auth/login호출 (직접 호출)Set-Cookie: accessToken; refreshToken; HttpOnly; SameSite=Lax응답Secure플래그가 없어서 HTTP에서도 작동router.replace('/main')실행useAuth()가/users/me호출 → 쿠키 전송isAuth: true시나리오 2: 페이지 새로고침 (Page Refresh)
HTTPS 모드
/main에서 새로고침 (F5 또는 주소창 입력)accessToken쿠키 확인/api/users/me호출하여 토큰 유효성 검증accessToken쿠키 확인/api/users/me호출하여 유저 정보 prefetchHydrationBoundary가 캐시 복원useAuth()가 캐시된 데이터 즉시 반환HTTP 모드
/main에서 새로고침useAuth()가 캐시 확인 → 빈 캐시GET https://soso.dreampaste.com/users/me호출isAuth: true시나리오 3: 토큰 만료 - 미들웨어 (Token Expiry in Middleware)
HTTPS 모드
/main접근 (액세스 토큰 만료됨)accessToken쿠키 확인 → 존재함/api/users/me호출 → 401 UnauthorizedPOST /api/auth/refresh호출 (refreshToken 쿠키 자동 전송)accessToken발급Set-Cookie: accessToken응답시나리오 4: 토큰 만료 - SSR Prefetch (Token Expiry in SSR)
HTTPS 모드
getServerCurrentUser()호출 → 401 Unauthorizednull설정isAuth: falseuseAuth()가 자동으로/users/me재호출 (staleTime 만료)isAuth: true시나리오 5: 토큰 만료 - 클라이언트 요청 (Token Expiry in Client)
공통 (HTTPS/HTTP 모두)
/main에서 API 호출 (예: 게시글 목록)isRefreshing = true→POST /auth/refresh호출failedQueue에 추가 (대기)processQueue(null)실행 → 대기 중인 요청들 재개🚨 HTTP 모드 문제와 해결 (HTTP Mode Issues and Solutions)
문제 1: 쿠키 도메인 격리 (Cookie Domain Isolation)
문제:
localhost:3000soso.dreampaste.comsoso.dreampaste.com에서 설정된 쿠키를localhost:3000에서 읽을 수 없음해결책: Next.js Rewrites 프록시
/api/*경로를 백엔드로 프록시localhost:3000에서 온 것처럼 보임문제 2: Secure Cookie 제약 (Secure Cookie Restrictions)
문제:
Secure플래그가 있는 쿠키는 HTTPS에서만 전송됨해결책: 선택적 프록시
Secure플래그 없이 쿠키 전송 (개발용)문제 3: 캐시 초기화 문제 (Cache Initialization Problem)
문제:
null설정null이 있으면 API 호출하지 않음 (staleTime 때문)해결책: HTTP 모드에서는 캐시 설정 스킵
🔄 환경 비교 (Environment Comparison)
npm run dev)npm run dev:https)/api/*프록시)Secure플래그 없이 전송Secure플래그로 안전하게 전송📦 기타 개선사항 (Additional Improvements)
1. Next.js Image 경고 수정
파일:
apps/web/src/assets/images/LogoImage.tsx문제:
해결:
2. ESLint 설정 개선
파일:
apps/web/.eslintrc.cjsserver.mjs파일을 ignorePatterns에 추가하여 파싱 에러 방지3. 환경 변수 문서화
파일:
apps/web/.env.example모든 환경 변수에 대한 설명 추가 및 사용 예시 제공
추가 노트
📝 SSL 인증서 설치 가이드
로컬 HTTPS 서버를 실행하려면 신뢰할 수 있는 SSL 인증서가 필요합니다. 브라우저가 신뢰하지 않는 자체 서명 인증서를 사용하면 보안 경고가 발생하고 쿠키가 제대로 작동하지 않을 수 있습니다.
mkcert란?
설치 및 설정 단계:
macOS
Windows
방법 1: Chocolatey 사용 (권장)
Chocolatey가 설치되어 있지 않다면 먼저 설치:
mkcert 설치:
방법 2: Scoop 사용
중요 사항:
certs/디렉토리는.gitignore에 추가해야 합니다 (개인키 유출 방지)localhost-key.pem은 절대 공개 저장소에 커밋하면 안 됩니다문제 해결:
"mkcert: command not found" 에러
브라우저가 여전히 "안전하지 않음" 경고 표시
mkcert -install을 다시 실행server.mjs와 일치하는지 확인"ENOENT: no such file or directory" 에러
대안: 자체 서명 인증서 (권장하지 않음)
mkcert를 사용할 수 없는 환경이라면 OpenSSL로 자체 서명 인증서를 생성할 수 있지만, 브라우저 경고를 수동으로 무시해야 합니다:
이 방법은 브라우저가 인증서를 신뢰하지 않아 매번 경고를 무시해야 하므로 mkcert 사용을 강력히 권장합니다.