JavaScript(React)에서 이미지를 PDF로 변환하기

2026-09-10 05:33:38 Allen Yang
AI Summarize:
ChatGPT
ChatGPT ✓
Claude ✓
Grok ✓
Perplexity ✓
Quick
Quick
Concise overview
Highlights
Key takeaways
Detailed
Structured explanation
Brief
One sentence summary
Summarize |

Multiple images assembled into a single multi-page PDF using Spire.PDF for JavaScript in React

폴더에 있는 이미지들은 다른 사람에게 건네기 번거롭습니다. PDF는 파일 하나로 어디서든 열리고, 예측 가능하게 인쇄되며 — 사람들이 실제로 신경 쓰는 부분인 — 고정된 순서를 유지합니다. 그래서 스캔한 페이지, 사진 세트, 영수증 사진, 내보낸 디자인 프레임은 어디로 보내기 전에 PDF로 조립되는 경우가 많습니다.

브라우저에서 그 PDF를 만드는 것은 PDF를 이미지로 렌더링하는 것과는 다른 문제입니다. 이미 존재하는 것을 디코딩하는 것이 아니라, 일반적으로 문서 형식이 대신 내려 주는 결정을 직접 내려야 합니다: 페이지는 얼마나 커야 하는지, 이미지는 페이지 어디에 놓이는지, 이미지가 페이지와 다른 모양일 때 어떻게 되는지, 페이지는 어떤 순서로 나오는지.

Spire.PDF for JavaScript는 페이지 캔버스를 통해 이러한 결정을 드러냅니다. 페이지를 추가하고, 이미지를 로드한 뒤, 계산한 크기로 해당 페이지에 그린 다음, 저장합니다. 모든 것이 WebAssembly를 통해 클라이언트 측에서 실행되므로 이미지는 업로드되지 않습니다.


PDF에 이미지가 들어가는 이유

이 시나리오들은 한 가지 형태를 공유합니다: 하나의 문서처럼 동작해야 하는 여러 이미지입니다.

  • 스캔하거나 촬영한 여러 페이지 문서 — 계약서를 페이지별로 촬영해 파일로 보관하거나 이메일로 보낼 수 있는 단일 파일로 다시 조립한 경우.
  • 사진 세트와 포트폴리오 — 누군가 선택한 순서대로 페이지당 이미지 하나.
  • 영수증 및 경비 보고서 — 회계에서 하나의 첨부 파일로 원하는 휴대폰 사진 여러 장.
  • 디자인 및 다이어그램 내보내기 — 도구에서 내보낸 프레임을 검토 가능한 무언가로 모은 경우.

각 경우에서 PDF는 사실 PDF 형식 자체에 관한 것이 아닙니다. 이미지 더미에서 안정적이고 단일 파일이며 순서가 있는 결과물을 얻는 것이 핵심입니다.


사전 요구 사항

이 안내는 Spire.PDF for JavaScript가 설치되고 WASM 모듈이 초기화된 React 프로젝트를 가정합니다. 설정 방법은 React 프로젝트에 Spire.PDF for JavaScript 통합하기를 참조하세요.

필요한 항목:

  • VFS에 로드된 하나 이상의 이미지 파일
  • window.wasmModule.spirepdf에서 접근 가능한 WASM 모듈

이미지 하나, 페이지 하나

기본 흐름은 네 단계입니다: 문서를 만들고, 페이지를 추가하고, 이미지를 로드하고, 그립니다. 흥미로운 부분은 그리기입니다 — 페이지에서 이미지가 얼마나 커야 하는지 결정해야 합니다.

function App() {
  const convertImageToPDF = async () => {
    // Get the Spire.PDF WASM module
    const pdfModule = window.wasmModule?.spirepdf;

    // Check if the WASM module is ready
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // Load the image file into VFS
    const inputFileName = 'Scenery.png';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // Create a PdfDocument object
    let doc = new pdfModule.PdfDocument();

    // Add a page
    let page = doc.Pages.Add();

    // Load the image
    let image = pdfModule.PdfImage.FromFile(inputFileName);

    // Calculate the scale ratio so the image fits the page completely
    let widthFitRate = image.PhysicalDimension.Width / page.Canvas.ClientSize.Width;
    let heightFitRate = image.PhysicalDimension.Height / page.Canvas.ClientSize.Height;
    let fitRate = Math.max(widthFitRate, heightFitRate);

    // Calculate the scaled dimensions of the image
    let fitWidth = image.PhysicalDimension.Width / fitRate;
    let fitHeight = image.PhysicalDimension.Height / fitRate;

    // Center the image on the page
    let x = (page.Canvas.ClientSize.Width - fitWidth) / 2;
    let y = (page.Canvas.ClientSize.Height - fitHeight) / 2;

    // Draw the image onto the page
    page.Canvas.DrawImage({ image: image, x: x, y: y, width: fitWidth, height: fitHeight });

    const outputFileName = 'ImageToPDF.pdf';

    // Save as PDF format
    doc.SaveToFile({ fileName: outputFileName, fileFormat: pdfModule.FileFormat.PDF });
    doc.Close();

    // Read the converted file from VFS and trigger download
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Convert Image To PDF</h1>
      <button onClick={convertImageToPDF}>
        Generate
      </button>
    </div>
  );
}

export default App;

PdfImage.FromFile로 이미지를 로드하고 Canvas.DrawImage로 그린 후 생성된 PDF 문서

PDF document generated after loading an image via PdfImage.FromFile and drawing it with Canvas.DrawImage

크기 계산을 헬퍼로 분리하세요. 아래의 다른 모든 레시피에서 재사용하게 될 코드 조각이기 때문입니다. 계산 방식: page.Canvas.ClientSize — 페이지에서 그릴 수 있는 영역(포인트 단위) — 를 예산으로 삼고, 이미지의 자연스러운 PhysicalDimension과 비교한 뒤, 가로세로 비율이 유지되도록 두 치수를 하나의 비율로 나눕니다. 버그가 생기기 쉬운 부분이 정확히 한 곳에 있도록 함수로 감싸 두세요:

// Contain: scale until the whole image fits inside the page
function fitContain(imgW, imgH, pageW, pageH) {
  const rate = Math.max(imgW / pageW, imgH / pageH);
  const width = imgW / rate;
  const height = imgH / rate;
  return { width, height, x: (pageW - width) / 2, y: (pageH - height) / 2 };
}

이제 위 예제의 그리기 호출은 세 줄로 줄어들고, "contain 또는 cover" 결정은 수학에서 빠져나와 함수 이름으로 이동합니다:

let page = doc.Pages.Add();
let box = fitContain(
  image.PhysicalDimension.Width, image.PhysicalDimension.Height,
  page.Canvas.ClientSize.Width, page.Canvas.ClientSize.Height
);
page.Canvas.DrawImage({ image: image, x: box.x, y: box.y, width: box.width, height: box.height });

Math.max는 "contain" 선택입니다 — 더 제한적인 축을 기준으로 크기를 조정하여 전체 이미지가 보이게 유지합니다. 대신 페이지를 채우고 넘치는 부분을 잘라내고 싶다면 Math.min으로 바꾸세요. 크기 조정 섹션에서 fitCover 대응 버전과 여백 변형을 확인할 수 있습니다.


여러 이미지, 하나의 문서

페이지당 이미지 하나는 이미지마다 Pages.Add() 하나와 DrawImage 하나를 의미합니다. 파일 이름 배열을 반복하면 배열 순서가 페이지 순서가 됩니다 — 사용자가 방금 썸네일을 순서대로 드래그해 놓았을 때 정확히 원하는 동작입니다.

const combineImagesToPdf = async () => {
  const pdfModule = window.wasmModule?.spirepdf;
  if (!pdfModule) return;

  // The order of this array is the order of pages in the PDF
  const imageFiles = ['scan_01.png', 'scan_02.png', 'scan_03.png', 'scan_04.png'];

  for (const fileName of imageFiles) {
    await window.spire.FetchFileToVFS(fileName, "", `${process.env.PUBLIC_URL}/data/`);
  }

  let doc = new pdfModule.PdfDocument();

  for (const fileName of imageFiles) {
    let page = doc.Pages.Add();
    let image = pdfModule.PdfImage.FromFile(fileName);

    let box = fitContain(
      image.PhysicalDimension.Width, image.PhysicalDimension.Height,
      page.Canvas.ClientSize.Width, page.Canvas.ClientSize.Height
    );

    page.Canvas.DrawImage({ image: image, x: box.x, y: box.y, width: box.width, height: box.height });
  }

  const outputFileName = 'ScannedDocument.pdf';
  doc.SaveToFile({ fileName: outputFileName, fileFormat: pdfModule.FileFormat.PDF });
  doc.Close();

  const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
  const blob = new Blob([fileArray], { type: 'application/pdf' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = outputFileName;
  a.click();
  URL.revokeObjectURL(url);
};

실용적인 참고 사항 두 가지. 각 페이지의 크기가 독립적으로 조정되므로 서로 다른 크기의 이미지도 문제없습니다 — 가로 사진과 세로 스캔이 특별한 처리 없이 같은 문서에 들어갈 수 있습니다. 또한 전체 문서가 SaveToFile 전에 메모리에서 만들어지므로 이미지가 몇 장 들어갔든 다운로드는 마지막에 한 번만 발생합니다.

이런 글도 좋아할 수 있습니다: 이미지를 조립하는 것은 렌더링의 반대입니다. 이미 PDF가 있고 각 페이지를 그림으로 원한다면, JavaScript(React)에서 PDF 페이지를 이미지로 변환하는 방법을 참조하세요.


크기 조정: 페이지에 이미지 맞추기

페이지에 이미지를 넣는 합리적인 방법은 두 가지이며, 어느 쪽을 원할지는 이미지의 일부가 잘려도 괜찮은지에 달려 있습니다.

Contain (Math.max) Cover (Math.min)
수행하는 작업 전체 이미지가 들어갈 때까지 크기를 조정합니다 페이지가 채워질 때까지 크기를 조정합니다
전체 이미지 표시 예 아니요 — 넘치는 부분이 잘립니다
빈 공간 한 축에 생길 수 있음 없음
적합한 용도 스캔, 문서, 완전하게 유지되어야 하는 모든 것 전면 인쇄 사진, 표지 페이지, 슬라이드

첫 번째 예제는 contain — fitContain 헬퍼 — 를 사용합니다. cover로 전환하는 것은 해당 함수의 거울상입니다: Math.max 대신 Math.min을 사용하고, 페이지를 채우며 넘치는 부분은 캔버스가 잘라내도록 하고, 중앙 정렬 오프셋은 음수가 됩니다:

// Cover: fill the page, clipping whatever overflows
function fitCover(imgW, imgH, pageW, pageH) {
  const rate = Math.min(imgW / pageW, imgH / pageH);
  const width = imgW / rate;
  const height = imgH / rate;
  return {
    width, height,
    x: (pageW - width) / 2,   // negative when the image is wider than the page
    y: (pageH - height) / 2   // negative when it is taller
  };
}

가장자리까지 꽉 찬 출력 대신 눈에 보이는 여백을 원한다면 이미지가 아니라 사용 가능한 영역을 줄이세요 — 여백을 조정한 예산을 같은 fitContain 헬퍼에 넣습니다:

const margin = 36; // 36 points = 0.5 inch
let usableWidth = page.Canvas.ClientSize.Width - margin * 2;
let usableHeight = page.Canvas.ClientSize.Height - margin * 2;

let box = fitContain(
  image.PhysicalDimension.Width, image.PhysicalDimension.Height,
  usableWidth, usableHeight
);
page.Canvas.DrawImage({ image: image, x: box.x, y: box.y, width: box.width, height: box.height });

PhysicalDimension에 대해 알아 둘 만한 점: 이 값은 이미지의 물리적 크기를 반영하며, 항상 픽셀 크기와 같지는 않습니다. 다른 DPI 태그로 저장된 4000 × 3000 사진은 예상과 다른 숫자를 보고할 수 있습니다. 그래서 위의 비율 기반 접근 방식이 픽셀 치수를 하드코딩하는 것보다 더 안전합니다 — 이미지에 어떤 태그가 붙었든 작동합니다.


메모리에서 이미지 로드

PdfImage.FromFile은 이미지가 이미 VFS에 있기를 기대합니다. 이미지가 항상 그곳에 있는 것은 아닙니다 — API 응답, 데이터베이스 blob, 캔버스 내보내기는 모두 대신 메모리에 바이트를 제공합니다. PdfImage.FromStream은 그 바이트를 직접 받습니다.

// Read image bytes from VFS and build a memory stream
let bytes = window.dotnetRuntime.Module.FS.readFile(inputFileName);
let stream = new pdfModule.Stream(bytes);

// Load the image from the memory stream
let image = pdfModule.PdfImage.FromStream(stream);

그다음부터는 다른 이미지와 동일합니다 — 크기를 계산하고 그리면 됩니다:

let page = doc.Pages.Add();
let box = fitContain(
  image.PhysicalDimension.Width, image.PhysicalDimension.Height,
  page.Canvas.ClientSize.Width, page.Canvas.ClientSize.Height
);
page.Canvas.DrawImage({ image: image, x: box.x, y: box.y, width: box.width, height: box.height });

동일한 bytes 형태는 어디에서 왔든 작동합니다. 이미지가 fetch에서 ArrayBuffer로 도착한다면 스트림을 만들기 전에 Uint8Array로 감싸세요:

const response = await fetch('/api/images/invoice-001');
const bytes = new Uint8Array(await response.arrayBuffer());
let stream = new pdfModule.Stream(bytes);
let image = pdfModule.PdfImage.FromStream(stream);

PDF가 서버에서 트리거된 이미지, 상태에 보관된 사용자 업로드, 또는 캔버스가 런타임에 생성한 모든 것으로 조립될 때 사용할 패턴입니다 — VFS를 거치는 왕복이 필요하지 않습니다.

추천 문서: Spire.PDF는 위의 새 문서뿐만 아니라 이미 열어 둔 PDF의 페이지에도 그릴 수 있습니다. 기존 문서에 이미지를 배치하려면 JavaScript(React)에서 PDF에 이미지를 추가하는 방법을 참조하세요.


일반적인 문제

이미지가 늘어나거나 찌그러져 나옵니다. 거의 항상 두 가지 서로 다른 배율 인자 때문입니다. 하나의 fitRate를 계산하고 너비와 높이를 모두 그 값으로 나누세요 — 축을 독립적으로 조정하지 마세요.

이미지가 크고 빈 페이지 중앙에 아주 작게 나타납니다. 이미지의 가로세로 비율이 페이지와 크게 다를 때 예상되는 결과입니다. 세로 페이지에 파노라마 사진을 넣으면 항상 위아래에 띠가 생깁니다. 이를 받아들이거나(문서에는 올바름), cover로 전환하거나, 최소한 여백이 대칭을 이루도록 여백 조정 버전을 사용하세요.

이미지 가장자리가 잘립니다. 의도했든 아니든 cover 동작을 사용 중입니다. fitRate가 Math.min을 사용했는지 확인하고, 전체 이미지가 보여야 한다면 Math.max로 바꾸세요.

고해상도 사진이 거대한 PDF를 만듭니다. 이미지는 자체 해상도로 삽입됩니다. 파일 크기가 중요하다면 그리기 전에 축소하세요 — 대상 크기의 캔버스에 그린 뒤 내보내고, 그 바이트를 PdfImage.FromStream과 함께 사용하세요.

첫 클릭에 아무 일도 일어나지 않습니다. WASM 모듈은 비동기로 로드됩니다. if (!pdfModule) return; 가드는 그 때문에 존재합니다. 실제 앱에서는 경고를 띄우기보다 모듈 준비 상태에 따라 버튼을 활성화하세요.


자주 묻는 질문

새 PDF를 만드는 대신 기존 PDF에 이미지를 삽입할 수 있나요?

예. 여기의 예제는 새 문서를 만들지만, 기존 PDF를 열고 같은 방식으로 해당 페이지에 그릴 수 있습니다. 그 작업 흐름은 JavaScript(React)에서 PDF에 이미지를 추가하는 방법을 참조하세요.

어떤 이미지 형식을 로드할 수 있나요?

일반적인 비트맵 형식 — PNG, JPEG, BMP 및 유사 형식 — 은 PdfImage.FromFile과 PdfImage.FromStream에서 지원됩니다. 빌드 시점에 형식을 모르거나 바이트가 네트워크 응답에서 오는 경우 FromStream을 사용하세요.

페이지 순서를 제어할 수 있나요?

예. 페이지는 Pages.Add()를 호출한 순서대로 생성되므로 파일 이름 배열을 정렬하면 출력도 정렬됩니다. 이것이 드래그로 순서를 바꾸는 인터페이스의 메커니즘입니다: 배열을 다시 정렬하고 PDF를 다시 만드세요.

백엔드가 필요한가요?

아니요. 문서는 WebAssembly 모듈에 의해 브라우저에서 조립되고, 완성된 PDF는 Blob으로 변환할 바이트로 반환됩니다. 이미지는 기기를 떠나지 않습니다.

한 PDF에 세로 및 가로 이미지를 섞을 수 있나요?

예. 각 페이지는 독립적으로 크기가 조정되고 그려지므로 세로 스캔과 가로 사진이 나란히 있을 수 있습니다. 페이지 방향을 통일하고 싶다면 고정 페이지 크기를 사용하고 이미지가 그 크기에 맞게 조정되도록 하는 것이 좋습니다.

PDF가 있고 그 페이지들을 이미지로 원합니다. 반대 방향은 아닙니다.

그것은 반대 작업입니다 — 조립이 아니라 렌더링입니다. JavaScript(React)에서 PDF 페이지를 이미지로 변환하는 방법을 참조하세요.

이미지를 VFS에 넣어야 하나요?

FromFile에만 필요합니다. FromStream은 어디에서든 바이트를 받습니다 — fetch 응답, 캔버스 내보내기, 또는 상태 — 그리고 VFS를 완전히 건너뜁니다.


같이 보기

여기의 조립 레시피는 완전히 새로운 문서를 만듭니다. 이미지가 기존 PDF 안으로 들어가야 한다면 — 이미 가지고 있는 페이지에 그리는 경우 — JavaScript(React)에서 PDF에 이미지를 추가하는 방법을 참조하세요. 이미지-PDF 파이프라인의 다른 유용한 부분: