- BOM 항목 추가/수정/삭제 시 섹션탭 즉시 반영 - 섹션 복제 시 UI 즉시 업데이트 (null vs undefined 이슈 해결) - 항목 수정 기능 추가 (useTemplateManagement) - 실시간 동기화 문서 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
13 KiB
13 KiB
Safari 쿠키 호환성 및 크로스 브라우저 가이드
📋 목차
문제 상황
Safari에서 발생한 인증 문제
- 로그인: 성공했으나 대시보드로 이동 불가 ({"error":"Not authenticated"})
- 로그아웃: 로그아웃 버튼 클릭 시 정상 동작하지 않음
- 크롬/파이어폭스: 정상 작동
증상
# Safari 브라우저
✅ 로그인 API 호출 성공 (200 OK)
❌ 대시보드 접근 실패 (401 Unauthorized)
❌ 쿠키가 저장되지 않음
# Chrome/Firefox 브라우저
✅ 모든 기능 정상 작동
원인 분석
Safari의 엄격한 쿠키 정책
Safari는 다른 브라우저보다 쿠키 보안 정책이 엄격합니다:
1. Secure 속성 제한
// ❌ 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의 제약
// SameSite=Strict: 모든 크로스 사이트 요청에서 쿠키 차단
// - 너무 엄격하여 일부 정상적인 요청도 차단될 수 있음
// SameSite=Lax: CSRF 보호 + 유연성
// - GET 요청과 top-level navigation에서는 쿠키 전송 허용
// - 대부분의 웹 애플리케이션에 적합
3. 쿠키 삭제 시 속성 불일치
Safari는 쿠키를 삭제할 때 설정할 때와 정확히 동일한 속성을 요구합니다:
// ❌ Safari에서 쿠키 삭제 실패
// 설정: HttpOnly + SameSite=Lax (Secure 없음)
// 삭제: HttpOnly + Secure + SameSite=Strict
// ✅ Safari에서 쿠키 삭제 성공
// 설정: HttpOnly + SameSite=Lax (Secure 없음)
// 삭제: HttpOnly + SameSite=Lax (Secure 없음)
해결 방법
핵심 원칙: 환경별 조건부 쿠키 설정
// 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 라인
// ❌ 기존 코드 (Safari 비호환)
const accessTokenCookie = [
`access_token=${data.access_token}`,
'HttpOnly',
'Secure', // 개발 환경에서 문제 발생
'SameSite=Strict', // 너무 엄격
'Path=/',
`Max-Age=${data.expires_in || 7200}`,
].join('; ');
// ✅ 수정 코드 (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 라인 (토큰 갱신 시)
// ✅ 수정 코드
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 라인 (쿠키 삭제)
// ❌ 기존 코드 (Safari에서 쿠키 삭제 실패)
const clearAccessToken = [
'access_token=',
'HttpOnly',
'Secure', // 설정 시와 속성 불일치
'SameSite=Strict', // 설정 시와 속성 불일치
'Path=/',
'Max-Age=0',
].join('; ');
// ✅ 수정 코드 (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. 환경별 조건부 설정
// ✅ 항상 환경 체크
const isProduction = process.env.NODE_ENV === 'production';
const isSecure = isProduction; // HTTPS 여부
// ✅ Secure 속성은 항상 조건부로
...(isSecure ? ['Secure'] : [])
2. HttpOnly는 항상 유지
// ✅ XSS 공격 방지를 위해 HttpOnly는 항상 포함
'HttpOnly', // 절대 제거하지 말 것
3. SameSite는 Lax 권장
// ✅ CSRF 보호 + 유연성
'SameSite=Lax', // 대부분의 웹 앱에 적합
// ⚠️ Strict는 너무 엄격
'SameSite=Strict', // 특별한 이유가 있을 때만 사용
4. 쿠키 삭제 시 속성 일치
// ✅ 설정할 때와 삭제할 때 속성이 정확히 일치해야 함
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. 네트워크 요청
// ✅ Safari는 credentials 설정에 민감
fetch('/api/auth/check', {
method: 'GET',
credentials: 'include', // Safari에서 쿠키 전송 필수
});
3. 로컬스토리지
// ✅ Safari Private Mode에서 localStorage 제한
try {
localStorage.setItem('key', 'value');
} catch (error) {
// Safari Private Mode 대응
console.warn('LocalStorage unavailable:', error);
}
4. 날짜/시간
// ❌ Safari에서 파싱 실패 가능
new Date('2024-01-01 12:00:00');
// ✅ ISO 8601 형식 사용
new Date('2024-01-01T12:00:00Z');
크로스 브라우저 테스트 도구
개발 환경 테스트
# Chrome
open -a "Google Chrome" http://localhost:3000
# Safari
open -a Safari http://localhost:3000
# Firefox
open -a Firefox http://localhost:3000
개발자 도구 활용
// 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 개발 환경
// 체크 포인트:
// ✅ Secure 속성이 조건부로 설정되어 있는가?
...(isProduction ? ['Secure'] : [])
// ✅ SameSite가 Lax인가?
'SameSite=Lax'
// ✅ HttpOnly는 포함되어 있는가?
'HttpOnly'
2. Safari Private Mode
Safari Private Mode에서는 일부 쿠키가 제한될 수 있습니다. → 일반 모드에서 테스트하세요.
3. 쿠키 도메인 설정
// ✅ localhost에서는 Domain 속성 생략
// ❌ 'Domain=localhost' (불필요)
쿠키가 삭제되지 않는 경우
Safari 로그아웃 문제
// ❌ 설정 시와 삭제 시 속성 불일치
// 설정: 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('; ');
관련 문서
업데이트 히스토리
| 날짜 | 내용 | 작성자 |
|---|---|---|
| 2024-XX-XX | Safari 쿠키 호환성 문서 작성 | Claude |
📌 기억하세요: 브라우저 관련 기능 개발 시 Safari를 기준으로 개발하면 다른 브라우저에서도 작동합니다!