메인 콘텐츠로 건너뛰기

파일 스토리지란

Laravel은 Flysystem이라는 PHP 패키지를 베이스로 한 강력한 파일시스템 추상화 레이어를 제공하고 있습니다. 로컬 디스크, SFTP, Amazon S3 등, 서로 다른 스토리지 백엔드를 같은 API로 조작할 수 있기 때문에, 환경에 따라 전환해도 코드의 변경이 불필요합니다.
드라이버를 전환해도 코드는 변하지 않습니다. 개발 환경에서는 로컬, 운영 환경에서는 S3라는 구성을 간단히 실현할 수 있습니다.

설정

파일시스템의 설정은 config/filesystems.php에 정리되어 있습니다. 여기서 “디스크”를 정의합니다. 디스크는 특정 드라이버와 스토리지 장소의 조합입니다. 주요 드라이버는 다음과 같습니다.
드라이버용도
local서버의 로컬 파일시스템
publicWeb 공개용의 로컬 디스크
s3Amazon S3(또는 S3 호환 서비스)
sftpSFTP 서버
ftpFTP 서버

local 드라이버

local 드라이버를 사용하면 filesystems 설정의 root 디렉터리를 기준으로 파일을 조작합니다. 기본적으로는 storage/app/privateroot입니다.
use Illuminate\Support\Facades\Storage;

Storage::disk('local')->put('example.txt', 'Contents');
// storage/app/private/example.txt 에 기록됨

기본 디스크의 변경

FILESYSTEM_DISK 환경 변수로 기본 디스크를 전환할 수 있습니다.
FILESYSTEM_DISK=s3

public 디스크와 심볼릭 링크

public 디스크는 Web에서 접근할 수 있는 파일을 놓기 위한 것입니다. 기본적으로는 storage/app/public 디렉터리에 저장됩니다. Web 서버에서 접근할 수 있도록 하려면 public/storage에서 storage/app/public으로의 심볼릭 링크를 작성합니다.
1

심볼릭 링크의 작성

php artisan storage:link
2

파일의 URL 을 취득

심볼릭 링크 작성 후, asset 헬퍼로 URL을 생성할 수 있습니다.
echo asset('storage/file.txt');
추가의 심볼릭 링크가 필요한 경우, config/filesystems.phplinks 배열에서 설정할 수 있습니다.
'links' => [
    public_path('storage') => storage_path('app/public'),
    public_path('images') => storage_path('app/images'),
],
심볼릭 링크를 삭제하려면 storage:unlink를 사용합니다.
php artisan storage:unlink

Storage 파사드의 기본 조작

파일의 읽기

use Illuminate\Support\Facades\Storage;

// 파일의 내용을 문자열로 취득
$contents = Storage::get('file.jpg');

// JSON 파일을 취득해서 디코드
$data = Storage::json('orders.json');

// 파일의 존재 확인
if (Storage::exists('file.jpg')) {
    // 파일이 존재
}

// 파일이 존재하지 않음을 확인
if (Storage::missing('file.jpg')) {
    // 파일이 존재하지 않음
}

파일의 쓰기

// 파일에 기록
Storage::put('file.jpg', $contents);

// 리소스를 스트림으로 기록
Storage::put('file.jpg', $resource);

// 선두·말미에 추가 기록
Storage::prepend('file.log', 'Prepended Text');
Storage::append('file.log', 'Appended Text');

// 파일의 복사와 이동
Storage::copy('old/file.jpg', 'new/file.jpg');
Storage::move('old/file.jpg', 'new/file.jpg');
put이 실패한 경우, 기본적으로는 false를 반환합니다. 디스크 설정에서 'throw' => true로 함으로써 예외를 스로우시킬 수도 있습니다.

파일의 삭제

// 단일 파일의 삭제
Storage::delete('file.jpg');

// 여러 파일의 삭제
Storage::delete(['file.jpg', 'file2.jpg']);

// 특정 디스크의 파일을 삭제
Storage::disk('s3')->delete('path/file.jpg');

파일의 다운로드 응답

// 브라우저에 다운로드를 촉구하는 응답을 반환
return Storage::download('file.jpg');

// 파일 이름과 헤더를 지정하는 경우
return Storage::download('file.jpg', 'my-file.jpg', $headers);

URL 의 생성

일반 URL

use Illuminate\Support\Facades\Storage;

$url = Storage::url('file.jpg');
local 드라이버에서는 /storage/file.jpg와 같은 상대 URL을 반환합니다. s3 드라이버에서는 완전한 원격 URL을 반환합니다.

일시 URL(Temporary URL)

기한이 있는 URL을 생성하고 싶은 경우 temporaryUrl 메서드를 사용합니다. locals3 드라이버에서 이용할 수 있습니다.
use Illuminate\Support\Facades\Storage;

$url = Storage::temporaryUrl(
    'file.jpg',
    now()->plus(minutes: 5)
);
S3에서는 추가의 요청 파라미터도 지정할 수 있습니다.
$url = Storage::temporaryUrl(
    'file.jpg',
    now()->plus(minutes: 5),
    [
        'ResponseContentType' => 'application/octet-stream',
        'ResponseContentDisposition' => 'attachment; filename=file.jpg',
    ]
);
일시 업로드 URL이 필요한 경우 temporaryUploadUrl 메서드를 사용합니다. 클라이언트 사이드에서 직접 S3로 업로드하는 것과 같은 서버리스 구성에서 활용할 수 있습니다.
['url' => $url, 'headers' => $headers] = Storage::temporaryUploadUrl(
    'file.jpg', now()->plus(minutes: 5)
);

파일의 업로드

사용자가 폼에서 업로드한 파일을 저장하는 일반적인 패턴입니다.

store 메서드(파일 이름을 자동 생성)

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class UserAvatarController extends Controller
{
    public function update(Request $request): string
    {
        // 파일 이름은 MIME 타입에서 확장자를 판정해 자동 생성됨
        $path = $request->file('avatar')->store('avatars');

        return $path;
    }
}

storeAs 메서드(파일 이름을 지정)

$path = $request->file('avatar')->storeAs(
    'avatars',
    $request->user()->id
);

디스크를 지정해서 업로드

// s3 디스크에 저장
$path = $request->file('avatar')->store(
    'avatars/' . $request->user()->id,
    's3'
);

Storage 파사드 경유로 업로드

use Illuminate\Support\Facades\Storage;

// 파일 이름을 자동 생성
$path = Storage::putFile('avatars', $request->file('avatar'));

// 파일 이름을 지정
$path = Storage::putFileAs(
    'avatars',
    $request->file('avatar'),
    $request->user()->id
);
getClientOriginalName()이나 getClientOriginalExtension()은 사용자가 위변조할 수 있어 안전하지 않습니다. 파일 이름에는 hashName(), 확장자에는 extension()을 사용해 주십시오.
$file = $request->file('avatar');

$name = $file->hashName();   // 유니크한 랜덤 이름을 생성
$extension = $file->extension(); // MIME 타입에서 확장자를 판정

파일의 가시성(Visibility)

Flysystem에서는 파일의 공개·비공개를 visibility로 관리합니다.
use Illuminate\Support\Facades\Storage;

// 기록 시에 가시성을 지정
Storage::put('file.jpg', $contents, 'public');

// 가시성의 취득과 변경
$visibility = Storage::getVisibility('file.jpg');
Storage::setVisibility('file.jpg', 'public');
업로드된 파일을 공개 상태로 저장하는 경우 storePublicly를 사용합니다.
$path = $request->file('avatar')->storePublicly('avatars', 's3');

여러 디스크의 사용 구분

disk 메서드로 조작할 디스크를 전환할 수 있습니다.
// 기본 디스크에 저장
Storage::put('avatars/1', $content);

// s3 디스크에 저장
Storage::disk('s3')->put('avatars/1', $content);

// 온디맨드로 디스크를 작성
$disk = Storage::build([
    'driver' => 'local',
    'root' => '/path/to/root',
]);
$disk->put('image.jpg', $content);

클라우드 스토리지(S3)의 설정

패키지의 설치

composer require league/flysystem-aws-s3-v3 "^3.0" --with-all-dependencies

환경 변수의 설정

.env 파일에 S3의 인증 정보를 설정합니다.
AWS_ACCESS_KEY_ID=your-key-id
AWS_SECRET_ACCESS_KEY=your-secret-access-key
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=your-bucket-name
AWS_USE_PATH_STYLE_ENDPOINT=false
DigitalOcean Spaces, Cloudflare R2, Vultr Object Storage 등의 S3 호환 서비스도 s3 드라이버로 이용할 수 있습니다. endpoint 옵션에 서비스의 엔드포인트 URL을 지정해 주십시오.
'endpoint' => env('AWS_ENDPOINT', 'https://your-endpoint.example.com'),

파일 메타데이터의 취득

use Illuminate\Support\Facades\Storage;

// 파일 사이즈(바이트)
$size = Storage::size('file.jpg');

// 최종 업데이트 일시(UNIX 타임스탬프)
$time = Storage::lastModified('file.jpg');

// MIME 타입
$mime = Storage::mimeType('file.jpg');

// 파일의 절대 경로(local 드라이버)
$path = Storage::path('file.jpg');

디렉터리 조작

use Illuminate\Support\Facades\Storage;

// 디렉터리 내의 파일 목록
$files = Storage::files($directory);

// 서브 디렉터리를 포함한 파일 목록
$files = Storage::allFiles($directory);

// 디렉터리 목록
$directories = Storage::directories($directory);

// 디렉터리의 작성
Storage::makeDirectory($directory);

// 디렉터리와 그 안의 파일을 함께 삭제
Storage::deleteDirectory($directory);

테스트

Storage::fake()를 사용하면 실제 디스크에 접근하지 않고 파일 조작의 테스트를 작성할 수 있습니다.
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;

test('아바타를 업로드할 수 있다', function () {
    Storage::fake('avatars');

    $file = UploadedFile::fake()->image('avatar.jpg');

    $this->post('/user/avatar', ['avatar' => $file]);

    // 파일이 저장되었음을 확인
    Storage::disk('avatars')->assertExists('avatar.jpg');

    // 파일이 저장되어 있지 않음을 확인
    Storage::disk('avatars')->assertMissing('other.jpg');
});
UploadedFile::fake()->image()를 사용하려면 PHP의 GD 확장이 필요합니다.

실전적인 유스케이스: 프로필 이미지의 업로드

밸리데이션·저장·DB에의 경로 기록을 조합한 실전적인 컨트롤러의 예시입니다.
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Storage;

class ProfileController extends Controller
{
    public function updateAvatar(Request $request): RedirectResponse
    {
        $request->validate([
            'avatar' => ['required', 'image', 'max:2048'],
        ]);

        $user = $request->user();

        // 기존의 아바타를 삭제
        if ($user->avatar_path) {
            Storage::disk('public')->delete($user->avatar_path);
        }

        // 새 아바타를 저장
        $path = $request->file('avatar')->store('avatars', 'public');

        // 경로를 DB 에 저장
        $user->update(['avatar_path' => $path]);

        return back()->with('status', 'avatar-updated');
    }
}
템플릿에서는 Storage::url()을 사용해 URL을 취득합니다.
<img src="{{ Storage::disk('public')->url($user->avatar_path) }}" alt="아바타">
마지막 수정일 2026년 7월 13일