티스토리 뷰

사용자가 올린 이미지를 그대로 저장하면 문제가 금방 생긴다. 요즘 폰 사진은 한 장에 5MB를 넘고, 목록 화면에서 원본을 그대로 뿌리면 로딩이 느려진다. 세로로 찍은 사진이 가로로 뒤집혀 보이는 문제도 흔하다.

Laravel에서 이미지를 다룰 때 표준처럼 쓰이는 게 Intervention Image다. 리사이즈, 크롭, 회전, 워터마크, 포맷 변환을 전부 지원한다.

설치

composer require intervention/image

v2와 v3의 사용법이 꽤 다르다. 신규 프로젝트라면 v3를 쓰는 게 좋다.

v2 (레거시)

v2는 서비스 프로바이더와 파사드를 수동 등록해야 했다.

// config/app.php
'providers' => [
    Intervention\Image\ImageServiceProvider::class,
],
'aliases' => [
    'Image' => Intervention\Image\Facades\Image::class,
],

v3 (현행)

v3는 파사드 없이 매니저 인스턴스를 직접 만들어 쓴다. 드라이버를 명시하는 방식으로 바뀌었다.

use Intervention\Image\ImageManager;
use Intervention\Image\Drivers\Gd\Driver;

$manager = new ImageManager(new Driver());
$image = $manager->read($request->file('photo'));

GD와 Imagick, 무엇을 쓸까

  • GD: PHP에 기본 번들. 설치가 간편하고 가볍다. 일반적인 리사이즈·크롭은 충분하다.
  • Imagick: 별도 설치 필요. 화질이 더 좋고 지원 포맷이 많다(PSD, PDF 등). 메모리를 더 쓴다.

대부분의 웹 서비스는 GD로 충분하다. 다만 서버리스 환경에서는 GD 확장 자체가 빠져 있는 경우가 있으니 배포 전에 확인이 필요하다.

실전 1: 업로드 이미지 리사이즈

use Intervention\Image\Laravel\Facades\Image;   // v3 Laravel 어댑터

public function store(Request $request)
{
    $request->validate([
        'photo' => 'required|image|mimes:jpeg,png,webp|max:10240',
    ]);

    $file = $request->file('photo');
    $name = uniqid() . '.webp';

    $image = Image::read($file)
        ->scaleDown(width: 1200)     // 비율 유지, 확대는 안 함
        ->toWebp(quality: 82);

    Storage::disk('public')->put("photos/{$name}", (string) $image);

    return response()->json(['path' => "photos/{$name}"]);
}

scaleDown은 원본이 지정 크기보다 작으면 그대로 둔다. 작은 이미지를 억지로 늘려서 화질이 깨지는 걸 막아준다.

실전 2: 썸네일 여러 개 만들기

$sizes = ['thumb' => 200, 'medium' => 600, 'large' => 1200];

foreach ($sizes as $key => $width) {
    $img = Image::read($file)->scaleDown(width: $width)->toWebp(quality: 80);
    Storage::disk('public')->put("photos/{$key}/{$name}", (string) $img);
}

목록에는 thumb, 상세에는 medium, 확대 보기에만 large를 쓰는 식으로 나누면 트래픽이 크게 줄어든다.

실전 3: 정사각 크롭 (프로필 이미지)

// 비율 무시하고 딱 맞춤 (잘려나감)
$img = Image::read($file)->cover(300, 300);

// 여백을 채워서 맞춤
$img = Image::read($file)->contain(300, 300, 'ffffff');

실전 4: 워터마크

$img = Image::read($file)
    ->scaleDown(width: 1200)
    ->place(public_path('watermark.png'), 'bottom-right', 20, 20);

꼭 챙겨야 할 것들

1. EXIF 회전 정보

아이폰으로 세로로 찍은 사진이 웹에서 가로로 눕는 문제의 원인이다. 실제 픽셀은 가로로 저장돼 있고, EXIF의 Orientation 값으로 "돌려서 보여줘"라고 표시하는 방식이기 때문이다.

$img = Image::read($file)->orient()->scaleDown(width: 1200);

orient()를 호출하면 EXIF 정보대로 실제 픽셀을 회전시킨다. 업로드 파이프라인에 이걸 안 넣으면 사용자 문의가 반드시 들어온다.

2. EXIF 개인정보 제거

사진 EXIF에는 촬영 위치(GPS)가 들어 있을 수 있다. 집에서 찍은 사진을 올리면 집 주소가 노출되는 셈이다.

Intervention은 리사이즈나 포맷 변환 과정에서 EXIF를 기본적으로 제거한다. 원본을 그대로 저장하는 경로가 있다면 그쪽은 따로 처리해야 한다.

3. 메모리 한계

이미지 처리는 메모리를 많이 쓴다. 픽셀당 대략 4바이트가 필요해서, 6000×4000 사진 한 장이 압축 파일로는 5MB여도 메모리에 올리면 약 96MB다.

// php.ini
memory_limit = 256M
upload_max_filesize = 20M
post_max_size = 20M

업로드 크기 제한을 validation에서 먼저 거는 게 중요하다. 서버 메모리가 터지기 전에 애플리케이션 레벨에서 막는 것이다.

4. 무거운 처리는 큐로

썸네일을 여러 개 만들면 응답이 느려진다. 사용자에게는 업로드 완료만 알리고 실제 처리는 큐로 넘기는 게 좋다.

// 원본만 저장하고 응답
$path = $request->file('photo')->store('originals', 'public');
ProcessImage::dispatch($path);

return response()->json(['status' => 'processing']);

요즘 고려할 대안

  • WebP / AVIF: JPEG 대비 용량이 크게 줄어든다. WebP는 사실상 모든 브라우저가 지원한다.
  • 이미지 CDN: Cloudflare Images, imgproxy 같은 서비스는 URL 파라미터로 리사이즈를 처리한다. 서버 부하가 사라지고 캐싱도 알아서 된다.
  • spatie/laravel-medialibrary: 업로드·변환·저장을 모델에 붙여 관리한다. 이미지가 도메인의 핵심이면 이쪽이 편하다.

정리

  • composer require intervention/image, v3는 파사드 대신 매니저 방식
  • scaleDown은 확대를 안 해서 화질 손상이 없다
  • orient()를 반드시 넣는다. 안 넣으면 사진이 눕는다
  • EXIF에는 GPS가 들어 있다. 원본 저장 경로는 별도 처리
  • 메모리는 압축 크기가 아니라 픽셀 수로 계산된다
  • 썸네일 다중 생성은 큐로, 규모가 커지면 이미지 CDN 검토
댓글


최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday