[DEVELOP]
- [DEVELOP] DDD, TDD, BDD
- [DEVELOP] 개인정보 보호 웹사이트 구축을 위한
- [DEVELOP] 예제로 이해하는 웹 접근성 (accessibility)
- [DEVELOP] 예제로 보는 이미지 사용법 (Images)
- [DEVELOP] 예제로 보는 반응형 디자인 사용법 (Responsive Design)
- [DEVELOP] PWA 이해하기 (Progressive Web App)
- [DEVELOP] 개발 프로세스 Agile / Waterfall 이란?
- [DEVELOP] 주니어 개발자의 역습
- [DEVELOP] MCP(Model Context Protocol)
- [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장 핵심 포인트
-
JPEG
- 사진이나 복잡한 이미지에 적합
- 작은 파일 크기
- 투명도 지원 안 함
-
PNG
- 로고, 아이콘에 적합
- 투명도 지원
- 파일 크기가 큼
-
WebP
- JPEG보다 좋은 압축률
- 투명도와 애니메이션 지원
- 대부분의 최신 브라우저 지원
-
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장 핵심 포인트
-
srcset과 sizes
- 다양한 크기의 이미지 제공
- 브라우저가 최적 이미지 선택
- 디바이스 특성 고려
-
picture 요소
- 아트 디렉션 지원
- 다양한 이미지 포맷 지원
- 폴백 이미지 제공
-
지연 로딩
- 성능 최적화
- 불필요한 다운로드 방지
- 사용자 경험 개선
-
반응형 배경
- 미디어 쿼리 활용
- 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장 핵심 포인트
-
이미지 최적화
- 적절한 크기로 리사이징
- 품질 조정으로 용량 줄이기
- 최신 포맷(WebP, AVIF) 활용
-
CDN 활용
- 사용자와 가까운 서버에서 전송
- 자동 최적화 기능 활용
- 캐싱 효과 극대화
-
캐싱 전략
- 브라우저 캐시 활용
- 서비스 워커 캐싱
- 효율적인 캐시 관리
-
성능 모니터링
- 로딩 시간 측정
- 에러 추적
- 최적화 추천
네! 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장 핵심 포인트
-
로딩 상태
- 스켈레톤 UI 사용
- 부드러운 전환 효과
- 에러 처리
-
점진적 로딩
- 썸네일 먼저 표시
- 고품질 이미지 후로드
- 부드러운 전환
-
인터랙션
- 이미지 줌 기능
- 갤러리 네비게이션
- 키보드 지원
-
접근성
- 의미있는 대체 텍스트
- 키보드 네비게이션
- 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장 핵심 포인트
-
자동화 파이프라인
- 자동 리사이징
- 포맷 변환
- 메타데이터 관리
-
관리 시스템
- 중앙집중식 저장소
- 메타데이터 관리
- 검색 기능
-
빌드 통합
- 웹팩 설정
- 자동 최적화
- 포맷 변환
-
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장 핵심 포인트
-
보안 관리
- 파일 유효성 검사
- 안전한 URL 생성
- 워터마크 추가
-
성능 측정
- 로딩 시간 측정
- 캐시 효율성
- LCP 모니터링
-
실시간 모니터링
- 대시보드 구현
- 성능 지표 시각화
- 알림 설정
-
최적화 추천
- 자동 분석
- 개선 제안
- 우선순위 설정
🎯 마무리!! 이미지 최적화의 핵심 포인트
-
올바른 이미지 포맷 선택
- JPEG: 사진에 적합
- PNG: 투명도가 필요할 때
- WebP: 현대적이고 효율적
- AVIF: 최고의 압축률
-
반응형 이미지 필수
- srcset과 sizes 활용
- Picture 엘리먼트 사용
- 적절한 크기 제공
- 아트 디렉션 고려
-
성능 최적화
- 이미지 압축하기
- 지연 로딩 사용
- CDN 활용하기
- 캐시 전략 수립
-
자동화와 관리
- 빌드 파이프라인 구축
- 자동 최적화 도구 사용
- 성능 모니터링
- 보안 관리
쉽게 말해서 “적절한 이미지를 적절한 크기로, 적절한 시점에 제공하자!” 라는 마인드로 개발하면 됩니다! 👍