[DEVELOP] 웹에서 사용하는 이미지 알고 사용하기 (Images)

예제로 보는 이미지 사용법

Posted by lim.Chuck on February 11, 2025

alt text

[DEVELOP]

  1. [DEVELOP] DDD, TDD, BDD
  2. [DEVELOP] 개인정보 보호 웹사이트 구축을 위한
  3. [DEVELOP] 예제로 이해하는 웹 접근성 (accessibility)
  4. [DEVELOP] 예제로 보는 이미지 사용법 (Images)
  5. [DEVELOP] 예제로 보는 반응형 디자인 사용법 (Responsive Design)
  6. [DEVELOP] PWA 이해하기 (Progressive Web App)
  7. [DEVELOP] 개발 프로세스 Agile / Waterfall 이란?
  8. [DEVELOP] 주니어 개발자의 역습
  9. [DEVELOP] MCP(Model Context Protocol)
  10. [DEVELOP] MCP claude 적용하고 사용해보기

📚 1장: 이미지 포맷 이해하기

1️⃣ 주요 이미지 포맷 비교

<!-- 각 포맷별 최적 사용 사례 -->
<div class="image-examples">
  <!-- JPEG: 사진이나 복잡한 이미지 -->
  <img
    src="photo.jpg"
    alt="풍경 사진"
    loading="lazy"
  >

  <!-- PNG: 투명도가 필요한 로고 -->
  <img
    src="logo.png"
    alt="회사 로고"
    width="200"
    height="100"
  >

  <!-- WebP: 최신 브라우저 지원 -->
  <picture>
    <source
      srcset="image.webp"
      type="image/webp">
    <img
      src="image.jpg"
      alt="대체 이미지"
    >
  </picture>

  <!-- AVIF: 최고의 압축률 -->
  <picture>
    <source
      srcset="photo.avif"
      type="image/avif">
    <source
      srcset="photo.webp"
      type="image/webp">
    <img
      src="photo.jpg"
      alt="대체 이미지"
    >
  </picture>
</div>

2️⃣ 포맷별 특징

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const imageFormats = {
  jpeg: {
    pros: ["작은 파일 크기", "넓은 브라우저 지원", "사진에 적합"],
    cons: ["투명도 지원 안 함", "손실 압축으로 인한 품질 저하"],
    bestFor: "사진, 복잡한 이미지",
  },

  png: {
    pros: ["무손실 압축", "투명도 지원", "선명한 텍스트와 라인"],
    cons: ["큰 파일 크기", "사진에는 비효율적"],
    bestFor: "로고, 아이콘, 스크린샷",
  },

  webp: {
    pros: ["JPEG보다 나은 압축률", "투명도 지원", "애니메이션 지원"],
    cons: ["일부 구형 브라우저 미지원"],
    bestFor: "웹 최적화가 필요한 모든 이미지",
  },

  avif: {
    pros: ["최고의 압축률", "뛰어난 품질", "HDR 지원"],
    cons: ["제한적인 브라우저 지원", "인코딩 시간이 김"],
    bestFor: "최신 브라우저 사용자를 위한 최적화",
  },
};

3️⃣ 이미지 변환 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class ImageConverter {
  // 이미지 포맷 변환
  async convertImage(file, format) {
    try {
      const image = await loadImage(file);

      switch (format) {
        case "webp":
          return await this.toWebP(image, {
            quality: 0.8,
            lossless: false,
          });

        case "avif":
          return await this.toAVIF(image, {
            quality: 0.7,
            speed: 5,
          });

        case "jpeg":
          return await this.toJPEG(image, {
            quality: 0.85,
            progressive: true,
          });
      }
    } catch (error) {
      console.error("이미지 변환 실패:", error);
      throw error;
    }
  }

  // 최적의 포맷 추천
  recommendFormat(imageType, requirements) {
    if (requirements.transparency) {
      return ["webp", "png"];
    }

    if (requirements.animation) {
      return ["webp", "gif"];
    }

    if (requirements.quality === "high") {
      return ["avif", "webp", "jpeg"];
    }

    return ["jpeg"];
  }
}

🌟 1장 핵심 포인트

  1. JPEG

    • 사진이나 복잡한 이미지에 적합
    • 작은 파일 크기
    • 투명도 지원 안 함
  2. PNG

    • 로고, 아이콘에 적합
    • 투명도 지원
    • 파일 크기가 큼
  3. WebP

    • JPEG보다 좋은 압축률
    • 투명도와 애니메이션 지원
    • 대부분의 최신 브라우저 지원
  4. AVIF

    • 최고의 압축률과 품질
    • HDR 지원
    • 제한적인 브라우저 지원

📚 2장: 반응형 이미지

1️⃣ srcset과 sizes 사용

<!-- 기본적인 반응형 이미지 -->
<img
  src="small.jpg"
  srcset="
    small.jpg 300w,
    medium.jpg 600w,
    large.jpg 900w"
  sizes="
    (max-width: 320px) 280px,
    (max-width: 640px) 580px,
    800px"
  alt="반응형 이미지 예제"
  loading="lazy"
>

<!-- art direction을 위한 picture 요소 -->
<picture>
  <!-- 모바일용 세로 이미지 -->
  <source
    media="(max-width: 640px)"
    srcset="
      mobile.jpg 300w,
      mobile-hd.jpg 600w"
    sizes="90vw">

  <!-- 데스크톱용 가로 이미지 -->
  <source
    media="(min-width: 641px)"
    srcset="
      desktop.jpg 800w,
      desktop-hd.jpg 1600w"
    sizes="80vw">

  <!-- 폴백 이미지 -->
  <img
    src="fallback.jpg"
    alt="반응형 이미지"
    loading="lazy">
</picture>

2️⃣ 반응형 이미지 헬퍼 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class ResponsiveImageHelper {
  constructor() {
    this.breakpoints = {
      mobile: 320,
      tablet: 768,
      desktop: 1024,
      wide: 1440,
    };
  }

  // srcset 문자열 생성
  generateSrcset(imagePath, widths) {
    return widths
      .map((width) => {
        const imageUrl = this.generateImageUrl(imagePath, width);
        return `${imageUrl} ${width}w`;
      })
      .join(", ");
  }

  // sizes 문자열 생성
  generateSizes(config) {
    return Object.entries(config)
      .map(([breakpoint, size]) => {
        if (breakpoint === "default") {
          return size;
        }
        return `(max-width: ${this.breakpoints[breakpoint]}px) ${size}`;
      })
      .join(", ");
  }

  // 이미지 URL 생성 (CDN 활용)
  generateImageUrl(path, width) {
    return `https://cdn.example.com/images/${path}?width=${width}`;
  }
}

// 사용 예시
const helper = new ResponsiveImageHelper();
const srcset = helper.generateSrcset("photo.jpg", [300, 600, 900]);
const sizes = helper.generateSizes({
  mobile: "90vw",
  tablet: "80vw",
  default: "70vw",
});

3️⃣ 성능 최적화된 지연 로딩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class LazyLoadImages {
  constructor() {
    this.images = document.querySelectorAll("img[data-src]");
    this.setupIntersectionObserver();
  }

  setupIntersectionObserver() {
    const options = {
      root: null,
      rootMargin: "50px 0px",
      threshold: 0.1,
    };

    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          this.loadImage(entry.target);
          observer.unobserve(entry.target);
        }
      });
    }, options);

    this.images.forEach((image) => observer.observe(image));
  }

  loadImage(image) {
    // srcset이 있으면 먼저 설정
    if (image.dataset.srcset) {
      image.srcset = image.dataset.srcset;
    }

    // 기본 src 설정
    image.src = image.dataset.src;

    // 로딩 완료 후 플레이스홀더 제거
    image.onload = () => {
      image.classList.remove("placeholder");
      image.classList.add("loaded");
    };
  }
}

4️⃣ 반응형 배경 이미지

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* 반응형 배경 이미지 */
.hero-section {
  /* 기본 배경 */
  background-image: url("mobile.jpg");
  background-size: cover;
  background-position: center;

  /* 태블릿 */
  @media (min-width: 768px) {
    background-image: url("tablet.jpg");
  }

  /* 데스크톱 */
  @media (min-width: 1024px) {
    background-image: url("desktop.jpg");
  }

  /* Retina 디스플레이 */
  @media (min-resolution: 2dppx) {
    background-image: url("desktop@2x.jpg");
  }
}

/* 아트 디렉션을 위한 반응형 컨테이너 */
.image-container {
  position: relative;
  aspect-ratio: 16 / 9;

  @media (max-width: 767px) {
    aspect-ratio: 4 / 5;
  }
}

🌟 2장 핵심 포인트

  1. srcset과 sizes

    • 다양한 크기의 이미지 제공
    • 브라우저가 최적 이미지 선택
    • 디바이스 특성 고려
  2. picture 요소

    • 아트 디렉션 지원
    • 다양한 이미지 포맷 지원
    • 폴백 이미지 제공
  3. 지연 로딩

    • 성능 최적화
    • 불필요한 다운로드 방지
    • 사용자 경험 개선
  4. 반응형 배경

    • 미디어 쿼리 활용
    • Retina 디스플레이 지원
    • 아트 디렉션 고려

📚 3장: 이미지 성능 최적화

1️⃣ 이미지 압축 및 최적화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class ImageOptimizer {
  constructor() {
    this.quality = {
      jpeg: 0.85, // 85% 품질
      webp: 0.8, // 80% 품질
      avif: 0.75, // 75% 품질
    };
  }

  // 이미지 최적화 설정
  async optimizeImage(file) {
    const optimized = await sharp(file)
      // 적절한 크기로 리사이징
      .resize({
        width: 1200,
        height: 800,
        fit: "inside",
        withoutEnlargement: true,
      })
      // 메타데이터 제거
      .removeMetadata()
      // 다양한 포맷으로 변환
      .toFormat("webp", {
        quality: 80,
        effort: 6,
      });

    return optimized;
  }

  // 여러 크기의 이미지 생성
  async generateSizes(file) {
    const sizes = [300, 600, 900, 1200];
    const outputs = [];

    for (const size of sizes) {
      const optimized = await sharp(file)
        .resize(size)
        .toFormat("webp")
        .toBuffer();

      outputs.push({
        size,
        buffer: optimized,
      });
    }

    return outputs;
  }
}

2️⃣ CDN 활용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class ImageCDN {
  constructor() {
    this.cdnUrl = "https://cdn.example.com/images";
    this.defaultParams = {
      format: "auto", // 자동 포맷 선택
      quality: "auto", // 자동 품질 조정
      compress: true, // 압축 활성화
    };
  }

  // CDN URL 생성
  generateUrl(imagePath, options = {}) {
    const params = {
      ...this.defaultParams,
      ...options,
    };

    const queryString = Object.entries(params)
      .map(([key, value]) => `${key}=${value}`)
      .join("&");

    return `${this.cdnUrl}/${imagePath}?${queryString}`;
  }

  // 반응형 이미지 URL 세트 생성
  generateResponsiveSet(imagePath) {
    const widths = [300, 600, 900, 1200];

    return widths.map((width) => ({
      url: this.generateUrl(imagePath, { width }),
      width,
    }));
  }
}

3️⃣ 캐싱 전략

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class ImageCache {
  constructor() {
    this.cacheName = "image-cache-v1";
  }

  // 서비스 워커 캐싱
  async cacheImage(request, response) {
    const cache = await caches.open(this.cacheName);
    await cache.put(request, response);
  }

  // 캐시 우선 전략
  async getCachedImage(request) {
    const cache = await caches.open(this.cacheName);
    const cached = await cache.match(request);

    if (cached) {
      // 캐시된 이미지 반환
      return cached;
    }

    // 네트워크에서 가져오기
    const response = await fetch(request);
    await this.cacheImage(request, response.clone());
    return response;
  }

  // 캐시 정리
  async cleanOldCache() {
    const keys = await caches.keys();
    const oldCaches = keys.filter(
      (key) => key.startsWith("image-cache") && key !== this.cacheName
    );

    await Promise.all(oldCaches.map((key) => caches.delete(key)));
  }
}

4️⃣ 성능 모니터링

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class ImagePerformanceMonitor {
  constructor() {
    this.metrics = {
      loadTime: [],
      size: [],
      cacheHits: 0,
      errors: [],
    };
  }

  // 이미지 로딩 성능 측정
  measureLoadTime(imageUrl) {
    const start = performance.now();

    return new Promise((resolve, reject) => {
      const img = new Image();

      img.onload = () => {
        const loadTime = performance.now() - start;
        this.metrics.loadTime.push({
          url: imageUrl,
          time: loadTime,
        });
        resolve(loadTime);
      };

      img.onerror = (error) => {
        this.metrics.errors.push({
          url: imageUrl,
          error,
        });
        reject(error);
      };

      img.src = imageUrl;
    });
  }

  // 성능 리포트 생성
  generateReport() {
    return {
      averageLoadTime: this.calculateAverageLoadTime(),
      totalErrors: this.metrics.errors.length,
      cacheEfficiency: this.calculateCacheEfficiency(),
      recommendations: this.generateRecommendations(),
    };
  }
}

🌟 3장 핵심 포인트

  1. 이미지 최적화

    • 적절한 크기로 리사이징
    • 품질 조정으로 용량 줄이기
    • 최신 포맷(WebP, AVIF) 활용
  2. CDN 활용

    • 사용자와 가까운 서버에서 전송
    • 자동 최적화 기능 활용
    • 캐싱 효과 극대화
  3. 캐싱 전략

    • 브라우저 캐시 활용
    • 서비스 워커 캐싱
    • 효율적인 캐시 관리
  4. 성능 모니터링

    • 로딩 시간 측정
    • 에러 추적
    • 최적화 추천

네! 4장 “이미지 사용자 경험(UX) 최적화”를 설명해드리겠습니다! 사용자에게 더 나은 이미지 경험을 제공하는 방법을 알아볼게요.

📚 4장: 이미지 UX 최적화

1️⃣ 로딩 상태 처리

<!-- 이미지 로딩 스켈레톤 -->
<div class="image-wrapper">
  <!-- 스켈레톤 UI -->
  <div class="skeleton" aria-hidden="true">
    <div class="pulse"></div>
  </div>

  <!-- 실제 이미지 -->
  <img
    src="photo.jpg"
    alt="풍경 사진"
    loading="lazy"
    onload="this.classList.add('loaded')"
    onerror="this.classList.add('error')"
  >

  <!-- 에러 상태 -->
  <div class="error-state" hidden>
    <span>😢 이미지 로드 실패</span>
    <button onclick="retryLoad()">다시 시도</button>
  </div>
</div>

<style>
.skeleton {
  background: #f0f0f0;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
}

.pulse {
  animation: pulse 1.5s infinite;
}

.loaded {
  opacity: 1;
  transition: opacity 0.3s;
}

@keyframes pulse {
  0% { opacity: 0.6; }
  50% { opacity: 1; }
  100% { opacity: 0.6; }
}
</style>

2️⃣ 점진적 이미지 로딩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class ProgressiveImageLoader {
  constructor() {
    this.loadQueue = new Map();
  }

  // 점진적 이미지 로딩
  loadProgressively(imageUrl, container) {
    // 작은 블러된 썸네일 먼저 로드
    this.loadThumbnail(imageUrl, container);

    // 고품질 이미지 로드
    this.loadFullImage(imageUrl, container);
  }

  async loadThumbnail(imageUrl, container) {
    const thumbnailUrl = this.getThumbnailUrl(imageUrl);
    const img = new Image();

    img.onload = () => {
      container.style.backgroundImage = `url(${thumbnailUrl})`;
      container.classList.add("blur");
    };

    img.src = thumbnailUrl;
  }

  async loadFullImage(imageUrl, container) {
    const img = new Image();

    img.onload = () => {
      container.style.backgroundImage = `url(${imageUrl})`;
      container.classList.remove("blur");
      container.classList.add("loaded");
    };

    img.src = imageUrl;
  }

  // 썸네일 URL 생성 (작고 블러된 이미지)
  getThumbnailUrl(imageUrl) {
    return `${imageUrl}?w=20&blur=true`;
  }
}

3️⃣ 이미지 줌 및 갤러리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class ImageZoomViewer {
  constructor(container) {
    this.container = container;
    this.setupZoom();
  }

  setupZoom() {
    this.container.addEventListener("mousemove", (e) => {
      this.handleZoom(e);
    });

    this.container.addEventListener("mouseleave", () => {
      this.resetZoom();
    });
  }

  handleZoom(event) {
    const bounds = this.container.getBoundingClientRect();
    const x = (event.clientX - bounds.left) / bounds.width;
    const y = (event.clientY - bounds.top) / bounds.height;

    this.container.style.transformOrigin = `${x * 100}% ${y * 100}%`;
    this.container.classList.add("zoomed");
  }

  resetZoom() {
    this.container.classList.remove("zoomed");
  }
}

// 이미지 갤러리
class ImageGallery {
  constructor() {
    this.currentIndex = 0;
    this.setupGallery();
  }

  setupGallery() {
    this.container.addEventListener("keydown", (e) => {
      if (e.key === "ArrowRight") this.next();
      if (e.key === "ArrowLeft") this.previous();
    });
  }

  showImage(index) {
    // 현재 이미지 페이드 아웃
    this.fadeOut(this.currentImage);

    // 새 이미지 페이드 인
    this.fadeIn(this.images[index]);

    this.currentIndex = index;
    this.updateThumbnails();
  }
}

4️⃣ 접근성과 대체 텍스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class ImageAccessibility {
  constructor() {
    this.setupAccessibility();
  }

  // 동적 대체 텍스트 생성
  generateAltText(image) {
    const context = {
      type: image.dataset.type,
      subject: image.dataset.subject,
      action: image.dataset.action,
    };

    return this.formatAltText(context);
  }

  // 대체 텍스트 포맷팅
  formatAltText(context) {
    const templates = {
      product: `${context.subject} 제품 이미지`,
      banner: `${context.subject} 프로모션 배너`,
      avatar: `${context.subject}의 프로필 사진`,
    };

    return templates[context.type] || context.subject;
  }

  // 키보드 네비게이션
  setupKeyboardNav() {
    document.addEventListener("keydown", (e) => {
      if (e.key === "Enter" && e.target.matches(".zoomable")) {
        this.toggleZoom(e.target);
      }
    });
  }
}

🌟 4장 핵심 포인트

  1. 로딩 상태

    • 스켈레톤 UI 사용
    • 부드러운 전환 효과
    • 에러 처리
  2. 점진적 로딩

    • 썸네일 먼저 표시
    • 고품질 이미지 후로드
    • 부드러운 전환
  3. 인터랙션

    • 이미지 줌 기능
    • 갤러리 네비게이션
    • 키보드 지원
  4. 접근성

    • 의미있는 대체 텍스트
    • 키보드 네비게이션
    • ARIA 레이블

네! 5장 “이미지 관리와 워크플로우”를 설명해드리겠습니다! 이미지를 효율적으로 관리하고 자동화하는 방법을 알아볼게요.

📚 5장: 이미지 관리와 워크플로우

1️⃣ 이미지 자동화 파이프라인

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class ImagePipeline {
  constructor() {
    this.processors = [];
    this.setupDefaultProcessors();
  }

  // 기본 프로세서 설정
  setupDefaultProcessors() {
    this.addProcessor({
      name: "resize",
      process: async (image) => {
        const sizes = [300, 600, 900, 1200];
        return Promise.all(sizes.map((size) => this.resize(image, size)));
      },
    });

    this.addProcessor({
      name: "optimize",
      process: async (image) => {
        return this.optimize(image, {
          quality: 85,
          format: "webp",
        });
      },
    });

    this.addProcessor({
      name: "metadata",
      process: async (image) => {
        return this.addMetadata(image, {
          copyright: "© 2024",
          lastModified: new Date(),
        });
      },
    });
  }

  // 이미지 처리 실행
  async processImage(image) {
    let processed = image;

    for (const processor of this.processors) {
      try {
        processed = await processor.process(processed);
      } catch (error) {
        console.error(`${processor.name} 처리 실패:`, error);
        throw error;
      }
    }

    return processed;
  }
}

2️⃣ 이미지 관리 시스템

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class ImageManagementSystem {
  constructor() {
    this.storage = new ImageStorage();
    this.database = new ImageDatabase();
  }

  // 이미지 업로드 및 처리
  async uploadImage(file, metadata) {
    // 유효성 검사
    this.validateImage(file);

    // 이미지 처리
    const pipeline = new ImagePipeline();
    const processed = await pipeline.processImage(file);

    // 저장
    const url = await this.storage.store(processed);

    // 메타데이터 저장
    await this.database.saveMetadata({
      url,
      metadata,
      size: processed.size,
      dimensions: processed.dimensions,
      format: processed.format,
      createdAt: new Date(),
    });

    return url;
  }

  // 이미지 검색
  async searchImages(query) {
    const results = await this.database.search({
      text: query.text,
      tags: query.tags,
      dateRange: query.dateRange,
    });

    return results.map((result) => ({
      ...result,
      url: this.storage.getUrl(result.id),
    }));
  }

  // 이미지 최적화 상태 모니터링
  getOptimizationStats() {
    return {
      totalImages: this.database.count(),
      optimizedCount: this.database.countOptimized(),
      averageSize: this.database.getAverageSize(),
      potentialSavings: this.calculatePotentialSavings(),
    };
  }
}

3️⃣ 빌드 시스템 통합

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpe?g|gif|webp)$/i,
        use: [
          {
            loader: "responsive-loader",
            options: {
              adapter: require("responsive-loader/sharp"),
              sizes: [300, 600, 900, 1200],
              placeholder: true,
              placeholderSize: 40,
              format: "webp",
              quality: 85,
            },
          },
        ],
      },
    ],
  },
  plugins: [
    new ImageMinimizerPlugin({
      minimizerOptions: {
        plugins: [
          ["mozjpeg", { quality: 85 }],
          ["optipng", { optimizationLevel: 5 }],
          ["webp", { quality: 85 }],
        ],
      },
    }),
  ],
};

4️⃣ CI/CD 파이프라인

# .github/workflows/image-optimization.yml
name: Image Optimization

on:
  push:
    paths:
      - '**.jpg'
      - '**.png'
      - '**.webp'

jobs:
  optimize:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '16'

      - name: Install dependencies
        run: npm install

      - name: Optimize images
        run: |
          npm run optimize-images

      - name: Check optimization results
        run: node scripts/check-image-sizes.js

      - name: Commit changes
        uses: stefanzweifel/git-auto-commit-action@v4
        with:
          commit_message: "🗜️ Optimize images"
          file_pattern: "**/*.{jpg,png,webp}"

🌟 5장 핵심 포인트

  1. 자동화 파이프라인

    • 자동 리사이징
    • 포맷 변환
    • 메타데이터 관리
  2. 관리 시스템

    • 중앙집중식 저장소
    • 메타데이터 관리
    • 검색 기능
  3. 빌드 통합

    • 웹팩 설정
    • 자동 최적화
    • 포맷 변환
  4. CI/CD

    • 자동 최적화
    • 품질 체크
    • 자동 커밋

네! 마지막 6장 “이미지 보안과 성능 모니터링”을 설명해드리겠습니다! 이미지를 안전하게 관리하고 성능을 측정하는 방법을 알아볼게요.

📚 6장: 이미지 보안과 성능 모니터링

1️⃣ 이미지 보안

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class ImageSecurity {
  constructor() {
    this.allowedFormats = ["jpg", "jpeg", "png", "webp", "avif"];
    this.maxFileSize = 5 * 1024 * 1024; // 5MB
  }

  // 이미지 유효성 검사
  validateImage(file) {
    // 파일 크기 검사
    if (file.size > this.maxFileSize) {
      throw new Error("파일이 너무 큽니다");
    }

    // 파일 형식 검사
    const extension = this.getFileExtension(file.name);
    if (!this.allowedFormats.includes(extension)) {
      throw new Error("지원하지 않는 파일 형식입니다");
    }

    // 이미지 콘텐츠 검사
    return this.scanImageContent(file);
  }

  // 안전한 URL 생성
  generateSecureUrl(imageId, options = {}) {
    const timestamp = Date.now();
    const signature = this.generateSignature(imageId, timestamp);

    return {
      url: `/images/${imageId}`,
      token: signature,
      expires: timestamp + 60 * 60 * 1000, // 1시간
    };
  }

  // 워터마크 추가
  async addWatermark(image, text) {
    return sharp(image)
      .composite([
        {
          input: this.createWatermark(text),
          gravity: "southeast",
        },
      ])
      .toBuffer();
  }
}

2️⃣ 성능 모니터링

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class ImagePerformanceMonitor {
  constructor() {
    this.metrics = new Map();
    this.initializeObservers();
  }

  // 성능 관찰자 초기화
  initializeObservers() {
    // 이미지 로딩 성능 측정
    this.loadObserver = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.entryType === "resource" && entry.initiatorType === "img") {
          this.recordMetric(entry);
        }
      }
    });

    // Largest Contentful Paint 측정
    this.lcpObserver = new PerformanceObserver((list) => {
      const entries = list.getEntries();
      const lastEntry = entries[entries.length - 1];
      this.recordLCP(lastEntry);
    });

    this.loadObserver.observe({
      entryTypes: ["resource"],
    });
    this.lcpObserver.observe({
      entryTypes: ["largest-contentful-paint"],
    });
  }

  // 메트릭 기록
  recordMetric(entry) {
    const metrics = {
      loadTime: entry.duration,
      size: entry.transferSize,
      protocol: entry.nextHopProtocol,
      cache: entry.transferSize < entry.encodedBodySize ? "HIT" : "MISS",
    };

    this.metrics.set(entry.name, metrics);
  }

  // 성능 리포트 생성
  generateReport() {
    return {
      summary: this.calculateSummary(),
      details: Array.from(this.metrics.entries()),
      recommendations: this.generateRecommendations(),
    };
  }
}

3️⃣ 실시간 모니터링 대시보드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class ImageMonitoringDashboard {
  constructor() {
    this.charts = new Map();
    this.initializeDashboard();
  }

  // 대시보드 초기화
  initializeDashboard() {
    // 로딩 시간 차트
    this.charts.set(
      "loadTime",
      new Chart("loadTime", {
        type: "line",
        options: {
          title: "이미지 로딩 시간 추이",
          yAxis: { title: "로딩 시간 (ms)" },
        },
      })
    );

    // 캐시 히트율 차트
    this.charts.set(
      "cacheHitRate",
      new Chart("cacheHit", {
        type: "pie",
        options: {
          title: "캐시 히트율",
        },
      })
    );

    // 크기 분포 차트
    this.charts.set(
      "sizeDistribution",
      new Chart("size", {
        type: "histogram",
        options: {
          title: "이미지 크기 분포",
        },
      })
    );
  }

  // 실시간 업데이트
  updateDashboard(metrics) {
    this.updateLoadTimeChart(metrics.loadTime);
    this.updateCacheHitChart(metrics.cacheHits);
    this.updateSizeDistribution(metrics.sizes);
  }

  // 알림 설정
  setAlerts(thresholds) {
    this.alerts = {
      loadTime: (threshold) => {
        if (threshold > 3000) {
          // 3초 이상
          this.sendAlert("로딩 시간이 너무 깁니다");
        }
      },
      size: (threshold) => {
        if (threshold > 1000000) {
          // 1MB 이상
          this.sendAlert("이미지 크기가 너무 큽니다");
        }
      },
    };
  }
}

🌟 6장 핵심 포인트

  1. 보안 관리

    • 파일 유효성 검사
    • 안전한 URL 생성
    • 워터마크 추가
  2. 성능 측정

    • 로딩 시간 측정
    • 캐시 효율성
    • LCP 모니터링
  3. 실시간 모니터링

    • 대시보드 구현
    • 성능 지표 시각화
    • 알림 설정
  4. 최적화 추천

    • 자동 분석
    • 개선 제안
    • 우선순위 설정

🎯 마무리!! 이미지 최적화의 핵심 포인트

  1. 올바른 이미지 포맷 선택

    • JPEG: 사진에 적합
    • PNG: 투명도가 필요할 때
    • WebP: 현대적이고 효율적
    • AVIF: 최고의 압축률
  2. 반응형 이미지 필수

    • srcset과 sizes 활용
    • Picture 엘리먼트 사용
    • 적절한 크기 제공
    • 아트 디렉션 고려
  3. 성능 최적화

    • 이미지 압축하기
    • 지연 로딩 사용
    • CDN 활용하기
    • 캐시 전략 수립
  4. 자동화와 관리

    • 빌드 파이프라인 구축
    • 자동 최적화 도구 사용
    • 성능 모니터링
    • 보안 관리

쉽게 말해서 “적절한 이미지를 적절한 크기로, 적절한 시점에 제공하자!” 라는 마인드로 개발하면 됩니다! 👍