HTTP/HTTPS 디버깅 악몽: Mixed Content 지옥을 통과한 24시간의 여정
React 앱이 HTTPS 페이지에서 HTTP 요청을 계속 보내는 이유를 디버깅하는 데 24시간을 보냈습니다—코드가 변환하고 있었는데도요. 범인에 충격을 받았습니다.
---
업데이트 (2025년 11월): STRAŦUM이 이제 Private Alpha로 라이브입니다! 이 글에서 언급된 9개 에이전트 마케팅 플랫폼이 초기 테스터를 받고 있습니다. stratum.chandlernguyen.com에서 접근을 요청하거나 전체 출시 이야기를 읽어보세요.
---
컨텍스트: 마케팅 플랫폼 여정 계속
낮잠 자면서 10개 에이전트 마케팅 플랫폼을 스피드런하고 있다고 했던 9월 글 기억하시나요? 4주차에 3개의 작동하는 에이전트가 있었고 10월/11월 알파 출시를 목표로 했습니다.
자, 이제 10월 하순입니다. 진행 상황 업데이트 시간입니다.
좋은 소식:
- 플랫폼이 드디어 이름을 갖게 되었습니다: STRAŦUM (Intelligence Over Execution)
- 완전한 브랜드 가이드라인과 디자인 시스템 (알고 보니 브랜딩이 코딩보다 더 오래 걸림)
- 10개 중 9개 에이전트 구축 및 통합 완료
- 멀티테넌트 아키텍처가 실제로 작동
- 최종 단계: 프리알파, 초대 전용 테스트 준비
현실 점검:
8일간 휴가를 간 후 10일간 아팠습니다(인생이란 게 그렇죠.) 모든 것이 일시 정지되고 타임라인이 밀렸습니다. 10월 출시는 무산, 맞죠?
하지만 솔로 개발의 장점은: 속도를 자신이 통제한다는 것입니다. 준비가 안 되었을 때 배포해야 하는 압박이 없습니다. 투자자가 목을 조르는 것도 없습니다. 그냥... 제대로 만드는 것입니다.
이 블로그 글은 6시간 디버깅 마라톤으로 변한 "제대로 만들기" 순간 중 하나에 대한 것입니다. 아프고 난 후 다시 전속력으로 돌아가려는데, 배포 플랫폼이 다른 계획을 가지고 있었기 때문입니다.
AWS가 다운되면 엔지니어는 창의적이 됩니다 (그리고 때때로 후회합니다)
코딩 부트캠프에서 가르쳐주지 않는 것이 있습니다: 때때로 배포 플랫폼이 그냥... 사라집니다. 여러분이 뭔가 잘못해서가 아니라, AWS가 Vercel 배포에 전 세계적으로 영향을 미치는 대규모 장애를 겪기로 했기 때문입니다.
제 월요일은 그렇게 시작되었습니다. 프로덕션 사이트가 다운, Vercel이 에러를 표시, 그리고 딱 하나의 생각: "백업이 필요해. 빨리."
Cloudflare Pages 등장. 좋은 소문을 들었습니다. 훌륭한 CDN, 자동 배포, 간단한 설정. 무엇이 잘못될 수 있겠어?
내레이터: 모든 것. 모든 것이 잘못될 수 있었습니다.
너무 쉬워 보인 전환
Cloudflare Pages로의 마이그레이션은 놀라울 정도로 매끄러웠습니다. GitHub 레포 연결, 대시보드에서 환경 변수 설정, main에 푸시. 3분 후: 배포 완료.
"와," 생각했습니다. "이것은 거의 너무 쉬운데."
그런 다음 프로덕션 사이트를 열었습니다.
```
Mixed Content: The page at 'https://my-site.com/...' was loaded over HTTPS,
but requested an insecure resource 'http://stratum-api.us-central1.run.app/...'
```
축하가 성급했음을 깨달을 때의 그 가라앉는 느낌? 네, 그거요.
문제: HTTPS 페이지에서의 HTTP 호출
React 앱이 페이지 자체는 HTTPS로 로드되었는데 백엔드 API에 HTTP 요청을 보내고 있었습니다. 브라우저는 (당연히) 이것을 보안 위험으로 차단합니다. Mixed Content 에러. 모든 API 호출이 실패하고 있었습니다.
"잠깐," 스스로에게 말했습니다, "코드에 ensureHttpsInProduction()이 있잖아! HTTP를 HTTPS로 자동 변환하게 되어있는데!"
배포된 번들을 확인했습니다. 함수는 있었습니다. 로직은 맞았습니다. 브라우저 콘솔이 변환이 일어나는 것을 보여줬습니다. 그런데 왜 HTTP 요청이 여전히 통과하는 걸까?
첫 번째 디버깅 시도: 환경 변수 탐색
Cloudflare의 환경 변수가 적용되지 않는 건 아닌가?
```bash
# Cloudflare 대시보드 확인
VITE_API_URL=https://stratum-api.us-central1.run.app ✓
VITE_SUPABASE_URL=https://your-project.supabase.co ✓
```
모두 HTTPS. 모두 정확함.
리빌드를 트리거했습니다. 기다렸습니다. 배포했습니다. 사이트를 열었습니다.
같은 에러. 여전히 HTTP 요청.
두 번째 시도: 대대적인 임포트 변경
파일들이 중앙집중된 API_BASE_URL을 사용하지 않는 건 아닌가?
다음 한 시간을 import.meta.env.VITE_API_URL을 직접 사용하는 대신 @/lib/api에서 임포트하도록 24개 파일을 업데이트하는 데 보냈습니다. API 호출을 하는 모든 컴포넌트가 처리를 받았습니다.
```typescript
// 이전
const response = await fetch(`${import.meta.env.VITE_API_URL}/api/v1/...`);
// 이후
import { API_BASE_URL } from '@/lib/api';
const response = await fetch(`${API_BASE_URL}/api/v1/...`);
```
푸시했습니다. 배포했습니다. 기다렸습니다.
여전히 망가져있습니다.
이 시점에서 제 인생 선택을 의심하기 시작했습니다.
반전: Git에서 누락된 파일
하지만 잠깐, 더 있습니다.
HTTPS 강제가 왜 작동하지 않는지 조사하면서 무서운 것을 발견했습니다. ensureHttpsInProduction()이 포함된 api.ts 파일이 Git 레포에 없었습니다.
authService.ts도 없었습니다. csvSanitizer.ts도요. 세 개의 중요한 프론트엔드 파일이 그냥... 누락.
어떻게? .gitignore 파일에 이것이 있었습니다:
```
# Python 관련
lib/
build/
dist/
```
Python에는 합리적으로 보이죠? 하지만 프론트엔드 유틸리티가 apps/web/src/lib/에 있었습니다. 넓은 lib/ 패턴이 실수로 전체 프론트엔드 lib 디렉토리를 무시하고 있었습니다!
이것이 의미하는 것:
1. Cloudflare가 레포에서 빌드 (이 파일들이 누락됨)
2. 로컬 개발에서는 이 파일들이 있음 (로컬에서는 잘 작동)
3. 추적되지 않고 있다는 것을 전혀 몰랐음
수정:
```diff
# .gitignore - 이전
-lib/
# .gitignore - 이후
+apps/api/lib/ # Python 전용
+!apps/web/src/lib/ # 프론트엔드 lib 명시적 포함
```
누락된 파일 추가, 커밋, 푸시. 이제 Cloudflare에 HTTPS 강제 코드가 있습니다!
그런데... HTTP 에러가 계속되었습니다.
세 번째 시도: 빌드 타임 검증
이 시점에서 모든 것을 의심하고 있었습니다. "있잖아," 생각했습니다. "이것이 계속 발생한다면 프로덕션에 도달하는 것을 방지해야 해."
프로덕션에서 HTTP URL을 감지하면 빌드를 실패시키는 Vite 플러그인을 작성했습니다:
```typescript
function validateProductionUrls(mode: string) {
if (mode !== 'production') return;
const apiUrl = process.env.VITE_API_URL || '';
if (apiUrl && apiUrl.trim().startsWith('http://')) {
if (!apiUrl.includes('localhost') && !apiUrl.includes('127.0.0.1')) {
throw new Error(
`❌ HTTPS ENFORCEMENT FAILED
Environment Variable: VITE_API_URL
Current Value: ${apiUrl}
This will cause Mixed Content errors in production!`
);
}
}
}
```
천재적이죠? 이제 HTTP URL로 배포하는 것이 불가능합니다.
다시 배포. 빌드 통과 (환경 변수가 HTTPS). 사이트 로드.
같은. 빌어먹을. 에러.
깨달음의 순간: 로컬 파일이 배포되고 있었다
아주 늦은 (매우 늦은) 밤, 깨달음이 왔습니다.
배포된 JavaScript 번들을 다시 확인했습니다. 정말 자세히 봤습니다. 안에 있는 URL은:
```javascript
"http://stratum-api.us-central1.run.app"
```
하지만 Cloudflare 환경 변수는 HTTPS였습니다. 그러면 이 HTTP URL은 어디에서 온 걸까?
그때 떠올랐습니다. 로컬 .env.production 파일.
```bash
# apps/web/.env.production (로컬 파일)
VITE_API_URL=http://stratum-api.us-central1.run.app
```
Cloudflare Pages가 대시보드 변수 대신 로컬 환경 파일을 배포하고 있었습니다!
.cloudflare-pages-ignore를 확인했습니다:
```
# 환경 파일
.env
.env.local
.env.development
.env.test
# .env.production ← 누락!
```
얼굴. 손바닥.
수정: 한 줄
```diff
# apps/web/.cloudflare-pages-ignore
.env
.env.local
.env.development
.env.test
+.env.production
```
배포했습니다. 기다렸습니다.
이번에는 다른 에러! 진전!
```
Access to fetch at 'https://stratum-api.us-central1.run.app/...'
from origin 'https://preview-xyz.stratum-marketing-suite.pages.dev'
has been blocked by CORS policy
```
CORS 에러! 아름다운, 아름다운 CORS 에러! HTTPS가 작동한다는 의미입니다!
하지만 잠깐, 더 있습니다: 캐시 음모
CORS 수정. 다시 배포. 커스텀 도메인을 열었습니다.
HTTP 에러가 다시.
뭐?!
알고 보니 Cloudflare의 CDN이 이전 번들을 공격적으로 캐싱하고 있었습니다. 새 배포(HTTPS 포함)는 프리뷰 URL에서 라이브였지만, 커스텀 도메인은 HTTP URL이 포함된 캐시된 콘텐츠를 제공하고 있었습니다.
Cloudflare의 캐시 퍼지는:
1. 올바른 zone 설정 찾기 (Pages 대시보드에 없음)
2. 도메인 설정 탐색 (명확하지 않음)
3. 수동으로 캐시 퍼지 (모든 배포마다)
수 시간의 HTTP/HTTPS 문제 디버깅 후, 결정을 내렸습니다.
Vercel로의 복귀: 때로는 지루한 것이 더 좋다
AWS가 복구되었습니다. Vercel이 다시 작동했습니다.
모든 것을 Vercel로 다시 마이그레이션했습니다. 왜?
1. 자동 캐시 무효화 - 수동 퍼지 불필요
2. 더 간단한 환경 변수 처리 - 설정한 것이 그대로 적용
3. 더 빠른 디버깅 - 추론해야 할 인프라가 적음
4. 검증됨 - 특이점을 알고 있음
Vercel 배포는 3분이 걸렸습니다. HTTP 에러 없음. 캐시 문제 없음. 그냥... 작동.
(어렵게) 배운 것
1. 배포 플랫폼에서 항상 .env.production을 무시하세요
```
# .vercelignore
# .cloudflare-pages-ignore
# .netlify-ignore
.env
.env.local
.env.development
.env.test
.env.production ← 이것을 잊지 마세요
```
2. 모노레포에서 넓은 .gitignore 패턴은 위험합니다
```diff
# ❌ 나쁨 - 프론트엔드와 백엔드 lib 폴더 모두 무시
-lib/
-build/
-dist/
# ✅ 좋음 - 각 컨텍스트에 특화
+apps/api/lib/ # Python 전용
+apps/api/build/
+apps/api/dist/
+apps/web/dist/ # Vite 출력만
```
항상 물어보세요: "이 패턴이 실수로 중요한 것을 무시할 수 있나?"
여러 언어(Python + TypeScript)를 사용하는 모노레포에서, 한 생태계를 위한 넓은 패턴이 실수로 다른 생태계의 중요한 파일을 무시할 수 있습니다.
3. 빌드 타임 검증은 여전히 가치 있습니다
로컬 파일 문제를 잡지 못했지만, 빌드 타임 검증은 미래의 잘못된 구성을 방지합니다:
```typescript
// vite.config.ts
export default defineConfig(({ mode }) => {
validateProductionUrls(mode);
return {
// ... config
};
});
```
4. 다층 방어가 효과적입니다
최종 아키텍처에는 세 개의 레이어가 있습니다:
- 빌드 타임: HTTP URL이 감지되면 빌드 실패
- 런타임: 페이지가 HTTPS로 로드되면 HTTP → HTTPS 변환
- 배포: 로컬 .env 파일 제외
5. 프리뷰 URL은 여러분의 친구입니다
항상 프리뷰 URL에서 먼저 테스트하세요. 그것이 작동하지만 커스텀 도메인이 작동하지 않으면 보통 캐싱 문제입니다.
6. 플랫폼의 특이점을 파악하세요
- Vercel: 간단, 자동 캐시 무효화, 환경 변수 "그냥 작동"
- Cloudflare Pages: 훌륭한 CDN이지만 수동 캐시 퍼지와 더 복잡한 설정
나를 구한 코드
최종 ensureHttpsInProduction() 함수:
```typescript
function ensureHttpsInProduction(url: string): string {
// 사이트가 HTTPS로 로드된 브라우저 컨텍스트에서만 변환
if (typeof window !== 'undefined' && window.location.protocol === 'https:') {
// localhost/127.0.0.1 URL은 변환하지 않음 (로컬 개발)
if (url.startsWith('http://') &&
!url.includes('localhost') &&
!url.includes('127.0.0.1')) {
console.warn('[API] Converting HTTP to HTTPS:', url);
return url.replace('http://', 'https://');
}
}
return url;
}
```
그리고 vite.config.ts의 빌드 타임 검증:
```typescript
function validateProductionUrls(mode: string) {
if (mode !== 'production') return;
const apiUrl = process.env.VITE_API_URL || '';
// HTTP 확인 (HTTPS여야 함)
if (apiUrl && apiUrl.trim().startsWith('http://')) {
if (!apiUrl.includes('localhost') && !apiUrl.includes('127.0.0.1')) {
throw new Error(`
❌ HTTPS ENFORCEMENT FAILED
Environment Variable: VITE_API_URL
Current Value: ${apiUrl}
Mixed Content Error Prevention:
Browsers block HTTP requests from HTTPS pages.
Fix: Update environment variables to use HTTPS URLs.
`);
}
}
}
```
진짜 교훈: 디버깅은 탐정 작업이다
이것은 코딩 문제가 아니었습니다. 구성 고고학 탐험이었습니다.
진짜 버그들:
1. ✅ 넓은 .gitignore 패턴이 중요한 프론트엔드 파일 무시
2. ✅ .cloudflare-pages-ignore에서 .env.production 누락
3. ✅ 수정을 숨기는 공격적인 CDN 캐싱
4. ✅ 환경 변수 우선순위에 대한 가정
기술적 솔루션은 .ignore 파일의 한 줄이었습니다.
디버깅? 6시간, 14번의 배포, 너무 많은 커피가 필요했습니다.
그만한 가치가 있었나?
절대적으로요. 얻은 것:
1. Mixed Content 보안 정책에 대한 깊은 이해
2. 미래 문제를 방지하는 빌드 타임 검증
3. 플랫폼에 구애받지 않는 다층 HTTPS 강제
4. Vercel의 단순함에 대한 진정한 감사
그리고 가장 중요한 것: 공유할 훌륭한 디버깅 이야기. :P
Git 로그가 이야기를 들려줍니다
```bash
2033b9a fix: add .env.production to .vercelignore
3555390 docs: migrate deployment documentation from Cloudflare Pages to Vercel
a4edb09 chore: force clean Cloudflare Pages rebuild
590c271 fix: enhance ensureHttpsInProduction logging
15d69c6 chore: force rebuild of UserProfile bundle
a1c345f fix: add .env.production to cloudflare-pages-ignore ← .ENV 수정
635d80d docs: update documentation for Cloudflare Pages migration
edfa611 feat: add build-time HTTPS enforcement
802c1e9 fix: enforce HTTPS for all API calls across frontend
26f6b87 refactor: comprehensive .gitignore audit and cleanup
3758a9c fix: unignore frontend lib directory and add missing files ← GITIGNORE 수정
```
각 커밋은 이전 세상에서는 한 시간의 디버깅이지만 Claude Code 덕분에 감사하게도 제 속도는 더 빨랐습니다. 테스트된 가설. 배운 교훈.
Mixed Content 에러와 싸우는 다른 엔지니어들을 위해
같은 문제를 디버깅하면서 이 글을 읽고 있다면, 체크리스트입니다:
1. 환경 변수를 확인하세요:
```bash
# 사용 중인 실제 값 출력
console.log('API URL:', import.meta.env.VITE_API_URL);
```
2. 배포된 번들을 확인하세요:
```bash
# JavaScript 번들을 다운로드하고 검색
curl https://your-site.com/assets/index-ABC123.js | grep "http://"
```
3. ignore 파일을 확인하세요:
```bash
# .env.production이 제외되었는지 확인
cat .vercelignore
cat .cloudflare-pages-ignore
cat .netlify-ignore
```
4. .gitignore를 확인하세요 (모노레포):
```bash
# 중요한 파일이 무시되지 않는지 확인
git ls-files apps/web/src/lib/ # api.ts 등이 표시되어야 함
# 비어있으면 넓은 패턴 확인
grep "^lib/" .gitignore # ❌ 너무 넓음
grep "^apps/api/lib/" .gitignore # ✅ 구체적
```
5. 캐시를 확인하세요:
```bash
# 먼저 프리뷰 URL에서 테스트
# 프리뷰는 작동하지만 프로덕션이 안 되면 = 캐시 문제
```
6. 빌드 타임 검증을 추가하세요:
```typescript
// 다시는 발생하지 않도록 방지
if (mode === 'production' && url.startsWith('http://')) {
throw new Error('HTTPS required in production!');
}
```
있었으면 좋았을 마이그레이션 체크리스트
배포 플랫폼 전환 시:
- [ ] 이전 플랫폼의 모든 환경 변수 목록 작성
- [ ] 새 플랫폼에서 환경 변수를 먼저 설정
- [ ] 모든 .env 파일을 .ignore에 추가 (.env.production 포함)
- [ ] .gitignore가 중요한 파일을 무시하지 않는지 확인 (git ls-files로 체크)
- [ ] 커스텀 도메인 전에 프리뷰 URL에서 테스트
- [ ] 배포된 번들에서 HTTP URL 확인
- [ ] 백엔드가 별도이면 CORS 설정 확인
- [ ] 플랫폼별 특이점 문서화
아직도 코딩하고, 배우고, (때때로) 부수고 있습니다
한 줄 수정을 위한 6시간의 디버깅. 그것이 소프트웨어 엔지니어링의 핵심입니다.
우리가 배포하는 코드도 중요하지만, 우리가 개발하는 디버깅 기술? 그것이 우리를 더 나은 엔지니어로 만듭니다.
다음에 배포 플랫폼이 다운될 때(그리고 다음이 있을 것입니다), 준비가 되어 있을 것입니다. 저에게는:
- ✅ 플랫폼에 구애받지 않는 HTTPS 강제
- ✅ 빌드 타임 검증
- ✅ 환경 변수 우선순위에 대한 더 나은 이해
- ✅ 백업 배포 전략
그리고 이 블로그 글이 다른 누군가의 그 6시간 중 몇 시간을 절약해주길 바랍니다. :)
당황스러울 정도로 오랜 시간이 걸린 "한 줄 수정"을 경험해보신 적 있나요? 여러분의 디버깅 탐정 이야기를 듣고 싶습니다 — 불행은 함께라면 나눌 수 있으니까요!
감사합니다,
Chandler





