← 포스트 목록으로

이미지 변형 스튜디오 브라우저 기반 실시간 이미지 변형 도구

📖 8분 소요
ReactOpenCVImage ProcessingProject

프로젝트 소개

이미지 변형 스튜디오는 브라우저에서 직접 이미지를 자유롭게 변형하고 처리할 수 있는 웹 애플리케이션입니다. 별도의 설치 없이 웹에서 Perspective Transform, 프레임 추가, 이미지 분할 등 다양한 기능을 제공합니다.

체험해보기

핵심 기능

  • 실시간 Perspective Transform: 이미지를 자유롭게 왜곡하고 원근감을 적용할 수 있습니다.
  • 프레임 효과: 사각형, 둥근 모서리, 원형, 폴라로이드 등 다양한 스타일의 프레임을 추가할 수 있습니다.
  • 이미지 분할: 이미지를 가로/세로로 균등 분할하여 ZIP 파일로 다운로드합니다.
  • 일괄 처리: 여러 이미지를 동일한 설정으로 한 번에 처리할 수 있습니다.
  • 고품질 출력: WebP 포맷을 지원하여 용량을 최적화했습니다.

기술 스택

Core

  • React 19 - UI 프레임워크
  • TypeScript 5.8 - 타입 안정성
  • Vite 7 - 빌드 도구

State Management

  • Jotai 2.13 - 가벼운 원자 기반 상태 관리

Graphics & Image Processing

  • OpenCV.js - 브라우저에서 Perspective Transform 연산
  • Konva 9 + react-konva 19 - Canvas 기반 인터랙티브 UI
  • JSZip - 다중 파일 ZIP 다운로드

Styling

  • Tailwind CSS 4 - 유틸리티 기반 스타일링
  • Emotion - CSS-in-JS
  • Material-UI 7 - UI 컴포넌트 라이브러리

File Handling

  • react-dropzone - 드래그 앤 드롭 파일 업로드

개발 과정

Phase 1: 기본 이미지 변형 기능 (8주 전)

프로젝트의 시작은 단순했습니다. 브라우저에서 이미지를 자유롭게 변형할 수 있는 도구를 만들고 싶었습니다.

초기 구현 사항

feat: 이미지 변형 웹
feat: 압축 다운로드
feat: 이미지 압축 저장

OpenCV.js를 활용해 기본적인 Perspective Transform을 구현하고, 파일 업로드 및 다운로드 기능을 추가했습니다. Canvas 기반으로 실시간 미리보기를 제공하면서, 브라우저에서 OpenCV 연산을 처리하고 메모리를 효율적으로 관리하는 것이 첫 번째 도전 과제였습니다.

Phase 2: 아키텍처 개선 (4주 전)

프로토타입이 어느 정도 완성되자 코드가 점점 복잡해졌습니다. 이 시점에서 전면적인 리팩토링을 결정했습니다.

주요 리팩토링 내용

feat: introduce cn helper and absolute imports
refactor: migrate all useState hooks to jotai atoms
style: redesign with monochrome theme and improved layout

가장 큰 변화는 상태 관리였습니다. useState로 흩어져 있던 상태들을 Jotai로 마이그레이션하면서 전역 상태 관리의 일관성을 확보했고, 불필요한 리렌더링도 크게 줄일 수 있었습니다.

코드 구조도 개선했습니다. @/ 별칭을 도입해 절대 경로 임포트를 사용하면서 코드 가독성이 향상되었고, 리팩토링도 훨씬 수월해졌습니다.

디자인 측면에서는 Monochrome 테마를 적용해 미니멀한 UI/UX를 구현했습니다. Tailwind를 기반으로 일관된 디자인 시스템을 정립하면서 유지보수성도 크게 개선되었습니다.

Phase 3: 프레임 기능 추가 (23시간 전)

사용자들이 이미지에 프레임을 추가하고 싶어할 것 같다는 생각이 들었습니다. 단순히 테두리를 추가하는 것을 넘어, 다양한 스타일의 프레임을 제공하고 싶었습니다.

구현한 기능

feat: add frame styling controls
style: compact desktop processor layout
fix: sync frame pipeline and sidebar controls

5가지 프레임 스타일(없음, 사각형, 둥근 사각형, 원형, 폴라로이드)을 구현했고, 패딩, 테두리, 그림자까지 커스터마이징할 수 있게 만들었습니다.

여기서 흥미로운 문제가 하나 있었습니다. PNG처럼 투명 영역이 있는 이미지에 프레임을 적용할 때, 빈 공간까지 포함해서 프레임이 그려지는 문제였죠. 이를 해결하기 위해 투명 픽셀을 제외한 실제 컨텐츠 영역만 감지하는 알고리즘을 구현했습니다.

// 실제 이미지 컨텐츠 영역 감지
const getImageBounds = (img: HTMLImageElement) => {
  // 투명 픽셀 제외하고 실제 컨텐츠 영역 계산
  // 프레임이 정확히 이미지에만 적용되도록
};

Phase 4: 이미지 분할 기능 (21시간 전)

인스타그램 캐러셀을 만들 때마다 포토샵으로 이미지를 일일이 분할하는 게 번거로웠습니다. 이 기능을 웹에서 간단하게 처리할 수 있으면 어떨까 하는 생각에서 이미지 분할 기능을 추가했습니다.

새로운 모드 추가

feat: add split mode types and tab mode
feat: add split and zip download utilities
feat: add split mode components
feat: integrate split mode into processor

Transform 모드와 Split 모드를 탭으로 전환할 수 있게 만들었고, 가로/세로 방향으로 1-20개까지 균등 분할할 수 있도록 구현했습니다. 분할된 이미지들은 ZIP 파일로 한 번에 다운로드됩니다.

이 기능은 인스타그램 캐러셀용 이미지 분할뿐만 아니라 타일형 이미지 생성, 프린트용 이미지 분할 등 다양한 용도로 활용할 수 있습니다.

Phase 5: 성능 최적화 (오늘)

기능은 완성되었지만, 큰 이미지를 처리할 때 렌더링이 버벅이는 문제가 있었습니다. 사용자 경험을 개선하기 위해 성능 최적화에 집중했습니다.

UI 인터랙션 개선

먼저 버튼 피드백이 부족하다는 느낌이 들어 모든 인터랙티브 요소에 호버와 액티브 상태를 추가했습니다.

className={cn(
  'transition-all duration-200',
  'hover:bg-slate-50 hover:shadow-sm',
  'active:scale-95'
)}

렌더링 성능 최적화

성능 문제의 근본 원인은 OpenCV 연산이었습니다. 큰 이미지를 실시간으로 변형하려면 상당한 CPU 연산이 필요한데, 이를 세 가지 방법으로 해결했습니다.

첫째, Debounce를 적용해 드래그 중 불필요한 연산을 방지했습니다.

useEffect(() => {
  const timeout = setTimeout(() => {
    applyPerspectiveTransform();
  }, 150);

  return () => clearTimeout(timeout);
}, [dependencies]);

둘째, 미리보기용 이미지를 다운샘플링했습니다. 원본 4000x3000 이미지를 800px로 축소하면 픽셀 수가 1/25로 감소하면서 10-20배 빠른 처리가 가능해집니다.

const maxPreviewSize = 800;
const scale = Math.min(1, maxPreviewSize / Math.max(width, height));

셋째, 압축 포맷을 개선했습니다. JPEG 0.85에서 WebP 0.5로 변경하면서 미리보기는 저품질로 빠르게 처리하고, 다운로드는 원본 품질을 유지하도록 분리했습니다.

canvas.toDataURL('image/webp', 0.5);

이 최적화로 실시간 미리보기 속도가 10-20배 향상되었고, 메모리 사용량도 대폭 감소했습니다. 무엇보다 다운로드 품질은 원본 그대로 유지됩니다.

Phase 6: 코드 품질 개선

성능 최적화가 끝나고 나니 코드를 다시 정리하고 싶어졌습니다. 개발 과정에서 쌓인 불필요한 코드와 UI 요소들을 제거했습니다.

refactor: streamline settings sidebar
refactor: simplify file sidebar layout
refactor: clean up workspace component
style: remove redundant page header

설정 사이드바를 간소화하고, 파일 사이드바 레이아웃을 단순화했습니다. 워크스페이스 컴포넌트도 정리하고, 중복되던 페이지 헤더도 제거했습니다. 기능은 그대로지만 코드가 훨씬 깔끔해졌습니다.

주요 기술적 도전과 해결

1. OpenCV 브라우저 연산 최적화

OpenCV.js는 CPU 기반 연산이라 큰 이미지를 처리할 때 렌더링 렉이 발생했습니다. 특히 실시간 미리보기를 제공하려다 보니 드래그할 때마다 화면이 버벅이는 문제가 심각했습니다.

이를 해결하기 위해 미리보기와 다운로드를 완전히 분리했습니다. 미리보기는 800px로 다운샘플링하고 WebP 0.5 품질로 압축해서 빠르게 처리하고, 다운로드는 원본 크기 그대로 고품질로 처리합니다. 여기에 Debounce를 적용해 불필요한 연산을 방지했습니다.

2. 프레임 렌더링 정확도

투명 영역이 있는 PNG 이미지에 프레임을 적용하면 빈 공간까지 포함해서 프레임이 그려지는 문제가 있었습니다. 사용자가 원하는 건 실제 이미지 컨텐츠에만 프레임이 적용되는 것이었죠.

// 투명 픽셀을 제외한 실제 컨텐츠 영역 계산
const bounds = getImageBounds(img);
// 바운딩 박스 기준으로 프레임 적용

이미지의 모든 픽셀을 순회하며 투명하지 않은 영역만 찾아내는 알고리즘을 구현했습니다. 이제 프레임이 정확히 이미지 컨텐츠에만 적용됩니다.

3. 상태 관리 복잡도

초기에는 useState를 사용했는데, 상태가 늘어나면서 관리가 복잡해졌습니다. 컴포넌트 간 상태 공유도 어렵고, 불필요한 리렌더링도 많이 발생했습니다.

Jotai로 전체 마이그레이션하면서 이 문제를 해결했습니다. Atom 단위로 세밀하게 구독할 수 있어 리렌더링이 최소화되었고, 파생 상태(derived atoms)를 활용해 연산도 최적화할 수 있었습니다.

아키텍처

폴더 구조

src/
├── features/          # 기능 단위 모듈
│   ├── free-transform/
│   └── image-upload/
├── widgets/           # 복합 컴포넌트
│   └── image-processor/
├── pages/            # 라우트 페이지
├── shared/           # 공유 리소스
│   ├── stores/       # Jotai atoms
│   ├── types/        # TypeScript 타입
│   ├── utils/        # 유틸리티 함수
│   └── ui/           # 공통 UI 컴포넌트
└── App.tsx

데이터 플로우

User Upload → ImageFile Atom
           → Image Element Atom
           → Transform Bounds Atom
           → Corner Points Atom

Transform Action → OpenCV Processing
                → Frame Application
                → Download/Preview

현재 기능 목록

Transform 모드

  • 4가지 변형 모드 (Free, Perspective, Distort, Skew)
  • 8가지 프리셋 (좌/우/상/하 원근, 좌/우 기울기, 좌/우 틸트)
  • 픽셀 단위 미세 조정
  • 상하/좌우 이동
  • 줌 인/아웃

Frame 모드

  • 5가지 프레임 스타일
  • 패딩, 테두리 두께 조절
  • 색상 및 투명도 커스터마이징
  • 그림자 효과 (색상, 흐림, 위치)
  • 모서리 반경 조절 (둥근 사각형)

Split 모드

  • 가로/세로 분할
  • 1-20개 균등 분할
  • ZIP 다운로드

기타

  • 일괄 처리
  • WebP 포맷 지원
  • 폴더 구조 다운로드

앞으로 할 것들

단기 계획 (1-2주)

1. 성능 최적화 심화

  • Web Worker로 OpenCV 연산 오프로드
  • OffscreenCanvas 활용
  • IndexedDB 캐싱

2. 추가 변형 효과

  • 회전 (90도, 180도, 자유 각도)
  • 좌우/상하 반전
  • 크롭 기능

3. 프레임 고도화

  • 커스텀 그라데이션 프레임
  • 이미지 프레임 (PNG 오버레이)
  • 텍스트 워터마크

중기 계획 (1-2개월)

4. 필터 & 보정

  • 밝기, 대비, 채도 조절
  • 사전 정의된 필터 (흑백, 세피아, 빈티지 등)
  • 블러, 샤픈 효과

5. 배치 설정 저장

  • 설정값 프리셋 저장/불러오기
  • LocalStorage 활용
  • JSON Export/Import

6. 히스토리 & Undo/Redo

  • 변형 이력 추적
  • Ctrl+Z / Ctrl+Y 단축키
  • 이력 목록 UI

장기 계획 (3개월+)

7. AI 기능 통합

  • 배경 제거 (Remove.bg API)
  • 자동 크롭 (얼굴 인식)
  • 화질 개선 (Super Resolution)

8. 콜라주 & 레이아웃

  • 여러 이미지 합성
  • 템플릿 기반 레이아웃
  • 드래그 앤 드롭으로 배치

9. 협업 기능

  • 클라우드 저장
  • 프로젝트 공유 링크
  • 실시간 협업 편집

10. 모바일 최적화

  • 터치 제스처 지원
  • PWA 전환
  • 네이티브 앱 느낌의 UX

기술적 개선 계획

성능

  • WebGL Shader 도입: GPU 가속 변형
  • WASM 최적화: OpenCV WASM 빌드 커스터마이징
  • Lazy Loading: 기능별 코드 스플리팅

개발 경험

  • Storybook: 컴포넌트 문서화
  • Vitest: 단위 테스트
  • Playwright: E2E 테스트

배포

  • Vercel/Netlify: CI/CD 파이프라인
  • Sentry: 에러 트래킹
  • Analytics: 사용자 행동 분석

배운 점

1. 브라우저 그래픽 성능

Canvas API는 강력하지만 한계가 명확합니다. 큰 이미지를 실시간으로 처리하려면 최적화가 필수입니다. Debounce나 Throttle 같은 기법이 단순해 보여도 사용자 경험에 큰 차이를 만들어냅니다. 무엇보다 미리보기와 최종 출력을 분리하는 전략이 효과적이었습니다.

2. 상태 관리

Jotai를 사용하면서 상태 관리가 얼마나 간결해질 수 있는지 깨달았습니다. Atom을 어떻게 설계하느냐에 따라 성능이 크게 달라지고, 파생 상태를 잘 활용하면 불필요한 연산을 크게 줄일 수 있습니다.

3. 사용자 경험

사소한 것 같지만 호버나 액티브 상태 같은 실시간 피드백이 사용자 경험에 큰 영향을 줍니다. 로딩 인디케이터도 마찬가지입니다. 기술적으로 완벽해도 사용자가 느끼는 반응성이 떨어지면 의미가 없다는 걸 배웠습니다.

마무리

이 프로젝트를 시작하면서 품었던 의문은 "브라우저만으로 충분할까?"였습니다. 결론부터 말하자면, 충분합니다. OpenCV, Konva, React의 조합으로 네이티브 앱 수준의 성능과 UX를 웹에서 구현할 수 있었습니다.

앞으로 AI 기능, 협업 기능, 모바일 최적화 등을 추가하면서 누구나 쉽게 사용할 수 있는 이미지 편집 도구로 발전시켜 나갈 계획입니다. 복잡한 이미지 처리 기술을 사용자가 의식하지 못하는 사이 자연스럽게 사용할 수 있도록 만드는 것, 그것이 이 프로젝트의 목표입니다.


개발 기간: 2024.12 ~ 현재 진행 중 주요 기술: React, TypeScript, OpenCV.js, Konva, Jotai, Tailwind CSS 라이선스: MIT (예정)