메인 콘텐츠로 바로가기
노트 목록

iPhone Chrome/Safari에서 YouTube 임베드가 간헐적으로 검은 화면일 때: CSP와 WebKit 대응기

김성박560
목차17

문제 상황

블로그 상세 페이지의 마크다운 본문에서 YouTube URL을 자동 임베드하고 있었다.

예시 URL:

  • https://www.fullstackfamily.com/@urstory/posts/13778/...

PC에서는 대부분 정상 재생되는데, iPhone Chrome(그리고 Safari 계열)에서는 아래처럼 검은 화면 + 차단 메시지가 간헐적으로 나타났다.

이 콘텐츠는 차단되어 있습니다. 문제를 해결하려면 사이트 소유자에게 문의하세요.

처음엔 "로그인했을 때만 안 보이나?"처럼 보였지만, 실제로는 로그인 여부와 무관하게 보일 때도 있고 안 보일 때도 있는 간헐 이슈였다.


핵심 힌트

결정적인 단서는 위 차단 문구였다.

이 문구는 YouTube 자체 오류라기보다, 브라우저가 현재 문서의 보안 정책(CSP) 때문에 iframe을 막을 때 흔히 보이는 패턴이다.

즉, 원인 후보가 이쪽으로 좁혀졌다.

  • 네트워크 불안정 ❌
  • 로그인 상태 버그 ❌
  • 브라우저 정책 + 응답 헤더(CSP) ✅

CSP가 정확히 뭔가

CSP는 Content Security Policy의 약자다.

한 줄로 요약하면:

브라우저에게 "이 페이지에서 어떤 외부 리소스를 어디까지 허용할지"를 선언하는 보안 규칙

서버는 응답 헤더로 이런 값을 내려준다.

Content-Security-Policy: default-src 'self'; frame-src https://www.youtube.com

브라우저는 이 규칙을 보고, 허용되지 않은 리소스는 로딩을 막는다.

왜 필요한가

가장 큰 목적은 XSS 같은 스크립트 주입 공격을 줄이는 것이다.

  • 내가 의도하지 않은 스크립트 실행 차단
  • 의도하지 않은 외부 도메인 요청 차단
  • iframe, 이미지, 폰트, API 호출 출처를 제한

이번 이슈와 직접 관련된 지시어

지시어의미이번 케이스
default-src기본 허용 출처전체 기본 안전망
script-srcJS 로드 허용 출처분석/광고 스크립트 허용
connect-srcXHR/fetch/SSE 허용 출처API/Analytics 호출
img-src이미지 허용 출처CDN/스토리지 이미지
frame-srciframe 허용 출처YouTube 임베드 핵심

이번 문제는 frame-src에 YouTube가 없어서, 브라우저가 YouTube iframe을 보안상 차단한 케이스였다.

즉, 코드가 맞아도 CSP가 틀리면 화면은 막힌다.

왜 "검은 화면 + 차단 문구"가 뜨나

iframe 내부 콘텐츠는 브라우저가 먼저 보안 정책으로 검사한다.

  • frame-src에 도메인이 있으면 렌더링 시도
  • 없으면 로딩 자체 차단
  • 사용자 입장에서는 "영상 플레이어가 고장난 것처럼" 보임

그래서 이슈를 디버깅할 때는 JS 에러 콘솔만 볼 게 아니라, 문서 응답 헤더의 CSP를 함께 확인해야 한다.


왜 "간헐적"으로 보였나

여기가 이번 이슈의 포인트였다.

우리 서비스는 Next.js 기반이고, 블로그 상세 진입이 상황에 따라 두 가지로 갈린다.

  1. 문서 새로 로드(하드 네비게이션)
  2. SPA 클라이언트 라우팅(소프트 네비게이션)

기존 CSP는 next.config.js에서 특정 경로에만 적용하고 있었고, 경로별 진입 방식/캐시 상태에 따라 체감이 달라졌다. 그래서 같은 글인데도 어떤 때는 보이고, 어떤 때는 막히는 것처럼 보였다.


실제 원인

frontend/next.config.js에서 블로그 경로에 CSP를 넣어둔 것은 맞았는데, frame-src에 YouTube 도메인이 빠져 있었다.

기존 설정(문제):

"frame-src https://*.googlesyndication.com https://googleads.g.doubleclick.net"

즉, 광고 iframe은 허용하지만 YouTube iframe은 허용하지 않는 상태였다.


수정 1: CSP에 YouTube 도메인 허용

frame-src를 아래처럼 확장했다.

"frame-src https://*.googlesyndication.com https://googleads.g.doubleclick.net https://www.youtube.com https://youtube.com https://www.youtube-nocookie.com https://youtube-nocookie.com"

수정 파일:

  • frontend/next.config.js

수정 2: iOS(WebKit) 임베드 안정화

CSP만으로도 차단 문제는 해결되지만, iPhone 계열에서 재생 안정성을 더 높이기 위해 임베드 컴포넌트도 함께 보강했다.

변경 내용:

  • youtube.com/embed → youtube-nocookie.com/embed 사용
  • playsinline=1, rel=0, modestbranding=1 파라미터 추가
  • iframe 레이아웃 단순화 (absolute 제거, w-full h-full block)
  • loading="lazy" 제거

수정 파일:

  • frontend/src/shared/ui/YouTubeEmbed.tsx

핵심 코드:

const embedUrl = buildYouTubeEmbedUrl(videoId, startTime) const iframeUrl = new URL(embedUrl) iframeUrl.hostname = 'www.youtube-nocookie.com' iframeUrl.searchParams.set('rel', '0') iframeUrl.searchParams.set('playsinline', '1') iframeUrl.searchParams.set('modestbranding', '1')

검증

배포 전 로컬 검증은 아래로 통과했다.

  • cd frontend && npm run build ✅
  • cd backend && ./gradlew test ✅

추가 확인 포인트:

  1. iPhone Chrome/Safari에서 블로그 상세 새로고침 진입
  2. YouTube iframe 정상 렌더링 확인
  3. 문제가 있던 문서에서 차단 메시지 재발 여부 확인
  4. 네트워크 탭에서 문서 응답의 Content-Security-Policy 헤더에 frame-src ... youtube ... 포함 확인

이번 이슈에서 배운 점

1) "검은 화면"은 플레이어 버그가 아닐 수 있다

임베드 문제는 플레이어 코드보다 **보안 헤더(CSP)**가 원인인 경우가 많다.

2) SPA + 헤더 정책은 체감이 복잡해진다

사용자는 "간헐적"으로 느끼지만, 실제로는 라우팅 방식/캐시/헤더 적용 범위의 조합일 수 있다.

3) iframe 기능을 만들 때 체크리스트가 필요하다

  • frame-src 허용 도메인 점검
  • 모바일 WebKit 동작 점검
  • youtube-nocookie/playsinline 같은 실전 파라미터 적용

마무리

이번 장애는 "YouTube가 가끔 고장 난다"가 아니라,

CSP 정책 누락 + iOS WebKit 특성이 겹친 문제였다.

결국 해결은 화려한 코드가 아니라, 헤더 한 줄과 임베드 설정 몇 가지였다.

하지만 이런 "한 줄"을 찾기까지가 늘 제일 어렵다.

댓글

불러오는 중...

댓글을 작성하려면 이 필요합니다.