# Safari 쿠키 호환성 및 크로스 브라우저 가이드 ## 📋 목차 1. [문제 상황](#문제-상황) 2. [원인 분석](#원인-분석) 3. [해결 방법](#해결-방법) 4. [수정된 파일](#수정된-파일) 5. [크로스 브라우저 개발 가이드라인](#크로스-브라우저-개발-가이드라인) 6. [테스트 체크리스트](#테스트-체크리스트) --- ## 문제 상황 ### Safari에서 발생한 인증 문제 - **로그인**: 성공했으나 대시보드로 이동 불가 ({"error":"Not authenticated"}) - **로그아웃**: 로그아웃 버튼 클릭 시 정상 동작하지 않음 - **크롬/파이어폭스**: 정상 작동 ### 증상 ```bash # Safari 브라우저 ✅ 로그인 API 호출 성공 (200 OK) ❌ 대시보드 접근 실패 (401 Unauthorized) ❌ 쿠키가 저장되지 않음 # Chrome/Firefox 브라우저 ✅ 모든 기능 정상 작동 ``` --- ## 원인 분석 ### Safari의 엄격한 쿠키 정책 Safari는 다른 브라우저보다 **쿠키 보안 정책이 엄격**합니다: #### 1. Secure 속성 제한 ```typescript // ❌ Safari에서 작동하지 않음 (HTTP 환경) const cookie = 'access_token=xxx; HttpOnly; Secure; SameSite=Strict'; // Safari 로직: // - HTTP (localhost:3000) + Secure 속성 = 쿠키 저장 거부 // - HTTPS만 Secure 쿠키 허용 ``` Chrome/Firefox는 `localhost`에서 `Secure` 속성을 허용하지만, **Safari는 허용하지 않습니다**. #### 2. SameSite=Strict의 제약 ```typescript // SameSite=Strict: 모든 크로스 사이트 요청에서 쿠키 차단 // - 너무 엄격하여 일부 정상적인 요청도 차단될 수 있음 // SameSite=Lax: CSRF 보호 + 유연성 // - GET 요청과 top-level navigation에서는 쿠키 전송 허용 // - 대부분의 웹 애플리케이션에 적합 ``` #### 3. 쿠키 삭제 시 속성 불일치 Safari는 쿠키를 삭제할 때 **설정할 때와 정확히 동일한 속성**을 요구합니다: ```typescript // ❌ Safari에서 쿠키 삭제 실패 // 설정: HttpOnly + SameSite=Lax (Secure 없음) // 삭제: HttpOnly + Secure + SameSite=Strict // ✅ Safari에서 쿠키 삭제 성공 // 설정: HttpOnly + SameSite=Lax (Secure 없음) // 삭제: HttpOnly + SameSite=Lax (Secure 없음) ``` --- ## 해결 방법 ### 핵심 원칙: 환경별 조건부 쿠키 설정 ```typescript // 1. 환경 감지 const isProduction = process.env.NODE_ENV === 'production'; // 2. 조건부 Secure 속성 const cookie = [ 'access_token=xxx', 'HttpOnly', // ✅ 항상 유지 (XSS 보호) ...(isProduction ? ['Secure'] : []), // ✅ HTTPS에서만 적용 'SameSite=Lax', // ✅ CSRF 보호 + 호환성 'Path=/', 'Max-Age=7200', ].join('; '); ``` ### 환경별 쿠키 속성 | 환경 | Secure | SameSite | HttpOnly | 설명 | |------|--------|----------|----------|------| | **Development** (HTTP) | ❌ 없음 | Lax | ✅ 있음 | Safari 호환성 | | **Production** (HTTPS) | ✅ 있음 | Lax | ✅ 있음 | 완전한 보안 | --- ## 수정된 파일 ### 1. `src/app/api/auth/login/route.ts` **수정 위치**: 150-170 라인 ```typescript // ❌ 기존 코드 (Safari 비호환) const accessTokenCookie = [ `access_token=${data.access_token}`, 'HttpOnly', 'Secure', // 개발 환경에서 문제 발생 'SameSite=Strict', // 너무 엄격 'Path=/', `Max-Age=${data.expires_in || 7200}`, ].join('; '); ``` ```typescript // ✅ 수정 코드 (Safari 호환) const isProduction = process.env.NODE_ENV === 'production'; const accessTokenCookie = [ `access_token=${data.access_token}`, 'HttpOnly', // ✅ JavaScript cannot access (XSS 보호) ...(isProduction ? ['Secure'] : []), // ✅ HTTPS only in production 'SameSite=Lax', // ✅ CSRF protection (Lax for compatibility) 'Path=/', `Max-Age=${data.expires_in || 7200}`, ].join('; '); const refreshTokenCookie = [ `refresh_token=${data.refresh_token}`, 'HttpOnly', ...(isProduction ? ['Secure'] : []), 'SameSite=Lax', 'Path=/', 'Max-Age=604800', // 7 days ].join('; '); ``` **변경 사항**: - ✅ `Secure` 속성을 환경에 따라 조건부 적용 - ✅ `SameSite`를 `Strict`에서 `Lax`로 변경 - ✅ `refresh_token`도 동일하게 적용 --- ### 2. `src/app/api/auth/check/route.ts` **수정 위치**: 75-95 라인 (토큰 갱신 시) ```typescript // ✅ 수정 코드 if (refreshResponse.ok) { const data = await refreshResponse.json(); // Safari compatibility: Secure only in production const isProduction = process.env.NODE_ENV === 'production'; const accessTokenCookie = [ `access_token=${data.access_token}`, 'HttpOnly', ...(isProduction ? ['Secure'] : []), 'SameSite=Lax', 'Path=/', `Max-Age=${data.expires_in || 7200}`, ].join('; '); const refreshTokenCookie = [ `refresh_token=${data.refresh_token}`, 'HttpOnly', ...(isProduction ? ['Secure'] : []), 'SameSite=Lax', 'Path=/', 'Max-Age=604800', ].join('; '); // ... 쿠키 설정 } ``` **변경 사항**: - ✅ 토큰 갱신 시에도 동일한 쿠키 설정 적용 - ✅ login/route.ts와 일관성 유지 --- ### 3. `src/app/api/auth/logout/route.ts` **수정 위치**: 52-71 라인 (쿠키 삭제) ```typescript // ❌ 기존 코드 (Safari에서 쿠키 삭제 실패) const clearAccessToken = [ 'access_token=', 'HttpOnly', 'Secure', // 설정 시와 속성 불일치 'SameSite=Strict', // 설정 시와 속성 불일치 'Path=/', 'Max-Age=0', ].join('; '); ``` ```typescript // ✅ 수정 코드 (Safari에서 쿠키 삭제 성공) // Safari compatibility: Must use same attributes as when setting cookies const isProduction = process.env.NODE_ENV === 'production'; const clearAccessToken = [ 'access_token=', 'HttpOnly', ...(isProduction ? ['Secure'] : []), // ✅ login과 동일 'SameSite=Lax', // ✅ login과 동일 'Path=/', 'Max-Age=0', // Delete immediately ].join('; '); const clearRefreshToken = [ 'refresh_token=', 'HttpOnly', ...(isProduction ? ['Secure'] : []), 'SameSite=Lax', 'Path=/', 'Max-Age=0', ].join('; '); ``` **변경 사항**: - ✅ 쿠키 삭제 시 설정 시와 **정확히 동일한 속성** 사용 - ✅ Safari의 엄격한 쿠키 삭제 정책 대응 --- ## 크로스 브라우저 개발 가이드라인 ### 필수 테스트 브라우저 모든 브라우저 관련 기능 개발 시 **다음 브라우저에서 반드시 테스트**: | 브라우저 | 우선순위 | 주요 특징 | 테스트 환경 | |---------|---------|----------|------------| | **Chrome** | 🔴 High | 가장 관대한 정책 | macOS/Windows | | **Safari** | 🔴 High | 가장 엄격한 정책 | macOS/iOS | | **Firefox** | 🟡 Medium | 중간 수준 정책 | macOS/Windows | | **Edge** | 🟢 Low | Chrome 기반 | Windows | **개발 우선순위**: Safari 기준으로 개발하면 다른 브라우저에서도 작동합니다. --- ### 쿠키 관련 개발 원칙 #### 1. 환경별 조건부 설정 ```typescript // ✅ 항상 환경 체크 const isProduction = process.env.NODE_ENV === 'production'; const isSecure = isProduction; // HTTPS 여부 // ✅ Secure 속성은 항상 조건부로 ...(isSecure ? ['Secure'] : []) ``` #### 2. HttpOnly는 항상 유지 ```typescript // ✅ XSS 공격 방지를 위해 HttpOnly는 항상 포함 'HttpOnly', // 절대 제거하지 말 것 ``` #### 3. SameSite는 Lax 권장 ```typescript // ✅ CSRF 보호 + 유연성 'SameSite=Lax', // 대부분의 웹 앱에 적합 // ⚠️ Strict는 너무 엄격 'SameSite=Strict', // 특별한 이유가 있을 때만 사용 ``` #### 4. 쿠키 삭제 시 속성 일치 ```typescript // ✅ 설정할 때와 삭제할 때 속성이 정확히 일치해야 함 const setCookie = 'token=xxx; HttpOnly; SameSite=Lax'; const deleteCookie = 'token=; HttpOnly; SameSite=Lax; Max-Age=0'; ``` --- ### 로컬스토리지 vs 쿠키 선택 가이드 | 저장소 | 용도 | 보안 | Safari 호환성 | |--------|------|------|---------------| | **HttpOnly Cookie** | 인증 토큰 | ✅ 높음 (XSS 방지) | ✅ 조건부 설정 필요 | | **LocalStorage** | 사용자 정보, 설정 | ⚠️ 낮음 (XSS 취약) | ✅ 호환성 좋음 | **원칙**: 민감한 데이터(토큰)는 HttpOnly 쿠키, 일반 데이터는 LocalStorage --- ### Safari 개발 시 주의사항 #### 1. 쿠키 관련 - ✅ HTTP 환경에서 `Secure` 속성 제거 - ✅ 쿠키 설정과 삭제 시 속성 일치 - ✅ `SameSite=Lax` 사용 권장 #### 2. 네트워크 요청 ```typescript // ✅ Safari는 credentials 설정에 민감 fetch('/api/auth/check', { method: 'GET', credentials: 'include', // Safari에서 쿠키 전송 필수 }); ``` #### 3. 로컬스토리지 ```typescript // ✅ Safari Private Mode에서 localStorage 제한 try { localStorage.setItem('key', 'value'); } catch (error) { // Safari Private Mode 대응 console.warn('LocalStorage unavailable:', error); } ``` #### 4. 날짜/시간 ```typescript // ❌ Safari에서 파싱 실패 가능 new Date('2024-01-01 12:00:00'); // ✅ ISO 8601 형식 사용 new Date('2024-01-01T12:00:00Z'); ``` --- ### 크로스 브라우저 테스트 도구 #### 개발 환경 테스트 ```bash # Chrome open -a "Google Chrome" http://localhost:3000 # Safari open -a Safari http://localhost:3000 # Firefox open -a Firefox http://localhost:3000 ``` #### 개발자 도구 활용 ```javascript // Safari: Develop → Show Web Inspector → Storage // Chrome: DevTools → Application → Cookies // Firefox: DevTools → Storage → Cookies // 쿠키 확인 사항: // - Name: access_token, refresh_token // - HttpOnly: ✅ 체크 // - Secure: 환경에 따라 조건부 // - SameSite: Lax ``` --- ## 테스트 체크리스트 ### 로그인 기능 테스트 #### Chrome - [ ] 로그인 성공 - [ ] 대시보드 접근 가능 - [ ] 쿠키 저장 확인 (DevTools → Application → Cookies) - [ ] HttpOnly 속성 확인 - [ ] 로그아웃 성공 - [ ] 쿠키 삭제 확인 #### Safari - [ ] 로그인 성공 - [ ] 대시보드 접근 가능 - [ ] 쿠키 저장 확인 (Web Inspector → Storage → Cookies) - [ ] HttpOnly 속성 확인 - [ ] Secure 속성 **없음** 확인 (개발 환경) - [ ] 로그아웃 성공 - [ ] 쿠키 삭제 확인 #### Firefox (선택) - [ ] 로그인 성공 - [ ] 대시보드 접근 가능 - [ ] 쿠키 저장 확인 - [ ] 로그아웃 성공 --- ### 인증 상태 확인 테스트 #### 시나리오 1: 페이지 새로고침 - [ ] Chrome: 로그인 상태 유지 - [ ] Safari: 로그인 상태 유지 - [ ] Firefox: 로그인 상태 유지 #### 시나리오 2: 브라우저 재시작 - [ ] Chrome: 로그인 상태 유지 (Remember me) - [ ] Safari: 로그인 상태 유지 - [ ] Firefox: 로그인 상태 유지 #### 시나리오 3: 토큰 만료 - [ ] Chrome: 자동 토큰 갱신 - [ ] Safari: 자동 토큰 갱신 - [ ] Firefox: 자동 토큰 갱신 --- ### 프로덕션 배포 전 체크리스트 #### 환경 설정 - [ ] `NODE_ENV=production` 설정 확인 - [ ] HTTPS 인증서 설정 완료 - [ ] 환경 변수 `.env.production` 확인 #### 쿠키 설정 확인 - [ ] Production 환경에서 `Secure` 속성 포함 확인 - [ ] `HttpOnly` 속성 유지 확인 - [ ] `SameSite=Lax` 설정 확인 - [ ] `Max-Age` 적절히 설정 (access: 2h, refresh: 7d) #### 브라우저 테스트 (HTTPS) - [ ] Chrome: 로그인/로그아웃 정상 - [ ] Safari: 로그인/로그아웃 정상 - [ ] Firefox: 로그인/로그아웃 정상 - [ ] Safari iOS: 모바일 테스트 --- ## 문제 해결 가이드 ### 쿠키가 저장되지 않는 경우 #### 1. Safari 개발 환경 ```typescript // 체크 포인트: // ✅ Secure 속성이 조건부로 설정되어 있는가? ...(isProduction ? ['Secure'] : []) // ✅ SameSite가 Lax인가? 'SameSite=Lax' // ✅ HttpOnly는 포함되어 있는가? 'HttpOnly' ``` #### 2. Safari Private Mode Safari Private Mode에서는 일부 쿠키가 제한될 수 있습니다. → 일반 모드에서 테스트하세요. #### 3. 쿠키 도메인 설정 ```typescript // ✅ localhost에서는 Domain 속성 생략 // ❌ 'Domain=localhost' (불필요) ``` --- ### 쿠키가 삭제되지 않는 경우 #### Safari 로그아웃 문제 ```typescript // ❌ 설정 시와 삭제 시 속성 불일치 // 설정: HttpOnly + SameSite=Lax // 삭제: HttpOnly + Secure + SameSite=Strict // ✅ 설정 시와 삭제 시 속성 일치 const isProduction = process.env.NODE_ENV === 'production'; const cookie = [ 'token=', 'HttpOnly', ...(isProduction ? ['Secure'] : []), // 일치 'SameSite=Lax', // 일치 'Max-Age=0', ].join('; '); ``` --- ## 관련 문서 - [MDN - HTTP Cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) - [MDN - SameSite Cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) - [Safari Cookie Policy](https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/) --- ## 업데이트 히스토리 | 날짜 | 내용 | 작성자 | |------|------|--------| | 2024-XX-XX | Safari 쿠키 호환성 문서 작성 | Claude | --- **📌 기억하세요**: 브라우저 관련 기능 개발 시 **Safari를 기준으로 개발**하면 다른 브라우저에서도 작동합니다!