์๋ฌธ
๊ฐ๋ฐ ๊ณผ์ ์์ ์ข ์ข PDF๋ฅผ ๋ฏธ๋ฆฌ ๋ณผ ํ์๊ฐ ์์ต๋๋ค. ์ด ์๊ฐ์ ์ผ๋ฐ์ ์ผ๋ก PDF ์ฃผ์์ด๋ฉฐ, ์ด๋ค ๋ฐฉ๋ฒ์ผ๋ก ํ์ด์ง์์ PDF ํ์ผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๊ฐ ๋ ๊ฒ์ ๋๋ค. ์๋ฅผ ๋ค์ด, ๋ค์๊ณผ ๊ฐ์ ๋ฐฉ๋ฒ ๋๋ ํ์ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ:
- embed
- iframe
- pdf.js
- react-pdf
๊ทธ๋ฌ๋ PDF๊ฐ ์๋์ ์ผ๋ก ํฐ ๊ฒฝ์ฐ ๋๋ฆฐ ๋ก๋ฉ ์๋์ ๋๋ฌด ๋ง์ ๋ฉ๋ชจ๋ฆฌ๋ ๋ ์ฌ๊ฐํ ๋ฌธ์ ๊ฐ ๋ฉ๋๋ค. PDF ์ฉ๋์ด ํด์๋ก ๋คํธ์ํฌ ์ ์ก ์๊ฐ์ด ๊ธธ์ด์ง ์๋ฐ์ ์์ด ์ฌ์ฉ์ ๊ฒฝํ์ด ๋๋น ์ง๊ณ , ํฐ PDF๋ฅผ ์ด๋ฉด ๋ ๋ง์ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ์ฐจ์งํ์ฌ ํญ/๋ธ๋ผ์ฐ์ ๊ฐ ๋ฉ์ถ๊ฑฐ๋ ์ถฉ๋์ด ๋ฐ์ํ ์ ์์ต๋๋ค.
์ผ๋ฐ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ฐฉ๋ฒ
์ฐ์ , ๊ธฐ์กด์ PDF ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ฐฉ๋ฒ์ ์ดํด ๋ณด๊ฒ ์ต๋๋ค. ์ฌ๊ธฐ์์๋ ๋ ๊ฐ์ง ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ฐฉ๋ฒ์ ๊ฐ๋จํ ์ธ๊ธํ๊ณ ๋งค์ฐ ๊ฐ๋จํ๋ฉฐ ์ฝ๋๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
jsimport { PDF_URL } from "./constant" const Normal = () => { return ( <div> <embed src={PDF_URL} type="application/pdf" width="100%" height="600px"></embed> <iframe width={'100%'} height={600} src={PDF_URL} /> </div> ) } export default Normal
ํ๋๋ ๋ฏธ๋ฆฌ ๋ณด๊ธฐ์ ์๋ฒ ๋ ํ๊ทธ๋ฅผ ์ฌ์ฉํ๊ณ , ๋ค๋ฅธ ํ๋๋ ๋ธ๋ผ์ฐ์ ์ ์์ฒด PDF ๋ ๋๋ง ์์ง์ ์ฌ์ฉํ์ฌ ๋ฏธ๋ฆฌ ๋ณด๊ธฐ์ iframe ํ๊ทธ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ๋๋ค. ๋ํ ์ผ๋ฐ์ ์ผ๋ก ๋ ๋ง์ ๋ฐฉ์์ผ๋ก ์ฌ์ฉ๋ฉ๋๋ค.
ํ์ง๋ง ์ ์๊ฒ๋ 100ํ์ด์ง + PDF๊ฐ 15M ์ ๋์ด๊ณ , ๋์ญํญ์ด 4M์ธ ์๋ฒ์์๋ ์ ์ก์ 30์ด ์ ๋ ๊ฑธ๋ฆฌ๋๋ฐ, ์ฌ์ ํ ๊ฝค ๊ธด ์๊ฐ์ ๋๋ค.
๋ค์์ PDF ํ์ด์ง ๋ก๋ฉ ๋ฐ ๋ ๋๋ง์ ๋ํด ์๊ฐํฉ๋๋ค. ๋์ฉ๋ PDF ๋คํธ์ํฌ ์ ์ก์ด ๋๋ฌด ๋๋ฆฌ๊ณ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ๋๋ฌด ๋ง์ด ์ฐจ์งํ๋ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ์ค๊ณ๋์์ต๋๋ค.
๊ทธ๋ฆผ์ผ๋ก ๋๋๊ธฐ
์ ์ฒด PDF ํ์ผ์ ํ ๋ฒ ๋ก๋ํ๋ ๊ฒ์ด ๋๋ฌด ๋๋ฆฌ๊ธฐ ๋๋ฌธ์ ๋ฏธ๋ฆฌ ๋ณด๊ธฐ๋ฅผ ์ํด ๋ ์์ธํ ๋จ์๋ก ๋ถํ ํ๋ ๊ฒ์ ๊ณ ๋ คํ ์ ์์ต๋๋ค. ์ฌ๊ธฐ์์๋ ๋จผ์ ํ์ด์ง ๋ฒํธ๋ณ๋ก ์ ์ฒด PDF ํ์ผ์ ๊ทธ๋ฆผ์ผ๋ก ๋ฐ๊ฟ ์ ์์ผ๋ฉฐ, 1ํ์ด์ง PDF๋ 1์ฅ์ ๊ทธ๋ฆผ์ ํด๋นํฉ๋๋ค. ๊ทธ๋ฐ ๋ค์ ์ด๋ฌํ ์ฌ์ ์ฒ๋ฆฌ ํ์๋ ์ ์ฒด PDF ํ์ผ์ ํ ๋ฒ๋ง ๋ก๋ํ๋ ๊ฒ์ด ์๋๋ผ ์จ๋๋งจ๋ ๋ก๋ ๋๋ ์ฌ๋ผ์ด์ค ๋ก๋๋ฅผ ์ฝ๊ฒ ์ํํ ์ ์์ต๋๋ค. ๋ฐ๋ผ์ ์ฌ์ฉ์์ ์ฒซ ํ๋ฉด ๋๊ธฐ ์๊ฐ์ด ํฌ๊ฒ ์ค์ด๋ค์ด ์ฌ์ฉ์ ๊ฒฝํ์ ํฅ์์ํฌ ์ ์์ต๋๋ค.
์ฌ๊ธฐ์ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ ์ฒ๋ฆฌํด์ผ ํ๋ฉฐ PDF ํ์ผ์ด ์ด๋ฏธ์ง๋ก ๋ณํ๋ฉ๋๋ค. ์ฌ๊ธฐ์ ์ฌ์ฉํ๋ ๊ฒ์ ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ pdf-image, ํน์ ์ค์น ํํ ๋ฆฌ์ผ์ ๋งํฌ๋ฅผ ํด๋ฆญํ์ฌ ๋ณผ ์ ์์ต๋๋ค.
์ค์น ํ ๋ ธ๋๋ฅผ ์ฌ์ฉํ์ฌ PDF ํ์ผ์ ๊ฐ ํ์ด์ง๋ฅผ ์ด๋ฏธ์ง๋ก ๋ณํํ๋ ์ ์ฒ๋ฆฌ ์คํฌ๋ฆฝํธ๋ฅผ ์์ฑํ ์ ์์ต๋๋ค.
jsconst PDFImage = require("pdf-image").PDFImage; const path = require("path"); const fs = require("fs"); const FILE_PATH = path.join(__dirname, "../public/files/test.pdf"); const outputDirectory = path.join(__dirname, "../public/files/tmp"); const pdfImage = new PDFImage(FILE_PATH, { convertOptions: { "-density": 250, // ํด์๋๋ฅผ ๋์ด๊ณ ํ์์ ๋ฐ๋ผ ์กฐ์ "-quality": 100, "-trim": null, }, outputDirectory, }); const run = async () => { const info = await pdfImage.getInfo(); for (let i = 0; i < Number(info.Pages); i++) { try { await pdfImage.convertPage(i); } catch (error) { console.log("error", error); } } }; run();
๊ฐ ํ์ด์ง์ ๋ํ ์ด๋ฏธ์ง๊ฐ ์ค๋น๋๋ฉด ํด๋น ์ด๋ฏธ์ง๋ฅผ ๊ฐ์ ธ์์ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ธฐ๋ฅ์ ์ฌ์ฉํ ์ ์์ต๋๋ค. ์ฌ๊ธฐ์๋ 100๊ฐ๊ฐ ๋๋ ์ด๋ฏธ์ง๊ฐ ์์ผ๋ฏ๋ก ๋ ๋๋งํ ๋ ํ ๋ฒ์ ๋ชจ๋ ๋ ๋๋งํ ํ์ ์์ด ์ฝ๊ฐ์ ์ง์ฐ ๋ก๋ฉ์ ์ํํ๋ฉด ๋ฉ๋๋ค. ์ฌ๊ธฐ์๋ IntersectionObserver; ์ฌ์ฉํ์ฌ ์ด๋ฏธ์ง์ ์ง์ฐ ๋ก๋ฉ์ ๊ตฌํํฉ๋๋ค. ๋จผ์ ๋ค์๊ณผ ๊ฐ์ด ์ง์ฐ ๋ก๋ฉ ํ
์ ๊ตฌํํฉ๋๋ค:
- ์ฌ๊ธฐ์์ ๊ฐ ์์์ ๊ธฐ๋ณธ ๋์ด๋ฅผ ์ง์ ํ ๋ค์
IntersectionObserver;์ฌ์ฉํ์ฌ ์์๊ฐ ๋ทฐํฌํธ์ ํ์๋๋์ง ์ฌ๋ถ๋ฅผ ํ์ธํ ์ ์์ต๋๋ค. - ๋ทฐํฌํธ ๋ฐํ์ ํ์๋๋ ๊ฒฝ์ฐ ์ด ์ด๋ฏธ์ง๋ฅผ ๋ก๋ํฉ๋๋ค.
jsimport { useEffect,useRef,useCallback } from "react"; const useObserve = ({ rootRef, itemSelector }) => { const observer = useRef(null); const handlerObserve = entries => { entries.forEach(({ isIntersecting, target }) => { if (isIntersecting) { const targetImg = target.children[0]; targetImg.src = targetImg.dataset.src; // src ์์ฑ์ ์์ ํ ํ ๋ฐ์ดํฐ-src ์์ฑ์ ์ ๊ฑฐํ๊ณ ๋ชจ๋ํฐ๋ง์ ์ทจ์ํ ์ ์์ต๋๋ค! targetImg.removeAttribute("data-src"); observer.current.unobserve(target); } }); }; const addObserve = () => { const list = document.querySelectorAll(itemSelector); list.forEach(item => { observer.current.observe(item); }); }; const initObserver = useCallback(() => { observer.current = new IntersectionObserver(handlerObserve, { root: rootRef.current, rootMargin: "0px 0px 200px 0px", // ๋ชจ๋ํฐ ์์ญ ์๋์ชฝ์ผ๋ก ํ์ฅ 200px }); addObserve(); }, []); useEffect(() => { initObserver(); return () => { observer.current.disconnect(); }; }, []); }; export default useObserve;
๊ทธ๋ฐ ๋ค์ ๋ค์๊ณผ ๊ฐ์ด ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ปดํฌ๋ํธ๋ฅผ ๊ตฌํํฉ๋๋ค:
jsimport { Carousel } from "antd"; import styles from "./index.module.less"; import { LeftOutlined, RightOutlined } from "@ant-design/icons"; import { useState, useEffect, useRef, useMemo, useCallback } from "react"; import { API_PREFIX } from "../../../../env"; import useObserve from "./useObserve"; const defaultImage = ""; const Preview = () => { const [list, setList] = useState(() => { return new Array(105).fill(0).map((item, index) => { return { page: index, originSrc: `${API_PREFIX}/files/tmp/test-${index}.png`, }; }); }); const contentRef = useRef(null); useObserve({ rootRef: contentRef, itemSelector: ".image-item-observe", }); return ( <div className={styles.preview}> <div ref={contentRef} className={styles.content}> {list.map((item, index) => { return ( <div class={`${styles["image-item-wrapper"]} image-item-observe`}> {/* ๋ก๋ฉ ๊ณผ์ ์์ ํฐ์ ํ๋ฉด ํ์์ ํผํ๊ธฐ ์ํด ๊ธฐ๋ณธ ๊ธฐ๋ณธ ๋งต์ ์ค์ ํฉ๋๋ค.*/} <img class={styles["image-item"]} src={defaultImage} data-src={item.originSrc} key={index} width="100%" /> </div> ); })} </div> </div> ); }; export default Preview;
์ด๋ฅผ ํตํด PDF ๋ฏธ๋ฆฌ ๋ณด๊ธฐ ๊ธฐ๋ฅ์ ์จ๋๋งจ๋ ๋ฐฉ์์ผ๋ก ๋ก๋ํ ์ ์์ต๋๋ค.
ํ์ด์ง๋ณ๋ก ๋๋๊ธฐ
์๋ ์ฌ๋ฌ ์ด๋ฏธ์ง๋ฅผ ๋ก๋ํ์ฌ PDF์ ์ง์ฐ ๋ก๋ฉ์ ์ํํ ํ PDF๋ฅผ ์ด๋ฏธ์ง๋ก ๋ณํํ๋ ๋ฐฉ๋ฒ์ ๋๋ค. ๋ค์ ๋ฐฉ๋ฒ์ ํฐ PDF๋ฅผ ์ฌ๋ฌ ๊ฐ์ ์์ PDF๋ก ์๋ฅธ ๋ค์ ์ผ๋ฐ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ์ฌ์ฉํ์ฌ ๋ฏธ๋ฆฌ ๋ณด๋ ๊ฒ์ ๋๋ค. ๋จผ์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฒ๋ฆฌํ๋ ์คํฌ๋ฆฝํธ๋ฅผ ์์ฑํ๊ณ PDF ํ์ผ์ ์๋ฆ ๋๋ค.
jsconst fs = require("fs"); const { PDFDocument } = require("pdf-lib"); const path = require("path"); async function splitPDF(inputPath, outputPrefix) { const pdfBytes = await fs.promises.readFile(inputPath); const pdfDoc = await PDFDocument.load(pdfBytes); const totalPages = pdfDoc.getPageCount(); const pagesPerChunk = 10; for (let startPage = 1; startPage <= totalPages; startPage += pagesPerChunk) { const endPage = Math.min(startPage + pagesPerChunk - 1, totalPages); const newPdfDoc = await PDFDocument.create(); for (let pageNum = startPage; pageNum <= endPage; pageNum++) { const [copiedPage] = await newPdfDoc.copyPages(pdfDoc, [pageNum - 1]); newPdfDoc.addPage(copiedPage); } const outputPath = `${outputPrefix}/${Math.floor(endPage / 10)}.pdf`; const newPdfBytes = await newPdfDoc.save(); await fs.promises.writeFile(outputPath, newPdfBytes); console.log(`PDF successfully split from page ${startPage} to ${endPage}.`); } } const FILE_PATH = path.join(__dirname, "../public/files/test.pdf"); const outputDirectory = path.join(__dirname, "../public/files/tmp-file"); splitPDF(FILE_PATH, outputDirectory);
์ฌ๋ผ์ด์ฑ ํ ๊ฒฐ๊ณผ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
๊ทธ๋ฐ ๋ค์ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ ๋งค์ฐ ๊ฐ๋จํฉ๋๋ค. iframe์ ์ฌ์ฉํ์ฌ ์ง์ ํ๋์ฉ ๋ฏธ๋ฆฌ๋ณด๊ณ ํด๋ฆญํ์ฌ ๋ ๋ง์ด๋ก๋ ํ ๋ค์ ๋ค์ ์ธ๊ทธ๋จผํธ๋ฅผ๋ก๋ํฉ๋๋ค. ๊ฐ๋จํ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ตฌ์ฑ ์์๋ ๋ค์๊ณผ ๊ฐ์ผ๋ฉฐ ๋ถํ ๋ PDF ํ์ผ์ ํ๋์ฉ๋ก๋ํฉ๋๋ค:
jsimport React, { useState, useEffect, useRef } from "react"; import { API_PREFIX } from "../../../../env"; const pdfs = new Array(11) .fill(0) .map((_, index) => `${API_PREFIX}/files/tmp-file/${index + 1}.pdf`); const Split = () => { const [list, setList] = useState([pdfs[0]]); return ( <div> {list.map((url, index) => { return ( <div> <iframe id={`iframe-${index}`} key={index} width={"100%"} height={600} src={url} /> </div> ); })} <button disabled={list.length === pdfs.length} onClick={() => { const arr = [...list]; arr.push(pdfs[arr.length]); setList(arr) }} > ๋ ๋ณด๊ธฐ </button> </div> ); }; export default Split;
๋ง์ง๋ง์ผ๋ก
์์ ๋ด์ฉ์ ๋ ๊ฐ์ ํฐ PDF ํ์ผ์ ๋ ๋น ๋ฅด๊ฒ ๋ฏธ๋ฆฌ ๋ณด๋ ๋ฐฉ๋ฒ์ ๋ํด ์ค๋ช ํฉ๋๋ค. ํฅ๋ฏธ๋ก์ด ๋ฐฉ๋ฒ์ด ์์ผ๋ฉด ๋ธ๋ ์ธ ์คํ ๋ฐ ํ ๋ก ๊ณผ ํจ๊ป ์๊ฒฌ ์น์ ์ ์ค์ ๊ฒ์ ํ์ํฉ๋๋ค. ์ฝ์ด ์ฃผ์ ์ ๊ฐ์ฌํฉ๋๋ค, ์ํ๋ค๋ฉด ์ฐ๋ ค๋๋ ์ ์ ์ง์ ํ๊ณ ์นญ์ฐฌํด ์ฃผ์ธ์~!





