로컬 처리형 온라인 툴박스 프론트엔드 아키텍처 분석

최근 개인정보 보호가 중요시되는 환경에서 브라우저 기반의 로컬 처리 방식이 주목받고 있습니다. 본문에서는 순수 프론트엔드 로직으로 동작하는 툴박스 서비스의 구현 패턴과 성능 최적화 전략을 코드 레벨에서 살펴보겠습니다.

아키텍처 전략: 경량 백엔드와 모듈화된 프론트엔드

해당 서비스는 모든 데이터 처리를 클라이언트 측에서 수행하며, 서버는 정적 리소스 전달과 기본 통계 수집에만 활용됩니다. 이 구조는 "제로 데이터 전송"을 실현하는 핵심입니다. 기술 스택으로는 Vite 번들러와 Tailwind CSS를 조합했으며, 대규모 프레임워크를 배제함으로써 초기 로딩 시간을 300ms 이내로 제어했습니다.

모듈 로딩 전략으로는 동적 임포트와 Rollup의 수동 청크 분할을 결합해 각 도구 기능을 독립적인 번들로 격리했습니다.

// rollup.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('/modules/image-processor')) return 'img-chunk'
          if (id.includes('/modules/text-engine')) return 'text-chunk'
          if (id.includes('/modules/document-handler')) return 'doc-chunk'
          if (id.includes('/shared/helpers')) return 'common-chunk'
        }
      }
    },
    cssCodeSplit: true
  }
})

도구 모듈의 독립적 구성

프로젝트 구조는 기능별로 완전히 분리되어 있습니다:

app/
├── shared/           # 공용 유틸리티 (캐시, 포매터, 검증기)
├── ui/               # 재사용 가능한 컴포넌트 (버튼, 카드, 다이얼로그)
├── processors/       # 도시 프로세서 (이미지, 텍스트, 네트워크)
├── workers/          # 백그라운드 워커 스크립트
└── app.js            # 진입점

코드 품질은 ESLint와 Prettier로 관리되며, 빌드 결과물에서 일관된 명명 규칙과 최소화된 데드 코드를 확인할 수 있습니다.

핵심 처리 로직 구현

이미지 처리: Canvas 기반 리사이징

FileReader와 Canvas API를 조합해 브라우저에서 직접 이미지 압축을 수행합니다. OffscreenCanvas를 활용한 최적화도 포함되어 있습니다.

// processors/image-resizer.js
export async function resizeImage(file, compression = 0.75) {
  const bitmap = await createImageBitmap(file)
  const maxWidth = 1920, maxHeight = 1080
  const ratio = Math.min(maxWidth / bitmap.width, maxHeight / bitmap.height, 1)
  
  const canvas = new OffscreenCanvas(
    bitmap.width * ratio, 
    bitmap.height * ratio
  )
  const context = canvas.getContext('2d', { alpha: false })
  context.imageSmoothingQuality = 'medium'
  context.drawImage(bitmap, 0, 0, canvas.width, canvas.height)
  
  return canvas.convertToBlob({ 
    type: 'image/jpeg', 
    quality: compression 
  })
}

텍스트 처리: 파싱 및 시각화

JSON 파싱은 내장 API로 처리하며, 문법 강조는 정규식 기반의 가벼운 구현체를 사용합니다.

// processors/text-formatter.js
export function parseAndFormat(jsonString) {
  try {
    const parsed = JSON.parse(jsonString)
    return JSON.stringify(parsed, null, '\t')
  } catch (e) {
    return `// 파싱 오류: ${e.message}\n${jsonString}`
  }
}

export function simpleHighlight(source) {
  const patterns = {
    keyword: /\b(const|let|var|function|return|if|else|for|while|class)\b/g,
    string: /(["'`])([^"'`]*?)\1/g,
    comment: /(\/\/.*$|\/\*[\s\S]*?\*\/)/gm
  }
  
  return source
    .replace(patterns.comment, '<span class="text-slate-400">$1</span>')
    .replace(patterns.string, '<span class="text-emerald-600">$1$2$1</span>')
    .replace(patterns.keyword, '<span class="text-sky-600">$1</span>')
}

로컬 스토리지 관리

만료 시간을 지원하는 스토리지 래퍼로 사용자 설정과 검색 기록을 관리합니다.

// shared/storage-manager.js
export const CacheStore = {
  save(key, payload, ttl = 86400000) {
    const record = {
      data: payload,
      expiry: Date.now() + ttl
    }
    sessionStorage.setItem(key, JSON.stringify(record))
  },
  load(key) {
    const raw = sessionStorage.getItem(key)
    if (!raw) return null
    const { data, expiry } = JSON.parse(raw)
    if (Date.now() > expiry) {
      sessionStorage.removeItem(key)
      return null
    }
    return data
  }
}

네트워크 도구: 클라이언트 측 Ping 측정

서버 없이 정적 리소스 요청 시간을 측정해 네트워크 지연 시간을 추정합니다.

// processors/network-tester.js
export async function measureLatency() {
  const start = performance.now()
  try {
    await fetch('/robots.txt', { 
      method: 'HEAD', 
      cache: 'no-store' 
    })
    return Math.round(performance.now() - start)
  } catch {
    return -1
  }
}

대용량 파일 처리: Web Worker 아키텍처

10MB 이상 파일 처리 시 메인 스레드 차단을 방지하기 위해 Web Worker를 활용합니다.

// workers/file-processor.js
self.addEventListener('message', async (event) => {
  const { file, operation } = event.data
  const buffer = await file.arrayBuffer()
  
  if (operation === 'compress') {
    const bitmap = await createImageBitmap(new Blob([buffer]))
    const canvas = new OffscreenCanvas(bitmap.width * 0.6, bitmap.height * 0.6)
    const ctx = canvas.getContext('2d')
    ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height)
    
    const result = await canvas.convertToBlob({ type: 'image/webp', quality: 0.7 })
    self.postMessage({ success: true, payload: result })
  }
})

// shared/worker-client.js
export function offloadTask(workerFile, payload) {
  return new Promise((resolve, reject) => {
    const worker = new Worker(workerFile, { type: 'module' })
    worker.onmessage = (e) => {
      resolve(e.data)
      worker.terminate()
    }
    worker.onerror = reject
    worker.postMessage(payload)
  })
}

백엔드 및 배포: 최소 오퍼레이션 설계

통계 수집을 위한 Go 서비스는 파일 기반 저장소와 고루틴 안전한 맵으로 구성됩니다.

// server/stats.go
package main

import (
    "encoding/json"
    "net/http"
    "os"
    "sync"
)

type Counter struct {
    Hits map[string]int `json:"hits"`
    sync.RWMutex
}

func (c *Counter) record(tool string) {
    c.Lock()
    defer c.Unlock()
    c.Hits[tool]++
    c.persist()
}

func (c *Counter) persist() {
    data, _ := json.Marshal(c)
    os.WriteFile("metrics.json", data, 0644)
}

func main() {
    metrics := &Counter{Hits: make(map[string]int)}
    
    http.HandleFunc("/telemetry", func(w http.ResponseWriter, r *http.Request) {
        if r.Method == http.MethodPost {
            var body struct{ Tool string `json:"tool"` }
            json.NewDecoder(r.Body).Decode(&body)
            metrics.record(body.Tool)
            w.WriteHeader(http.StatusAccepted)
        }
    })
    
    http.Handle("/", http.FileServer(http.Dir("./public")))
    http.ListenAndServe(":3000", nil)
}

정적 에셋은 CDN에 배치하고 Go 바이너리는 클라우드 인스턴스에서 실행합니다. 전체 운영 비용을 월 5만원 이하로 제어할 수 있으며, Cloudflare Pages를 통한 정적 호스팅으로 비용을 더욱 절감할 수 있습니다.

태그: Vite Tailwind CSS Web Worker Canvas API go

8월 2일 23:40에 게시됨