📚 파일 업로드 처리
InputFile 컴포넌트로 파일을 선택·업로드하고 서버에 저장하는 흐름을 구현합니다.
📌 학습 목표
- InputFile 컴포넌트의 기본 사용법과 주요 속성을 이해합니다.
- IBrowserFile 인터페이스의 Name, Size, ContentType, OpenReadStream을 활용할 수 있습니다.
- 단일 파일 및 다중 파일 업로드를 각각 구현할 수 있습니다.
- 업로드된 파일을 서버 디렉터리에 안전하게 저장하는 코드를 작성할 수 있습니다.
- 파일 크기 제한, MIME 타입 검증, 무작위 파일명 생성 등 보안 처리를 올바르게 적용할 수 있습니다.
📝 개념 설명
InputFile 컴포넌트란?
Blazor는 HTML의 <input type='file'>에 대응하는 내장 컴포넌트 InputFile을 제공합니다. 이 컴포넌트는 브라우저의 파일 선택 대화상자를 열고, 선택된 파일의 정보를 IBrowserFile 형태로 .NET 코드에 전달합니다.
Blazor Server와 Blazor WebAssembly 모두 동일한 InputFile 컴포넌트를 사용하지만, 파일을 최종 저장하는 방식이 다릅니다.
- Blazor Server: 서버 측 코드가 직접 실행되므로 서버 파일 시스템에 바로 저장 가능
- Blazor WebAssembly: 브라우저에서 실행되므로 Web API 엔드포인트를 통해 서버로 전송 필요
IBrowserFile 인터페이스
파일이 선택되면 OnChange 이벤트가 발생하고, 이벤트 인수인 InputFileChangeEventArgs에서 IBrowserFile 객체를 꺼낼 수 있습니다. 이 인터페이스는 파일 처리에 필요한 핵심 정보를 제공합니다.
파일 업로드 전체 흐름
Blazor에서 파일이 업로드되어 서버에 저장되기까지의 과정을 순서대로 이해하면 구현이 명확해집니다.
파일 선택
이벤트 발생
객체 획득
검증
스트림 열기
서버에 저장
💡 예제 & 실습
1. 단일 파일 업로드 (Blazor Server)
가장 기본적인 형태의 단일 파일 업로드입니다. wwwroot/uploads 폴더에 저장하며, 보안을 위해 원본 파일명 대신 GUID 기반 무작위 이름을 사용합니다.
@page "/file-upload"
@inject IWebHostEnvironment Env
<h3>파일 업로드</h3>
<InputFile />
@if (!string.IsNullOrEmpty(message))
{
<p>@message</p>
}
@code {
private string message = string.Empty;
private async Task HandleFileSelected(InputFileChangeEventArgs e)
{
IBrowserFile file = e.File;
// 1. 파일 크기 검증 (최대 5 MB)
const long maxSize = 5 * 1024 * 1024;
if (file.Size > maxSize)
{
message = "파일 크기가 5 MB를 초과합니다.";
return;
}
// 2. 저장 경로 설정 — 무작위 파일명(보안)
var uploadsDir = Path.Combine(Env.WebRootPath, "uploads");
Directory.CreateDirectory(uploadsDir);
var ext = Path.GetExtension(file.Name);
var safeFileName = $"{Guid.NewGuid()}{ext}";
var filePath = Path.Combine(uploadsDir, safeFileName);
// 3. 스트림을 열어 파일로 복사
await using var stream = file.OpenReadStream(maxAllowedSize: maxSize);
await using var fs = File.Create(filePath);
await stream.CopyToAsync(fs);
message = $"업로드 완료: {safeFileName}";
}
}
컴포넌트 상단에
@inject IWebHostEnvironment Env를 선언하면 Env.WebRootPath로 wwwroot의 절대 경로를 얻을 수 있습니다. ASP.NET Core가 자동으로 등록하므로 Program.cs에 별도 코드 없이 사용 가능합니다.2. 다중 파일 업로드
multiple 속성을 추가하고 e.GetMultipleFiles()로 파일 목록을 순회하여 저장합니다. maximumFileCount를 명시해 과도한 파일 전송을 차단합니다.
@page "/multi-upload"
@inject IWebHostEnvironment Env
<h3>다중 파일 업로드</h3>
<InputFile multiple />
@if (uploadedNames.Count > 0)
{
<ul>
@foreach (var name in uploadedNames)
{
<li>@name</li>
}
</ul>
}
@code {
private List<string> uploadedNames = new();
private async Task HandleMultipleFiles(InputFileChangeEventArgs e)
{
uploadedNames.Clear();
const int maxFiles = 5; // 최대 5개
const long maxSize = 5 * 1024 * 1024; // 파일당 5 MB
var files = e.GetMultipleFiles(maximumFileCount: maxFiles);
var uploadsDir = Path.Combine(Env.WebRootPath, "uploads");
Directory.CreateDirectory(uploadsDir);
foreach (var file in files)
{
var ext = Path.GetExtension(file.Name);
var safeFileName = $"{Guid.NewGuid()}{ext}";
var filePath = Path.Combine(uploadsDir, safeFileName);
await using var stream = file.OpenReadStream(maxAllowedSize: maxSize);
await using var fs = File.Create(filePath);
await stream.CopyToAsync(fs);
uploadedNames.Add($"{file.Name} → {safeFileName}");
StateHasChanged(); // 파일마다 목록 즉시 갱신
}
}
}
3. 파일 형식 필터링
HTML accept 속성은 파일 선택 대화상자에서 표시할 형식을 브라우저에 힌트로 줍니다. 단, 클라이언트 측 제한이므로 서버에서도 반드시 ContentType을 검증해야 합니다.
<!-- 이미지 파일만 표시 -->
<InputFile accept=".jpg,.jpeg,.png,.gif,.webp" />
<!-- PDF만 표시 -->
<InputFile accept=".pdf" />
서버 측 MIME 타입 화이트리스트 검증:
private static readonly HashSet<string> AllowedTypes = new()
{
"image/jpeg", "image/png", "image/gif", "image/webp"
};
private async Task HandleImage(InputFileChangeEventArgs e)
{
var file = e.File;
if (!AllowedTypes.Contains(file.ContentType))
{
message = $"허용되지 않는 파일 형식: {file.ContentType}";
return;
}
// 크기 검증 및 저장 코드 (위 예제와 동일)
}
4. 업로드 진행률 표시
대용량 파일을 처리할 때는 청크(buffer) 단위로 읽으면서 진행률을 계산하고, StateHasChanged()를 호출해 UI를 실시간으로 갱신합니다.
@page "/upload-progress"
@inject IWebHostEnvironment Env
<h3>파일 업로드 (진행률)</h3>
<InputFile />
@if (progress > 0)
{
<progress value="@progress" max="100" style="width:320px"></progress>
<span> @progress%</span>
}
@code {
private int progress = 0;
private async Task HandleWithProgress(InputFileChangeEventArgs e)
{
progress = 0;
var file = e.File;
const long maxSize = 50 * 1024 * 1024; // 50 MB
long totalBytes = file.Size;
long readBytes = 0;
const int bufferSize = 81920; // 80 KB 청크
var uploadsDir = Path.Combine(Env.WebRootPath, "uploads");
Directory.CreateDirectory(uploadsDir);
var filePath = Path.Combine(uploadsDir,
$"{Guid.NewGuid()}{Path.GetExtension(file.Name)}");
await using var src = file.OpenReadStream(maxAllowedSize: maxSize);
await using var dest = File.Create(filePath);
var buffer = new byte[bufferSize];
int bytesRead;
while ((bytesRead = await src.ReadAsync(buffer)) != 0)
{
await dest.WriteAsync(buffer.AsMemory(0, bytesRead));
readBytes += bytesRead;
progress = (int)(readBytes * 100 / totalBytes);
StateHasChanged(); // UI 갱신
}
progress = 100;
StateHasChanged();
}
}
5. Blazor WebAssembly에서 Web API로 전송
Blazor WebAssembly는 서버 파일 시스템에 직접 접근할 수 없으므로, MultipartFormDataContent로 파일 스트림을 HTTP POST 요청에 담아 Web API 컨트롤러로 전송합니다.
@inject HttpClient Http
private string message = string.Empty;
private async Task UploadToApi(InputFileChangeEventArgs e)
{
var file = e.File;
const long maxSize = 5 * 1024 * 1024;
var stream = file.OpenReadStream(maxAllowedSize: maxSize);
var content = new MultipartFormDataContent();
content.Add(new StreamContent(stream), "file", file.Name);
var response = await Http.PostAsync("/api/upload", content);
message = response.IsSuccessStatusCode ? "업로드 성공" : "업로드 실패";
}
[ApiController]
[Route("api/[controller]")]
public class UploadController : ControllerBase
{
private readonly IWebHostEnvironment _env;
public UploadController(IWebHostEnvironment env) => _env = env;
[HttpPost]
public async Task<IActionResult> Upload(IFormFile file)
{
if (file == null || file.Length == 0)
return BadRequest("파일이 없습니다.");
var dir = Path.Combine(_env.WebRootPath, "uploads");
Directory.CreateDirectory(dir);
var safeFileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
var path = Path.Combine(dir, safeFileName);
await using var fs = System.IO.File.Create(path);
await file.CopyToAsync(fs);
return Ok(new { FileName = safeFileName });
}
}
⚠️ 자주 틀리는 것 / 주의사항
file.OpenReadStream()을 인수 없이 호출하면 기본 최대 500 KB까지만 읽으며, 그보다 큰 파일에서 예외가 발생합니다. 반드시 maxAllowedSize를 명시하세요.예)
file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024)업로드된 파일명에
../../../etc/passwd 같은 경로 탐색 문자열이 포함될 수 있습니다(Path Traversal 공격). 반드시 Guid.NewGuid()로 무작위 이름을 생성하고, Path.GetExtension(file.Name)으로 확장자만 가져오세요.HTML
accept 속성은 파일 선택 대화상자의 표시 형식을 제한하지만, 공격자는 이를 무시하고 임의 파일을 전송할 수 있습니다. 서버 측 file.ContentType 화이트리스트 검증을 반드시 추가하세요.e.GetMultipleFiles()를 매개변수 없이 호출하면 최대 10개 파일을 허용합니다. 의도적인 대량 파일 전송 공격을 막으려면 maximumFileCount를 명시하여 허용 개수를 제한하세요.Blazor Server는 SignalR을 통해 서버-클라이언트가 통신합니다. 기본 메시지 크기 제한 때문에 대용량 파일 업로드 시 오류가 발생할 수 있습니다.
Program.cs에서 제한을 명시적으로 늘려주세요.builder.Services.AddSignalR(o => o.MaximumReceiveMessageSize = 10 * 1024 * 1024);🎯 마무리
Blazor 파일 업로드의 핵심은 InputFile 컴포넌트와 IBrowserFile 인터페이스를 조합하는 것입니다. OnChange 이벤트에서 파일을 받아 크기·형식을 검증하고, OpenReadStream으로 스트림을 열어 CopyToAsync로 서버에 저장하는 패턴이 핵심입니다. 이 패턴은 단일 파일, 다중 파일, 진행률 표시 어느 경우에나 동일하게 적용됩니다.
실전 팁: 프로덕션 환경에서는 로컬 디스크 대신 Azure Blob Storage나 Amazon S3 같은 클라우드 스토리지를 사용하는 것이 확장성과 가용성 면에서 유리합니다. 이 경우에도 OpenReadStream으로 스트림을 열어 클라우드 SDK의 업로드 메서드에 전달하는 패턴은 동일하게 적용됩니다.
- InputFile은
OnChange이벤트로 파일 선택을 감지하고IBrowserFile객체를 제공하는 Blazor 내장 컴포넌트입니다. - 단일 파일:
e.File/ 다중 파일:e.GetMultipleFiles(maximumFileCount: N) OpenReadStream(maxAllowedSize)로 스트림을 열고CopyToAsync로 서버 파일 시스템에 저장합니다.- 보안 3원칙: ① 파일명은 Guid로 교체 ② ContentType 서버 화이트리스트 검증 ③ 파일 크기 제한 명시
- Blazor WebAssembly는
MultipartFormDataContent로 Web API에 전송하고, 서버는IFormFile로 수신합니다. - 대용량 파일은 청크 단위 읽기로 진행률을 계산하고
StateHasChanged()로 UI를 실시간 갱신합니다. - Blazor Server 사용 시 SignalR의
MaximumReceiveMessageSize를Program.cs에서 반드시 조정해야 합니다.