파일 스토리지란
Laravel은 Flysystem이라는 PHP 패키지를 베이스로 한 강력한 파일시스템 추상화 레이어를 제공하고 있습니다.
로컬 디스크, SFTP, Amazon S3 등, 서로 다른 스토리지 백엔드를 같은 API로 조작할 수 있기 때문에, 환경에 따라 전환해도 코드의 변경이 불필요합니다.
드라이버를 전환해도 코드는 변하지 않습니다. 개발 환경에서는 로컬, 운영 환경에서는 S3라는 구성을 간단히 실현할 수 있습니다.
파일시스템의 설정은 config/filesystems.php에 정리되어 있습니다.
여기서 “디스크”를 정의합니다. 디스크는 특정 드라이버와 스토리지 장소의 조합입니다.
주요 드라이버는 다음과 같습니다.
| 드라이버 | 용도 |
|---|
local | 서버의 로컬 파일시스템 |
public | Web 공개용의 로컬 디스크 |
s3 | Amazon S3(또는 S3 호환 서비스) |
sftp | SFTP 서버 |
ftp | FTP 서버 |
local 드라이버
local 드라이버를 사용하면 filesystems 설정의 root 디렉터리를 기준으로 파일을 조작합니다.
기본적으로는 storage/app/private가 root입니다.
use Illuminate\Support\Facades\Storage;
Storage::disk('local')->put('example.txt', 'Contents');
// storage/app/private/example.txt 에 기록됨
기본 디스크의 변경
FILESYSTEM_DISK 환경 변수로 기본 디스크를 전환할 수 있습니다.
public 디스크와 심볼릭 링크
public 디스크는 Web에서 접근할 수 있는 파일을 놓기 위한 것입니다.
기본적으로는 storage/app/public 디렉터리에 저장됩니다.
Web 서버에서 접근할 수 있도록 하려면 public/storage에서 storage/app/public으로의 심볼릭 링크를 작성합니다.
파일의 URL 을 취득
심볼릭 링크 작성 후, asset 헬퍼로 URL을 생성할 수 있습니다.echo asset('storage/file.txt');
추가의 심볼릭 링크가 필요한 경우, config/filesystems.php의 links 배열에서 설정할 수 있습니다.'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 메서드를 사용합니다.
local과 s3 드라이버에서 이용할 수 있습니다.
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="아바타">