Nuxt와 pdf-lib를 사용해 여러 PDF 파일을 브라우저에서 합치는 PDF 병합 도구를 구현하는 방법을 설명합니다. 파일 업로드, 드래그 순서 변경, PDF 병합, 다운로드, SEO 설정까지 개발자 관점에서 정리했습니다.

Nuxt로 PDF 합치기 도구 만들기: 서버 업로드 없이 브라우저에서 PDF 병합 구현하기
PDF 합치기 기능은 여러 개의 PDF 파일을 하나의 문서로 병합해야 할 때 자주 사용되는 웹 도구입니다. 특히 사용자가 계약서, 견적서, 스캔 문서, 보고서 등을 하나로 묶고 싶을 때 검색하는 대표적인 키워드가 “PDF 합치기”, “PDF 병합”, “PDF 파일 합치기”입니다.
이번 글에서는 실제 PDF 합치기 도구 페이지를 참고해, 개발자 입장에서 Nuxt로 서버 업로드 없이 브라우저에서 PDF 병합 도구를 만드는 방법을 정리합니다. 참고 페이지는 여러 PDF를 원하는 순서로 정렬한 뒤 하나의 파일로 합치는 기능, 서버 업로드 없음, 브라우저 메모리 처리, 원본 품질 유지 등을 핵심 가치로 보여주고 있습니다.
PDF 합치기 도구의 핵심 검색 의도 분석
사용자가 “PDF 합치기”를 검색할 때 원하는 것은 단순한 설명이 아닙니다. 대부분은 바로 사용할 수 있는 도구를 찾고 있습니다.
검색 의도는 다음과 같이 나눌 수 있습니다.
검색 키워드사용자 의도페이지에서 제공해야 할 내용
| PDF 합치기 | 여러 PDF를 하나로 병합 | 파일 선택, 순서 변경, 병합 버튼 |
| PDF 병합 | 업무용 PDF 문서 결합 | 품질 유지, 빠른 다운로드 |
| PDF 파일 합치기 | 여러 파일을 한 파일로 만들기 | 사용법 안내, 오류 안내 |
| 무료 PDF 합치기 | 비용 없이 사용 | 무료, 로그인 없음 강조 |
| 서버 업로드 없는 PDF 합치기 | 개인정보 보호 | 브라우저 처리 방식 설명 |
개발자 관점에서 보면 이 기능은 단순 파일 업로드 UI가 아니라, 다음 기능들이 결합된 작은 웹 애플리케이션입니다.
- PDF 파일 다중 선택
- 드래그 앤 드롭 업로드
- 파일 목록 관리
- PDF 순서 변경
- PDF 유효성 검사
- 브라우저 메모리에서 PDF 병합
- 병합 파일 다운로드
- 오류 처리
- SEO 메타 태그 설정
- 모바일 대응 UI
Nuxt로 PDF 합치기 도구를 만들 때의 전체 구조
Nuxt에서 PDF 합치기 기능을 만들 때는 서버 API를 사용하지 않고 클라이언트에서만 처리하는 구조가 적합합니다.
PDF 파일은 개인정보가 포함될 가능성이 높기 때문에, 서버로 업로드하지 않는 방식은 사용자 신뢰를 높이는 중요한 요소입니다. 참고 페이지에서도 모든 PDF 처리가 브라우저 안에서 이루어지고 파일이 외부 서버로 전송되지 않는다는 점을 강조하고 있습니다.
구현 구조는 다음과 같습니다.
pages/
tools/
pdf-merger.vue
components/
PdfDropZone.vue
PdfFileList.vue
PdfMergeButton.vue
utils/
pdfMerger.ts
실무에서는 처음부터 컴포넌트를 과하게 나누기보다, 기능이 안정화된 뒤 분리하는 것이 좋습니다. 하지만 티스토리 글이나 포트폴리오용 프로젝트라면 구조를 명확하게 보여주기 위해 컴포넌트 단위로 나누는 편이 좋습니다.
사용할 라이브러리: pdf-lib
브라우저에서 PDF를 병합하려면 JavaScript PDF 처리 라이브러리가 필요합니다. 대표적으로 pdf-lib를 사용할 수 있습니다.
pdf-lib는 PDF 생성, 수정, 페이지 추가, 페이지 제거, PDF 분할과 병합 기능을 제공합니다. 공식 문서에서도 PDF 문서를 분할하거나 여러 PDF를 하나로 합치는 기능을 지원한다고 설명합니다.
설치 명령어는 다음과 같습니다.
npm install pdf-lib
또는 pnpm을 사용한다면 다음과 같이 설치합니다.
pnpm add pdf-lib
Nuxt 페이지 기본 코드
먼저 pages/tools/pdf-merger.vue 파일을 만듭니다.
<script setup lang="ts">
import { PDFDocument } from 'pdf-lib'
type PdfFileItem = {
id: string
file: File
name: string
size: number
}
const pdfFiles = ref<PdfFileItem[]>([])
const isMerging = ref(false)
const errorMessage = ref('')
const successMessage = ref('')
useSeoMeta({
title: 'PDF 합치기 - 여러 PDF 파일 무료 병합',
description: '여러 PDF 파일을 원하는 순서로 정렬한 뒤 브라우저에서 바로 하나의 PDF로 합칠 수 있는 무료 PDF 병합 도구입니다.',
ogTitle: 'PDF 합치기 - 여러 PDF 파일 무료 병합',
ogDescription: '서버 업로드 없이 브라우저에서 PDF 파일을 안전하게 병합하세요.',
ogType: 'website'
})
const formatFileSize = (size: number) => {
if (size < 1024) return `${size} B`
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
return `${(size / 1024 / 1024).toFixed(1)} MB`
}
const handleFileChange = (event: Event) => {
const input = event.target as HTMLInputElement
const files = input.files
if (!files) return
addPdfFiles(Array.from(files))
input.value = ''
}
const addPdfFiles = (files: File[]) => {
errorMessage.value = ''
successMessage.value = ''
const validFiles = files.filter((file) => {
return file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')
})
if (validFiles.length === 0) {
errorMessage.value = 'PDF 파일만 선택할 수 있습니다.'
return
}
const maxSize = 100 * 1024 * 1024
for (const file of validFiles) {
if (file.size > maxSize) {
errorMessage.value = '파일당 최대 100MB까지만 처리할 수 있습니다.'
continue
}
pdfFiles.value.push({
id: crypto.randomUUID(),
file,
name: file.name,
size: file.size
})
}
}
const removeFile = (id: string) => {
pdfFiles.value = pdfFiles.value.filter((item) => item.id !== id)
}
const moveUp = (index: number) => {
if (index <= 0) return
const files = [...pdfFiles.value]
const temp = files[index - 1]
files[index - 1] = files[index]
files[index] = temp
pdfFiles.value = files
}
const moveDown = (index: number) => {
if (index >= pdfFiles.value.length - 1) return
const files = [...pdfFiles.value]
const temp = files[index + 1]
files[index + 1] = files[index]
files[index] = temp
pdfFiles.value = files
}
const mergePdfFiles = async () => {
if (pdfFiles.value.length < 2) {
errorMessage.value = 'PDF 파일을 2개 이상 선택하세요.'
return
}
isMerging.value = true
errorMessage.value = ''
successMessage.value = ''
try {
const mergedPdf = await PDFDocument.create()
for (const item of pdfFiles.value) {
const arrayBuffer = await item.file.arrayBuffer()
const pdf = await PDFDocument.load(arrayBuffer)
const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices())
copiedPages.forEach((page) => {
mergedPdf.addPage(page)
})
}
const mergedPdfBytes = await mergedPdf.save()
const blob = new Blob([mergedPdfBytes], {
type: 'application/pdf'
})
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = 'merged.pdf'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
successMessage.value = 'PDF 파일이 성공적으로 병합되었습니다.'
} catch (error) {
console.error(error)
errorMessage.value = 'PDF 병합 중 오류가 발생했습니다. 암호가 설정된 PDF이거나 손상된 파일일 수 있습니다.'
} finally {
isMerging.value = false
}
}
</script>
<template>
<main class="pdf-merger-page">
<section class="hero">
<p class="category">PDF</p>
<h1>PDF 합치기</h1>
<p class="description">
여러 PDF 파일을 원하는 순서로 정렬한 뒤 하나의 PDF 파일로 병합하세요.
모든 처리는 브라우저에서 실행되며 파일은 서버로 업로드되지 않습니다.
</p>
</section>
<section class="upload-box">
<label class="file-drop">
<input
type="file"
accept="application/pdf"
multiple
@change="handleFileChange"
/>
<strong>PDF 파일을 선택하세요</strong>
<span>여러 개의 PDF 파일을 한 번에 선택할 수 있습니다.</span>
</label>
<p class="notice">
암호가 설정되지 않은 PDF · 파일당 최대 100MB
</p>
</section>
<section v-if="pdfFiles.length > 0" class="file-list-section">
<h2>병합할 PDF 파일 목록</h2>
<p>
위에서 아래 순서대로 PDF 파일이 합쳐집니다. 순서를 변경한 뒤 PDF 합치기 버튼을 누르세요.
</p>
<ul class="file-list">
<li
v-for="(item, index) in pdfFiles"
:key="item.id"
class="file-item"
>
<div class="file-info">
<strong>{{ index + 1 }}. {{ item.name }}</strong>
<span>{{ formatFileSize(item.size) }}</span>
</div>
<div class="file-actions">
<button type="button" @click="moveUp(index)" :disabled="index === 0">
위로
</button>
<button
type="button"
@click="moveDown(index)"
:disabled="index === pdfFiles.length - 1"
>
아래로
</button>
<button type="button" @click="removeFile(item.id)">
삭제
</button>
</div>
</li>
</ul>
<button
type="button"
class="merge-button"
:disabled="isMerging"
@click="mergePdfFiles"
>
{{ isMerging ? 'PDF 병합 중...' : 'PDF 합치기' }}
</button>
</section>
<p v-if="errorMessage" class="message error">
{{ errorMessage }}
</p>
<p v-if="successMessage" class="message success">
{{ successMessage }}
</p>
<section class="guide">
<h2>PDF 파일 합치는 방법</h2>
<ol>
<li>PDF 파일 선택 버튼을 눌러 두 개 이상의 PDF를 선택합니다.</li>
<li>파일 목록에서 위로, 아래로 버튼을 눌러 병합 순서를 정합니다.</li>
<li>PDF 합치기 버튼을 누르면 브라우저에서 PDF 병합이 실행됩니다.</li>
<li>병합된 PDF 파일이 자동으로 다운로드됩니다.</li>
</ol>
</section>
</main>
</template>
<style scoped>
.pdf-merger-page {
max-width: 860px;
margin: 0 auto;
padding: 48px 20px;
}
.hero {
margin-bottom: 32px;
}
.category {
color: #2563eb;
font-weight: 700;
margin-bottom: 8px;
}
.hero h1 {
font-size: 36px;
margin: 0 0 12px;
}
.description {
color: #4b5563;
line-height: 1.7;
}
.upload-box {
border: 2px dashed #cbd5e1;
border-radius: 16px;
padding: 32px;
text-align: center;
background: #f8fafc;
}
.file-drop {
display: flex;
flex-direction: column;
gap: 8px;
cursor: pointer;
}
.file-drop input {
display: none;
}
.file-drop strong {
font-size: 20px;
}
.file-drop span,
.notice {
color: #64748b;
}
.file-list-section {
margin-top: 40px;
}
.file-list {
list-style: none;
padding: 0;
margin: 20px 0;
}
.file-item {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: center;
padding: 16px;
border: 1px solid #e5e7eb;
border-radius: 12px;
margin-bottom: 12px;
background: #fff;
}
.file-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.file-info span {
color: #6b7280;
font-size: 14px;
}
.file-actions {
display: flex;
gap: 8px;
}
button {
border: 1px solid #d1d5db;
background: #fff;
border-radius: 8px;
padding: 8px 12px;
cursor: pointer;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.merge-button {
width: 100%;
border: none;
background: #2563eb;
color: #fff;
font-weight: 700;
padding: 14px 20px;
border-radius: 12px;
}
.message {
margin-top: 20px;
padding: 14px 16px;
border-radius: 10px;
}
.error {
background: #fef2f2;
color: #b91c1c;
}
.success {
background: #ecfdf5;
color: #047857;
}
.guide {
margin-top: 56px;
line-height: 1.8;
}
@media (max-width: 640px) {
.file-item {
flex-direction: column;
align-items: flex-start;
}
.file-actions {
width: 100%;
flex-wrap: wrap;
}
}
</style>
코드에서 중요한 부분 설명
1. PDF 파일은 서버로 업로드하지 않는다
이 예제에서는 File.arrayBuffer()를 사용해 사용자의 브라우저 메모리에서 PDF 파일을 읽습니다.
const arrayBuffer = await item.file.arrayBuffer()
const pdf = await PDFDocument.load(arrayBuffer)
이 방식은 서버에 파일을 보내지 않기 때문에 개인정보가 포함된 문서 처리에 유리합니다. 다만 모든 처리가 사용자 기기에서 실행되므로, 파일 크기가 크거나 PDF 개수가 많으면 기기 성능에 따라 처리 시간이 길어질 수 있습니다.
2. PDF 페이지를 복사해서 새 PDF에 추가한다
PDF 병합의 핵심은 기존 PDF의 페이지를 새 PDF 문서에 복사하는 것입니다.
const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices())
copiedPages.forEach((page) => {
mergedPdf.addPage(page)
})
pdf.getPageIndices()는 PDF의 모든 페이지 인덱스를 가져옵니다. 이후 copyPages()로 페이지를 복사하고, addPage()로 새 PDF에 추가합니다.
3. 병합 순서가 결과 PDF 순서가 된다
PDF 합치기 도구에서 가장 중요한 UX는 순서 변경입니다. 사용자는 보통 다음과 같은 문서 순서를 기대합니다.
1. 표지.pdf
2. 본문.pdf
3. 첨부자료.pdf
4. 서명페이지.pdf
따라서 파일 배열의 순서가 곧 병합 순서가 되도록 만들어야 합니다.
for (const item of pdfFiles.value) {
const arrayBuffer = await item.file.arrayBuffer()
const pdf = await PDFDocument.load(arrayBuffer)
const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices())
copiedPages.forEach((page) => {
mergedPdf.addPage(page)
})
}
Nuxt SEO 설정
Nuxt에서는 useSeoMeta()를 사용해 페이지별 SEO 메타 태그를 설정할 수 있습니다. Nuxt 공식 문서에서도 useSeoMeta는 타입 지원이 있는 SEO 메타 태그 설정 방법이며, 일반적인 메타 태그 실수를 줄이는 데 도움이 된다고 설명합니다.
PDF 합치기 도구 페이지라면 다음과 같이 설정할 수 있습니다.
useSeoMeta({
title: 'PDF 합치기 - 여러 PDF 파일 무료 병합',
description: '여러 PDF 파일을 원하는 순서로 정렬한 뒤 서버 업로드 없이 브라우저에서 하나의 PDF로 합칠 수 있습니다.',
ogTitle: 'PDF 합치기 - 여러 PDF 파일 무료 병합',
ogDescription: 'PDF 파일을 안전하게 병합하세요. 모든 처리는 브라우저에서 실행됩니다.',
ogType: 'website'
})
SEO 관점에서는 제목과 설명에 다음 키워드를 자연스럽게 포함하는 것이 좋습니다.
- PDF 합치기
- PDF 병합
- PDF 파일 합치기
- 무료 PDF 합치기
- 브라우저 PDF 병합
- 서버 업로드 없는 PDF 합치기
드래그 앤 드롭 업로드 추가하기
실제 도구처럼 사용성을 높이려면 파일 선택 버튼뿐 아니라 드래그 앤 드롭 업로드도 지원하는 것이 좋습니다.
<script setup lang="ts">
const isDragging = ref(false)
const handleDrop = (event: DragEvent) => {
event.preventDefault()
isDragging.value = false
const files = event.dataTransfer?.files
if (!files) return
addPdfFiles(Array.from(files))
}
const handleDragOver = (event: DragEvent) => {
event.preventDefault()
isDragging.value = true
}
const handleDragLeave = () => {
isDragging.value = false
}
</script>
<template>
<div
class="drop-zone"
:class="{ active: isDragging }"
@drop="handleDrop"
@dragover="handleDragOver"
@dragleave="handleDragLeave"
>
PDF 파일을 여기에 놓으세요
</div>
</template>
<style scoped>
.drop-zone {
border: 2px dashed #cbd5e1;
padding: 40px;
border-radius: 16px;
text-align: center;
background: #f8fafc;
}
.drop-zone.active {
border-color: #2563eb;
background: #eff6ff;
}
</style>
드래그 앤 드롭은 단순한 편의 기능처럼 보이지만, 실제 사용자는 여러 파일을 한 번에 끌어다 놓는 경우가 많기 때문에 PDF 도구에서는 매우 중요한 UX입니다.
암호가 걸린 PDF 처리
브라우저 기반 PDF 병합 도구에서 자주 발생하는 오류 중 하나는 암호가 설정된 PDF입니다.
try {
const pdf = await PDFDocument.load(arrayBuffer)
} catch (error) {
errorMessage.value = '암호가 설정되었거나 손상된 PDF 파일은 병합할 수 없습니다.'
}
사용자에게는 단순히 “오류 발생”이라고 보여주기보다 다음처럼 원인을 설명하는 것이 좋습니다.
PDF 파일을 열 수 없습니다. 암호가 설정된 PDF이거나 손상된 파일일 수 있습니다.
암호를 해제한 뒤 다시 시도해 주세요.
이 문구는 SEO에도 도움이 됩니다. “PDF 암호”, “PDF 병합 오류”, “PDF 파일이 안 열림” 같은 롱테일 키워드를 자연스럽게 포함할 수 있기 때문입니다.
파일 크기 제한을 두어야 하는 이유
PDF 합치기를 브라우저에서 처리하면 서버 비용은 줄어들지만, 사용자의 브라우저 메모리를 사용합니다.
따라서 파일 크기 제한은 반드시 필요합니다.
const maxSize = 100 * 1024 * 1024
if (file.size > maxSize) {
errorMessage.value = '파일당 최대 100MB까지만 처리할 수 있습니다.'
return
}
실무에서는 다음 기준을 고려할 수 있습니다.
제한 항목권장 기준
| 파일당 용량 | 50MB ~ 100MB |
| 전체 파일 개수 | 10개 ~ 30개 |
| 처리 위치 | 브라우저 |
| 서버 업로드 | 없음 |
| 오류 안내 | 암호 PDF, 손상 PDF, 대용량 PDF 안내 |
PDF 병합 도구의 SEO 콘텐츠 구성
단순히 도구만 만들면 검색 유입이 부족할 수 있습니다. PDF 합치기 페이지에는 기능 아래에 설명 콘텐츠를 추가하는 것이 좋습니다.
추천 콘텐츠 구조는 다음과 같습니다.
H1: PDF 합치기
H2: PDF 파일 합치는 방법
H2: PDF 병합이 필요한 경우
H2: 서버 업로드 없이 PDF를 합칠 수 있나요?
H2: 암호가 걸린 PDF도 합칠 수 있나요?
H2: PDF 합치기 오류가 발생하는 이유
H2: 자주 묻는 질문
검색 사용자는 도구를 바로 쓰고 싶어 하지만, 구글은 페이지의 맥락도 함께 평가합니다. 따라서 도구 UI 아래에 실질적인 설명을 충분히 넣는 것이 좋습니다.
FAQ 예시
Q. PDF 합치기 기능은 서버에 파일을 업로드하나요?
아니요. 브라우저 기반으로 구현하면 PDF 파일을 서버로 업로드하지 않고 사용자의 기기에서 직접 병합할 수 있습니다.
Q. PDF 병합 후 품질이 떨어지나요?
일반적인 PDF 병합은 페이지를 이미지로 다시 변환하는 방식이 아니라 기존 PDF 페이지를 복사해 새 문서에 추가하는 방식입니다. 따라서 불필요한 재압축을 하지 않으면 원본 품질을 유지할 수 있습니다.
Q. 암호가 걸린 PDF도 합칠 수 있나요?
암호로 보호된 PDF는 브라우저에서 열 수 없을 수 있습니다. 이 경우 암호를 해제한 뒤 다시 병합해야 합니다.
Q. 모바일에서도 PDF 합치기가 가능한가요?
가능합니다. 다만 대용량 PDF 파일을 여러 개 병합할 경우 모바일 기기의 메모리 성능에 따라 처리 시간이 길어질 수 있습니다.
실무에서 추가하면 좋은 기능
PDF 합치기 도구를 실제 서비스로 운영한다면 다음 기능을 추가할 수 있습니다.
기능설명
| 드래그 정렬 | 파일 순서를 마우스로 직접 변경 |
| PDF 미리보기 | 첫 페이지 썸네일 표시 |
| 파일명 자동 생성 | 날짜 기반 파일명 생성 |
| 다크 모드 | 장시간 작업 사용자 대응 |
| 공유 버튼 | 카카오톡, URL 복사 |
| 관련 도구 연결 | PDF 분할, PDF 압축, PDF JPG 변환 |
| 에러 로그 수집 | 클라이언트 오류 개선용 |
특히 PDF 관련 도구는 하나만 만들기보다 여러 도구를 묶어서 운영하는 것이 좋습니다. 예를 들어 PDF 합치기 페이지에서 PDF 분할, PDF 압축, PDF JPG 변환 같은 내부 도구로 연결하면 체류 시간과 내부 링크 구조를 함께 개선할 수 있습니다.
마무리
Nuxt로 PDF 합치기 도구를 만들 때 핵심은 파일을 서버로 업로드하지 않고 브라우저에서 처리하는 구조입니다. pdf-lib를 사용하면 여러 PDF 파일을 읽고, 각 PDF의 페이지를 복사해 하나의 문서로 병합할 수 있습니다.
검색 유입을 고려한다면 단순히 기능만 구현하는 것보다 “PDF 합치기”, “PDF 병합”, “PDF 파일 합치기”, “서버 업로드 없는 PDF 합치기” 같은 키워드를 제목, 첫 문단, 소제목, FAQ에 자연스럽게 배치하는 것이 좋습니다.
PDF 도구 페이지는 사용자의 목적이 명확하기 때문에, 빠른 기능 실행과 신뢰를 주는 설명이 함께 있어야 합니다. Nuxt의 SEO 설정, 브라우저 기반 PDF 처리, 명확한 오류 안내, 내부링크 구조까지 함께 설계하면 검색 유입과 사용자 만족도를 모두 높일 수 있습니다.
추천 태그
Nuxt, PDF 합치기, PDF 병합, pdf-lib, JavaScript PDF, 브라우저 PDF 처리, 서버 업로드 없는 PDF, Nuxt SEO, 웹도구 만들기, PDF 파일 합치기, 프론트엔드 개발
'실무개발 > Front' 카테고리의 다른 글
| Nuxt3로 제비뽑기 웹 서비스를 만든 이유와 랜덤 추첨 기능 구현 방법 (0) | 2026.06.30 |
|---|---|
| Nuxt와 PHP로 만든 정적 서비스 개발 사례 플랜앱트하우(플하) -plan.apthow.com 구조 분석 (0) | 2026.06.16 |
| PWA 앱 설치 방법 총정리: 안드로이드, 아이폰, PC에서 설치하는 방법 (0) | 2026.06.07 |
| Nuxt와 Codex로 만든 알뜰폰 요금제 비교 서비스 개발기 (0) | 2026.06.02 |
| Nuxt4 설치부터 실제 배포까지 완벽 정리 (0) | 2026.05.21 |
