> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 파일 스토리지

> Laravel의 Flysystem 통합을 사용한 파일의 읽기·쓰기, 업로드, 클라우드 스토리지 연계 방법을 설명합니다.

## 파일 스토리지란

Laravel은 [Flysystem](https://github.com/thephpleague/flysystem)이라는 PHP 패키지를 베이스로 한 강력한 파일시스템 추상화 레이어를 제공하고 있습니다.
로컬 디스크, SFTP, Amazon S3 등, 서로 다른 스토리지 백엔드를 같은 API로 조작할 수 있기 때문에, 환경에 따라 전환해도 **코드의 변경이 불필요**합니다.

<Info>
  드라이버를 전환해도 코드는 변하지 않습니다. 개발 환경에서는 로컬, 운영 환경에서는 S3라는 구성을 간단히 실현할 수 있습니다.
</Info>

## 설정

파일시스템의 설정은 `config/filesystems.php`에 정리되어 있습니다.
여기서 "디스크"를 정의합니다. 디스크는 특정 드라이버와 스토리지 장소의 조합입니다.

주요 드라이버는 다음과 같습니다.

| 드라이버     | 용도                      |
| -------- | ----------------------- |
| `local`  | 서버의 로컬 파일시스템            |
| `public` | Web 공개용의 로컬 디스크         |
| `s3`     | Amazon S3(또는 S3 호환 서비스) |
| `sftp`   | SFTP 서버                 |
| `ftp`    | FTP 서버                  |

### local 드라이버

`local` 드라이버를 사용하면 `filesystems` 설정의 `root` 디렉터리를 기준으로 파일을 조작합니다.
기본적으로는 `storage/app/private`가 `root`입니다.

```php theme={null}
use Illuminate\Support\Facades\Storage;

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

### 기본 디스크의 변경

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

```ini theme={null}
FILESYSTEM_DISK=s3
```

## public 디스크와 심볼릭 링크

`public` 디스크는 Web에서 접근할 수 있는 파일을 놓기 위한 것입니다.
기본적으로는 `storage/app/public` 디렉터리에 저장됩니다.

Web 서버에서 접근할 수 있도록 하려면 `public/storage`에서 `storage/app/public`으로의 심볼릭 링크를 작성합니다.

<Steps>
  <Step title="심볼릭 링크의 작성">
    ```shell theme={null}
    php artisan storage:link
    ```
  </Step>

  <Step title="파일의 URL 을 취득">
    심볼릭 링크 작성 후, `asset` 헬퍼로 URL을 생성할 수 있습니다.

    ```php theme={null}
    echo asset('storage/file.txt');
    ```
  </Step>
</Steps>

<Tip>
  추가의 심볼릭 링크가 필요한 경우, `config/filesystems.php`의 `links` 배열에서 설정할 수 있습니다.

  ```php theme={null}
  'links' => [
      public_path('storage') => storage_path('app/public'),
      public_path('images') => storage_path('app/images'),
  ],
  ```
</Tip>

심볼릭 링크를 삭제하려면 `storage:unlink`를 사용합니다.

```shell theme={null}
php artisan storage:unlink
```

## Storage 파사드의 기본 조작

### 파일의 읽기

```php theme={null}
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')) {
    // 파일이 존재하지 않음
}
```

### 파일의 쓰기

```php theme={null}
// 파일에 기록
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');
```

<Info>
  `put`이 실패한 경우, 기본적으로는 `false`를 반환합니다.
  디스크 설정에서 `'throw' => true`로 함으로써 예외를 스로우시킬 수도 있습니다.
</Info>

### 파일의 삭제

```php theme={null}
// 단일 파일의 삭제
Storage::delete('file.jpg');

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

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

### 파일의 다운로드 응답

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

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

## URL 의 생성

### 일반 URL

```php theme={null}
use Illuminate\Support\Facades\Storage;

$url = Storage::url('file.jpg');
```

`local` 드라이버에서는 `/storage/file.jpg`와 같은 상대 URL을 반환합니다.
`s3` 드라이버에서는 완전한 원격 URL을 반환합니다.

### 일시 URL(Temporary URL)

기한이 있는 URL을 생성하고 싶은 경우 `temporaryUrl` 메서드를 사용합니다.
`local`과 `s3` 드라이버에서 이용할 수 있습니다.

```php theme={null}
use Illuminate\Support\Facades\Storage;

$url = Storage::temporaryUrl(
    'file.jpg',
    now()->plus(minutes: 5)
);
```

S3에서는 추가의 요청 파라미터도 지정할 수 있습니다.

```php theme={null}
$url = Storage::temporaryUrl(
    'file.jpg',
    now()->plus(minutes: 5),
    [
        'ResponseContentType' => 'application/octet-stream',
        'ResponseContentDisposition' => 'attachment; filename=file.jpg',
    ]
);
```

<Tip>
  일시 업로드 URL이 필요한 경우 `temporaryUploadUrl` 메서드를 사용합니다.
  클라이언트 사이드에서 직접 S3로 업로드하는 것과 같은 서버리스 구성에서 활용할 수 있습니다.

  ```php theme={null}
  ['url' => $url, 'headers' => $headers] = Storage::temporaryUploadUrl(
      'file.jpg', now()->plus(minutes: 5)
  );
  ```
</Tip>

## 파일의 업로드

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

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

```php theme={null}
<?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 메서드(파일 이름을 지정)

```php theme={null}
$path = $request->file('avatar')->storeAs(
    'avatars',
    $request->user()->id
);
```

### 디스크를 지정해서 업로드

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

### Storage 파사드 경유로 업로드

```php theme={null}
use Illuminate\Support\Facades\Storage;

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

// 파일 이름을 지정
$path = Storage::putFileAs(
    'avatars',
    $request->file('avatar'),
    $request->user()->id
);
```

<Warning>
  `getClientOriginalName()`이나 `getClientOriginalExtension()`은 사용자가 위변조할 수 있어 안전하지 않습니다.
  파일 이름에는 `hashName()`, 확장자에는 `extension()`을 사용해 주십시오.

  ```php theme={null}
  $file = $request->file('avatar');

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

## 파일의 가시성(Visibility)

Flysystem에서는 파일의 공개·비공개를 `visibility`로 관리합니다.

```php theme={null}
use Illuminate\Support\Facades\Storage;

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

// 가시성의 취득과 변경
$visibility = Storage::getVisibility('file.jpg');
Storage::setVisibility('file.jpg', 'public');
```

업로드된 파일을 공개 상태로 저장하는 경우 `storePublicly`를 사용합니다.

```php theme={null}
$path = $request->file('avatar')->storePublicly('avatars', 's3');
```

## 여러 디스크의 사용 구분

`disk` 메서드로 조작할 디스크를 전환할 수 있습니다.

```php theme={null}
// 기본 디스크에 저장
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)의 설정

### 패키지의 설치

```shell theme={null}
composer require league/flysystem-aws-s3-v3 "^3.0" --with-all-dependencies
```

### 환경 변수의 설정

`.env` 파일에 S3의 인증 정보를 설정합니다.

```ini theme={null}
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
```

<Info>
  DigitalOcean Spaces, Cloudflare R2, Vultr Object Storage 등의 S3 호환 서비스도 `s3` 드라이버로 이용할 수 있습니다.
  `endpoint` 옵션에 서비스의 엔드포인트 URL을 지정해 주십시오.

  ```php theme={null}
  'endpoint' => env('AWS_ENDPOINT', 'https://your-endpoint.example.com'),
  ```
</Info>

## 파일 메타데이터의 취득

```php theme={null}
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');
```

## 디렉터리 조작

```php theme={null}
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()`를 사용하면 실제 디스크에 접근하지 않고 파일 조작의 테스트를 작성할 수 있습니다.

```php theme={null}
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');
});
```

<Info>
  `UploadedFile::fake()->image()`를 사용하려면 PHP의 [GD 확장](https://www.php.net/manual/ja/book.image.php)이 필요합니다.
</Info>

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

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

```php theme={null}
<?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을 취득합니다.

```blade theme={null}
<img src="{{ Storage::disk('public')->url($user->avatar_path) }}" alt="아바타">
```


## Related topics

- [2026년 5월 Laravel 업데이트](/ko/blog/changelog/202605.md)
- [Laravel AI SDK](/ko/ai-sdk.md)
- [Laravel Sail](/ko/sail.md)
- [Laravel Console Starter](/ko/packages/laravel-console-starter/index.md)
- [HTTP 요청](/ko/requests.md)
