- [React.js] 네이버 지도 API PDF 변환 시 CORS 오류 완벽 해결법2025년 06월 16일 17시 27분 56초에 업로드 된 글입니다.작성자: DandyNow728x90반응형
네이버 지도 API PDF 변환 시 CORS 오류 완벽 해결법
문제 상황
React.js 프로젝트에서
react-naver-maps
라이브러리를 사용하여 지도를 구현하고,html2canvas
나html-to-image
를 활용해 PDF로 저장하려고 할 때 다음과 같은 오류가 발생한다.SecurityError: Failed to read the 'cssRules' property from 'CSSStyleSheet': Cannot access rules
이는 네이버 지도 API가 외부 도메인의 CSS와 이미지 리소스를 사용하기 때문에 발생하는 CORS(Cross-Origin Resource Sharing) 보안 정책 위반 문제이다.
핵심 해결 원리
1. 동적 요소 사전 처리
CORS 오류의 근본 원인은 외부 도메인의 리소스에 접근하려고 할 때 발생하는 보안 제한이다. 이를 해결하기 위해서는 캡처 전에 문제가 되는 요소들을 임시로 무력화하는 것이 핵심이다.
const prepareElementForCapture = (element) => { // 외부 스타일시트 무력화 const externalLinks = element.querySelectorAll('link[rel="stylesheet"]'); externalLinks.forEach(link => { if (link.href && !link.href.startsWith(window.location.origin)) { link.setAttribute('data-html2canvas-ignore', 'true'); } }); // 외부 배경 이미지 임시 제거 const elementsWithBgImage = element.querySelectorAll('*[style*="background-image"]'); elementsWithBgImage.forEach(el => { if (el.style.backgroundImage.includes('http') && !el.style.backgroundImage.includes(window.location.origin)) { el.setAttribute('data-original-bg', el.style.backgroundImage); el.style.backgroundImage = 'none'; } }); };
2. onclone 콜백을 통한 DOM 정리
html2canvas
의onclone
콜백은 DOM을 복제한 후 렌더링하기 전에 실행되는 함수이다. 이 시점에서 클론된 문서에서 외부 리소스를 물리적으로 제거하는 것이 가장 확실한 방법이다.const getHtml2CanvasOptions = () => ({ useCORS: true, allowTaint: true, onclone: (clonedDoc, element) => { // 외부 스타일시트 완전 제거 const stylesheets = clonedDoc.querySelectorAll('link[rel="stylesheet"]'); stylesheets.forEach(link => { if (link.href && !link.href.startsWith(window.location.origin)) { link.remove(); } }); // 지도 컨테이너 스타일 강제 적용 const mapContainers = clonedDoc.querySelectorAll('.react-naver-map'); mapContainers.forEach(container => { container.style.background = '#f0f0f0'; container.style.overflow = 'hidden'; }); } });
3. 필터링 기반 선택적 렌더링
html-to-image
라이브러리에서는filter
옵션을 통해 렌더링할 요소를 선별적으로 제어할 수 있다. 이를 활용하여 외부 리소스만 배제하고 나머지는 정상 렌더링한다.const getHtmlToImageOptions = () => ({ quality: 1.0, pixelRatio: 2, cacheBust: true, filter: (node) => { // 외부 스타일시트 필터링 if (node.tagName === 'LINK' && node.rel === 'stylesheet') { return !node.href || node.href.startsWith(window.location.origin); } // 외부 이미지 필터링 if (node.tagName === 'IMG' && node.src) { return node.src.startsWith('data:') || node.src.startsWith(window.location.origin); } return true; } });
완전한 해결책 구현
안전한 캡처 함수
모든 해결 기법을 통합한 안전한 캡처 함수는 다음과 같다.
const safeCapture = async (element, method = 'html-to-image') => { if (!element) { throw new Error('캡처할 요소가 없습니다.'); } // 1. 캡처 전 사전 처리 const problematicElements = prepareElementForCapture(element); // 2. 지도 타일 로딩 대기 await new Promise(resolve => setTimeout(resolve, 1500)); let result = null; try { if (method === 'html-to-image') { result = await toPng(element, getHtmlToImageOptions()); } else if (method === 'html2canvas') { const canvas = await html2canvas(element, getHtml2CanvasOptions()); result = canvas.toDataURL('image/png', 1.0); } } catch (error) { console.error(`${method} 캡처 실패:`, error); throw error; } finally { // 3. 캡처 후 원상 복구 restoreElementAfterCapture(problematicElements); } return result; };
하이브리드 폴백 전략
각 라이브러리는 서로 다른 장단점을 가지고 있다.
html-to-image
는 지도 영역 처리에 더 효과적이고,html2canvas
는 일반적인 차트나 텍스트 요소에 안정적이다. 이를 활용한 하이브리드 접근법이 최적의 결과를 제공한다.const createPdfImage = async () => { setIsLoading(true); try { const tempPdfImages = []; // 1페이지(지도 포함): html-to-image 우선 let chartImageUrl1p; try { chartImageUrl1p = await safeCapture(pdfRef1p.current, 'html-to-image'); } catch (error) { console.warn('html-to-image 실패, html2canvas로 재시도:', error); const canvas = await html2canvas(pdfRef1p.current, getHtml2CanvasOptions()); chartImageUrl1p = canvas.toDataURL('image/png', 1.0); } // 2페이지(일반 차트): html2canvas 우선 let chartCanvas2p = await html2canvas(pdfRef2p.current, getHtml2CanvasOptions()); let chartImageUrl2p = chartCanvas2p.toDataURL('image/png', 1.0); tempPdfImages.push({ canvas: null, image: chartImageUrl1p }); tempPdfImages.push({ canvas: chartCanvas2p, image: chartImageUrl2p }); tempPdfImages.period = [openDate, closeDate]; return tempPdfImages; } finally { setIsLoading(false); } };
기술적 원리
원본 DOM → 클론 생성 → 외부 리소스 제거 → 캔버스 렌더링 → 이미지 변환 ↓ ↓ ↓ ↓ ↓ CORS 위험 안전한 복사 보안 정책 우회 성공적 렌더링 PDF 출력
추가 최적화 방법
1. CSS 스타일 보완
컴포넌트에 다음 CSS를 추가하면 더욱 안정적인 결과를 얻을 수 있다.
.pdf-container { position: relative; overflow: hidden; } .pdf-container [data-html2canvas-ignore] { display: none !important; } .pdf-container .react-naver-map { background-color: #f0f0f0 !important; } .pdf-container .gm-err-container, .pdf-container .naver-splugin { display: none !important; }
2. 네이버 Static Map API 활용
가장 확실한 방법은 동적 지도 대신 정적 지도 이미지를 사용하는 것이다. 이는 CORS 문제를 근본적으로 해결한다.
const captureWithStaticMap = async () => { const center = mapRef.current?.getCenter(); const zoom = mapRef.current?.getZoom(); const CLIENT_ID = process.env.REACT_APP_NAVER_CLIENT_ID; const staticMapUrl = `https://naveropenapi.apigw.ntruss.com/map-static/v2/raster?` + `w=800&h=600¢er=${center.lng()},${center.lat()}&level=${zoom}&` + `X-NCP-APIGW-API-KEY-ID=${CLIENT_ID}`; const response = await fetch(staticMapUrl); const blob = await response.blob(); // PDF 생성 로직 };
핵심 성공 요인
이 해결책이 성공한 가장 중요한 이유는 다음과 같다.
- 타이밍 제어: 브라우저 보안 검사가 실행되기 전에 문제 요소를 제거
- DOM 레벨 해결: 네트워크나 CSS 레벨이 아닌 DOM 구조 자체를 수정
- 라이브러리 독립적: 여러 캡처 라이브러리에 공통 적용 가능
- 상태 복원: 캡처 후 원래 상태로 복원하여 사용자 경험 유지
결론
네이버 지도 API의 CORS 문제는 외부 리소스 사전 제거와 DOM 클론 정리 기법을 통해 완벽하게 해결할 수 있다. 핵심은 브라우저가 보안 검사를 실행하기 전에 문제가 되는 요소들을 미리 무력화하는 것이다.
이 방법론은 네이버 지도뿐만 아니라 다른 외부 API나 서드파티 위젯을 PDF로 변환할 때도 동일하게 적용할 수 있는 범용적인 해결책이다. 특히 React 기반의 웹 애플리케이션에서 복잡한 UI 요소들을 PDF로 변환해야 하는 상황에서 매우 유용하다.
참고 라이브러리
html2canvas
: ^1.4.1html-to-image
: ^1.11.11react-naver-maps
: ^0.1.2jspdf
: ^2.5.1
728x90반응형'언어·프레임워크 > React.js' 카테고리의 다른 글
[React.js] Spring Boot와 연동 시 `Invalid URL` 오류 해결기(리액트 프로젝트 중단점 설정) (0) 2025.06.18 [React.js] 메모이제이션 완벽 가이드: memo, useCallback, useMemo와 Profiler 활용 (0) 2025.05.25 [React.js] `useEffect`와 `useLayoutEffect`의 차이: 깜박임 현상과 중간 값 노출 (0) 2025.05.23 [React.js] 네이버 지도 API 마커 중앙 정렬과 레이어 제어 (0) 2025.05.14 [React.js] Naver 지도 resize 이벤트 오류 해결하기 (`__event_relations__` 에러) (0) 2025.04.30 다음글이 없습니다.이전글이 없습니다.댓글