클라이언트만? 서버도? PDF 뷰어 개발, 어떤 선택이 좋을까?

궁금하면 500원·2024년 11월 10일
0

미생의 개발 이야기

목록 보기
22/58

순수 웹 기술과 Spring으로 PDF 뷰어 만들기: 현실적인 선택

"순수하게 웹 기술(HTML, CSS, JavaScript)만으로 PDF 뷰어를 만들 수 있을까?"라는 의문에서 시작해, 서버 연동까지 확장된 개발 여정을 정리해 보았습니다. 결론부터 말씀드리면, 순수 웹 기술만으로도 가능하지만, Spring 같은 백엔드 프레임워크와 결합하면 훨씬 강력하고 안정적인 뷰어를 만들 수 있습니다.


순수 웹 기술만으로 뷰어 만들기

처음에는 서버 없이 오직 클라이언트(브라우저)에서 모든 것을 처리하는 방식을 고려할 수 있습니다. 이 방식은 PDF 파일의 복잡한 구조를 해석하고, 이를 웹 페이지에 렌더링하는 자바스크립트 기반의 렌더링 라이브러리HTML Canvas API를 활용합니다.

작동 원리

  • 사용자가 브라우저에서 PDF 파일을 선택하거나 업로드합니다.
  • 자바스크립트 라이브러리가 PDF 파일 데이터를 읽어 들입니다.
  • 라이브러리가 파일 데이터를 분석해 페이지별 렌더링 정보를 추출합니다.
  • HTML <canvas> 요소를 '도화지' 삼아 추출한 정보를 그려냅니다.

장점

  • 서버리스(Serverless) 구조: 별도의 백엔드 서버가 필요 없어 운영 비용이 절감됩니다.
  • 빠른 로딩 속도: 모든 처리가 클라이언트에서 이루어지므로 서버와의 통신 지연이 적습니다.

하지만 이 방법은 파일 관리, 보안, 대용량 파일 처리 등에서 한계가 명확합니다.


더 나은 서비스를 위한 선택: Spring 백엔드 연동

안정적이고 확장 가능한 PDF 뷰어 서비스를 구축하려면, 스프링(Spring) 같은 백엔드 프레임워크와 결합하는 것이 효과적입니다. 백엔드는 파일을 안전하게 관리하고, 클라이언트는 그 파일을 받아와 렌더링에만 집중하는 역할 분담이 이루어집니다.

Spring 서버의 역할: 파일 관리와 제공

스프링 서버는 PDF 파일을 안전하게 저장하고, 요청이 들어올 때 인증된 사용자에게만 파일을 제공하는 REST API를 구현합니다.

import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

@RestController
public class PdfFileController {
    
    // PDF 파일이 저장된 서버 경로
    private static final String FILE_STORAGE_PATH = "/path/to/your/pdf/files/";

    // PDF 파일을 제공하는 API 엔드포인트
    @GetMapping("/api/pdf/{filename}")
    public ResponseEntity<byte[]> getPdfFile(@PathVariable String filename) throws IOException {
        Path filePath = Paths.get(FILE_STORAGE_PATH + filename);
        
        // 파일이 존재하지 않으면 404 Not Found 반환
        if (!Files.exists(filePath)) {
            return ResponseEntity.notFound().build();
        }

        // 파일의 모든 내용을 읽어옵니다.
        byte[] pdfContent = Files.readAllBytes(filePath);

        HttpHeaders headers = new HttpHeaders();
        // 응답 타입을 PDF로 설정
        headers.setContentType(MediaType.APPLICATION_PDF);
        // 다운로드 시 파일명 설정
        headers.setContentDispositionFormData(filename, filename);
        
        // 파일을 응답 본문에 담아 반환
        return ResponseEntity.ok()
                .headers(headers)
                .body(pdfContent);
    }
}

Spring + 웹 기술: 하이브리드 아키텍처

클라이언트는 스프링 서버가 제공하는 API를 호출하여 PDF 파일 데이터를 가져온 후, 이 데이터를 캔버스에 렌더링합니다.

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Spring PDF 뷰어</title>
    <style>
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; }
        #pdf-viewer-container { display: flex; justify-content: center; margin-bottom: 20px; }
        #pdf-viewer-canvas { border: 1px solid #ccc; max-width: 100%; height: auto; }
        .controls { text-align: center; }
        .controls button { padding: 8px 16px; margin: 0 5px; cursor: pointer; }
    </style>
</head>
<body>

    <div id="pdf-viewer-container">
        <canvas id="pdf-viewer-canvas"></canvas>
    </div>
    
    <div class="controls">
        <button id="prev-page">이전</button>
        <button id="next-page">다음</button>
        <span>페이지: <span id="page-num"></span> / <span id="page-count"></span></span>
    </div>

    <script type="module">

        const viewerCanvas = document.getElementById('pdf-viewer-canvas');
        const context = viewerCanvas.getContext('2d');
        // 스프링 서버의 API URL
        const fileUrl = 'http://localhost:8080/api/pdf/my_document.pdf';

        let pdfDoc = null;
        let pageNum = 1;

        // 스프링 서버로부터 PDF 파일을 가져와 렌더링
        fetch(fileUrl)
            .then(response => response.arrayBuffer()) // 파일을 ArrayBuffer 형태로 가져옴
            .then(arrayBuffer => {
                return getDocument({ data: arrayBuffer }).promise;
            })
            .then(pdf => {
                pdfDoc = pdf;
                document.getElementById('page-count').textContent = pdf.numPages;
                renderPage(pageNum);
            })
            .catch(error => {
                console.error('PDF 로드 중 오류 발생:', error);
            });

        function renderPage(num) {
            pdfDoc.getPage(num).then(page => {
                const viewport = page.getViewport({ scale: 1.5 });
                viewerCanvas.height = viewport.height;
                viewerCanvas.width = viewport.width;

                page.render({
                    canvasContext: context,
                    viewport: viewport
                });
                document.getElementById('page-num').textContent = num;
            });
        }

        // 페이지 이동 버튼 이벤트 리스너
        document.getElementById('prev-page').addEventListener('click', () => {
            if (pageNum <= 1) return;
            pageNum--;
            renderPage(pageNum);
        });

        document.getElementById('next-page').addEventListener('click', () => {
            if (pageNum >= pdfDoc.numPages) return;
            pageNum++;
            renderPage(pageNum);
        });
    </script>
</body>
</html>

하이브리드 접근법의 장점

  • 중앙 집중식 파일 관리: 모든 PDF 파일을 서버에서 체계적으로 관리할 수 있습니다.
  • 강력한 보안: 스프링 시큐리티를 활용해 특정 사용자만 PDF 파일에 접근하도록 권한을 부여할 수 있습니다.
  • 확장성: 서버 측에서 PDF 파일을 미리 압축하거나, 워터마크를 삽입하거나, 텍스트를 추출하는 등의 복잡한 사전 처리를 수행할 수 있습니다.
  • 안정적인 성능: 대용량 파일도 안정적으로 전송하며, 서버 자원을 효율적으로 관리할 수 있습니다.

결론적으로, 순수 웹 기술만으로도 PDF 뷰어의 기본 기능을 구현하는 것은 충분히 가능합니다. 하지만 상용 수준의 안정성, 보안, 확장성을 고려한다면 스프링 백엔드와 연동하는 하이브리드 접근법이 훨씬 더 현명한 선택입니다.

profile
꾸준히, 의미있는 사이드 프로젝트 경험과 문제해결 과정을 기록하기 위한 공간입니다.

0개의 댓글