본문 바로가기
개발 이모저모

Mixed Content 오류란?

by 코딩의 세계 2025. 11. 5.

요청에 대해

Mixed Content 오류란?

HTTPS로 로드된 웹페이지에서 HTTP 리소스(API, 이미지, 스크립트 등)를 요청할 때 발생하는 브라우저 보안 오류입니다.

Mixed Content: The page at 'https://example.com' was loaded over HTTPS, 
but requested an insecure XMLHttpRequest endpoint 'http://api.example.com'. 
This request has been blocked; the content must be served over HTTPS.

왜 차단될까?

  • 보안: HTTPS는 암호화된 연결, HTTP는 평문 연결
  • 중간자 공격 방지: HTTP 요청은 중간에 가로채기/변조 가능
  • 브라우저 정책: 모던 브라우저는 Mixed Content를 기본적으로 차단

프론트엔드 해결 방법

1. API URL을 HTTPS로 변경 (권장)

// ❌ 잘못된 예
const API_BASE_URL = 'http://api.example.com';

// ✅ 올바른 예
const API_BASE_URL = 'https://api.example.com';

2. 환경변수로 URL 관리

# .env.development (로컬 개발)
VITE_API_URL=http://localhost:8080

# .env.production (배포 환경)
VITE_API_URL=https://api.example.com
// src/config/api.js
const API_BASE_URL = import.meta.env.VITE_API_URL || 'https://api.example.com';

export default API_BASE_URL;

3. 프로토콜 상대 URL 사용 (임시방편)

// 현재 페이지의 프로토콜을 따라감
const API_BASE_URL = '//api.example.com';
// https://example.com → https://api.example.com으로 요청
// http://example.com → http://api.example.com으로 요청

⚠️ 주의: 이 방법은 로컬 개발(http)과 배포(https) 모두에서 작동하지만, 백엔드가 HTTPS를 지원해야 합니다.

4. 개발 환경에서 프록시 설정

// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://43.201.107.27:8080',
        changeOrigin: true,
        secure: false,
        rewrite: (path) => path.replace(/^\/api/, '/api')
      }
    }
  }
}
// 프론트엔드 코드에서는 상대 경로 사용
const response = await fetch('/api/auth/login', {
  method: 'POST',
  // ...
});

장점:

  • CORS 문제도 함께 해결
  • 로컬 개발 시 편리

단점:

  • 배포 환경에서는 다른 설정 필요
  • 프로덕션에서는 실제 URL로 요청해야 함

5. 전체 프로젝트에서 HTTP URL 검색

# HTTP URL이 남아있는지 확인
grep -r "http://" src/
find . -name "*.js" -o -name "*.jsx" | xargs grep "http://"

# 특정 도메인 검색
grep -r "http://43.201.107.27" .

6. 배포 후 캐시 클리어

# 1. 새 커밋 푸시
git add .
git commit -m "fix: Change API URL to HTTPS"
git push origin main

# 2. 브라우저 강력 새로고침
# Windows/Linux: Ctrl + Shift + R
# Mac: Cmd + Shift + R

# 3. 시크릿 모드로 테스트

백엔드 해결 방법

1. SSL/TLS 인증서 설치 (근본적 해결)

A. Let's Encrypt (무료, 권장)

# Certbot 설치
sudo apt-get update
sudo apt-get install certbot

# 도메인이 있는 경우
sudo certbot certonly --standalone -d api.example.com

# IP 주소만 있는 경우 (제한적)
# Let's Encrypt는 IP 주소에 대한 인증서 발급 불가

B. AWS Certificate Manager (AWS 사용 시)

  • AWS ELB/ALB에 HTTPS 리스너 추가
  • ACM에서 무료 SSL 인증서 발급
  • 백엔드는 HTTP로 유지 가능 (ELB가 SSL 처리)

C. 자체 서명 인증서 (개발/테스트용)

# 자체 서명 인증서 생성
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes

⚠️ 주의: 자체 서명 인증서는 브라우저에서 경고 발생

2. 웹 서버 HTTPS 설정

Nginx 설정

server {
    listen 443 ssl;
    server_name api.example.com;

    ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;

    location / {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

# HTTP → HTTPS 리다이렉트
server {
    listen 80;
    server_name api.example.com;
    return 301 https://$server_name$request_uri;
}

Node.js (Express) HTTPS 설정

const https = require('https');
const fs = require('fs');
const express = require('express');

const app = express();

const options = {
  key: fs.readFileSync('/path/to/private-key.pem'),
  cert: fs.readFileSync('/path/to/certificate.pem')
};

https.createServer(options, app).listen(443, () => {
  console.log('HTTPS Server running on port 443');
});

Spring Boot HTTPS 설정

# application.yml
server:
  port: 8443
  ssl:
    enabled: true
    key-store: classpath:keystore.p12
    key-store-password: your-password
    key-store-type: PKCS12
    key-alias: tomcat

3. CORS 설정 함께 해결

HTTPS로 전환하면 Origin이 변경되므로 CORS 설정도 업데이트해야 합니다.

Express.js

const cors = require('cors');

app.use(cors({
  origin: [
    'http://localhost:5173',           // 로컬 개발
    'http://localhost:4173',           // 로컬 프리뷰
    'https://yourapp.vercel.app',      // 프로덕션
    'https://www.yourdomain.com'       // 커스텀 도메인
  ],
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

Spring Boot

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins(
                    "http://localhost:5173",
                    "http://localhost:4173",
                    "https://yourapp.vercel.app"
                )
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true);
    }
}

4. 클라우드 서비스 활용

A. AWS ELB/ALB

[브라우저] --HTTPS--> [ELB:443] --HTTP--> [EC2:8080]
  • ELB에서 SSL/TLS 종료
  • 백엔드는 HTTP로 유지 가능
  • 가장 간단하고 권장되는 방법

B. Cloudflare

  • DNS를 Cloudflare로 변경
  • 자동으로 HTTPS 적용
  • 무료 SSL 인증서 제공

C. ngrok (개발/테스트용)

# ngrok 실행
ngrok http 8080

# HTTPS URL 생성됨
# https://abc123.ngrok.io → http://localhost:8080

단계별 해결 전략

빠른 임시 해결 (개발 중)

  1. 프론트엔드에서 프록시 설정 (vite.config.js)
  2. 상대 경로로 API 호출

정식 해결 (배포 환경)

  1. 백엔드팀: SSL 인증서 설치 + HTTPS 설정
  2. 프론트엔드팀: API URL을 HTTPS로 변경
  3. 양쪽 모두: CORS 설정 업데이트

IP 주소만 있는 경우

  1. 도메인 구매 (무료: Freenom, 저렴: Namecheap)
  2. AWS ELB 사용 (도메인 없이 HTTPS 가능)
  3. 또는 개발 환경에서만 프록시 사용

체크리스트 >

프론트엔드

  • [ ] 모든 API 호출이 HTTPS인지 확인
  • [ ] 환경변수로 URL 관리
  • [ ] 배포 후 캐시 클리어
  • [ ] 브라우저 콘솔에서 Mixed Content 오류 확인
  • [ ] 개발 환경에서 프록시 설정 (선택)

백엔드

  • [ ] SSL 인증서 설치
  • [ ] HTTPS 포트(443) 오픈
  • [ ] 웹 서버 HTTPS 설정
  • [ ] HTTP → HTTPS 리다이렉트 설정
  • [ ] CORS 설정에 HTTPS origin 추가
  • [ ] 방화벽/보안 그룹 설정 확인

자주 하는 실수

1. 일부 URL만 변경

// ❌ 나쁜 예
const API_URL = 'https://api.example.com';
const IMAGE_URL = 'http://cdn.example.com'; // 여기도 HTTPS 필요!

2. 환경변수 미적용

// ❌ 코드에 하드코딩
const API_URL = 'https://43.201.107.27:8080';

// ✅ 환경변수 사용
const API_URL = import.meta.env.VITE_API_URL;

3. 배포 후 캐시 미클리어

  • 코드를 변경했는데도 오류가 계속 발생
  • 브라우저 캐시나 CDN 캐시 때문

4. CORS 설정 누락

  • HTTPS로 변경하면 Origin이 바뀜
  • CORS 설정도 함께 업데이트 필요

디버깅 팁

1. 실제 요청 URL 확인

// API 파일에 임시 로그 추가
console.log('🔍 API_BASE_URL:', API_BASE_URL);
console.log('🔍 Full URL:', `${API_BASE_URL}/api/auth/login`);

2. 네트워크 탭 확인

  • F12 → Network 탭
  • 실제 요청된 URL과 프로토콜 확인
  • Status Code 확인

3. curl로 백엔드 테스트

# HTTP 테스트
curl -I http://43.201.107.27:8080/api/health

# HTTPS 테스트
curl -I https://43.201.107.27:8080/api/health

# 자세한 정보
curl -v https://43.201.107.27:8080/api/health

4. 전체 프로젝트 검색

# HTTP URL 찾기
grep -r "http://" src/ --include="*.js" --include="*.jsx"

# 특정 도메인 찾기
find . -type f \( -name "*.js" -o -name "*.jsx" \) -exec grep -l "43.201.107.27" {} \;

결론

Mixed Content 오류는 HTTPS 페이지에서 HTTP 리소스를 요청할 때 발생합니다.

근본적 해결책: 백엔드를 HTTPS로 전환 임시 해결책: 프론트엔드에서 프록시 사용 (개발 환경만)

프론트엔드와 백엔드 팀이 협업하여 HTTPS 전환을 완료하면, 보안과 사용자 경험 모두를 개선할 수 있습니다.


(추가 글)

https://velog.io/@live_in_truth/Mixed-Content-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0-feat-Vercel-Serverless-Function

 

Mixed Content 에러 해결하기 (feat: Vercel Serverless Function)

HTTPS로 배포된 React 애플리케이션에서 HTTP API(서울시 문화행사 정보)를 호출하려고 할 때 Mixed Content 에러가 발생한다.하필 서울시 open api는 https 프로토콜을 제공하지 않기 때문에 (https로 변환하

velog.io


https://velog.io/@giant_toothpick/mixed-content-error-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0-with.-Nginx

 

mixed content error 처리하기 (with. Nginx)

mixed content란?"혼합 콘텐츠 오류"는 웹 페이지에서 보안 연결(HTTPS)을 통해 제공되는 페이지에 비해 보안되지 않은 연결(HTTP)을 통해 로드되는 콘텐츠가 있는 경우 발생한다. 일반적으로 HTTPS로 보

velog.io

 

'개발 이모저모' 카테고리의 다른 글

cors 에러는 무엇인가.  (4) 2025.11.05
무중단 배포란?  (0) 2025.10.26
CodeRabbit이 무엇인가?  (0) 2025.10.24
드래그 막아둔 블로그 뚫기  (1) 2025.10.01
제대로 이해하는 RESTful - API  (10) 2025.09.02