์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ–ผ๏ธ ๋Œ€๋Ÿ‰ ์ด๋ฏธ์ง€ ๋ฆฌ์‚ฌ์ด์ฆˆ ์ž๋™ํ™”์†Œ์Šค์ฝ”๋“œ ์™„์ „์ •๋ณต
๐Ÿ› ๏ธ ํ”„๋กœ๊ทธ๋žจ/์†Œ์Šค ยท ํ”„๋กœ๊ทธ๋žจ๊ฐœ๋ฐœ

๐Ÿ–ผ๏ธ ๋Œ€๋Ÿ‰ ์ด๋ฏธ์ง€ ๋ฆฌ์‚ฌ์ด์ฆˆ ์ž๋™ํ™”
์†Œ์Šค์ฝ”๋“œ ์™„์ „์ •๋ณต

์ˆ˜๋ฐฑ ์žฅ๋„ 1์ดˆ์ปท? ๊ทธ๊ฑฐ ์ฝ”๋“œ๋กœ ๊ฐ€๋Šฅํ•จ ใ…‹ใ…‹
Python๋ถ€ํ„ฐ Node.js๊นŒ์ง€ ์‹ค์ „ ์†Œ์Šค ์ด์ •๋ฆฌ!

๋Œ€๋Ÿ‰ ์ด๋ฏธ์ง€ ์ž๋™ ๋ฆฌ์‚ฌ์ด์ฆˆ ์›๋ณธ 1์žฅ โ†’ ์ž๋™์œผ๋กœ ์—ฌ๋Ÿฌ ์‚ฌ์ด์ฆˆ๋กœ ๋ณ€ํ™˜! ์›๋ณธ ์ด๋ฏธ์ง€ ์ž๋™ํ™” ์ฝ”๋“œ ๋‹ค์–‘ํ•œ ์‚ฌ์ด์ฆˆ ๊ฒฐ๊ณผ๋ฌผ ์ˆ˜๋ฐฑ ์žฅ๋„ 1์ดˆ์ปท ์ž๋™ํ™” ๋งˆ๋ฒ• โœจ โœฆ โœฆ โœฆ โœฆ

๐Ÿ“‹ ์˜ค๋Š˜ ๋ฐฐ์šธ ๋‚ด์šฉ ๋ฏธ๋ฆฌ๋ณด๊ธฐ

๐Ÿ“Œ
์™œ ์ž๋™ํ™”?
์ˆ˜๋™ ๋ฆฌ์‚ฌ์ด์ฆˆ์˜ ํ•œ๊ณ„
๐Ÿ
Python Pillow
๊ฐ€์žฅ ๋Œ€์ค‘์ ์ธ ๋ฐฉ๋ฒ•
โšก
Node.js Sharp
์ดˆ๊ณ ์† ์ฒ˜๋ฆฌ
๐Ÿ–ฅ๏ธ
Bash ์Šคํฌ๋ฆฝํŠธ
ImageMagick ํ™œ์šฉ
๐Ÿ—‚๏ธ
ํด๋” ๊ตฌ์กฐ ์„ค๊ณ„
์‹ค์ „ ํ”„๋กœ์ ํŠธ ์„ธํŒ…
๐Ÿ”ง
๊ณ ๊ธ‰ ์˜ต์…˜
์›Œํ„ฐ๋งˆํฌยทํฌ๋งท๋ณ€ํ™˜
๐Ÿš€
์„ฑ๋Šฅ ์ตœ์ ํ™”
๋ฉ€ํ‹ฐ์Šค๋ ˆ๋”ฉ ์ฒ˜๋ฆฌ
๐Ÿ’ก
์‹ค์ „ ํŒ
์—๋Ÿฌ ํ•ธ๋“ค๋ง๊นŒ์ง€

๐Ÿค” ์ˆ˜๋™์œผ๋กœ ํ•˜๋ฉด ์•ˆ ๋˜๋‚˜์š”? (๋ฉ๋‹ˆ๋‹ค๋งŒ...)

์†”์งํžˆ ๋งํ•˜๋ฉด ์ด๋ฏธ์ง€ 1~2์žฅ์€ ํฌํ† ์ƒต์ด๋‚˜ ๊ทธ๋ฆผํŒ์œผ๋กœ๋„ ์ถฉ๋ถ„ํ•จ ใ…‹ใ…‹
๊ทผ๋ฐ ํ˜„์‹ค์€ ๊ทธ๋ ‡์ง€ ์•Š์ž–์•„์š”?

์‡ผํ•‘๋ชฐ ์šด์˜ํ•˜๋ฉด์„œ ์ƒํ’ˆ ์ด๋ฏธ์ง€ 500์žฅ์„ ์›น์šฉยท์ธ๋„ค์ผ์šฉยท๋ชจ๋ฐ”์ผ์šฉ์œผ๋กœ ๊ฐ๊ฐ ๋งŒ๋“ค์–ด์•ผ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•ด๋ด์š”.
500์žฅ ร— 3๊ฐ€์ง€ ์‚ฌ์ด์ฆˆ = 1,500๋ฒˆ์˜ ์ˆ˜๋™ ์ž‘์—… ๐Ÿ˜ฑ

์ด๊ฒŒ ๋ฐ”๋กœ ์ž๋™ํ™”๊ฐ€ ํ•„์š”ํ•œ ์ด์œ ์ž„. ์ฝ”๋“œ ํ•œ ๋ฒˆ ์งœ๋†“์œผ๋ฉด ๊ทธ ๋‹ค์Œ๋ถ€ํ„ฐ๋Š” ๊ทธ๋ƒฅ ์‹คํ–‰๋งŒ ํ•˜๋ฉด ๋˜๊ฑฐ๋“ ์š”!

๐Ÿ’ก ์ด๋Ÿฐ ์ƒํ™ฉ์—์„œ ์ž๋™ํ™”๊ฐ€ ๋น›์„ ๋ฐœํ•จ! โœ… ์‡ผํ•‘๋ชฐ ์ƒํ’ˆ ์ด๋ฏธ์ง€ ๋Œ€๋Ÿ‰ ์—…๋กœ๋“œ ์ „์ฒ˜๋ฆฌ
โœ… ๋ธ”๋กœ๊ทธ/SNS์šฉ ์ด๋ฏธ์ง€ ์‚ฌ์ด์ฆˆ ๊ทœ๊ฒฉ ๋งž์ถ”๊ธฐ
โœ… ์•ฑ ๊ฐœ๋ฐœ ์‹œ ์•„์ด์ฝ˜ ์—ฌ๋Ÿฌ ํ•ด์ƒ๋„ ์ƒ์„ฑ (1x, 2x, 3x)
โœ… ์›น์‚ฌ์ดํŠธ ๋ฐ˜์‘ํ˜• ์ด๋ฏธ์ง€ ์„ธํŠธ ์ƒ์„ฑ
โœ… ์‚ฌ์ง„ ์•„์นด์ด๋ธŒ ์ •๋ฆฌ ๋ฐ ์ธ๋„ค์ผ ์ผ๊ด„ ์ƒ์„ฑ
  • โš ๏ธ ์ˆ˜๋™ ์ž‘์—…์˜ ํ˜„์‹ค์ ์ธ ๋ฌธ์ œ์ 
  • โŒ ์‹œ๊ฐ„์ด ๋„ˆ๋ฌด ์˜ค๋ž˜ ๊ฑธ๋ฆผ (500์žฅ ์ˆ˜๋™ = ๋ช‡ ์‹œ๊ฐ„)
  • โŒ ์‚ฌ๋žŒ์ด ํ•˜๋‹ค ๋ณด๋ฉด ์‹ค์ˆ˜ ๋ฐœ์ƒ (์‚ฌ์ด์ฆˆ ์ž˜๋ชป ์ž…๋ ฅ ๋“ฑ)
  • โŒ ์ผ๊ด€์„ฑ์ด ์—†์–ด์ง (๋งค๋ฒˆ ์กฐ๊ธˆ์”ฉ ๋‹ค๋ฅธ ๊ฒฐ๊ณผ๋ฌผ)
  • โŒ ๋ฐ˜๋ณต ์ž‘์—…์œผ๋กœ ์ธํ•œ ๋ฒˆ์•„์›ƒ ใ… ใ… 

๐Ÿ“ ์ด๋ฏธ์ง€ ๋ฆฌ์‚ฌ์ด์ฆˆ ๊ธฐ๋ณธ ๊ฐœ๋… ์ •๋ฆฌ

์ฝ”๋“œ ๋“ค์–ด๊ฐ€๊ธฐ ์ „์— ๊ธฐ๋ณธ ๊ฐœ๋… ์ข€ ์žก๊ณ  ๊ฐ€์•ผ ํ•ด์š”. ์ด๊ฑฐ ๋ชจ๋ฅด๋ฉด ๋‚˜์ค‘์— ๊ฒฐ๊ณผ๋ฌผ ๋ณด๊ณ  "์™œ ์ด์ƒํ•˜์ง€?" ํ•˜๊ฒŒ ๋จ ใ…‹ใ…‹

๐Ÿ”ธ ๋ฆฌ์‚ฌ์ด์ฆˆ ๋ฐฉ์‹์˜ ์ข…๋ฅ˜

๋ฐฉ์‹ ์„ค๋ช… ์–ธ์ œ ์“ฐ๋‚˜
resize ๊ฐ€๋กœยท์„ธ๋กœ ๋น„์œจ ๋ฌด์‹œํ•˜๊ณ  ๊ฐ•์ œ ๋ณ€ํ™˜ ์ •ํ™•ํ•œ ์‚ฌ์ด์ฆˆ๊ฐ€ ํ•„์š”ํ•  ๋•Œ
thumbnail ๋น„์œจ ์œ ์ง€ํ•˜๋ฉฐ ์ง€์ • ํฌ๊ธฐ ์•ˆ์— ๋งž์ถค ์ธ๋„ค์ผ ์ƒ์„ฑ ์‹œ
crop ๋น„์œจ ์œ ์ง€ํ•˜๋ฉฐ ์ž˜๋ผ๋‚ด๊ธฐ ์ •์‚ฌ๊ฐํ˜• ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ๋“ฑ
fit/contain ๋น„์œจ ์œ ์ง€, ๋นˆ ๊ณต๊ฐ„์€ ๋ฐฐ๊ฒฝ์ƒ‰์œผ๋กœ ๊ท ์ผํ•œ ์บ”๋ฒ„์Šค ํฌ๊ธฐ ํ•„์š” ์‹œ
cover ๋น„์œจ ์œ ์ง€, ์˜์—ญ์„ ๊ฝ‰ ์ฑ„์šฐ๋ฉฐ ๋„˜์น˜๋Š” ๋ถ€๋ถ„ ์ž˜๋ผ๋ƒ„ ๋ฐฐ๋„ˆยท์ปค๋ฒ„ ์ด๋ฏธ์ง€

๐Ÿ”ธ ์ด๋ฏธ์ง€ ํฌ๋งท๋ณ„ ํŠน์ง•

JPEG
์‚ฌ์ง„์— ์ตœ์ ํ™”
์†์‹ค ์••์ถ•
ํˆฌ๋ช…๋„ ์—†์Œ
PNG
ํˆฌ๋ช…๋„ ์ง€์›
๋ฌด์†์‹ค ์••์ถ•
ํŒŒ์ผ ํฌ๊ธฐ ํผ
WebP
๊ตฌ๊ธ€ ๊ฐœ๋ฐœ
JPEG๋ณด๋‹ค 30% ์ž‘์Œ
์ตœ์‹  ๋ธŒ๋ผ์šฐ์ € ์ง€์›
AVIF
์ฐจ์„ธ๋Œ€ ํฌ๋งท
WebP๋ณด๋‹ค ๋” ์ž‘์Œ
์ง€์› ๋ฒ”์œ„ ํ™•๋Œ€ ์ค‘
๐Ÿ’ก ๋ฆฌ์ƒ˜ํ”Œ๋ง ์•Œ๊ณ ๋ฆฌ์ฆ˜๋„ ์ค‘์š”ํ•ด์š”! ์ด๋ฏธ์ง€๋ฅผ ์ค„์ด๊ฑฐ๋‚˜ ๋Š˜๋ฆด ๋•Œ ์–ด๋–ค ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์“ฐ๋А๋ƒ์— ๋”ฐ๋ผ ํ’ˆ์งˆ์ด ๋‹ฌ๋ผ์ง.

โ€ข NEAREST: ๊ฐ€์žฅ ๋น ๋ฆ„, ํ’ˆ์งˆ ๋‚ฎ์Œ (ํ”ฝ์…€์•„ํŠธ์— ์ ํ•ฉ)
โ€ข BILINEAR: ์ค‘๊ฐ„ ์†๋„, ์ค‘๊ฐ„ ํ’ˆ์งˆ
โ€ข BICUBIC: ๋А๋ฆฌ์ง€๋งŒ ํ’ˆ์งˆ ์ข‹์Œ (์ผ๋ฐ˜์ ์œผ๋กœ ๋งŽ์ด ์”€)
โ€ข LANCZOS: ๊ฐ€์žฅ ๊ณ ํ’ˆ์งˆ, ๊ฐ€์žฅ ๋А๋ฆผ (๊ณ ํ’ˆ์งˆ ์ถœ๋ ฅ ์‹œ)

๐Ÿ Python + Pillow๋กœ ๋Œ€๋Ÿ‰ ๋ฆฌ์‚ฌ์ด์ฆˆ ์ž๋™ํ™”

Python์˜ Pillow ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ์˜ ๊ตญ๋ฃฐ์ž„ ใ…‹ใ…‹
์„ค์น˜๋„ ์‰ฝ๊ณ , ๋ฌธ์„œ๋„ ์ž˜ ๋˜์–ด ์žˆ๊ณ , ์ปค๋ฎค๋‹ˆํ‹ฐ๋„ ํ™œ๋ฐœํ•ด์„œ ์ž…๋ฌธ์žํ•œํ…Œ ๊ฐ•์ถ”!

๐Ÿ“ฆ ์„ค์น˜ ๋ฐฉ๋ฒ•

# pip์œผ๋กœ ๊ฐ„๋‹จ ์„ค์น˜!
pip install Pillow

# ์„ค์น˜ ํ™•์ธ
python -c "from PIL import Image; print('Pillow ์„ค์น˜ ์™„๋ฃŒ!')"

๐Ÿ”ฐ ๊ธฐ๋ณธ ๋‹จ์ผ ์ด๋ฏธ์ง€ ๋ฆฌ์‚ฌ์ด์ฆˆ

๋จผ์ € ๊ธฐ๋ณธ๋ถ€ํ„ฐ ์ตํžˆ๊ณ  ๊ฐ€์•ผ์ฃ !

from PIL import Image

# ์ด๋ฏธ์ง€ ์—ด๊ธฐ
img = Image.open("์›๋ณธ์ด๋ฏธ์ง€.jpg")

# ๋ฐฉ๋ฒ• 1: ๊ฐ•์ œ ๋ฆฌ์‚ฌ์ด์ฆˆ (๋น„์œจ ๋ฌด์‹œ)
resized = img.resize((800, 600))
resized.save("๊ฒฐ๊ณผ_๊ฐ•์ œ.jpg")

# ๋ฐฉ๋ฒ• 2: ๋น„์œจ ์œ ์ง€ํ•˜๋ฉฐ ๋ฆฌ์‚ฌ์ด์ฆˆ (thumbnail)
img_copy = img.copy()
img_copy.thumbnail((800, 600), Image.LANCZOS)
img_copy.save("๊ฒฐ๊ณผ_๋น„์œจ์œ ์ง€.jpg")

# ๋ฐฉ๋ฒ• 3: ํ’ˆ์งˆ ์„ค์ •ํ•˜๋ฉฐ ์ €์žฅ (JPEG๋Š” 1~95)
resized.save("๊ฒฐ๊ณผ_๊ณ ํ’ˆ์งˆ.jpg", quality=90, optimize=True)

print(f"์›๋ณธ ํฌ๊ธฐ: {img.size}")
print(f"๋ณ€ํ™˜ ํ›„ ํฌ๊ธฐ: {resized.size}")

๐Ÿš€ ๋Œ€๋Ÿ‰ ์ด๋ฏธ์ง€ ๋ฆฌ์‚ฌ์ด์ฆˆ - ๊ธฐ๋ณธ ๋ฒ„์ „

์ด์ œ ์ง„์งœ ํ•ต์‹ฌ! ํด๋” ์•ˆ์˜ ๋ชจ๋“  ์ด๋ฏธ์ง€๋ฅผ ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌํ•˜๋Š” ์ฝ”๋“œ์ž„ ๐Ÿ‘‡

import os
from PIL import Image

# ===== ์„ค์ • ๋ถ€๋ถ„ (์—ฌ๊ธฐ๋งŒ ์ˆ˜์ •ํ•˜๋ฉด ๋จ!) =====
INPUT_FOLDER = "./์›๋ณธ์ด๋ฏธ์ง€"       # ์›๋ณธ ์ด๋ฏธ์ง€ ํด๋”
OUTPUT_FOLDER = "./๋ฆฌ์‚ฌ์ด์ฆˆ๊ฒฐ๊ณผ"    # ๊ฒฐ๊ณผ๋ฌผ ์ €์žฅ ํด๋”
TARGET_WIDTH = 800                  # ๋ชฉํ‘œ ๊ฐ€๋กœ ํฌ๊ธฐ
TARGET_HEIGHT = 600                 # ๋ชฉํ‘œ ์„ธ๋กœ ํฌ๊ธฐ
QUALITY = 85                        # JPEG ํ’ˆ์งˆ (1~95)
# ==========================================

# ์ง€์›ํ•˜๋Š” ์ด๋ฏธ์ง€ ํ™•์žฅ์ž
SUPPORTED_FORMATS = ('.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp', '.tiff')

def resize_images_bulk(input_folder, output_folder, width, height, quality=85):
    """ํด๋” ๋‚ด ๋ชจ๋“  ์ด๋ฏธ์ง€๋ฅผ ์ผ๊ด„ ๋ฆฌ์‚ฌ์ด์ฆˆ"""
    
    # ๊ฒฐ๊ณผ ํด๋” ์—†์œผ๋ฉด ์ž๋™ ์ƒ์„ฑ
    os.makedirs(output_folder, exist_ok=True)
    
    # ์ฒ˜๋ฆฌ ํ†ต๊ณ„
    success_count = 0
    fail_count = 0
    
    # ํด๋” ๋‚ด ํŒŒ์ผ ์ˆœํšŒ
    for filename in os.listdir(input_folder):
        # ์ด๋ฏธ์ง€ ํŒŒ์ผ๋งŒ ์ฒ˜๋ฆฌ
        if not filename.lower().endswith(SUPPORTED_FORMATS):
            continue
        
        input_path = os.path.join(input_folder, filename)
        output_path = os.path.join(output_folder, filename)
        
        try:
            with Image.open(input_path) as img:
                # RGBA(ํˆฌ๋ช…๋„) ์ด๋ฏธ์ง€๋Š” RGB๋กœ ๋ณ€ํ™˜ ํ›„ ์ €์žฅ
                if img.mode in ('RGBA', 'LA', 'P'):
                    img = img.convert('RGB')
                
                # ๋น„์œจ ์œ ์ง€ํ•˜๋ฉฐ ๋ฆฌ์‚ฌ์ด์ฆˆ
                img.thumbnail((width, height), Image.LANCZOS)
                
                # ์ €์žฅ (JPEG ํ’ˆ์งˆ ์„ค์ •)
                if filename.lower().endswith(('.jpg', '.jpeg')):
                    img.save(output_path, 'JPEG', quality=quality, optimize=True)
                else:
                    img.save(output_path)
                
                success_count += 1
                print(f"โœ… ์™„๋ฃŒ: {filename} โ†’ {img.size}")
                
        except Exception as e:
            fail_count += 1
            print(f"โŒ ์‹คํŒจ: {filename} - ์˜ค๋ฅ˜: {e}")
    
    print(f"\n๐ŸŽ‰ ์ฒ˜๋ฆฌ ์™„๋ฃŒ! ์„ฑ๊ณต: {success_count}์žฅ, ์‹คํŒจ: {fail_count}์žฅ")

# ์‹คํ–‰!
resize_images_bulk(INPUT_FOLDER, OUTPUT_FOLDER, TARGET_WIDTH, TARGET_HEIGHT, QUALITY)

โšก ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋”ฉ์œผ๋กœ ์†๋„ ์—…๊ทธ๋ ˆ์ด๋“œ!

์ด๋ฏธ์ง€๊ฐ€ ์ˆ˜๋ฐฑ ์žฅ์ด๋ฉด ์œ„ ์ฝ”๋“œ๋„ ์ข€ ๋А๋ฆด ์ˆ˜ ์žˆ์–ด์š”.
๋ฉ€ํ‹ฐ์Šค๋ ˆ๋”ฉ ์“ฐ๋ฉด CPU ์ฝ”์–ด๋ฅผ ์—ฌ๋Ÿฌ ๊ฐœ ํ™œ์šฉํ•ด์„œ ํ›จ์”ฌ ๋น ๋ฆ„! ๐Ÿ”ฅ

import os
from PIL import Image
from concurrent.futures import ThreadPoolExecutor, as_completed
import threading

INPUT_FOLDER = "./์›๋ณธ์ด๋ฏธ์ง€"
OUTPUT_FOLDER = "./๋ฆฌ์‚ฌ์ด์ฆˆ๊ฒฐ๊ณผ"
TARGET_SIZE = (800, 600)
QUALITY = 85
MAX_WORKERS = 8  # ๋™์‹œ ์ฒ˜๋ฆฌ ์Šค๋ ˆ๋“œ ์ˆ˜ (CPU ์ฝ”์–ด ์ˆ˜์— ๋งž๊ฒŒ ์กฐ์ •)

SUPPORTED_FORMATS = ('.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp', '.tiff')

# ์Šค๋ ˆ๋“œ ์•ˆ์ „ํ•œ ์นด์šดํ„ฐ
lock = threading.Lock()
success_count = 0
fail_count = 0

def process_single_image(args):
    """๋‹จ์ผ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜ (์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰๋จ)"""
    global success_count, fail_count
    input_path, output_path, filename = args
    
    try:
        with Image.open(input_path) as img:
            if img.mode in ('RGBA', 'LA', 'P'):
                img = img.convert('RGB')
            
            img.thumbnail(TARGET_SIZE, Image.LANCZOS)
            
            if filename.lower().endswith(('.jpg', '.jpeg')):
                img.save(output_path, 'JPEG', quality=QUALITY, optimize=True)
            else:
                img.save(output_path)
        
        with lock:
            success_count += 1
        return f"โœ… {filename}"
        
    except Exception as e:
        with lock:
            fail_count += 1
        return f"โŒ {filename}: {e}"

def bulk_resize_multithreaded():
    """๋ฉ€ํ‹ฐ์Šค๋ ˆ๋”ฉ ๋Œ€๋Ÿ‰ ๋ฆฌ์‚ฌ์ด์ฆˆ"""
    os.makedirs(OUTPUT_FOLDER, exist_ok=True)
    
    # ์ฒ˜๋ฆฌํ•  ํŒŒ์ผ ๋ชฉ๋ก ์ƒ์„ฑ
    tasks = []
    for filename in os.listdir(INPUT_FOLDER):
        if filename.lower().endswith(SUPPORTED_FORMATS):
            input_path = os.path.join(INPUT_FOLDER, filename)
            output_path = os.path.join(OUTPUT_FOLDER, filename)
            tasks.append((input_path, output_path, filename))
    
    print(f"๐Ÿ“ ์ด {len(tasks)}๊ฐœ ํŒŒ์ผ ์ฒ˜๋ฆฌ ์‹œ์ž‘...")
    
    # ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋”ฉ ์‹คํ–‰
    with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
        futures = [executor.submit(process_single_image, task) for task in tasks]
        
        for future in as_completed(futures):
            print(future.result())
    
    print(f"\n๐ŸŽ‰ ์™„๋ฃŒ! ์„ฑ๊ณต: {success_count}์žฅ, ์‹คํŒจ: {fail_count}์žฅ")

bulk_resize_multithreaded()

๐ŸŽฏ ์—ฌ๋Ÿฌ ์‚ฌ์ด์ฆˆ ๋™์‹œ ์ƒ์„ฑ (์‹ค์ „ ์ตœ๊ฐ• ๋ฒ„์ „!)

์›น ๊ฐœ๋ฐœํ•  ๋•Œ ์ง„์งœ ๋งŽ์ด ์“ฐ๋Š” ํŒจํ„ด์ž„.
์›๋ณธ ํ•˜๋‚˜๋กœ ์—ฌ๋Ÿฌ ์‚ฌ์ด์ฆˆ๋ฅผ ํ•œ ๋ฒˆ์— ๋ฝ‘์•„๋‚ด๋Š” ์ฝ”๋“œ! ๐Ÿ‘‡

import os
from PIL import Image
from pathlib import Path

INPUT_FOLDER = "./์›๋ณธ์ด๋ฏธ์ง€"
OUTPUT_FOLDER = "./๋ฆฌ์‚ฌ์ด์ฆˆ๊ฒฐ๊ณผ"

# ์ƒ์„ฑํ•  ์‚ฌ์ด์ฆˆ ํ”„๋ฆฌ์…‹ ์ •์˜
SIZE_PRESETS = {
    "thumbnail": (150, 150),    # ์ธ๋„ค์ผ
    "small": (320, 240),        # ์†Œํ˜•
    "medium": (640, 480),       # ์ค‘ํ˜•
    "large": (1280, 960),       # ๋Œ€ํ˜•
    "hd": (1920, 1080),         # HD
    "square_sm": (200, 200),    # ์ •์‚ฌ๊ฐํ˜• ์†Œ
    "square_lg": (500, 500),    # ์ •์‚ฌ๊ฐํ˜• ๋Œ€
}

SUPPORTED_FORMATS = ('.jpg', '.jpeg', '.png', '.bmp', '.webp')

def resize_to_multiple_sizes(input_folder, output_folder, presets):
    """์—ฌ๋Ÿฌ ์‚ฌ์ด์ฆˆ๋กœ ์ผ๊ด„ ๋ณ€ํ™˜"""
    
    # ๊ฐ ์‚ฌ์ด์ฆˆ๋ณ„ ํด๋” ์ƒ์„ฑ
    for preset_name in presets:
        os.makedirs(os.path.join(output_folder, preset_name), exist_ok=True)
    
    files = [f for f in os.listdir(input_folder) 
             if f.lower().endswith(SUPPORTED_FORMATS)]
    
    print(f"๐Ÿ“ {len(files)}๊ฐœ ํŒŒ์ผ ร— {len(presets)}๊ฐœ ์‚ฌ์ด์ฆˆ = {len(files)*len(presets)}๊ฐœ ์ƒ์„ฑ ์˜ˆ์ •")
    
    for i, filename in enumerate(files, 1):
        input_path = os.path.join(input_folder, filename)
        name_without_ext = Path(filename).stem
        ext = Path(filename).suffix
        
        print(f"\n[{i}/{len(files)}] ์ฒ˜๋ฆฌ ์ค‘: {filename}")
        
        try:
            with Image.open(input_path) as original:
                for preset_name, (width, height) in presets.items():
                    img = original.copy()
                    
                    if img.mode in ('RGBA', 'LA', 'P'):
                        img = img.convert('RGB')
                    
                    # ๋น„์œจ ์œ ์ง€ ๋ฆฌ์‚ฌ์ด์ฆˆ
                    img.thumbnail((width, height), Image.LANCZOS)
                    
                    # ํŒŒ์ผ๋ช…: ์›๋ณธ๋ช…_์‚ฌ์ด์ฆˆ.ํ™•์žฅ์ž
                    output_filename = f"{name_without_ext}_{preset_name}{ext}"
                    output_path = os.path.join(output_folder, preset_name, output_filename)
                    
                    if ext.lower() in ('.jpg', '.jpeg'):
                        img.save(output_path, 'JPEG', quality=85, optimize=True)
                    else:
                        img.save(output_path)
                    
                    print(f"  โœ… {preset_name}: {img.size}")
                    
        except Exception as e:
            print(f"  โŒ ์˜ค๋ฅ˜: {e}")
    
    print(f"\n๐ŸŽ‰ ๋ชจ๋“  ์ž‘์—… ์™„๋ฃŒ!")

resize_to_multiple_sizes(INPUT_FOLDER, OUTPUT_FOLDER, SIZE_PRESETS)
๐Ÿ’ก ์žฌ๋Šฅ๋„ท์—์„œ๋„ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ๊ด€๋ จ ํ”„๋ฆฌ๋žœ์„œ ์˜๋ขฐ๊ฐ€ ๊ฝค ๋งŽ์•„์š”!
์ด๋Ÿฐ ์ž๋™ํ™” ์Šคํฌ๋ฆฝํŠธ ํ•˜๋‚˜ ๋งŒ๋“ค์–ด๋‘๋ฉด ํด๋ผ์ด์–ธํŠธ ์ž‘์—… ์‹œ๊ฐ„์„ ์—„์ฒญ๋‚˜๊ฒŒ ๋‹จ์ถ•ํ•  ์ˆ˜ ์žˆ์–ด์„œ ๊ฒฝ์Ÿ๋ ฅ์ด ์ƒ๊น€ ใ…‹ใ…‹

๐Ÿ Python Pillow vs โšก Node.js Sharp ๐Ÿ Python + Pillow ์„ค์น˜: pip install Pillow ์ž…๋ฌธ์ž ์นœํ™”์ , ๋ฌธ์„œ ํ’๋ถ€ ๋‹ค์–‘ํ•œ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ๊ธฐ๋Šฅ ์†๋„: ๋ณดํ†ต (๋ฉ€ํ‹ฐ์Šค๋ ˆ๋”ฉ ํ•„์š”) โœ… ์ถ”์ฒœ: ๋ฒ”์šฉ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ โšก Node.js + Sharp ์„ค์น˜: npm install sharp libvips ๊ธฐ๋ฐ˜ ์ดˆ๊ณ ์† ์ฒ˜๋ฆฌ WebP, AVIF ๋ณ€ํ™˜ ๊ฐ•๋ ฅ ์ง€์› ์†๋„: ๋งค์šฐ ๋น ๋ฆ„ (๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ) โœ… ์ถ”์ฒœ: ์›น์„œ๋ฒ„ ์—ฐ๋™ ์ฒ˜๋ฆฌ VS ๋‘˜ ๋‹ค ํ›Œ๋ฅญํ•จ! ์ƒํ™ฉ์— ๋งž๊ฒŒ ์„ ํƒํ•˜๋ฉด ๋จ ใ…‹ใ…‹

โšก Node.js + Sharp๋กœ ์ดˆ๊ณ ์† ๋ฆฌ์‚ฌ์ด์ฆˆ

Sharp๋Š” libvips ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ธฐ๋ฐ˜์œผ๋กœ ๋งŒ๋“ค์–ด์ง„ Node.js ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ๋ชจ๋“ˆ์ž„.
Pillow๋ณด๋‹ค ํ›จ์”ฌ ๋น ๋ฅด๊ณ , ํŠนํžˆ WebPยทAVIF ๊ฐ™์€ ์ตœ์‹  ํฌ๋งท ์ฒ˜๋ฆฌ์— ๊ฐ•ํ•จ! ๐Ÿ”ฅ

์›น ์„œ๋ฒ„์—์„œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ด๋ฏธ์ง€ ๋ฆฌ์‚ฌ์ด์ฆˆํ•  ๋•Œ ํŠนํžˆ ๋งŽ์ด ์”€.
Next.js์˜ Image ์ปดํฌ๋„ŒํŠธ๋„ ๋‚ด๋ถ€์ ์œผ๋กœ Sharp ์”€ ใ…‹ใ…‹

๐Ÿ“ฆ ์„ค์น˜ ๋ฐฉ๋ฒ•

# npm์œผ๋กœ ์„ค์น˜
npm install sharp

# ๋˜๋Š” yarn
yarn add sharp

# ์„ค์น˜ ํ™•์ธ
node -e "const sharp = require('sharp'); console.log('Sharp ์„ค์น˜ ์™„๋ฃŒ!');"

๐Ÿ”ฐ ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•

const sharp = require('sharp');

// ๊ธฐ๋ณธ ๋ฆฌ์‚ฌ์ด์ฆˆ
sharp('์›๋ณธ์ด๋ฏธ์ง€.jpg')
  .resize(800, 600)           // ๊ฐ€๋กœ 800, ์„ธ๋กœ 600
  .toFile('๊ฒฐ๊ณผ.jpg')
  .then(info => console.log('์™„๋ฃŒ:', info))
  .catch(err => console.error('์˜ค๋ฅ˜:', err));

// ๋น„์œจ ์œ ์ง€ ๋ฆฌ์‚ฌ์ด์ฆˆ (fit ์˜ต์…˜)
sharp('์›๋ณธ์ด๋ฏธ์ง€.jpg')
  .resize(800, 600, {
    fit: 'inside',            // ๋น„์œจ ์œ ์ง€, ์•ˆ์ชฝ์— ๋งž์ถค
    withoutEnlargement: true  // ์›๋ณธ๋ณด๋‹ค ํฌ๊ฒŒ ํ™•๋Œ€ ์•ˆ ํ•จ
  })
  .jpeg({ quality: 85 })     // JPEG ํ’ˆ์งˆ ์„ค์ •
  .toFile('๊ฒฐ๊ณผ_๋น„์œจ์œ ์ง€.jpg');

// WebP๋กœ ๋ณ€ํ™˜ํ•˜๋ฉฐ ๋ฆฌ์‚ฌ์ด์ฆˆ
sharp('์›๋ณธ์ด๋ฏธ์ง€.jpg')
  .resize(800)                // ๊ฐ€๋กœ๋งŒ ์ง€์ • (์„ธ๋กœ๋Š” ๋น„์œจ ์ž๋™)
  .webp({ quality: 80 })
  .toFile('๊ฒฐ๊ณผ.webp');

๐Ÿš€ ๋Œ€๋Ÿ‰ ์ฒ˜๋ฆฌ - Promise.all ํ™œ์šฉ

const sharp = require('sharp');
const fs = require('fs');
const path = require('path');

// ===== ์„ค์ • =====
const INPUT_DIR = './์›๋ณธ์ด๋ฏธ์ง€';
const OUTPUT_DIR = './๋ฆฌ์‚ฌ์ด์ฆˆ๊ฒฐ๊ณผ';
const TARGET_WIDTH = 800;
const TARGET_HEIGHT = 600;
const JPEG_QUALITY = 85;
// ================

const SUPPORTED_EXTS = ['.jpg', '.jpeg', '.png', '.webp', '.tiff', '.bmp'];

async function resizeSingleImage(filename) {
  const inputPath = path.join(INPUT_DIR, filename);
  const outputPath = path.join(OUTPUT_DIR, filename);
  const ext = path.extname(filename).toLowerCase();
  
  try {
    let pipeline = sharp(inputPath)
      .resize(TARGET_WIDTH, TARGET_HEIGHT, {
        fit: 'inside',
        withoutEnlargement: true
      });
    
    // ํ™•์žฅ์ž์— ๋”ฐ๋ผ ์ถœ๋ ฅ ํฌ๋งท ์„ค์ •
    if (ext === '.jpg' || ext === '.jpeg') {
      pipeline = pipeline.jpeg({ quality: JPEG_QUALITY, mozjpeg: true });
    } else if (ext === '.png') {
      pipeline = pipeline.png({ compressionLevel: 8 });
    } else if (ext === '.webp') {
      pipeline = pipeline.webp({ quality: JPEG_QUALITY });
    }
    
    const info = await pipeline.toFile(outputPath);
    return { success: true, filename, info };
    
  } catch (error) {
    return { success: false, filename, error: error.message };
  }
}

async function bulkResize() {
  // ์ถœ๋ ฅ ํด๋” ์ƒ์„ฑ
  if (!fs.existsSync(OUTPUT_DIR)) {
    fs.mkdirSync(OUTPUT_DIR, { recursive: true });
  }
  
  // ์ด๋ฏธ์ง€ ํŒŒ์ผ ๋ชฉ๋ก ํ•„ํ„ฐ๋ง
  const files = fs.readdirSync(INPUT_DIR)
    .filter(f => SUPPORTED_EXTS.includes(path.extname(f).toLowerCase()));
  
  console.log(`๐Ÿ“ ์ด ${files.length}๊ฐœ ํŒŒ์ผ ์ฒ˜๋ฆฌ ์‹œ์ž‘...`);
  
  // ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ (ํ•œ ๋ฒˆ์— ๋„ˆ๋ฌด ๋งŽ์ด ์—ด๋ฉด ๋ฉ”๋ชจ๋ฆฌ ๋ถ€์กฑ ๊ฐ€๋Šฅ)
  const BATCH_SIZE = 20;
  let successCount = 0;
  let failCount = 0;
  
  for (let i = 0; i < files.length; i += BATCH_SIZE) {
    const batch = files.slice(i, i + BATCH_SIZE);
    const results = await Promise.all(batch.map(resizeSingleImage));
    
    results.forEach(result => {
      if (result.success) {
        successCount++;
        console.log(`โœ… ${result.filename} (${result.info.width}x${result.info.height})`);
      } else {
        failCount++;
        console.log(`โŒ ${result.filename}: ${result.error}`);
      }
    });
    
    console.log(`์ง„ํ–‰: ${Math.min(i + BATCH_SIZE, files.length)}/${files.length}`);
  }
  
  console.log(`\n๐ŸŽ‰ ์™„๋ฃŒ! ์„ฑ๊ณต: ${successCount}์žฅ, ์‹คํŒจ: ${failCount}์žฅ`);
}

bulkResize();

๐ŸŽฏ ์—ฌ๋Ÿฌ ์‚ฌ์ด์ฆˆ + ํฌ๋งท ๋™์‹œ ๋ณ€ํ™˜ (Sharp ๊ณ ๊ธ‰)

const sharp = require('sharp');
const fs = require('fs');
const path = require('path');

const INPUT_DIR = './์›๋ณธ์ด๋ฏธ์ง€';
const OUTPUT_DIR = './๋ฆฌ์‚ฌ์ด์ฆˆ๊ฒฐ๊ณผ';

// ์ถœ๋ ฅ ํ”„๋ฆฌ์…‹ ์ •์˜
const OUTPUT_PRESETS = [
  { name: 'thumbnail', width: 150, height: 150, fit: 'cover', format: 'webp', quality: 80 },
  { name: 'small',     width: 320, height: null, format: 'webp', quality: 80 },
  { name: 'medium',    width: 640, height: null, format: 'jpeg', quality: 85 },
  { name: 'large',     width: 1280, height: null, format: 'jpeg', quality: 90 },
  { name: 'og_image',  width: 1200, height: 630, fit: 'cover', format: 'jpeg', quality: 90 },
];

async function processImageAllPresets(filename) {
  const inputPath = path.join(INPUT_DIR, filename);
  const nameWithoutExt = path.basename(filename, path.extname(filename));
  const results = [];
  
  for (const preset of OUTPUT_PRESETS) {
    const outputFilename = `${nameWithoutExt}_${preset.name}.${preset.format}`;
    const outputDir = path.join(OUTPUT_DIR, preset.name);
    const outputPath = path.join(outputDir, outputFilename);
    
    // ํด๋” ์ƒ์„ฑ
    fs.mkdirSync(outputDir, { recursive: true });
    
    try {
      let pipeline = sharp(inputPath).resize(preset.width, preset.height, {
        fit: preset.fit || 'inside',
        withoutEnlargement: true,
        position: 'center'
      });
      
      if (preset.format === 'webp') {
        pipeline = pipeline.webp({ quality: preset.quality });
      } else if (preset.format === 'jpeg') {
        pipeline = pipeline.jpeg({ quality: preset.quality, mozjpeg: true });
      } else if (preset.format === 'avif') {
        pipeline = pipeline.avif({ quality: preset.quality });
      }
      
      const info = await pipeline.toFile(outputPath);
      results.push({ preset: preset.name, success: true, size: `${info.width}x${info.height}`, bytes: info.size });
      
    } catch (err) {
      results.push({ preset: preset.name, success: false, error: err.message });
    }
  }
  
  return { filename, results };
}

async function main() {
  const SUPPORTED_EXTS = ['.jpg', '.jpeg', '.png', '.webp', '.tiff'];
  const files = fs.readdirSync(INPUT_DIR)
    .filter(f => SUPPORTED_EXTS.includes(path.extname(f).toLowerCase()));
  
  console.log(`๐Ÿš€ ${files.length}๊ฐœ ํŒŒ์ผ ร— ${OUTPUT_PRESETS.length}๊ฐœ ํ”„๋ฆฌ์…‹ ์ฒ˜๋ฆฌ ์‹œ์ž‘!`);
  
  for (const file of files) {
    const result = await processImageAllPresets(file);
    console.log(`\n๐Ÿ“ธ ${result.filename}`);
    result.results.forEach(r => {
      if (r.success) {
        console.log(`  โœ… ${r.preset}: ${r.size} (${(r.bytes/1024).toFixed(1)}KB)`);
      } else {
        console.log(`  โŒ ${r.preset}: ${r.error}`);
      }
    });
  }
  
  console.log('\n๐ŸŽ‰ ์ „์ฒด ์™„๋ฃŒ!');
}

main();

๐Ÿ–ฅ๏ธ Bash + ImageMagick์œผ๋กœ ์ปค๋งจ๋“œ๋ผ์ธ ์ž๋™ํ™”

Python์ด๋‚˜ Node.js ์—†์ด ํ„ฐ๋ฏธ๋„ ๋ช…๋ น์–ด๋งŒ์œผ๋กœ๋„ ๋Œ€๋Ÿ‰ ๋ฆฌ์‚ฌ์ด์ฆˆ ๊ฐ€๋Šฅํ•จ!
Linux/Mac ์„œ๋ฒ„ ํ™˜๊ฒฝ์—์„œ ํŠนํžˆ ์œ ์šฉํ•˜๊ณ , cron job์ด๋ž‘ ์—ฐ๋™ํ•˜๋ฉด ์Šค์ผ€์ค„ ์ž๋™ํ™”๋„ ๋จ ๐Ÿ‘

๐Ÿ“ฆ ImageMagick ์„ค์น˜

# Ubuntu/Debian
sudo apt-get install imagemagick

# macOS (Homebrew)
brew install imagemagick

# ์„ค์น˜ ํ™•์ธ
convert --version
# ๋˜๋Š” ์ตœ์‹  ๋ฒ„์ „์€
magick --version

๐Ÿ”ฐ ๊ธฐ๋ณธ ๋ช…๋ น์–ด

# ๋‹จ์ผ ์ด๋ฏธ์ง€ ๋ฆฌ์‚ฌ์ด์ฆˆ (๋น„์œจ ์œ ์ง€)
convert ์›๋ณธ.jpg -resize 800x600 ๊ฒฐ๊ณผ.jpg

# ๊ฐ•์ œ ๋ฆฌ์‚ฌ์ด์ฆˆ (๋น„์œจ ๋ฌด์‹œ, ! ์‚ฌ์šฉ)
convert ์›๋ณธ.jpg -resize 800x600! ๊ฒฐ๊ณผ.jpg

# ๊ฐ€๋กœ๋งŒ ์ง€์ • (์„ธ๋กœ ์ž๋™)
convert ์›๋ณธ.jpg -resize 800 ๊ฒฐ๊ณผ.jpg

# ์„ธ๋กœ๋งŒ ์ง€์ • (๊ฐ€๋กœ ์ž๋™)
convert ์›๋ณธ.jpg -resize x600 ๊ฒฐ๊ณผ.jpg

# ํ’ˆ์งˆ ์„ค์ •
convert ์›๋ณธ.jpg -resize 800x600 -quality 85 ๊ฒฐ๊ณผ.jpg

# WebP๋กœ ๋ณ€ํ™˜
convert ์›๋ณธ.jpg -resize 800x600 -quality 80 ๊ฒฐ๊ณผ.webp

๐Ÿš€ Bash ์Šคํฌ๋ฆฝํŠธ - ๋Œ€๋Ÿ‰ ์ฒ˜๋ฆฌ

#!/bin/bash
# bulk_resize.sh - ๋Œ€๋Ÿ‰ ์ด๋ฏธ์ง€ ๋ฆฌ์‚ฌ์ด์ฆˆ ์Šคํฌ๋ฆฝํŠธ

# ===== ์„ค์ • =====
INPUT_DIR="./์›๋ณธ์ด๋ฏธ์ง€"
OUTPUT_DIR="./๋ฆฌ์‚ฌ์ด์ฆˆ๊ฒฐ๊ณผ"
TARGET_SIZE="800x600"
QUALITY=85
# ================

# ๊ฒฐ๊ณผ ํด๋” ์ƒ์„ฑ
mkdir -p "$OUTPUT_DIR"

# ์นด์šดํ„ฐ ์ดˆ๊ธฐํ™”
SUCCESS=0
FAIL=0

echo "๐Ÿš€ ๋Œ€๋Ÿ‰ ๋ฆฌ์‚ฌ์ด์ฆˆ ์‹œ์ž‘..."
echo "์ž…๋ ฅ: $INPUT_DIR โ†’ ์ถœ๋ ฅ: $OUTPUT_DIR"
echo "๋ชฉํ‘œ ํฌ๊ธฐ: $TARGET_SIZE"
echo "================================"

# ์ง€์› ํฌ๋งท ์ฒ˜๋ฆฌ
for ext in jpg jpeg png bmp gif webp tiff; do
    for file in "$INPUT_DIR"/*.$ext "$INPUT_DIR"/*.${ext^^}; do
        # ํŒŒ์ผ์ด ์‹ค์ œ๋กœ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ
        [ -f "$file" ] || continue
        
        filename=$(basename "$file")
        output_path="$OUTPUT_DIR/$filename"
        
        # ImageMagick์œผ๋กœ ๋ฆฌ์‚ฌ์ด์ฆˆ
        if convert "$file" \
            -resize "$TARGET_SIZE" \
            -quality "$QUALITY" \
            -strip \
            "$output_path" 2>/dev/null; then
            
            echo "โœ… ์™„๋ฃŒ: $filename"
            ((SUCCESS++))
        else
            echo "โŒ ์‹คํŒจ: $filename"
            ((FAIL++))
        fi
    done
done

echo "================================"
echo "๐ŸŽ‰ ์™„๋ฃŒ! ์„ฑ๊ณต: ${SUCCESS}์žฅ, ์‹คํŒจ: ${FAIL}์žฅ"
# ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ ๊ถŒํ•œ ๋ถ€์—ฌ ํ›„ ์‹คํ–‰
chmod +x bulk_resize.sh
./bulk_resize.sh

# ๋˜๋Š” mogrify ๋ช…๋ น์–ด๋กœ ๋” ๊ฐ„๋‹จํ•˜๊ฒŒ! (์›๋ณธ ๋ฎ์–ด์“ฐ๊ธฐ ์ฃผ์˜!)
# ์›๋ณธ ํด๋” ๋ณต์‚ฌ ํ›„ ์‚ฌ์šฉ ๊ถŒ์žฅ
cp -r ./์›๋ณธ์ด๋ฏธ์ง€ ./๋ฆฌ์‚ฌ์ด์ฆˆ๊ฒฐ๊ณผ
mogrify -resize 800x600 -quality 85 ./๋ฆฌ์‚ฌ์ด์ฆˆ๊ฒฐ๊ณผ/*.jpg

# ์—ฌ๋Ÿฌ ํฌ๋งท ํ•œ ๋ฒˆ์—
mogrify -resize 800x600 -quality 85 ./๋ฆฌ์‚ฌ์ด์ฆˆ๊ฒฐ๊ณผ/*.{jpg,jpeg,png}
โš ๏ธ mogrify ์‚ฌ์šฉ ์‹œ ์ฃผ์˜! mogrify๋Š” ์›๋ณธ ํŒŒ์ผ์„ ์ง์ ‘ ์ˆ˜์ •ํ•จ!
๋ฐ˜๋“œ์‹œ ์›๋ณธ ํด๋”๋ฅผ ๋ฐฑ์—…ํ•˜๊ฑฐ๋‚˜ ๋ณต์‚ฌํ•œ ํ›„์— ์‚ฌ์šฉํ•˜์„ธ์š”.
์‹ค์ˆ˜๋กœ ์›๋ณธ ๋‚ ๋ฆฌ๋ฉด ๋ณต๊ตฌ ๋ถˆ๊ฐ€๋Šฅ ใ… ใ… 

๐Ÿ”ง ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ - ์›Œํ„ฐ๋งˆํฌ, ํฌ๋งท ๋ณ€ํ™˜, EXIF ์ฒ˜๋ฆฌ

๐Ÿ’ง ์›Œํ„ฐ๋งˆํฌ ์ž๋™ ์‚ฝ์ž… (Python)

๋ฆฌ์‚ฌ์ด์ฆˆํ•˜๋ฉด์„œ ์›Œํ„ฐ๋งˆํฌ๊นŒ์ง€ ํ•œ ๋ฒˆ์— ๋ฐ•์•„๋ฒ„๋ฆฌ๋Š” ์ฝ”๋“œ! ์‡ผํ•‘๋ชฐ์—์„œ ์ง„์งœ ๋งŽ์ด ์”€ ใ…‹ใ…‹

from PIL import Image, ImageDraw, ImageFont
import os

def add_watermark_and_resize(input_path, output_path, 
                              target_size=(800, 600),
                              watermark_text="ยฉ MyBrand",
                              opacity=128):
    """๋ฆฌ์‚ฌ์ด์ฆˆ + ์›Œํ„ฐ๋งˆํฌ ๋™์‹œ ์ฒ˜๋ฆฌ"""
    
    with Image.open(input_path) as img:
        # RGB ๋ณ€ํ™˜
        if img.mode != 'RGB':
            img = img.convert('RGB')
        
        # ๋ฆฌ์‚ฌ์ด์ฆˆ
        img.thumbnail(target_size, Image.LANCZOS)
        
        # ์›Œํ„ฐ๋งˆํฌ ๋ ˆ์ด์–ด ์ƒ์„ฑ (ํˆฌ๋ช…)
        watermark_layer = Image.new('RGBA', img.size, (0, 0, 0, 0))
        draw = ImageDraw.Draw(watermark_layer)
        
        # ํฐํŠธ ์„ค์ • (์‹œ์Šคํ…œ ํฐํŠธ ๋˜๋Š” ๊ธฐ๋ณธ ํฐํŠธ)
        try:
            font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 
                                       size=max(20, img.width // 20))
        except:
            font = ImageFont.load_default()
        
        # ํ…์ŠคํŠธ ํฌ๊ธฐ ๊ณ„์‚ฐ
        bbox = draw.textbbox((0, 0), watermark_text, font=font)
        text_width = bbox[2] - bbox[0]
        text_height = bbox[3] - bbox[1]
        
        # ์šฐ์ธก ํ•˜๋‹จ์— ์›Œํ„ฐ๋งˆํฌ ๋ฐฐ์น˜
        margin = 10
        x = img.width - text_width - margin
        y = img.height - text_height - margin
        
        # ๊ทธ๋ฆผ์ž ํšจ๊ณผ (๊ฐ€๋…์„ฑ ํ–ฅ์ƒ)
        draw.text((x+2, y+2), watermark_text, font=font, fill=(0, 0, 0, opacity))
        draw.text((x, y), watermark_text, font=font, fill=(255, 255, 255, opacity))
        
        # ์›Œํ„ฐ๋งˆํฌ ํ•ฉ์„ฑ
        img_rgba = img.convert('RGBA')
        img_rgba = Image.alpha_composite(img_rgba, watermark_layer)
        final_img = img_rgba.convert('RGB')
        
        # ์ €์žฅ
        final_img.save(output_path, 'JPEG', quality=85, optimize=True)
        print(f"โœ… ์™„๋ฃŒ: {os.path.basename(output_path)}")

# ํด๋” ์ „์ฒด ์ฒ˜๋ฆฌ
input_folder = "./์›๋ณธ์ด๋ฏธ์ง€"
output_folder = "./์›Œํ„ฐ๋งˆํฌ๊ฒฐ๊ณผ"
os.makedirs(output_folder, exist_ok=True)

for filename in os.listdir(input_folder):
    if filename.lower().endswith(('.jpg', '.jpeg', '.png')):
        add_watermark_and_resize(
            os.path.join(input_folder, filename),
            os.path.join(output_folder, filename),
            target_size=(1000, 800),
            watermark_text="ยฉ 2024 MyShop"
        )

๐Ÿ“‹ EXIF ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ

์Šค๋งˆํŠธํฐ์œผ๋กœ ์ฐ์€ ์‚ฌ์ง„์€ EXIF ๋ฐ์ดํ„ฐ์— ํšŒ์ „ ์ •๋ณด๊ฐ€ ๋“ค์–ด์žˆ์–ด์„œ
๊ทธ๋ƒฅ ๋ฆฌ์‚ฌ์ด์ฆˆํ•˜๋ฉด ์ด๋ฏธ์ง€๊ฐ€ ์˜†์œผ๋กœ ๋ˆ•๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Œ ใ…‹ใ…‹ ์ด๊ฑฐ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•จ!

from PIL import Image, ExifTags
import os

def fix_rotation_and_resize(input_path, output_path, target_size=(800, 600)):
    """EXIF ํšŒ์ „ ์ •๋ณด ๋ฐ˜์˜ + ๋ฆฌ์‚ฌ์ด์ฆˆ"""
    
    with Image.open(input_path) as img:
        # EXIF ํšŒ์ „ ์ •๋ณด ์ž๋™ ์ˆ˜์ •
        try:
            # EXIF ํƒœ๊ทธ์—์„œ Orientation ์ฐพ๊ธฐ
            exif = img._getexif()
            if exif:
                for tag_id, value in exif.items():
                    tag = ExifTags.TAGS.get(tag_id, tag_id)
                    if tag == 'Orientation':
                        if value == 3:
                            img = img.rotate(180, expand=True)
                        elif value == 6:
                            img = img.rotate(270, expand=True)
                        elif value == 8:
                            img = img.rotate(90, expand=True)
                        break
        except (AttributeError, Exception):
            pass  # EXIF ์—†์œผ๋ฉด ๊ทธ๋ƒฅ ์ง„ํ–‰
        
        # RGB ๋ณ€ํ™˜
        if img.mode != 'RGB':
            img = img.convert('RGB')
        
        # ๋ฆฌ์‚ฌ์ด์ฆˆ
        img.thumbnail(target_size, Image.LANCZOS)
        
        # EXIF ์ œ๊ฑฐํ•˜๊ณ  ์ €์žฅ (๊ฐœ์ธ์ •๋ณด ๋ณดํ˜ธ + ํŒŒ์ผ ํฌ๊ธฐ ๊ฐ์†Œ)
        img.save(output_path, 'JPEG', quality=85, optimize=True)
        print(f"โœ… {os.path.basename(input_path)} โ†’ {img.size}")

# ์‹คํ–‰
input_folder = "./์›๋ณธ์ด๋ฏธ์ง€"
output_folder = "./์ฒ˜๋ฆฌ๊ฒฐ๊ณผ"
os.makedirs(output_folder, exist_ok=True)

for filename in os.listdir(input_folder):
    if filename.lower().endswith(('.jpg', '.jpeg')):
        fix_rotation_and_resize(
            os.path.join(input_folder, filename),
            os.path.join(output_folder, filename)
        )

๐Ÿ”„ ํฌ๋งท ์ผ๊ด„ ๋ณ€ํ™˜ (PNG โ†’ WebP)

from PIL import Image
import os
from pathlib import Path

def convert_to_webp_bulk(input_folder, output_folder, quality=80, resize_width=None):
    """๋ชจ๋“  ์ด๋ฏธ์ง€๋ฅผ WebP๋กœ ๋ณ€ํ™˜ (์„ ํƒ์  ๋ฆฌ์‚ฌ์ด์ฆˆ ํฌํ•จ)"""
    
    os.makedirs(output_folder, exist_ok=True)
    
    supported = ('.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.gif')
    files = [f for f in os.listdir(input_folder) 
             if f.lower().endswith(supported)]
    
    print(f"๐Ÿ“ {len(files)}๊ฐœ ํŒŒ์ผ์„ WebP๋กœ ๋ณ€ํ™˜ ์‹œ์ž‘...")
    
    total_original_size = 0
    total_webp_size = 0
    
    for filename in files:
        input_path = os.path.join(input_folder, filename)
        # ํ™•์žฅ์ž๋ฅผ .webp๋กœ ๋ณ€๊ฒฝ
        output_filename = Path(filename).stem + '.webp'
        output_path = os.path.join(output_folder, output_filename)
        
        original_size = os.path.getsize(input_path)
        
        try:
            with Image.open(input_path) as img:
                if img.mode in ('RGBA', 'LA'):
                    # ํˆฌ๋ช…๋„ ์žˆ๋Š” ์ด๋ฏธ์ง€๋Š” RGBA ์œ ์ง€
                    pass
                else:
                    img = img.convert('RGB')
                
                # ์„ ํƒ์  ๋ฆฌ์‚ฌ์ด์ฆˆ
                if resize_width:
                    ratio = resize_width / img.width
                    new_height = int(img.height * ratio)
                    img = img.resize((resize_width, new_height), Image.LANCZOS)
                
                img.save(output_path, 'WEBP', quality=quality, method=6)
                
                webp_size = os.path.getsize(output_path)
                total_original_size += original_size
                total_webp_size += webp_size
                
                reduction = (1 - webp_size/original_size) * 100
                print(f"โœ… {filename} โ†’ {output_filename}")
                print(f"   {original_size//1024}KB โ†’ {webp_size//1024}KB ({reduction:.1f}% ๊ฐ์†Œ)")
                
        except Exception as e:
            print(f"โŒ {filename}: {e}")
    
    total_reduction = (1 - total_webp_size/total_original_size) * 100 if total_original_size > 0 else 0
    print(f"\n๐Ÿ“Š ์ „์ฒด ํ†ต๊ณ„:")
    print(f"   ์›๋ณธ ํ•ฉ๊ณ„: {total_original_size//1024//1024}MB")
    print(f"   ๋ณ€ํ™˜ ํ›„: {total_webp_size//1024//1024}MB")
    print(f"   ์ด ์ ˆ์•ฝ: {total_reduction:.1f}%")

convert_to_webp_bulk("./์›๋ณธ์ด๋ฏธ์ง€", "./webp๊ฒฐ๊ณผ", quality=80, resize_width=1200)

โšก ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ๋ฐฉ์‹๋ณ„ ์„ฑ๋Šฅ ๋น„๊ต (100์žฅ ๊ธฐ์ค€) 0์ดˆ 5์ดˆ 10์ดˆ 15์ดˆ 20์ดˆ 18์ดˆ Pillow ๋‹จ์ผ์Šค๋ ˆ๋“œ 6์ดˆ Pillow ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ 2.5์ดˆ Sharp ๋น„๋™๊ธฐ 8์ดˆ ImageMagick mogrify 3์ดˆ ImageMagick ๋ณ‘๋ ฌ์ฒ˜๋ฆฌ ๐Ÿ‘‘ * ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์Œ. ์ผ๋ฐ˜์ ์ธ JPEG 100์žฅ ๊ธฐ์ค€ ๋Œ€๋žต์ ์ธ ์ˆ˜์น˜

๐Ÿ—‚๏ธ ์‹ค์ „ ํ”„๋กœ์ ํŠธ ํด๋” ๊ตฌ์กฐ ์„ค๊ณ„

์ฝ”๋“œ๋งŒ ์ž˜ ์งœ๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ ํด๋” ๊ตฌ์กฐ๋„ ์ž˜ ์„ค๊ณ„ํ•ด์•ผ ๋‚˜์ค‘์— ๊ด€๋ฆฌ๊ฐ€ ํŽธํ•จ!
์‹ค๋ฌด์—์„œ ๋งŽ์ด ์“ฐ๋Š” ๊ตฌ์กฐ ์•Œ๋ ค๋“œ๋ฆผ ๐Ÿ‘‡

image-resize-project/
โ”‚
โ”œโ”€โ”€ ๐Ÿ“ input/                    # ์›๋ณธ ์ด๋ฏธ์ง€ ๋ณด๊ด€
โ”‚   โ”œโ”€โ”€ products/                # ์ƒํ’ˆ ์ด๋ฏธ์ง€
โ”‚   โ”œโ”€โ”€ banners/                 # ๋ฐฐ๋„ˆ ์ด๋ฏธ์ง€
โ”‚   โ””โ”€โ”€ profiles/                # ํ”„๋กœํ•„ ์ด๋ฏธ์ง€
โ”‚
โ”œโ”€โ”€ ๐Ÿ“ output/                   # ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ๋ฌผ
โ”‚   โ”œโ”€โ”€ products/
โ”‚   โ”‚   โ”œโ”€โ”€ thumbnail/           # 150x150
โ”‚   โ”‚   โ”œโ”€โ”€ small/               # 320x320
โ”‚   โ”‚   โ”œโ”€โ”€ medium/              # 640x640
โ”‚   โ”‚   โ””โ”€โ”€ large/               # 1200x1200
โ”‚   โ”œโ”€โ”€ banners/
โ”‚   โ”‚   โ”œโ”€โ”€ desktop/             # 1920x600
โ”‚   โ”‚   โ”œโ”€โ”€ tablet/              # 1024x320
โ”‚   โ”‚   โ””โ”€โ”€ mobile/              # 640x200
โ”‚   โ””โ”€โ”€ profiles/
โ”‚       โ”œโ”€โ”€ small/               # 50x50
โ”‚       โ””โ”€โ”€ large/               # 200x200
โ”‚
โ”œโ”€โ”€ ๐Ÿ“ config/
โ”‚   โ””โ”€โ”€ presets.json             # ์‚ฌ์ด์ฆˆ ํ”„๋ฆฌ์…‹ ์„ค์ •
โ”‚
โ”œโ”€โ”€ ๐Ÿ“ logs/
โ”‚   โ””โ”€โ”€ resize_log.txt           # ์ฒ˜๋ฆฌ ๋กœ๊ทธ
โ”‚
โ”œโ”€โ”€ ๐Ÿ“„ resize_main.py            # ๋ฉ”์ธ ์‹คํ–‰ ํŒŒ์ผ
โ”œโ”€โ”€ ๐Ÿ“„ utils.py                  # ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜
โ”œโ”€โ”€ ๐Ÿ“„ requirements.txt          # ์˜์กด์„ฑ ๋ชฉ๋ก
โ””โ”€โ”€ ๐Ÿ“„ README.md                 # ์‚ฌ์šฉ๋ฒ• ๋ฌธ์„œ

โš™๏ธ JSON ์„ค์ • ํŒŒ์ผ๋กœ ํ”„๋ฆฌ์…‹ ๊ด€๋ฆฌ

ํ•˜๋“œ์ฝ”๋”ฉ ๋Œ€์‹  JSON ํŒŒ์ผ๋กœ ์„ค์ • ๊ด€๋ฆฌํ•˜๋ฉด ์ฝ”๋“œ ์ˆ˜์ • ์—†์ด ํ”„๋ฆฌ์…‹ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ!

// config/presets.json
{
  "global": {
    "quality": 85,
    "format": "jpeg",
    "strip_exif": true
  },
  "presets": {
    "products": [
      {"name": "thumbnail", "width": 150, "height": 150, "fit": "cover"},
      {"name": "small",     "width": 320, "height": 320, "fit": "cover"},
      {"name": "medium",    "width": 640, "height": 640, "fit": "cover"},
      {"name": "large",     "width": 1200, "height": 1200, "fit": "inside"}
    ],
    "banners": [
      {"name": "desktop",  "width": 1920, "height": 600, "fit": "cover"},
      {"name": "tablet",   "width": 1024, "height": 320, "fit": "cover"},
      {"name": "mobile",   "width": 640,  "height": 200, "fit": "cover"}
    ],
    "profiles": [
      {"name": "small", "width": 50,  "height": 50,  "fit": "cover"},
      {"name": "large", "width": 200, "height": 200, "fit": "cover"}
    ]
  }
}
import json
import os
from PIL import Image

def load_config(config_path="./config/presets.json"):
    """JSON ์„ค์ • ํŒŒ์ผ ๋กœ๋“œ"""
    with open(config_path, 'r', encoding='utf-8') as f:
        return json.load(f)

def process_by_config(input_folder, output_base, preset_name, config):
    """์„ค์ • ํŒŒ์ผ ๊ธฐ๋ฐ˜ ์ฒ˜๋ฆฌ"""
    
    global_config = config['global']
    presets = config['presets'].get(preset_name, [])
    
    if not presets:
        print(f"โŒ ํ”„๋ฆฌ์…‹ '{preset_name}'์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ!")
        return
    
    supported = ('.jpg', '.jpeg', '.png', '.webp', '.bmp')
    files = [f for f in os.listdir(input_folder) 
             if f.lower().endswith(supported)]
    
    print(f"๐Ÿ“ '{preset_name}' ํ”„๋ฆฌ์…‹์œผ๋กœ {len(files)}๊ฐœ ํŒŒ์ผ ์ฒ˜๋ฆฌ")
    
    for filename in files:
        input_path = os.path.join(input_folder, filename)
        
        with Image.open(input_path) as img:
            if img.mode != 'RGB':
                img = img.convert('RGB')
            
            for preset in presets:
                output_dir = os.path.join(output_base, preset_name, preset['name'])
                os.makedirs(output_dir, exist_ok=True)
                output_path = os.path.join(output_dir, filename)
                
                img_copy = img.copy()
                fit = preset.get('fit', 'inside')
                
                if fit == 'cover':
                    # cover: ๋น„์œจ ์œ ์ง€ํ•˜๋ฉฐ ์˜์—ญ ๊ฝ‰ ์ฑ„์šฐ๊ธฐ
                    img_copy = img_copy.resize(
                        (preset['width'], preset['height']), 
                        Image.LANCZOS
                    )
                else:
                    # inside: ๋น„์œจ ์œ ์ง€ํ•˜๋ฉฐ ์•ˆ์ชฝ์— ๋งž์ถค
                    img_copy.thumbnail(
                        (preset['width'], preset['height']), 
                        Image.LANCZOS
                    )
                
                quality = preset.get('quality', global_config['quality'])
                img_copy.save(output_path, 'JPEG', quality=quality, optimize=True)
        
        print(f"  โœ… {filename} ์ฒ˜๋ฆฌ ์™„๋ฃŒ")

# ์‹คํ–‰
config = load_config()
process_by_config("./input/products", "./output", "products", config)
process_by_config("./input/banners", "./output", "banners", config)

๐Ÿ›ก๏ธ ์—๋Ÿฌ ํ•ธ๋“ค๋ง & ๋กœ๊น… - ์‹ค๋ฌด์—์„œ ํ•„์ˆ˜!

์ฝ”๋“œ ์งค ๋•Œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ์•ˆ ํ•˜๋ฉด ๋‚˜์ค‘์— ์ง„์งœ ๊ณ ์ƒํ•จ ใ…‹ใ…‹
ํŠนํžˆ ๋Œ€๋Ÿ‰ ์ฒ˜๋ฆฌํ•  ๋•Œ ์ค‘๊ฐ„์— ํ•˜๋‚˜ ์˜ค๋ฅ˜๋‚˜๋ฉด ์ „์ฒด๊ฐ€ ๋ฉˆ์ถ”๋Š” ์ƒํ™ฉ ๋ฐฉ์ง€ํ•ด์•ผ ํ•ด์š”!

import os
import logging
import json
from PIL import Image
from datetime import datetime
from pathlib import Path

# ===== ๋กœ๊น… ์„ค์ • =====
def setup_logger(log_dir="./logs"):
    """๋กœ๊ฑฐ ์„ค์ •"""
    os.makedirs(log_dir, exist_ok=True)
    
    log_filename = f"resize_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
    log_path = os.path.join(log_dir, log_filename)
    
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s [%(levelname)s] %(message)s',
        handlers=[
            logging.FileHandler(log_path, encoding='utf-8'),
            logging.StreamHandler()  # ์ฝ˜์†”์—๋„ ์ถœ๋ ฅ
        ]
    )
    return logging.getLogger(__name__)

logger = setup_logger()

# ===== ์•ˆ์ „ํ•œ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜ =====
def safe_resize(input_path, output_path, target_size, quality=85):
    """์—๋Ÿฌ ํ•ธ๋“ค๋ง์ด ํฌํ•จ๋œ ์•ˆ์ „ํ•œ ๋ฆฌ์‚ฌ์ด์ฆˆ"""
    
    # ํŒŒ์ผ ์กด์žฌ ํ™•์ธ
    if not os.path.exists(input_path):
        logger.error(f"ํŒŒ์ผ ์—†์Œ: {input_path}")
        return False
    
    # ํŒŒ์ผ ํฌ๊ธฐ ํ™•์ธ (0๋ฐ”์ดํŠธ ํŒŒ์ผ ๋ฐฉ์ง€)
    if os.path.getsize(input_path) == 0:
        logger.error(f"๋นˆ ํŒŒ์ผ: {input_path}")
        return False
    
    try:
        with Image.open(input_path) as img:
            # ์ด๋ฏธ์ง€ ์œ ํšจ์„ฑ ๊ฒ€์ฆ
            img.verify()
        
        # verify() ํ›„ ๋‹ค์‹œ ์—ด์–ด์•ผ ํ•จ (verify๋Š” ํŒŒ์ผ ํฌ์ธํ„ฐ๋ฅผ ์†Œ๋ชจํ•จ)
        with Image.open(input_path) as img:
            # ์ตœ๋Œ€ ์ด๋ฏธ์ง€ ํฌ๊ธฐ ์ œํ•œ (๋ฉ”๋ชจ๋ฆฌ ๋ณดํ˜ธ)
            MAX_PIXELS = 50_000_000  # 5์ฒœ๋งŒ ํ”ฝ์…€
            if img.width * img.height > MAX_PIXELS:
                logger.warning(f"์ด๋ฏธ์ง€๊ฐ€ ๋„ˆ๋ฌด ํผ: {img.size} - {input_path}")
                # ๊ทธ๋ž˜๋„ ์ฒ˜๋ฆฌ๋Š” ๊ณ„์†
            
            if img.mode not in ('RGB', 'L'):
                img = img.convert('RGB')
            
            img.thumbnail(target_size, Image.LANCZOS)
            
            # ์ถœ๋ ฅ ํด๋” ์ž๋™ ์ƒ์„ฑ
            os.makedirs(os.path.dirname(output_path), exist_ok=True)
            
            ext = Path(output_path).suffix.lower()
            if ext in ('.jpg', '.jpeg'):
                img.save(output_path, 'JPEG', quality=quality, optimize=True)
            elif ext == '.png':
                img.save(output_path, 'PNG', optimize=True)
            elif ext == '.webp':
                img.save(output_path, 'WEBP', quality=quality)
            else:
                img.save(output_path)
            
            logger.info(f"์„ฑ๊ณต: {Path(input_path).name} โ†’ {img.size}")
            return True
            
    except Image.UnidentifiedImageError:
        logger.error(f"์ด๋ฏธ์ง€ ํŒŒ์ผ์ด ์•„๋‹˜: {input_path}")
    except PermissionError:
        logger.error(f"๊ถŒํ•œ ์—†์Œ: {input_path}")
    except OSError as e:
        logger.error(f"OS ์˜ค๋ฅ˜ ({input_path}): {e}")
    except Exception as e:
        logger.error(f"์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜ ({input_path}): {type(e).__name__}: {e}")
    
    return False

# ===== ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ ๋ฆฌํฌํŠธ ์ƒ์„ฑ =====
def generate_report(results, output_path="./logs/report.json"):
    """์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ JSON ๋ฆฌํฌํŠธ ์ƒ์„ฑ"""
    
    success_list = [r for r in results if r['success']]
    fail_list = [r for r in results if not r['success']]
    
    report = {
        "generated_at": datetime.now().isoformat(),
        "summary": {
            "total": len(results),
            "success": len(success_list),
            "failed": len(fail_list),
            "success_rate": f"{len(success_list)/len(results)*100:.1f}%" if results else "0%"
        },
        "failed_files": [r['filename'] for r in fail_list]
    }
    
    with open(output_path, 'w', encoding='utf-8') as f:
        json.dump(report, f, ensure_ascii=False, indent=2)
    
    logger.info(f"๋ฆฌํฌํŠธ ์ƒ์„ฑ: {output_path}")
    logger.info(f"์ด {len(results)}๊ฐœ | ์„ฑ๊ณต {len(success_list)}๊ฐœ | ์‹คํŒจ {len(fail_list)}๊ฐœ")
    
    return report

# ===== ๋ฉ”์ธ ์‹คํ–‰ =====
def main():
    INPUT_DIR = "./์›๋ณธ์ด๋ฏธ์ง€"
    OUTPUT_DIR = "./๋ฆฌ์‚ฌ์ด์ฆˆ๊ฒฐ๊ณผ"
    TARGET_SIZE = (800, 600)
    
    SUPPORTED = ('.jpg', '.jpeg', '.png', '.bmp', '.webp', '.tiff')
    files = [f for f in os.listdir(INPUT_DIR) 
             if f.lower().endswith(SUPPORTED)]
    
    logger.info(f"์ฒ˜๋ฆฌ ์‹œ์ž‘: {len(files)}๊ฐœ ํŒŒ์ผ")
    results = []
    
    for filename in files:
        input_path = os.path.join(INPUT_DIR, filename)
        output_path = os.path.join(OUTPUT_DIR, filename)
        
        success = safe_resize(input_path, output_path, TARGET_SIZE)
        results.append({'filename': filename, 'success': success})
    
    generate_report(results)

main()

๐Ÿš€ ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ ๋ชจ์Œ

1
์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ํŒŒ์ผ ์Šคํ‚ตํ•˜๊ธฐ

๋งค๋ฒˆ ์ „์ฒด๋ฅผ ๋‹ค์‹œ ์ฒ˜๋ฆฌํ•˜๋ฉด ๋น„ํšจ์œจ์ ์ž„. ์ด๋ฏธ ๊ฒฐ๊ณผ๋ฌผ์ด ์žˆ์œผ๋ฉด ๊ฑด๋„ˆ๋›ฐ๋Š” ๋กœ์ง ์ถ”๊ฐ€!

# ์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ํŒŒ์ผ ์Šคํ‚ต
if os.path.exists(output_path):
    # ์›๋ณธ๋ณด๋‹ค ๊ฒฐ๊ณผ๋ฌผ์ด ๋” ์ตœ์‹ ์ด๋ฉด ์Šคํ‚ต
    if os.path.getmtime(output_path) > os.path.getmtime(input_path):
        print(f"โญ๏ธ ์Šคํ‚ต (์ด๋ฏธ ์ฒ˜๋ฆฌ๋จ): {filename}")
        continue
2
์ง„ํ–‰๋ฅ  ํ‘œ์‹œ (tqdm ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ)

๋Œ€๋Ÿ‰ ์ฒ˜๋ฆฌํ•  ๋•Œ ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ ์—†์œผ๋ฉด ๋ฉˆ์ถ˜ ๊ฑด์ง€ ๋Œ์•„๊ฐ€๋Š” ๊ฑด์ง€ ๋ชจ๋ฆ„ ใ…‹ใ…‹

# pip install tqdm
from tqdm import tqdm

files = [...]  # ์ฒ˜๋ฆฌํ•  ํŒŒ์ผ ๋ชฉ๋ก

for filename in tqdm(files, desc="๐Ÿ–ผ๏ธ ๋ฆฌ์‚ฌ์ด์ฆˆ ์ค‘", unit="์žฅ"):
    # ์ฒ˜๋ฆฌ ์ฝ”๋“œ
    process_image(filename)
3
๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์ ์ธ ์ฒ˜๋ฆฌ

์ด๋ฏธ์ง€๋ฅผ ํ•œ๊บผ๋ฒˆ์— ๋ฉ”๋ชจ๋ฆฌ์— ์˜ฌ๋ฆฌ์ง€ ๋ง๊ณ , ์ฒ˜๋ฆฌ ํ›„ ์ฆ‰์‹œ ํ•ด์ œ!

# ์ข‹์€ ๋ฐฉ๋ฒ•: with ๋ฌธ ์‚ฌ์šฉ (์ž๋™ ๋ฉ”๋ชจ๋ฆฌ ํ•ด์ œ)
with Image.open(input_path) as img:
    img.thumbnail(target_size, Image.LANCZOS)
    img.save(output_path)
# ์—ฌ๊ธฐ์„œ ์ž๋™์œผ๋กœ ๋ฉ”๋ชจ๋ฆฌ ํ•ด์ œ๋จ

# ๋‚˜์œ ๋ฐฉ๋ฒ•: ๋ช…์‹œ์  close ์—†์ด ๋ณ€์ˆ˜์— ๊ณ„์† ๋ณด๊ด€
img = Image.open(input_path)  # ๋ฉ”๋ชจ๋ฆฌ์— ๊ณ„์† ๋‚จ์•„์žˆ์Œ
# ... ์ฒ˜๋ฆฌ ...
img.save(output_path)
# img.close() ์•ˆ ํ•˜๋ฉด ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๊ฐ€๋Šฅ!
4
ProcessPoolExecutor๋กœ CPU ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ

ThreadPoolExecutor๋Š” GIL ๋•Œ๋ฌธ์— CPU ์ง‘์•ฝ์  ์ž‘์—…์— ํ•œ๊ณ„๊ฐ€ ์žˆ์Œ.
์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ๋Š” ProcessPoolExecutor๊ฐ€ ๋” ํšจ๊ณผ์ ์ผ ์ˆ˜ ์žˆ์–ด์š”!

from concurrent.futures import ProcessPoolExecutor
import multiprocessing

def process_image_worker(args):
    """๋ณ„๋„ ํ”„๋กœ์„ธ์Šค์—์„œ ์‹คํ–‰๋  ํ•จ์ˆ˜"""
    input_path, output_path, target_size = args
    try:
        with Image.open(input_path) as img:
            if img.mode != 'RGB':
                img = img.convert('RGB')
            img.thumbnail(target_size, Image.LANCZOS)
            img.save(output_path, 'JPEG', quality=85)
        return True, input_path
    except Exception as e:
        return False, f"{input_path}: {e}"

# CPU ์ฝ”์–ด ์ˆ˜ ์ž๋™ ๊ฐ์ง€
cpu_count = multiprocessing.cpu_count()
print(f"๐Ÿ–ฅ๏ธ CPU ์ฝ”์–ด ์ˆ˜: {cpu_count}๊ฐœ ํ™œ์šฉ")

tasks = [(inp, out, (800, 600)) for inp, out in file_pairs]

with ProcessPoolExecutor(max_workers=cpu_count) as executor:
    results = list(executor.map(process_image_worker, tasks))

success = sum(1 for ok, _ in results if ok)
print(f"โœ… ์™„๋ฃŒ: {success}/{len(tasks)}")
๐Ÿ’ก ์–ธ์ œ Thread vs Process ์“ธ๊นŒ? โ€ข ThreadPoolExecutor: I/O ๋ฐ”์šด๋“œ ์ž‘์—… (ํŒŒ์ผ ์ฝ๊ธฐ/์“ฐ๊ธฐ๊ฐ€ ์ฃผ๋œ ๋ณ‘๋ชฉ์ผ ๋•Œ)
โ€ข ProcessPoolExecutor: CPU ๋ฐ”์šด๋“œ ์ž‘์—… (์ด๋ฏธ์ง€ ๋ณ€ํ™˜ ์—ฐ์‚ฐ์ด ์ฃผ๋œ ๋ณ‘๋ชฉ์ผ ๋•Œ)

์ด๋ฏธ์ง€ ๋ฆฌ์‚ฌ์ด์ฆˆ๋Š” ๋ณดํ†ต CPU ๋ฐ”์šด๋“œ๋ผ์„œ ProcessPoolExecutor๊ฐ€ ๋” ํšจ๊ณผ์ ์ด์ง€๋งŒ,
SSD ํ™˜๊ฒฝ์—์„œ๋Š” I/O๋„ ๋น ๋ฅด๋‹ˆ๊นŒ ๋‘˜ ๋‹ค ํ…Œ์ŠคํŠธํ•ด๋ณด๊ณ  ๋” ๋น ๋ฅธ ๊ฑธ ์“ฐ๋ฉด ๋จ!

๐Ÿ’ผ ์‹ค์ „ ํ™œ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค & ์™„์„ฑ ์ฝ”๋“œ

๐Ÿ›’ ์‹œ๋‚˜๋ฆฌ์˜ค 1: ์‡ผํ•‘๋ชฐ ์ƒํ’ˆ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ

์‡ผํ•‘๋ชฐ์—์„œ ๊ฐ€์žฅ ๋งŽ์ด ์“ฐ๋Š” ํŒจํ„ด! ์ƒํ’ˆ ์ด๋ฏธ์ง€๋ฅผ ์—ฌ๋Ÿฌ ์‚ฌ์ด์ฆˆ๋กœ + WebP ๋ณ€ํ™˜๊นŒ์ง€!

"""
์‡ผํ•‘๋ชฐ ์ƒํ’ˆ ์ด๋ฏธ์ง€ ์ž๋™ ์ฒ˜๋ฆฌ ์Šคํฌ๋ฆฝํŠธ
- ์›๋ณธ ๋ณด์กด
- ์ธ๋„ค์ผ, ๋ชฉ๋ก, ์ƒ์„ธ ์‚ฌ์ด์ฆˆ ์ƒ์„ฑ
- JPEG + WebP ๋™์‹œ ์ƒ์„ฑ
- EXIF ํšŒ์ „ ์ž๋™ ์ˆ˜์ •
"""
from PIL import Image, ExifTags
import os
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import logging

logging.basicConfig(level=logging.INFO, format='%(message)s')
logger = logging.getLogger(__name__)

PRODUCT_SIZES = {
    'thumb':  (150, 150, 'cover'),   # ๋ชฉ๋ก ์ธ๋„ค์ผ
    'list':   (400, 400, 'cover'),   # ๋ชฉ๋ก ์ด๋ฏธ์ง€
    'detail': (800, 800, 'inside'),  # ์ƒ์„ธ ์ด๋ฏธ์ง€
    'zoom':   (1500, 1500, 'inside') # ํ™•๋Œ€ ์ด๋ฏธ์ง€
}

def fix_exif_rotation(img):
    """EXIF ํšŒ์ „ ์ •๋ณด ์ž๋™ ์ˆ˜์ •"""
    try:
        exif = img._getexif()
        if exif:
            for tag_id, value in exif.items():
                if ExifTags.TAGS.get(tag_id) == 'Orientation':
                    rotations = {3: 180, 6: 270, 8: 90}
                    if value in rotations:
                        img = img.rotate(rotations[value], expand=True)
                    break
    except:
        pass
    return img

def resize_cover(img, width, height):
    """cover ๋ฐฉ์‹: ๋น„์œจ ์œ ์ง€ํ•˜๋ฉฐ ์˜์—ญ ๊ฝ‰ ์ฑ„์šฐ๊ธฐ"""
    img_ratio = img.width / img.height
    target_ratio = width / height
    
    if img_ratio > target_ratio:
        new_height = height
        new_width = int(height * img_ratio)
    else:
        new_width = width
        new_height = int(width / img_ratio)
    
    img = img.resize((new_width, new_height), Image.LANCZOS)
    
    # ์ค‘์•™ ํฌ๋กญ
    left = (new_width - width) // 2
    top = (new_height - height) // 2
    img = img.crop((left, top, left + width, top + height))
    return img

def process_product_image(input_path, output_base):
    """์ƒํ’ˆ ์ด๋ฏธ์ง€ ์ „์ฒด ์ฒ˜๋ฆฌ"""
    filename = Path(input_path).stem
    
    try:
        with Image.open(input_path) as img:
            img = fix_exif_rotation(img)
            if img.mode != 'RGB':
                img = img.convert('RGB')
            
            for size_name, (w, h, fit) in PRODUCT_SIZES.items():
                size_dir = os.path.join(output_base, size_name)
                os.makedirs(size_dir, exist_ok=True)
                
                img_copy = img.copy()
                
                if fit == 'cover':
                    img_copy = resize_cover(img_copy, w, h)
                else:
                    img_copy.thumbnail((w, h), Image.LANCZOS)
                
                # JPEG ์ €์žฅ
                jpeg_path = os.path.join(size_dir, f"{filename}.jpg")
                img_copy.save(jpeg_path, 'JPEG', quality=85, optimize=True)
                
                # WebP ์ €์žฅ (์ถ”๊ฐ€)
                webp_path = os.path.join(size_dir, f"{filename}.webp")
                img_copy.save(webp_path, 'WEBP', quality=80)
            
            logger.info(f"โœ… {filename} - {len(PRODUCT_SIZES)}๊ฐœ ์‚ฌ์ด์ฆˆ ร— 2ํฌ๋งท ์™„๋ฃŒ")
            return True
            
    except Exception as e:
        logger.error(f"โŒ {filename}: {e}")
        return False

def process_all_products(input_dir, output_dir, max_workers=8):
    """์ „์ฒด ์ƒํ’ˆ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ"""
    supported = ('.jpg', '.jpeg', '.png', '.bmp', '.webp')
    files = [os.path.join(input_dir, f) 
             for f in os.listdir(input_dir) 
             if f.lower().endswith(supported)]
    
    logger.info(f"๐Ÿ›’ ์ƒํ’ˆ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ์‹œ์ž‘: {len(files)}๊ฐœ")
    
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        results = list(executor.map(
            lambda f: process_product_image(f, output_dir), 
            files
        ))
    
    success = sum(results)
    logger.info(f"\n๐ŸŽ‰ ์™„๋ฃŒ! {success}/{len(files)}๊ฐœ ์„ฑ๊ณต")

# ์‹คํ–‰!
process_all_products("./์ƒํ’ˆ์ด๋ฏธ์ง€_์›๋ณธ", "./์ƒํ’ˆ์ด๋ฏธ์ง€_์ฒ˜๋ฆฌ๊ฒฐ๊ณผ")

๐Ÿ“ฑ ์‹œ๋‚˜๋ฆฌ์˜ค 2: ์•ฑ ์•„์ด์ฝ˜ ์ž๋™ ์ƒ์„ฑ

iOS/Android ์•ฑ ๊ฐœ๋ฐœํ•  ๋•Œ ์•„์ด์ฝ˜ ์‚ฌ์ด์ฆˆ๊ฐ€ ์ง„์งœ ๋งŽ์Œ ใ…‹ใ…‹
์ด๊ฒƒ๋„ ์ž๋™ํ™”ํ•˜๋ฉด ํŽธํ•จ!

from PIL import Image
import os

# iOS ์•„์ด์ฝ˜ ์‚ฌ์ด์ฆˆ ๋ชฉ๋ก
IOS_ICON_SIZES = [
    (20, 1), (20, 2), (20, 3),
    (29, 1), (29, 2), (29, 3),
    (40, 1), (40, 2), (40, 3),
    (60, 2), (60, 3),
    (76, 1), (76, 2),
    (83.5, 2),
    (1024, 1)  # App Store
]

# Android ์•„์ด์ฝ˜ ์‚ฌ์ด์ฆˆ
ANDROID_ICON_SIZES = {
    'mipmap-mdpi':    48,
    'mipmap-hdpi':    72,
    'mipmap-xhdpi':   96,
    'mipmap-xxhdpi':  144,
    'mipmap-xxxhdpi': 192,
    'playstore':      512
}

def generate_ios_icons(source_image, output_dir):
    """iOS ์•„์ด์ฝ˜ ์„ธํŠธ ์ƒ์„ฑ"""
    os.makedirs(output_dir, exist_ok=True)
    
    with Image.open(source_image) as img:
        if img.mode != 'RGBA':
            img = img.convert('RGBA')
        
        for base_size, scale in IOS_ICON_SIZES:
            pixel_size = int(base_size * scale)
            icon = img.resize((pixel_size, pixel_size), Image.LANCZOS)
            
            filename = f"icon_{int(base_size)}x{int(base_size)}@{scale}x.png"
            icon.save(os.path.join(output_dir, filename), 'PNG')
            print(f"โœ… iOS: {filename} ({pixel_size}x{pixel_size}px)")

def generate_android_icons(source_image, output_dir):
    """Android ์•„์ด์ฝ˜ ์„ธํŠธ ์ƒ์„ฑ"""
    
    with Image.open(source_image) as img:
        if img.mode != 'RGBA':
            img = img.convert('RGBA')
        
        for density, size in ANDROID_ICON_SIZES.items():
            density_dir = os.path.join(output_dir, density)
            os.makedirs(density_dir, exist_ok=True)
            
            icon = img.resize((size, size), Image.LANCZOS)
            icon.save(os.path.join(density_dir, 'ic_launcher.png'), 'PNG')
            print(f"โœ… Android {density}: {size}x{size}px")

# ์‹คํ–‰ (1024x1024 ์ด์ƒ์˜ ์›๋ณธ ์ด๋ฏธ์ง€ ํ•„์š”)
generate_ios_icons("./app_icon_์›๋ณธ.png", "./ios_icons")
generate_android_icons("./app_icon_์›๋ณธ.png", "./android_icons")
print("\n๐ŸŽ‰ ์•ฑ ์•„์ด์ฝ˜ ์„ธํŠธ ์ƒ์„ฑ ์™„๋ฃŒ!")

๐Ÿ“Š ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ ํƒ ๊ฐ€์ด๋“œ

์–ด๋–ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์จ์•ผ ํ• ์ง€ ํ—ท๊ฐˆ๋ฆด ๋•Œ ์ฐธ๊ณ ํ•˜์„ธ์š”!

๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์–ธ์–ด ์†๋„ ๋‚œ์ด๋„ ํŠน์ง•
Pillow Python โญโญโญ ์‰ฌ์›€ ๐Ÿ˜Š ๊ฐ€์žฅ ๋ฒ”์šฉ์ , ๋ฌธ์„œ ํ’๋ถ€
Sharp Node.js โญโญโญโญโญ ๋ณดํ†ต ๐Ÿ˜ ์ดˆ๊ณ ์†, WebP/AVIF ๊ฐ•์ 
ImageMagick CLI/Bash โญโญโญโญ ๋ณดํ†ต ๐Ÿ˜ ์„œ๋ฒ„ ํ™˜๊ฒฝ, ์Šคํฌ๋ฆฝํŠธ ์—ฐ๋™
OpenCV Python/C++ โญโญโญโญโญ ์–ด๋ ค์›€ ๐Ÿ˜… ์ปดํ“จํ„ฐ ๋น„์ „, ๊ณ ๊ธ‰ ์ฒ˜๋ฆฌ
Jimp Node.js โญโญโญ ์‰ฌ์›€ ๐Ÿ˜Š ์ˆœ์ˆ˜ JS, ์˜์กด์„ฑ ์—†์Œ
GD Library PHP โญโญโญ ์‰ฌ์›€ ๐Ÿ˜Š PHP ์›น์„œ๋ฒ„ ํ™˜๊ฒฝ
๐Ÿ’ก ์ƒํ™ฉ๋ณ„ ์ถ”์ฒœ! ๐Ÿ Python ์Šคํฌ๋ฆฝํŠธ๋กœ ๋กœ์ปฌ ์ฒ˜๋ฆฌ โ†’ Pillow
โšก Node.js ์›น์„œ๋ฒ„ ์‹ค์‹œ๊ฐ„ ์ฒ˜๋ฆฌ โ†’ Sharp
๐Ÿ–ฅ๏ธ Linux ์„œ๋ฒ„ cron ์ž๋™ํ™” โ†’ ImageMagick + Bash
๐Ÿ”ฌ AI/ML ํŒŒ์ดํ”„๋ผ์ธ ์—ฐ๋™ โ†’ OpenCV
๐ŸŒ PHP ๊ธฐ๋ฐ˜ CMS โ†’ GD Library ๋˜๋Š” Imagick

์ฐธ๊ณ ๋กœ ์žฌ๋Šฅ๋„ท์—์„œ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ๊ด€๋ จ ๊ฐœ๋ฐœ ์˜๋ขฐ๋ฅผ ์ฐพ์•„๋ณด๋ฉด Pillow๋‚˜ Sharp ๊ธฐ๋ฐ˜ ์ž๋™ํ™” ์Šคํฌ๋ฆฝํŠธ ์š”์ฒญ์ด ๊ฝค ๋งŽ์•„์š”.
์ด ๊ธ€์—์„œ ๋ฐฐ์šด ๋‚ด์šฉ์œผ๋กœ ์ถฉ๋ถ„ํžˆ ๋Œ€์‘ ๊ฐ€๋Šฅํ•˜๋‹ˆ๊นŒ ํฌํŠธํด๋ฆฌ์˜ค๋กœ ํ™œ์šฉํ•ด๋ด์š”! ๐Ÿ˜Š


โ“ ์ž์ฃผ ๋ฌป๋Š” ์งˆ๋ฌธ (FAQ)

Q. ๋ฆฌ์‚ฌ์ด์ฆˆํ•˜๋ฉด ์ด๋ฏธ์ง€ ํ’ˆ์งˆ์ด ๋–จ์–ด์ง€๋‚˜์š”? A. ์ด๋ฏธ์ง€๋ฅผ ์ค„์ผ ๋•Œ๋Š” ํ’ˆ์งˆ ์†์‹ค์ด ๊ฑฐ์˜ ์—†์–ด์š”. ๋ฌธ์ œ๋Š” ๋Š˜๋ฆด ๋•Œ์ธ๋ฐ, ์›๋ณธ๋ณด๋‹ค ํฌ๊ฒŒ ํ™•๋Œ€ํ•˜๋ฉด ํ”ฝ์…€์ด ๋Š˜์–ด๋‚˜์„œ ํ๋ฆฟํ•ด์ง.
withoutEnlargement: true ์˜ต์…˜์ด๋‚˜ thumbnail() ๋ฉ”์„œ๋“œ๋ฅผ ์“ฐ๋ฉด ์›๋ณธ๋ณด๋‹ค ํฌ๊ฒŒ ํ™•๋Œ€๋˜๋Š” ๊ฑธ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์–ด์š”!
Q. PNG๋ฅผ JPEG๋กœ ๋ณ€ํ™˜ํ•˜๋ฉด ํˆฌ๋ช…๋„๊ฐ€ ์‚ฌ๋ผ์ง€๋Š”๋ฐ ์–ด๋–ป๊ฒŒ ํ•˜๋‚˜์š”?
A. JPEG๋Š” ํˆฌ๋ช…๋„(์•ŒํŒŒ ์ฑ„๋„)๋ฅผ ์ง€์› ์•ˆ ํ•ด์š”. ๋ณ€ํ™˜ ์ „์— ๋ฐฐ๊ฒฝ์ƒ‰์„ ์ฑ„์›Œ์ค˜์•ผ ํ•จ!
from PIL import Image

img = Image.open("ํˆฌ๋ช…๋ฐฐ๊ฒฝ.png")
if img.mode in ('RGBA', 'LA', 'P'):
    background = Image.new('RGB', img.size, (255, 255, 255))  # ํฐ ๋ฐฐ๊ฒฝ
    if img.mode == 'P':
        img = img.convert('RGBA')
    background.paste(img, mask=img.split()[-1])  # ์•ŒํŒŒ ์ฑ„๋„์„ ๋งˆ์Šคํฌ๋กœ
    img = background
img.save("๊ฒฐ๊ณผ.jpg", 'JPEG', quality=85)
Q. ์ฒ˜๋ฆฌ ์†๋„๊ฐ€ ๋„ˆ๋ฌด ๋А๋ฆฐ๋ฐ ์–ด๋–ป๊ฒŒ ๊ฐœ์„ ํ•˜๋‚˜์š”? A. ๋ช‡ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ์žˆ์–ด์š”!
โ‘  ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋”ฉ/๋ฉ€ํ‹ฐํ”„๋กœ์„ธ์‹ฑ ์ ์šฉ
โ‘ก Sharp(Node.js) ์‚ฌ์šฉ ๊ณ ๋ ค (Pillow๋ณด๋‹ค ํ›จ์”ฌ ๋น ๋ฆ„)
โ‘ข ์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ํŒŒ์ผ ์Šคํ‚ต ๋กœ์ง ์ถ”๊ฐ€
โ‘ฃ SSD ์‚ฌ์šฉ (HDD๋ณด๋‹ค I/O ์†๋„ ํ›จ์”ฌ ๋น ๋ฆ„)
โ‘ค ๋ฐฐ์น˜ ํฌ๊ธฐ ์กฐ์ ˆ (ํ•œ ๋ฒˆ์— ๋„ˆ๋ฌด ๋งŽ์ด ์ฒ˜๋ฆฌํ•˜๋ฉด ๋ฉ”๋ชจ๋ฆฌ ๋ถ€์กฑ)
Q. ์›๋ณธ ์ด๋ฏธ์ง€๊ฐ€ ์†์ƒ๋˜์ง€ ์•Š๊ฒŒ ํ•˜๋ ค๋ฉด? A. ํ•ญ์ƒ ์›๋ณธ ํด๋”์™€ ์ถœ๋ ฅ ํด๋”๋ฅผ ๋ถ„๋ฆฌํ•˜์„ธ์š”!
mogrify์ฒ˜๋Ÿผ ์›๋ณธ์„ ์ง์ ‘ ์ˆ˜์ •ํ•˜๋Š” ๋ช…๋ น์–ด๋Š” ํŠนํžˆ ์ฃผ์˜.
์ฝ”๋“œ์—์„œ input_path์™€ output_path๋ฅผ ๋‹ค๋ฅด๊ฒŒ ์„ค์ •ํ•˜๊ณ ,
์ฒ˜์Œ ์‹คํ–‰ ์ „์— ์›๋ณธ ํด๋” ๋ฐฑ์—…ํ•˜๋Š” ์Šต๊ด€ ๋“ค์ด๋Š” ๊ฒŒ ์ข‹์•„์š” ใ…‹ใ…‹
Q. GIF ์• ๋‹ˆ๋ฉ”์ด์…˜๋„ ๋ฆฌ์‚ฌ์ด์ฆˆ ๊ฐ€๋Šฅํ•œ๊ฐ€์š”?
A. ๊ฐ€๋Šฅํ•˜์ง€๋งŒ ์ข€ ๋ณต์žกํ•ด์š”. ๊ฐ ํ”„๋ ˆ์ž„์„ ๊ฐœ๋ณ„์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•จ!
from PIL import Image

def resize_gif(input_path, output_path, target_size):
    img = Image.open(input_path)
    frames = []
    
    try:
        while True:
            frame = img.copy()
            frame.thumbnail(target_size, Image.LANCZOS)
            frames.append(frame)
            img.seek(img.tell() + 1)
    except EOFError:
        pass
    
    frames[0].save(
        output_path,
        save_all=True,
        append_images=frames[1:],
        loop=0,
        optimize=True
    )

๐ŸŽฏ ๋งˆ๋ฌด๋ฆฌ & ํ•ต์‹ฌ ์š”์•ฝ

๐Ÿ“Œ ํ•ต์‹ฌ ์š”์•ฝ ํ•œ๋ˆˆ์— ๋ณด๊ธฐ ๐Ÿ Python Pillow ์ž…๋ฌธ์ž ์ถ”์ฒœ pip install Pillow thumbnail() ๋ฉ”์„œ๋“œ ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋”ฉ ์ง€์› EXIF ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ ๋ฒ”์šฉ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ โšก Node.js Sharp ์†๋„ ์ตœ๊ฐ• npm install sharp libvips ๊ธฐ๋ฐ˜ WebP/AVIF ๊ฐ•์  ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ์›น์„œ๋ฒ„ ์—ฐ๋™ ์ตœ์  ๐Ÿ–ฅ๏ธ ImageMagick ์„œ๋ฒ„ ํ™˜๊ฒฝ ๊ฐ•์  CLI ๋ช…๋ น์–ด Bash ์Šคํฌ๋ฆฝํŠธ ์—ฐ๋™ cron ์ž๋™ํ™” mogrify ์ผ๊ด„์ฒ˜๋ฆฌ Linux ์„œ๋ฒ„ ์ž๋™ํ™” ๐Ÿ’ก ํ•ต์‹ฌ ํŒ ๊ผญ ๊ธฐ์–ตํ•˜์„ธ์š”! ์›๋ณธ ํ•ญ์ƒ ๋ฐฑ์—… ์—๋Ÿฌ ํ•ธ๋“ค๋ง ํ•„์ˆ˜ ๋กœ๊ทธ ํŒŒ์ผ ์ƒ์„ฑ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ํ™œ์šฉ WebP ๋ณ€ํ™˜ ๊ณ ๋ ค! ์ƒํ™ฉ์— ๋งž๋Š” ๋„๊ตฌ ์„ ํƒ์ด ํ•ต์‹ฌ! ๋ชจ๋‘ ํ›Œ๋ฅญํ•œ ์„ ํƒ์ง€์ž„ โœจ

์˜ค๋Š˜ ๋Œ€๋Ÿ‰ ์ด๋ฏธ์ง€ ๋ฆฌ์‚ฌ์ด์ฆˆ ์ž๋™ํ™”์— ๋Œ€ํ•ด ์ง„์งœ ๋งŽ์€ ๋‚ด์šฉ์„ ๋‹ค๋ค˜๋Š”๋ฐ์š” ใ…‹ใ…‹
ํ•ต์‹ฌ๋งŒ ์ •๋ฆฌํ•˜๋ฉด ์ด๋ ‡๊ฒŒ ๋จ!

โœ… ์ตœ์ข… ์ •๋ฆฌ! 1๏ธโƒฃ Python Pillow: ์ž…๋ฌธ์ž ์นœํ™”์ , ๋ฒ”์šฉ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ์˜ ๊ตญ๋ฃฐ
2๏ธโƒฃ Node.js Sharp: ์†๋„๊ฐ€ ์ตœ์šฐ์„ ์ด๋ฉด ์ด๊ฑฐ, ํŠนํžˆ ์›น์„œ๋ฒ„ ํ™˜๊ฒฝ์—์„œ ๊ฐ•๋ ฅ
3๏ธโƒฃ ImageMagick + Bash: ์„œ๋ฒ„ ํ™˜๊ฒฝ ์ž๋™ํ™”, cron ์—ฐ๋™ ์‹œ ์ตœ๊ณ 
4๏ธโƒฃ ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋”ฉ/ํ”„๋กœ์„ธ์‹ฑ: ๋Œ€๋Ÿ‰ ์ฒ˜๋ฆฌ ์‹œ ํ•„์ˆ˜ ์ตœ์ ํ™”
5๏ธโƒฃ ์—๋Ÿฌ ํ•ธ๋“ค๋ง + ๋กœ๊น…: ์‹ค๋ฌด์—์„œ ์ ˆ๋Œ€ ๋น ์ง€๋ฉด ์•ˆ ๋จ
6๏ธโƒฃ WebP ๋ณ€ํ™˜: ์›น ์„ฑ๋Šฅ ์ตœ์ ํ™”๋ฅผ ์œ„ํ•ด ํ•จ๊ป˜ ๊ณ ๋ คํ•˜๋ฉด ์ข‹์Œ
7๏ธโƒฃ ์›๋ณธ ๋ฐฑ์—…: ํ•ญ์ƒ! ๋ฌด์กฐ๊ฑด! ๋ฐฑ์—…ํ•˜๊ณ  ์‹œ์ž‘ํ•  ๊ฒƒ ใ…‹ใ…‹

์ด ์ฝ”๋“œ๋“ค ์ž˜ ํ™œ์šฉํ•ด์„œ ๋ฐ˜๋ณต ์ž‘์—…์—์„œ ํ•ด๋ฐฉ๋˜๊ธธ ๋ฐ”๋žŒ! ๐ŸŽ‰
์ฝ”๋“œ ํ•œ ๋ฒˆ ์งœ๋†“์œผ๋ฉด ๊ทธ ๋‹ค์Œ๋ถ€ํ„ฐ๋Š” ์ง„์งœ ํŽธํ•ด์š”.
์ฒ˜์Œ ์„ธํŒ…ํ•˜๋Š” ๋ฐ 1์‹œ๊ฐ„ ํˆฌ์žํ•˜๋ฉด ์•ž์œผ๋กœ ์ˆ˜๋ฐฑ ์‹œ๊ฐ„์„ ์•„๋‚„ ์ˆ˜ ์žˆ์œผ๋‹ˆ๊นŒ์š” ๐Ÿ˜Š

๐Ÿ”— ์ฐธ๊ณ  ๊ณต์‹ ๋ฌธ์„œ

โ€ข Pillow ๊ณต์‹ ๋ฌธ์„œ: https://pillow.readthedocs.io
โ€ข Sharp ๊ณต์‹ ๋ฌธ์„œ: https://sharp.pixelplumbing.com
โ€ข ImageMagick ๊ณต์‹:
https://imagemagick.org/script/command-line-processing.php
๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.