바이브코딩

브라우저에서 바로 이미지를 PDF로 — SvelteKit 기반 Image to PDF 제작기

ethanjoh 2026. 7. 1. 16:39

TL;DR — 서버 없이, 개인 파일을 외부로 전송하지 않고 브라우저 안에서만 이미지를 PDF로 변환하는 웹앱을 SvelteKit + Tailwind CSS v4로 만들었습니다. GitHub Pages에 무료로 배포해서 누구나 쓸 수 있습니다.


왜 만들었나요?

스캔 이미지나 사진 몇 장을 PDF로 묶어 제출해야 할 때가 꽤 자주 있습니다. 그런데 vFlat 앱으로 사진을 찍었더니 유료 서비스에 가입을 해야 하더군요. 그래서 급히 직접 만들어보기로 했습니다.

 

어떻게 만들었나요?

안티그래비티에 대략적인 기능을 설명해 주었습니다.

그러자 스스로 개발초안을 만들며 개선안까지 제시를 해주었습니다.

처음에는 서버에서 동작하는 것으로 생각했으나, 브라우저에서도 처리할 수 있다고 제안까지 한 것입니다. 

그리고 안티그래비티가 임의로 프로그래밍 언어를 선택해 만드는 것보다 스벨트킷과 테일윈드 CSS로 만들어 보고 싶었습니다.

 

역시나 이번 프로젝트에서도 단 한줄의 코딩도 직접 하지 않았습니다.

그냥 프롬프트를 작성해 지시만 내렸습니다.

 


주요 기능

✅ 완전한 클라이언트 사이드 처리

모든 변환 로직이 사용자의 브라우저 안에서만 실행됩니다. 파일이 서버로 전송되지 않으므로 개인정보가 완벽하게 보호됩니다. pdf-lib 라이브러리를 통해 브라우저 내에서 직접 PDF 바이너리를 생성합니다.

📱 모바일 완벽 지원 — 드래그 & 드롭 순서 변경

데스크톱의 마우스 드래그는 물론, 모바일 터치 환경에서도 손가락으로 이미지 순서를 자유롭게 바꿀 수 있습니다. svelte-dnd-action 라이브러리가 이 부분을 담당합니다. 모바일에서 터치 이벤트와 브라우저 스크롤이 충돌하지 않도록 touch-action CSS 처리도 신경 썼습니다.

🔍 이미지 미리보기 (라이트박스)

썸네일을 클릭하면 해당 이미지를 큰 화면으로 미리볼 수 있는 라이트박스 팝업이 열립니다. 변환 전에 이미지 상태를 빠르게 확인할 수 있습니다.

🔄 반응형 그리드

화면 크기에 따라 모바일에서는 2열, 데스크톱에서는 4~5열로 썸네일이 자동 배치됩니다.


기술 스택

역할 기술
프레임워크 SvelteKit (Svelte 5, 룬 문법)
스타일링 Tailwind CSS v4
드래그 & 드롭 svelte-dnd-action
PDF 생성 pdf-lib
아이콘 lucide-svelte
배포 GitHub Pages + GitHub Actions

핵심 구현 포인트

1. 이미지를 PDF로 변환하는 핵심 로직

async function generatePDF() {
  const pdfDoc = await PDFDocument.create();

  for (const item of items) {
    const imageBytes = await item.file.arrayBuffer();
    let image;

    if (item.file.type === 'image/jpeg') {
      image = await pdfDoc.embedJpg(imageBytes);
    } else if (item.file.type === 'image/png') {
      image = await pdfDoc.embedPng(imageBytes);
    }

    const page = pdfDoc.addPage();
    const { width, height } = page.getSize();
    const imgDims = image.scaleToFit(width, height); // 페이지에 맞게 비율 유지

    // 페이지 중앙에 이미지 배치
    page.drawImage(image, {
      x: width / 2 - imgDims.width / 2,
      y: height / 2 - imgDims.height / 2,
      width: imgDims.width,
      height: imgDims.height,
    });
  }

  const pdfBytes = await pdfDoc.save();
  const blob = new Blob([pdfBytes], { type: 'application/pdf' });
  pdfUrl = URL.createObjectURL(blob); // 다운로드 링크 생성
}

pdf-libscaleToFit()을 사용하면 이미지 원본 비율을 유지하면서 A4 페이지 안에 꼭 맞게 배치할 수 있습니다. URL.createObjectURL()로 서버 요청 없이 브라우저 메모리에서 바로 다운로드 링크를 만드는 것이 핵심입니다.

2. Svelte 5 룬(Runes) 문법 적용

이번 프로젝트는 기존 $: 반응형 선언 대신 Svelte 5의 새로운 룬 문법을 적용했습니다.

// 기존 Svelte 4 방식
let items = [];
$: count = items.length;

// Svelte 5 룬 방식
let items = $state<ImageItem[]>([]);
let isConverting = $state(false);
let pdfUrl = $state<string | null>(null);

$state()로 반응형 상태를 선언하는 방식은 코드 의도가 훨씬 명확하게 드러나고, TypeScript와의 통합도 자연스럽습니다.

3. 드래그 & 드롭 영역 이중 처리 — 파일 투입구 vs 순서 변경

드롭존(파일을 올려놓는 영역)과 썸네일 그리드(순서 변경 영역)가 서로 다른 드래그 이벤트를 처리합니다.

  • 드롭존: 표준 ondrop / ondragover 이벤트로 파일 수신
  • 썸네일 그리드: svelte-dnd-actionuse:dndzone 디렉티브로 아이템 순서 제어

이 두 영역이 이벤트 충돌 없이 각자의 역할을 수행하도록 이벤트 전파(stopPropagation)를 적절히 처리했습니다.

4. GitHub Actions로 자동 배포 파이프라인 구성

main 브랜치에 푸시하면 자동으로 빌드 → GitHub Pages 배포까지 이어집니다.

on:
  push:
    branches: 'main'

jobs:
  build_site:
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run build
        env:
          BASE_PATH: '/${{ github.event.repository.name }}'
      - uses: actions/upload-pages-artifact@v3
        with:
          path: 'build/'

  deploy:
    needs: build_site
    uses: actions/deploy-pages@v4

SvelteKit의 adapter-static을 사용하면 서버리스 정적 사이트로 빌드되어 GitHub Pages에 바로 올릴 수 있습니다. BASE_PATH 환경변수로 GitHub Pages의 서브패스(/image2pdf/) 문제도 깔끔하게 해결했습니다.


겪었던 이슈와 해결 방법

🐛 Svelte 5 빌드 에러 — on:click vs onclick

Svelte 5로 넘어오면서 이벤트 디렉티브 문법이 바뀌었습니다. 기존의 on:click={handler} 대신 onclick={handler} 형태를 써야 하고, 혼용하면 빌드 경고가 발생합니다. 특히 svelte-dnd-action과 함께 쓸 때 onconsider, onfinalize 등 커스텀 이벤트도 동일한 방식으로 처리해야 한다는 점을 주의해야 합니다.

🐛 GitHub Pages BASE_PATH 문제

GitHub Pages에서 서브경로(/image2pdf/)로 배포할 경우, 내부 링크나 에셋 경로가 깨지는 문제가 발생합니다. SvelteKit의 vite.config.ts에서 base 옵션을 process.env.BASE_PATH로 지정하고, GitHub Actions에서 레포지토리명을 환경변수로 넘겨주는 방식으로 해결했습니다.


개선점

만들고 나니 이미지 하나 하나 삭제하는 기능이 없었습니다.

당장은 굳이 필요하지 않아 내버려뒀는데 추가로 기능을 넣을 생각입니다.


마치며

생각보다 완성도 있는 서비스를 만들 수 있었습니다. Svelte 5의 룬 문법과 Tailwind CSS v4를 실제 프로젝트에 적용해보는 좋은 기회이기도 했습니다.

오픈소스로 공개되어 있으니 필요하신 분은 자유롭게 사용하거나 포크해서 개선하셔도 됩니다.

🔗 라이브 데모: https://ethanjoh.github.io/image2pdf/
📁 GitHub: https://github.com/ethanjoh/image2pdf


#SvelteKit #Svelte5 #TailwindCSS #pdf-lib #GitHub Pages #프론트엔드 #사이드프로젝트

 

이 글은 안티그래비티에서 CHANGELOG.md와 README.md 파일을 참고하여 초안을 작성하였습니다.