Skip to content

Conversation

@DreamPaste
Copy link
Member

📌 개요

SOS-41 브랜치에서 구현한 HttpOnly Cookie 기반 인증 시스템의 엣지 케이스 해결 및 레거시 코드 정리 작업입니다.

  • ✅ 미들웨어 SocketError 해결: 순환 참조 문제를 백엔드 직접 호출로 수정
  • ✅ 쿠키 Domain 속성 제거: Set-Cookie 헤더를 현재 환경에 맞게 수정
  • ✅ SSR Prefetch 401 엣지케이스 해결: 동적 staleTime으로 즉시 재검증 가능
  • ✅ 레거시 authStore 제거: Zustand 기반 상태 관리 삭제
  • ✅ 인증 관련 페이지 정리: 회원가입/로그인/프로필 페이지 개선

🗒 상세 설명

1. 미들웨어 SocketError 해결

파일: apps/web/src/middleware.ts:64-99

미들웨어에서 토큰 갱신 시 발생하던 순환 참조 문제를 해결했습니다.

핵심 기술 및 구현사항

  • 문제: 미들웨어에서 localhost:3000/api/auth/refresh 호출 → 자기 자신에게 요청 → SocketError
  • 원인: Next.js 미들웨어가 실행된 후 다시 미들웨어가 호출되는 순환 참조
  • 해결: 프록시 대신 백엔드 API를 직접 호출하고 쿠키를 수동으로 전달

문제 오류 로그

[Middleware] 토큰 갱신  에러:  [TypeError: fetch failed] {
  cause:  [SocketError: other side closed] {
  name: 'SocketError',
  code: 'UND_ERR_SOCKET',
  socket: {
  localAddress: '::1',
  localPort: 65264,
  remoteAddress: '::1',
  remotePort: 3000,
  remoteFamily: 'IPv6',
  timeout: undefined,
  bytesWritten: 251,
  bytesRead: 0
}

사용 예시

// AS-IS: 프록시 사용 (순환 참조 발생)
const refreshResponse = await fetch(
  'http://localhost:3000/api/auth/refresh',
  {
    method: 'POST',
  },
);

// TO-BE: 백엔드 직접 호출
const refreshResponse = await fetch(`${API_BASE_URL}/auth/refresh`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Cookie: `refreshToken=${refreshToken}`, // 수동 전달
  },
});

2. 쿠키 Domain 속성 제거

파일: apps/web/src/middleware.ts:19-21, apps/web/src/middleware.ts:102-104

백엔드에서 전달받은 Set-Cookie 헤더의 Domain 속성을 제거하여 로컬 환경에서도 쿠키가 정상적으로 저장되도록 수정했습니다.

핵심 기술 및 구현사항

  • 문제: 백엔드가 Domain=soso.dreampaste.com 포함한 쿠키 전송 → localhost에서 저장 안 됨
  • 해결: 미들웨어에서 Domain 속성을 정규식으로 제거
  • 효과: 현재 도메인(localhost 또는 soso.dreampaste.com)에 자동으로 맞춰짐

사용 예시

/**
 * Set-Cookie 헤더를 현재 환경에 맞게 수정
 * - Domain 속성 제거 (현재 도메인으로 자동 설정)
 */
function modifySetCookie(cookie: string): string {
  return cookie.replace(/Domain=[^;]+;?\s*/gi, '');
}

// 적용
const newResponse = NextResponse.next();
setCookieHeaders.forEach((cookie) => {
  const modifiedCookie = modifySetCookie(cookie);
  newResponse.headers.append('Set-Cookie', modifiedCookie);
});

Before-After

Before:

Set-Cookie: accessToken=abc123; Domain=soso.dreampaste.com; Secure; HttpOnly
→ localhost에서 저장 실패

After:

Set-Cookie: accessToken=abc123; Secure; HttpOnly
→ 현재 도메인(localhost)에 저장 성공

3. SSR Prefetch 401 엣지케이스 해결

파일: apps/web/src/providers/AuthHydrationProvider.tsx:38-46

SSR Prefetch 시 401 에러가 발생했을 때 캐싱되는 문제를 해결했습니다.

핵심 기술 및 구현사항

  • 문제: SSR에서 401 에러 발생 → null로 13분간 캐싱 → 클라이언트에서 재시도 안 됨
  • 해결: 에러 상태일 때 staleTime: 0으로 설정하여 즉시 재검증
  • 효과: 클라이언트에서 즉시 다시 유저 정보 조회 가능

사용 예시

// AS-IS: 정적 staleTime
await queryClient.prefetchQuery({
  queryKey: getGetCurrentUserQueryKey(),
  queryFn: () => getServerCurrentUser(),
  staleTime: 13 * 60 * 1000, // 에러도 13분간 캐싱됨
});

// TO-BE: 동적 staleTime
await queryClient.prefetchQuery({
  queryKey: getGetCurrentUserQueryKey(),
  queryFn: () => getServerCurrentUser(),
  staleTime: (query) => {
    // 에러나 null이면 즉시 재검증
    if (query.state.status === 'error' || query.state.data === null) {
      return 0;
    }
    return 13 * 60 * 1000; // 13분
  },
});

Before-After

Before: SSR 401 에러 → 13분간 캐싱 → 클라이언트에서 재시도 없음

After: SSR 401 에러 → 즉시 stale → 클라이언트에서 즉시 재시도


4. 레거시 authStore 제거

파일: apps/web/src/stores/authStore.ts (삭제됨)

더 이상 사용하지 않는 Zustand 기반 인증 상태 관리를 제거했습니다.

핵심 기술 및 구현사항

  • 제거 이유: HttpOnly Cookie + React Query 기반으로 완전히 전환
  • 영향: 59줄 삭제, 불필요한 상태 관리 제거
  • 효과: 코드베이스 단순화, 상태 동기화 이슈 제거

사용 예시

// AS-IS: Zustand 스토어 (삭제됨)
import { create } from 'zustand';

interface AuthState {
  accessToken: string | null;
  refreshToken: string | null;
  setTokens: (accessToken: string, refreshToken: string) => void;
  clearTokens: () => void;
}

export const useAuthStore = create<AuthState>((set) => ({
  // ... 59 lines
}));

// TO-BE: React Query로 완전 대체
export function useAuth() {
  const { data: user, isLoading } = useGetCurrentUser();
  return { isAuth: !!user, isLoading, user };
}

5. 인증 관련 페이지 정리

파일:

회원가입, 로그인, 프로필 페이지의 인증 처리 로직을 개선했습니다.

핵심 기술 및 구현사항

  • 회원가입 처리 개선: 클라이언트 라우터 정리 로직 제거, 서버 리다이렉트로 통일
  • RedirectIfAuthed 컴포넌트 삭제: 미들웨어에서 이미 처리하므로 중복 제거
  • 로그아웃 적용: 프로필 페이지에 로그아웃 기능 추가
  • 루트 페이지 분리: 클라이언트 버튼을 별도 컴포넌트로 분리

사용 예시

// AS-IS: 클라이언트에서 라우터 정리 (삭제됨)
const router = useRouter();
useEffect(() => {
  if (isAuth) {
    // 브라우저 히스토리 정리
    window.history.pushState(null, '', '/main/profile');
    router.replace('/main/profile');
  }
}, [isAuth]);

// TO-BE: 서버 리다이렉트로 통일 (middleware.ts)
if (PUBLIC_ROUTES.some((route) => pathname.startsWith(route))) {
  if (hasAuth) {
    const mainUrl = new URL('/main/profile', request.url);
    return NextResponse.redirect(mainUrl);
  }
}
// AS-IS: RedirectIfAuthed 컴포넌트 (삭제됨 - 43 lines)
export function RedirectIfAuthed({ children }) {
  const { isAuth } = useAuth();
  useEffect(() => {
    if (isAuth) router.replace('/main/profile');
  }, [isAuth]);
  return children;
}

// TO-BE: 미들웨어에서 처리 (중복 제거)
// 로그아웃 기능 추가
export default function ProfilePage() {
  const { mutate: logout } = useLogout();

  return <button onClick={() => logout()}>로그아웃</button>;
}

📸 스크린샷

UI 변경 없음 (백엔드 인증 로직 버그 수정 및 코드 정리)

AS-IS

  • SocketError 로그 출력
  • SSR 401 시 장시간 캐싱
  • 불필요한 코드 산재

TO-BE

  • 정상적인 토큰 갱신
  • SSR 401 시 즉시 재검증
  • 깔끔한 코드베이스

🔗 이슈

closes #80


✅ 체크리스트

  • 코드가 스타일 가이드를 따릅니다
  • 자체 코드 리뷰를 완료했습니다
  • 복잡/핵심 로직에 주석을 추가했습니다
  • 관심사 분리를 확인했습니다
  • 잠재적 사이드이펙트를 점검했습니다
  • Vercel Preview로 테스트를 완료했습니다

🧪 테스트 방법

엣지 케이스 및 버그 수정을 검증했습니다.

1. 미들웨어 SocketError 해결 검증

  • HTTPS 모드에서 accessToken 만료 시 자동 갱신 확인
  • 미들웨어 로그에서 SocketError 없이 "토큰 갱신 성공" 확인
  • 갱신된 쿠키가 브라우저에 정상 저장되는지 확인

2. 쿠키 Domain 속성 제거 검증

  • localhost 환경에서 토큰 갱신 후 쿠키 저장 확인
  • 개발자 도구 → Application → Cookies에서 accessToken/refreshToken 확인
  • Domain 속성이 현재 도메인으로 설정되었는지 확인

3. SSR Prefetch 401 엣지케이스 검증

  • SSR에서 401 에러 발생 시나리오 테스트
  • 클라이언트에서 즉시 재시도되는지 확인 (13분 대기 없음)
  • React Query DevTools로 staleTime이 0으로 설정되었는지 확인

4. 레거시 authStore 제거 검증

  • useAuthStore 사용처가 모두 제거되었는지 확인
  • 빌드 에러 없이 성공적으로 빌드되는지 확인
  • 인증 플로우가 React Query만으로 동작하는지 확인

5. 인증 페이지 정리 검증

  • 회원가입 완료 후 프로필 페이지로 정상 이동
  • 로그인된 상태에서 로그인 페이지 접근 시 메인으로 리다이렉트
  • 프로필 페이지에서 로그아웃 기능 동작 확인
  • 루트 페이지의 클라이언트 버튼 정상 동작 확인

📝 추가 노트

엣지 케이스 수정 내역

이번 PR은 SOS-41에서 구현한 인증 시스템의 버그 및 엣지 케이스를 수정하는 작업입니다.

1. SocketError (순환 참조)

미들웨어에서 localhost:3000/api/auth/refresh를 호출하면 다시 미들웨어가 실행되어 무한 루프가 발생했습니다. 백엔드 API를 직접 호출하도록 수정하여 해결했습니다.

2. Cookie Domain 불일치

백엔드에서 프로덕션 도메인(soso.dreampaste.com)을 포함한 쿠키를 전송하면 로컬 환경(localhost)에서 저장되지 않았습니다. 미들웨어에서 Domain 속성을 제거하여 현재 환경에 맞게 자동 설정되도록 했습니다.

3. SSR Prefetch 캐싱 문제

SSR에서 401 에러가 발생하면 React Query가 null을 13분간 캐싱하여 클라이언트에서 재시도하지 않았습니다. 동적 staleTime을 사용하여 에러 시 즉시 재검증하도록 수정했습니다.

코드 정리 내역

1. Zustand 스토어 제거

HttpOnly Cookie + React Query로 완전히 전환했으므로 더 이상 필요 없는 Zustand 기반 authStore를 제거했습니다.

2. 중복 로직 제거

미들웨어에서 이미 인증 체크를 하므로, 클라이언트 컴포넌트의 중복 리다이렉트 로직(RedirectIfAuthed)을 제거했습니다.

Copilot AI review requested due to automatic review settings November 28, 2025 14:42
@linear
Copy link

linear bot commented Nov 28, 2025

@DreamPaste DreamPaste requested a review from youdaeng2 November 28, 2025 14:42
@DreamPaste DreamPaste self-assigned this Nov 28, 2025
@DreamPaste DreamPaste added Fix 🔧 문제가 있는 내용을 수정합니다! Refactor 🫧 기존 내용을 개선하거나 최적화합니다! labels Nov 28, 2025
@github-actions
Copy link

Copy link

Copilot AI left a 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은 HttpOnly Cookie 기반 인증 시스템의 엣지 케이스를 해결하고 레거시 코드를 정리하는 작업입니다. 주요 개선사항으로는 미들웨어의 순환 참조 문제 해결, 쿠키 도메인 속성 제거, SSR prefetch 캐싱 문제 해결, 그리고 더 이상 사용하지 않는 Zustand 기반 authStore 제거가 포함됩니다.

  • 미들웨어 순환 참조 해결: 프록시 대신 백엔드 API를 직접 호출하여 SocketError 해결
  • 쿠키 도메인 처리 개선: Set-Cookie 헤더의 Domain 속성을 제거하여 환경별 자동 설정
  • SSR 401 엣지케이스 해결: 동적 staleTime을 사용하여 에러 시 즉시 재검증 가능
  • 코드 정리: Zustand authStore 제거 및 React Query 기반 인증으로 완전 전환

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
apps/web/src/stores/authStore.ts Zustand 기반 인증 스토어 완전 제거 (59줄 삭제)
apps/web/src/middleware.ts 백엔드 직접 호출 방식으로 변경, Domain 속성 제거 로직 추가
apps/web/src/providers/AuthHydrationProvider.tsx 동적 staleTime으로 SSR 401 에러 시 즉시 재검증 설정
apps/web/src/components/buttons/Button.tsx inline-flex에서 flex로 변경 및 Pressable 래퍼에 className 추가
apps/web/src/app/page.tsx 클라이언트 버튼을 별도 StartButton 컴포넌트로 분리
apps/web/src/app/(auth)/components/StartButton.tsx 루트 페이지용 시작 버튼 컴포넌트 신규 추가
apps/web/src/app/main/profile/page.tsx authStore 대신 useAuth 훅을 사용한 로그아웃 구현
apps/web/src/app/(auth)/signup/layout.tsx 중복 RedirectIfAuthed 컴포넌트 제거 (미들웨어에서 처리)
apps/web/src/app/(auth)/signup/components/RedirectIfAuthed.tsx 미들웨어와 중복되는 클라이언트 리다이렉트 컴포넌트 삭제 (43줄)
apps/web/src/app/(auth)/signup/[type]/complete/page.tsx authStore 대신 useAuth 훅 사용, User 타입으로 간소화


const classes = cn(
'relative inline-flex items-center justify-center rounded-lg font-medium select-none overflow-hidden',
'relative flex items-center justify-center rounded-lg font-medium select-none overflow-hidden',
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

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

inline-flex에서 flex로 변경하면 버튼이 블록 레벨 요소처럼 동작하여 전체 너비를 차지하게 됩니다. 이는 의도하지 않은 레이아웃 변경을 초래할 수 있습니다.

영향:

  • 기존: 버튼이 콘텐츠 크기만큼만 차지 (inline-flex)
  • 변경 후: 버튼이 부모 요소의 전체 너비를 차지할 수 있음 (flex)

className="w-full"을 명시적으로 사용하는 경우가 아니라면 inline-flex를 유지하는 것이 안전합니다.

제안:

'relative inline-flex items-center justify-center rounded-lg font-medium select-none overflow-hidden',
Suggested change
'relative flex items-center justify-center rounded-lg font-medium select-none overflow-hidden',
'relative inline-flex items-center justify-center rounded-lg font-medium select-none overflow-hidden',

Copilot uses AI. Check for mistakes.
* - Domain 속성 제거 (현재 도메인으로 자동 설정)
*/
function modifySetCookie(cookie: string): string {
return cookie.replace(/Domain=[^;]+;?\s*/gi, '');
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

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

정규식이 Set-Cookie 헤더의 첫 번째 속성이 Domain인 경우를 올바르게 처리하지 못합니다.

문제 시나리오:

Domain=example.com; Path=/; Secure

이 경우 Domain=[^;]+;?\s*Domain=example.com; 를 제거하여 Path=/; Secure를 반환합니다. 하지만 첫 번째 속성이므로 세미콜론이 남아있으면 문제가 됩니다.

일반적으로 Set-Cookie 헤더는 name=value로 시작하고 속성들이 세미콜론으로 구분되므로, Domain은 첫 번째가 아닌 중간 속성일 가능성이 높습니다.

더 안전한 패턴:

function modifySetCookie(cookie: string): string {
  return cookie.replace(/;\s*Domain=[^;]+/gi, '');
}

이렇게 하면 세미콜론을 포함하여 제거하므로 다른 속성과의 구분이 명확해집니다.

Suggested change
return cookie.replace(/Domain=[^;]+;?\s*/gi, '');
// 중간/끝에 위치한 Domain 속성 제거
let result = cookie.replace(/;\s*Domain=[^;]+/gi, '');
// 맨 앞에 위치한 Domain 속성 제거
result = result.replace(/^Domain=[^;]+;?\s*/i, '');
return result;

Copilot uses AI. Check for mistakes.
method: 'POST',
headers: {
'Content-Type': 'application/json',
Cookie: `refreshToken=${refreshToken}`,
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

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

쿠키를 수동으로 구성할 때 refreshToken 값에 대한 URL 인코딩이 누락되어 있습니다. 토큰 값에 특수 문자가 포함된 경우 파싱 오류가 발생할 수 있습니다.

제안:

Cookie: `refreshToken=${encodeURIComponent(refreshToken)}`,
Suggested change
Cookie: `refreshToken=${refreshToken}`,
Cookie: `refreshToken=${encodeURIComponent(refreshToken)}`,

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +46
staleTime: (query) => {
if (
query.state.status === 'error' ||
query.state.data === null
) {
return 0;
}
return 13 * 60 * 1000; // 13분
},
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

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

새로 추가된 동적 staleTime 로직에 대한 주석이 없습니다. 이는 SSR Prefetch 401 엣지케이스를 해결하기 위한 중요한 변경사항이므로 코드에 설명을 추가하는 것이 좋습니다.

제안:

staleTime: (query) => {
  // SSR에서 401 에러가 발생한 경우 즉시 재검증하도록 설정
  // 클라이언트에서 즉시 다시 유저 정보를 조회할 수 있음
  if (
    query.state.status === 'error' ||
    query.state.data === null
  ) {
    return 0;
  }
  return 13 * 60 * 1000; // 13분
},

Copilot uses AI. Check for mistakes.
Comment on lines 10 to 11
logout();
router.push('/');
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

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

logout() 호출 직후 router.push('/')를 실행하면 로그아웃 API 요청이 완료되기 전에 페이지 이동이 발생할 수 있습니다. 이는 비동기 작업이므로 완료를 기다려야 합니다.

문제점:

  • logout()mutate 함수로 비동기 작업을 시작만 함
  • 즉시 router.push('/')가 실행되어 페이지 이동
  • 로그아웃 API 요청이 취소되거나 완료되지 않을 수 있음

제안:

const handleLogout = () => {
  logout(undefined, {
    onSettled: () => {
      router.push('/');
    }
  });
};

또는 useLogout 훅의 onSuccess 콜백에서 리다이렉트를 처리하는 방법도 있습니다.

Suggested change
logout();
router.push('/');
logout(undefined, {
onSettled: () => {
router.push('/');
},
});

Copilot uses AI. Check for mistakes.
@github-actions
Copy link

github-actions bot commented Nov 28, 2025

📦 번들 분석 결과

📊 번들 크기 요약

항목
📦 전체 번들 크기 3.7M
📄 JavaScript 크기 1.5M
🗂️ JavaScript 파일 수 58개

🔍 주요 청크 파일 (크기순)

fe98bb7c-75056deddb8826d9.js - 169K
framework-69e0f7d37422957b.js - 137K
main-ef574bfa9cb78477.js - 130K
7147-4b792b1c613a0a9e.js - 122K
1762-0e7232d83dcb3887.js - 121K
polyfills-42372ed130431b0a.js - 110K
3920-f5a845f8074e2bd5.js - 85K
6086-65069d4634f32053.js - 76K
page-3026af1cfd56a766.js - 31K
2906-230d0473dd8a9533.js - 28K

🤖 자동 생성된 번들 분석 리포트

@github-actions
Copy link

github-actions bot commented Nov 28, 2025

⚡ Lighthouse 성능 분석 결과

📊 전체 평균 점수

지표 점수
🚀 Performance 73점
♿ Accessibility 86점
✅ Best Practices 100점
🔍 SEO 100점

📈 측정 현황

  • 측정 성공: 15/16 페이지
  • 상태: success

📄 페이지별 상세 분석

🏠 커뮤니티 페이지: /main/community

지표 점수
🚀 Performance 68점
♿ Accessibility 78점
✅ Best Practices 100점
🔍 SEO 100점

📊 상세 분석 보기

👥 창업자 페이지: /main/founder

지표 점수
🚀 Performance 75점
♿ Accessibility 87점
✅ Best Practices 100점
🔍 SEO 100점

📊 상세 분석 보기

🏡 홈 페이지: /main/home

지표 점수
🚀 Performance 75점
♿ 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점

📊 상세 분석 보기

🔗 전체 상세 분석 결과

📊 전체 상세 Lighthouse 분석 결과 보기

📄 측정된 페이지

  • /main/community
  • /main/founder
  • /main/home
  • /main/maps
  • /main/profile

모든 페이지에서 성능 측정이 완료되었습니다.


🤖 자동 생성된 Lighthouse 성능 리포트

Copy link
Member

@youdaeng2 youdaeng2 left a comment

Choose a reason for hiding this comment

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

수정사항 잘 읽었습니다 고생하셨습니다. 제 로컬에서 있던 문제도 이 pr로 해결될 것 같네요!

@github-actions
Copy link

github-actions bot commented Dec 3, 2025

@DreamPaste DreamPaste merged commit 4a7cb8e into dev Dec 3, 2025
4 checks passed
@DreamPaste DreamPaste deleted the feat/SOS-42-auth branch December 3, 2025 09:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Fix 🔧 문제가 있는 내용을 수정합니다! Refactor 🫧 기존 내용을 개선하거나 최적화합니다! 휘건

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 자유게시판 리스트 SSR 기반 개선

3 participants