본문 바로가기

(개인Project)_개발/PLC-PC 연결

[Program][보족] PDF 매뉴얼을 알람 팝업에 연결하기까지 (3차 시도와 최종 해결)

반응형

이 글이 나한테 해당되는지 먼저 확인해보세요

이런 상황이 하나라도 해당된다면, 이 글이 도움이 될 겁니다.

  • pdf.js를 설치했는데 Setting up fake worker failed: Failed to fetch dynamically imported module 오류가 난다
  • Vite 환경에서 pdfjs-dist를 쓰려는데 워커 파일 경로 문제로 계속 막힌다
  • CDN 없는 오프라인 환경에서 PDF를 브라우저에 표시해야 한다
  • pdf.js?url, public/ 폴더 복사, 동적 import 전부 시도했는데 해결이 안 된다
  • PyMuPDF(fitz)가 뭔지는 모르지만 PDF를 서버에서 처리하는 방법이 궁금하다

결론부터 말하면, 저도 해결 못 했습니다.

대신 방향을 바꿨더니 10줄로 끝났어요.


용어 먼저 짚고 넘어갈게요

 

pdf.js란

Mozilla가 만든 JavaScript 기반 PDF 렌더링 라이브러리입니다.

브라우저에서 PDF 파일을 Canvas에 직접 그려주는 방식이에요.

Adobe Reader 같은 외부 프로그램 없이 웹 페이지 안에서 PDF를 표시할 수 있어서 많이 쓰입니다.

 

ESM (ES Module)이란

JavaScript 모듈을 불러오는 현대적인 방식입니다. import 문을 사용합니다.

pdfjs-dist v3 이후로 ESM 전용으로 전환됐는데, 이 과정에서 워커 파일 처리 방식이 크게 바뀌었어요.

Vite 같은 번들러와 함께 쓸 때 경로 문제가 생기기 쉽습니다.

 

워커 (Web Worker)란

브라우저에서 무거운 작업을 백그라운드 스레드에서 처리하는 방법입니다.

pdf.js가 PDF를 파싱하는 작업을 메인 스레드가 아닌 워커에서 처리합니다.

이 워커 파일 경로가 제대로 잡히지 않으면 "fake worker" 오류가 납니다.

 

PyMuPDF (fitz)란

Python에서 PDF를 처리하는 라이브러리입니다.

pip install PyMuPDF로 설치하고 import fitz로 씁니다.

PDF를 열고, 페이지를 이미지로 변환하고, 텍스트를 추출하는 등 다양한 작업을 할 수 있어요.

가볍고 빠르며, 서버 사이드에서 동작합니다.


왜 PDF 매뉴얼을 알람 팝업에 넣으려 했나

PLCLink Phase 3의 목표 중 하나는 알람이 났을 때 조치 방법을 팝업에서 바로 볼 수 있게 하는 것이었습니다.

현장에서 알람이 나면 이런 일이 생깁니다.

운전자가 팝업을 보고, "D1000 초과"라는 메시지를 확인하고, 그 다음에 할 일을 알아야 합니다.

그런데 "할 일"이 머릿속에 없으면, 서류함을 뒤지거나 엔지니어에게 전화를 해야 합니다.

PPT로 만든 조치 매뉴얼이 현장에 있습니다.

"D1000이 초과됐을 때는 1번 밸브를 확인하고, 이상 없으면 2번 밸브를 점검해라" 같은 내용이 슬라이드 몇 장에 정리되어 있어요.

이걸 PDF로 내보내서 알람 팝업에 연결하면, 알람이 나는 순간 팝업 안에서 바로 조치 순서를 볼 수 있습니다.

이게 목표였어요.


1차 시도 : 이미지 한 장씩 등록

가장 단순한 방법부터 시작했습니다.

PPT 슬라이드를 PNG로 내보내서, 알람 룰마다 이미지를 등록하는 방식이었어요.

동작은 했습니다.

이미지 캐러셀도 이미 만들어져 있었고요.

문제는 운영이었습니다.

현장 매뉴얼이 20~30장짜리인 경우가 많아요.

PPT를 수정하면 내보내기, 업로드, 다시 등록 과정을 반복해야 합니다.

시범 운영하면서 "이건 안 되겠다"는 결론이 바로 났습니다.


2차 시도 : PDF embed + #page=N

PDF 파일 자체를 올리고, <embed> 태그로 표시하는 방식입니다.

<embed src="/static/manual.pdf#page=2" type="application/pdf" />

#page=N으로 원하는 페이지로 이동할 수 있다는 게 포인트였어요.

Chrome에서 테스트하니까 해당 페이지로 스크롤은 됐습니다.

 

그런데 두 가지 문제가 있었어요.

첫째, Chrome 내장 PDF 뷰어는 스크롤을 막을 수 없습니다.

#page=2로 2페이지로 이동하지만, 스크롤하면 다른 페이지가 같이 보입니다.

"한 번에 한 페이지씩" 표시하는 게 안 됐어요.

 

둘째, 비율 제어가 어렵습니다.

PDF 뷰어가 내장 UI를 그리기 때문에 툴바가 같이 표시되고, 팝업 안에서 깔끔하게 맞추기가 힘들었습니다.


3차 시도 : pdf.js, 그리고 막혔다

pdf.js를 쓰면 Canvas에 한 페이지씩 직접 그릴 수 있습니다.

스크롤 문제도 없고, 비율도 내가 원하는 대로 잡을 수 있어요.

이게 정답이라고 생각했습니다.

 

설치는 간단했습니다.

npm install pdfjs-dist

그런데 실행하니까 바로 오류가 났습니다.

Setting up fake worker failed:
Failed to fetch dynamically imported module:
http://localhost:5173/node_modules/pdfjs-dist/build/pdf.worker.mjs

pdf.js는 PDF 파싱 작업을 별도 워커 스레드에서 처리합니다.

이 워커 파일(pdf.worker.mjs)이 브라우저에서 접근 가능한 위치에 있어야 하는데, Vite가 node_modules 안의 파일을 직접 서비스하지 않기 때문에 접근이 안 되는 거였어요.

해결 시도를 쭉 해봤습니다.

 

시도 1 : ?url 접미사로 워커 URL 가져오기

import pdfjsWorkerUrl from 'pdfjs-dist/build/pdf.worker.mjs?url'
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorkerUrl

Vite에서 ?url은 파일의 빌드 후 URL을 반환합니다.

개발 모드에서 잠깐 동작하는 것 같았는데, 빌드하면 경로가 깨졌어요.

 

시도 2 : public/ 폴더에 워커 파일 복사

public/
  pdf.worker.mjs  ← node_modules에서 복사
pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdf.worker.mjs'

public/ 폴더에 넣으면 Vite가 그대로 서비스해줍니다.

이건 동작했는데, pdfjs-dist v5부터 워커 파일이 ESM 형식으로 바뀌면서 정적 파일로 복사해도 import 경로 문제가 생겼어요.

 

시도 3 : CDN에서 워커 로드

pdfjsLib.GlobalWorkerOptions.workerSrc =
  'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.x.x/pdf.worker.mjs'

CDN에서 가져오면 경로 문제가 없습니다.

개발 환경에서는 바로 됐어요.

그런데 PLCLink는 제조 현장 PC에서 돌아갑니다.

인터넷이 안 되는 환경이 많아요. CDN은 선택지가 아니었습니다.

 

시도 4 : workerSrc를 빈 문자열로 설정 (fake worker 강제)

pdfjsLib.GlobalWorkerOptions.workerSrc = ''

워커 없이 메인 스레드에서 직접 처리하는 방식으로 폴백됩니다.

이 상태로 테스트하니까 오류 없이 동작하긴 했는데, 무거운 PDF에서 브라우저가 잠시 멈추는 문제가 생겼어요.

UX 품질이 떨어졌습니다.

여기까지 해보고 결정을 해야 했습니다.


포기하기로 결정한 순간

솔직히 말하면, 결정하는 데 시간이 걸렸습니다.

"여기까지 왔는데"라는 생각이 있었어요.

pdf.js를 선택한 이유가 있었고, 4가지 방법을 시도했고, 조금만 더 파면 해결될 것 같다는 느낌이 있었습니다.

 

그런데 현실을 정리해보면 이랬어요.

  • 오프라인 환경이 전제
  • pdfjs-dist v5 ESM 전환 이후 Vite와의 호환 문제가 이슈트래커에도 미해결 상태
  • 워커 없는 폴백은 UX 품질이 떨어짐
  • 이 문제만 붙잡고 있는 시간이 이미 다른 기능 개발보다 길어짐

목표는 "pdf.js를 동작시키는 것"이 아니었습니다.

"알람 팝업에서 PDF 매뉴얼을 볼 수 있게 하는 것"이었어요.

방법은 하나가 아닙니다.


최종 해결 : 서버에서 PNG로 변환, PyMuPDF

생각을 바꿨습니다.

브라우저가 PDF를 렌더링하는 게 문제라면, PDF를 브라우저에 보내지 않으면 됩니다.

서버에서 PNG로 변환해서 이미지로 보내면, 브라우저는 그냥 이미지를 표시하면 됩니다.

 

이미지 캐러셀은 이미 만들어져 있었어요.

기존 코드를 그대로 쓸 수 있습니다.

PyMuPDF 설치는 한 줄이면 됩니다.

pip install PyMuPDF

PDF를 페이지별 PNG로 변환하는 코드입니다.

import fitz  # PyMuPDF
import time

def pdf_to_images(filepath, site_id, rule_id, output_dir):
    doc = fitz.open(str(filepath))
    saved_files = []

    for i in range(len(doc)):
        page = doc.load_page(i)

        # 1.5배 해상도로 렌더링 (선명도 조절)
        matrix = fitz.Matrix(1.5, 1.5)
        pix = page.get_pixmap(matrix=matrix)

        # 파일명 : site_id + rule_id + 타임스탬프 + 페이지 번호
        filename = f"{site_id}_{rule_id}_{int(time.time()*1000)+i}_p{i+1:03d}.png"
        save_path = output_dir / filename
        pix.save(str(save_path))
        saved_files.append(filename)

    doc.close()
    return saved_files

이게 전부입니다.

FastAPI 엔드포인트에서 이 함수를 호출하면, 업로드된 PDF가 페이지별 PNG 파일로 변환되어 저장됩니다.

알람 팝업에서는 이미지 파일 목록을 API로 받아서 캐러셀을 만들어요.

// 팝업에서 이미지 목록 받아서 캐러셀 표시
const { data } = await api.get(`/alarm-rules/${ruleId}/manual-images`)
// data: ["site1_rule3_1234_p001.png", "site1_rule3_1234_p002.png", ...]

// 기존 이미지 캐러셀 컴포넌트 그대로 사용
<ImageCarousel images={data} />

pdf.js 워커 문제는 완전히 없어졌습니다.

브라우저는 이미지를 표시하기만 하면 되고, PDF 파싱은 서버에서 이미 끝났으니까요.


이 방법의 장단점

장점:

  • 오프라인 환경에 완전히 독립적입니다. CDN 불필요
  • 브라우저가 PDF를 해석할 필요 없습니다. 이미지라 어디서든 표시됩니다
  • 기존 이미지 캐러셀 코드를 재사용할 수 있습니다
  • fitz.Matrix(1.5, 1.5)로 해상도를 조절할 수 있습니다. 흐릿하면 2.0으로 높이면 됩니다
  • PyMuPDF가 Python 표준처럼 쓰이는 라이브러리라 안정적입니다

단점:

  • 파일 크기가 늘어납니다. PDF 한 파일이 PNG 여러 장이 됩니다
  • 업로드 시 변환 시간이 있습니다. 30장짜리 매뉴얼이면 몇 초 걸려요
  • PDF 원본의 텍스트 선택이나 검색 기능은 쓸 수 없습니다

현장 매뉴얼 용도라면 단점보다 장점이 훨씬 큽니다.

담당자가 조치 방법을 슬라이드로 보는 것이 목적이지, 텍스트를 복사하거나 검색할 필요는 없거든요.


pdf.js가 맞는 상황은 따로 있다

pdf.js를 포기한다고 해서 pdf.js가 나쁜 라이브러리라는 게 아닙니다.

pdf.js는 이런 경우에 적합합니다.

  • 인터넷이 되는 환경에서 CDN을 쓸 수 있는 경우
  • 사용자가 PDF 원본을 그대로 보아야 하는 경우 (계약서, 공문서 등)
  • 텍스트 선택, 검색, 주석 기능이 필요한 경우
  • Webpack 기반 프로젝트에서 워커 설정을 제대로 할 수 있는 경우

PLCLink의 경우는 이 모두에 해당하지 않았습니다.

오프라인, 슬라이드 보여주기가 목적, 텍스트 기능 불필요, Vite 환경.

조건이 맞지 않으면 다른 방법이 더 맞습니다.


마치며

기술적으로 막혔을 때 "계속 파야 하는가, 방향을 바꿔야 하는가"를 판단하는 게 생각보다 어렵습니다.

계속 파는 게 맞는 경우도 있습니다.

조금만 더 하면 해결될 것 같을 때, 이 기술 외에 대안이 없을 때.

방향을 바꾸는 게 맞는 경우는 이런 때입니다.

 

이미 여러 방법을 시도했는데 환경 자체가 지원이 안 될 때.

목표가 "이 기술을 쓰는 것"이 아니라 "이 결과물을 만드는 것"일 때.

저는 후자였습니다. 결과물은 "알람 팝업에서 매뉴얼을 볼 수 있게 하는 것"이었고, pdf.js가 그 유일한 방법은 아니었어요.

PyMuPDF로 10줄을 쓰는 데 30분 걸렸습니다.

pdf.js 워커 문제에 쓴 시간보다 훨씬 짧았어요.

반응형