Dandy Now!
  • [React.js] 네이버 지도 API PDF 변환 시 CORS 오류 완벽 해결법
    2025년 06월 16일 17시 27분 56초에 업로드 된 글입니다.
    작성자: DandyNow
    728x90
    반응형

    네이버 지도 API PDF 변환 시 CORS 오류 완벽 해결법

    문제 상황

    React.js 프로젝트에서 react-naver-maps 라이브러리를 사용하여 지도를 구현하고, html2canvashtml-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 정리

    html2canvasonclone 콜백은 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&center=${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 생성 로직
    };

    핵심 성공 요인

    이 해결책이 성공한 가장 중요한 이유는 다음과 같다.

    1. 타이밍 제어: 브라우저 보안 검사가 실행되기 전에 문제 요소를 제거
    2. DOM 레벨 해결: 네트워크나 CSS 레벨이 아닌 DOM 구조 자체를 수정
    3. 라이브러리 독립적: 여러 캡처 라이브러리에 공통 적용 가능
    4. 상태 복원: 캡처 후 원래 상태로 복원하여 사용자 경험 유지

    결론

    네이버 지도 API의 CORS 문제는 외부 리소스 사전 제거DOM 클론 정리 기법을 통해 완벽하게 해결할 수 있다. 핵심은 브라우저가 보안 검사를 실행하기 전에 문제가 되는 요소들을 미리 무력화하는 것이다.

    이 방법론은 네이버 지도뿐만 아니라 다른 외부 API나 서드파티 위젯을 PDF로 변환할 때도 동일하게 적용할 수 있는 범용적인 해결책이다. 특히 React 기반의 웹 애플리케이션에서 복잡한 UI 요소들을 PDF로 변환해야 하는 상황에서 매우 유용하다.


    참고 라이브러리

    • html2canvas: ^1.4.1
    • html-to-image: ^1.11.11
    • react-naver-maps: ^0.1.2
    • jspdf: ^2.5.1
    728x90
    반응형
    댓글