사진 찍는 개발자 jihun의 사진 갤러리입니다.
- 자동 EXIF 추출: 카메라 정보, 촬영 설정, GPS 좌표 자동 파싱
- Apple MapKit 통합: 촬영 위치를 지도에 표시
- 스마트 관리: 카테고리/태그 기반 사진 분류 및 썸네일 자동 생성
- 모달 기반 뷰: Next.js Parallel Routes로 부드러운 사진 탐색
- 역할 기반 인증: NextAuth.js 기반 보안 시스템
graph TB
subgraph "클라이언트"
Browser[웹 브라우저]
end
subgraph "리버스 프록시"
Caddy[Caddy<br/>HTTPS/SSL]
end
subgraph "애플리케이션 레이어"
NextJS[Next.js 16<br/>App Router + ISR]
end
subgraph "데이터 레이어"
PostgreSQL[(PostgreSQL 18<br/>메타데이터)]
MinIO[MinIO<br/>S3 호환 스토리지]
end
subgraph "외부 서비스"
MapKit[Apple MapKit JS<br/>지도 서비스]
end
Browser -->|HTTPS| Caddy
Caddy -->|리버스 프록시| NextJS
NextJS -->|Prisma ORM| PostgreSQL
NextJS -->|S3 API| MinIO
NextJS -->|토큰 인증| MapKit
style Caddy fill:#1F88C0,stroke:#fff,stroke-width:2px,color:#fff
style NextJS fill:#0070f3,stroke:#fff,stroke-width:2px,color:#fff
style PostgreSQL fill:#336791,stroke:#fff,stroke-width:2px,color:#fff
style MinIO fill:#C72E49,stroke:#fff,stroke-width:2px,color:#fff
style MapKit fill:#000,stroke:#fff,stroke-width:2px,color:#fff
graph LR
A[Git 푸시] -->|트리거| B[GitHub Actions]
B -->|빌드| C[멀티 스테이지<br/>Docker 빌드]
C -->|푸시| D[GHCR]
D -->|SSH| E[프로덕션]
E -->|풀| F[Docker Compose]
F -->|재시작| G[컨테이너<br/>재시작]
style A fill:#28a745,stroke:#fff,stroke-width:2px,color:#fff
style D fill:#2088ff,stroke:#fff,stroke-width:2px,color:#fff
style G fill:#28a745,stroke:#fff,stroke-width:2px,color:#fff
배포 특징: 멀티 플랫폼 Docker 빌드 (amd64/arm64) · Next.js Standalone 모드 · 자동 헬스 체크
erDiagram
User ||--o{ Image : 관리
Category ||--o{ Image : 포함
Image ||--o{ ImageTag : 가짐
Tag ||--o{ ImageTag : "태그됨"
User {
string id PK
string email UK
enum role
}
Category {
string id PK
string slug UK
int order
}
Image {
string id PK
string imageUrl
string categoryId FK
json metadata
datetime captureDate
}
Tag {
string id PK
string slug UK
}
- Frontend: Next.js 16 · TypeScript · Tailwind CSS 4
- Backend: Node.js 20 · PostgreSQL 18 · Prisma · NextAuth.js
- Infra: Docker · MinIO · Caddy · GitHub Actions
- Next.js App Router: Parallel Routes & Intercepting Routes를 활용한 모달 라우팅
- ISR 최적화:
unstable_cache로 Prisma 쿼리 캐싱 및 On-Demand Revalidation - EXIF 처리: exifr 라이브러리로 메타데이터를 JSON으로 저장 및 쿼리
- 이미지 최적화: Sharp 기반 썸네일 생성 + MinIO S3 호환 스토리지
- 프로덕션 배포: Docker 멀티 스테이지 빌드 + Caddy 리버스 프록시
초기에는 Cloudflare R2를 사용했으나, 데이터베이스와 애플리케이션은 셀프 호스팅 서버에 위치하고 이미지만 외부 CDN에 존재하여 레이턴시가 발생했습니다. 이를 해결하기 위해 스토리지를 서버 내 컨테이너로 구축하기로 결정했고, S3 호환 API를 제공하는 MinIO를 선택하여 기존 코드 수정을 최소화했습니다.
Next.js API Route를 통해 MinIO를 프록시하도록 구현한 결과, 이미지 로딩 속도가 개선되었고 모든 데이터를 단일 서버에서 관리하게 되어 백업과 복원 프로세스가 간소화되었습니다.
로컬 환경에서는 메인 페이지 응답 시간이 200-250ms로 측정되었으나, 프로덕션 환경에서는 1.5-2초가 소요되었습니다. 동일한 데이터베이스와 스토리지를 사용하는 환경에서 비교 테스트한 결과, Cloudflared 터널이 약 1.5초의 오버헤드를 발생시키는 것으로 확인되었습니다.
이를 해결하기 위해 Caddy 리버스 프록시로 전환했습니다. docker-compose에 Caddy 컨테이너를 추가하고 Let's Encrypt 자동 인증서 발급을 설정한 결과, 응답 시간이 150-250ms로 개선되어 약 87%의 성능 향상을 달성했습니다.
서비스를 운영하면서 매 요청마다 Prisma를 통한 데이터베이스 쿼리가 실행되어 서버 부하가 증가하고 불필요한 응답 지연이 발생하는 것을 발견했습니다. 특히 메인 페이지와 카테고리 목록처럼 자주 조회되지만 변경이 드문 데이터의 경우 캐싱이 효과적일 것으로 판단했습니다.
Next.js의 unstable_cache를 활용하여 메인, 카테고리, 사진 상세 페이지의 Prisma 쿼리 결과를 캐싱하고 10분 재검증 주기를 설정했습니다. 또한 On-Demand Revalidation을 구현하여 새로운 사진을 업로드하거나 데이터를 수정할 때 즉시 캐시를 무효화하도록 했습니다.
그 결과 동일 데이터 재요청 시 데이터베이스 접근 없이 캐시된 결과를 반환하게 되어 서버 부하가 감소하고 응답 속도가 개선되었습니다.