로컬 스토리지 활용
브라우저 LocalStorage와 SessionStorage에 데이터를 저장하고 불러오는 방법을 배웁니다.
📌 학습 목표
- LocalStorage와 SessionStorage의 차이점을 이해하고 상황에 맞는 스토리지를 선택할 수 있습니다.
- IJSRuntime을 이용한 JavaScript Interop으로 브라우저 스토리지에 데이터를 저장하고 불러올 수 있습니다.
- Blazored.LocalStorage 라이브러리를 프로젝트에 설치하고 서비스로 등록하여 활용할 수 있습니다.
- C# 객체를 JSON으로 자동 직렬화하여 스토리지에 저장하고 복원할 수 있습니다.
- 스토리지의 보안 제한사항과 용량 한계를 이해하고 실전 프로젝트에 적용할 수 있습니다.
📝 개념 설명
브라우저 스토리지란?
브라우저 스토리지(Web Storage)는 웹 브라우저가 제공하는 클라이언트 측 데이터 저장 공간입니다. 서버를 거치지 않고 사용자의 브라우저에 직접 데이터를 보관하므로 빠른 읽기·쓰기가 가능합니다.
기존 쿠키(Cookie)는 용량이 4KB로 제한되고 모든 HTTP 요청에 자동으로 포함되어 네트워크 부하를 유발합니다. 반면 Web Storage는 5~10MB의 넉넉한 용량을 제공하며 서버로 자동 전송되지 않아 훨씬 효율적입니다.
브라우저 스토리지는
프로토콜 + 도메인 + 포트가 동일한 페이지에서만 접근 가능합니다. 예를 들어 https://myapp.com의 데이터에 https://other.com은 접근할 수 없습니다.LocalStorage와 SessionStorage 비교
Web Storage API는 두 가지 저장소를 제공합니다. 데이터의 유지 기간과 공유 범위에서 차이가 납니다.
| 항목 | LocalStorage | SessionStorage |
|---|---|---|
| 데이터 유지 기간 | 명시적 삭제 전까지 영구 보관 | 탭/창 닫으면 자동 삭제 |
| 탭 간 공유 | 동일 origin의 모든 탭에서 공유 | 현재 탭에서만 유효 |
| 저장 용량 | 약 5~10MB (브라우저마다 다름) | 약 5~10MB |
| 주요 용도 | 사용자 설정, 토큰 캐싱, 장바구니 | 임시 폼 데이터, 단계별 마법사 상태 |
| JavaScript 객체 | window.localStorage | window.sessionStorage |
Blazor에서 브라우저 스토리지에 접근하는 방법
Blazor WebAssembly는 브라우저 안에서 .NET 런타임으로 실행됩니다. localStorage는 JavaScript 브라우저 API이므로 .NET 코드에서 직접 호출할 수 없습니다. 이를 위해 JavaScript Interop(JS 상호 운용)을 사용합니다.
브라우저 스토리지에 접근하는 방법은 크게 두 가지입니다:
- IJSRuntime 직접 사용: Blazor 기본 내장 인터페이스로 별도 설치 없이 JS 함수를 직접 호출합니다. 단순하지만 직렬화를 직접 처리해야 합니다.
- Blazored.LocalStorage 라이브러리: NuGet 패키지로 타입 안전성과 자동 JSON 직렬화를 제공합니다. 실무에서 권장됩니다.
(.NET 코드)
ILocalStorageService
Bridge
localStorage API
내부 스토리지
IJSRuntime 주요 메서드
InvokeVoidAsync("함수명", 인자...)— 반환값 없는 JS 함수 호출InvokeAsync<T>("함수명", 인자...)— T 타입 반환값이 있는 JS 함수 호출
브라우저 스토리지 관련 JavaScript 함수:
localStorage.setItem(key, value)— 키-값 저장 (문자열만 가능)localStorage.getItem(key)— 값 불러오기 (없으면 null 반환)localStorage.removeItem(key)— 특정 키 삭제localStorage.clear()— 전체 초기화- sessionStorage도 완전히 동일한 API 구조를 사용합니다.
Blazored.LocalStorage 라이브러리
Blazored.LocalStorage는 Steve Sanderson 팀이 만든 공식 커뮤니티 라이브러리로, ILocalStorageService를 통해 타입 안전한 스토리지 접근을 제공합니다. 내부적으로 System.Text.Json을 사용해 자동 직렬화·역직렬화를 처리합니다.
핵심 메서드:
SetItemAsync<T>(key, value)— 값 저장 (자동 JSON 직렬화)GetItemAsync<T>(key)— 값 불러오기 (자동 역직렬화)RemoveItemAsync(key)— 항목 삭제ClearAsync()— 전체 초기화ContainKeyAsync(key)— 키 존재 여부 확인LengthAsync()— 저장된 항목 수 조회
💡 예제 & 실습
실습 1: IJSRuntime으로 테마 설정 저장하기
사용자가 선택한 다크/라이트 테마를 LocalStorage에 저장하고 페이지 재방문 시 자동 복원하는 예제입니다.
@page "/theme-settings"
@inject IJSRuntime JS
<h2>테마 설정</h2>
<p>현재 테마: <strong>@currentTheme</strong></p>
<button @onclick='SaveDark'>🌙 다크 모드</button>
<button @onclick='SaveLight'>☀️ 라이트 모드</button>
<button @onclick='DeleteTheme'>설정 삭제</button>
@code {
private string currentTheme = "없음";
private async Task SaveDark()
{
await JS.InvokeVoidAsync("localStorage.setItem", "theme", "dark");
currentTheme = "dark (저장됨)";
}
private async Task SaveLight()
{
await JS.InvokeVoidAsync("localStorage.setItem", "theme", "light");
currentTheme = "light (저장됨)";
}
private async Task DeleteTheme()
{
await JS.InvokeVoidAsync("localStorage.removeItem", "theme");
currentTheme = "없음";
}
// 페이지 로드 시 자동으로 테마 복원
protected override async Task OnInitializedAsync()
{
var saved = await JS.InvokeAsync<string?>("localStorage.getItem", "theme");
currentTheme = saved ?? "저장된 테마 없음";
}
}컴포넌트 초기화 시 스토리지에서 값을 자동으로 불러오려면
OnInitializedAsync()를 오버라이드하세요. 단, Blazor Server에서는 사전 렌더링(pre-rendering) 단계에서 JS Interop이 제한되므로 OnAfterRenderAsync를 사용해야 합니다 (자주 틀리는 것 항목 참고).실습 2: Blazored.LocalStorage로 사용자 설정 저장하기
dotnet add package Blazored.LocalStorageusing Blazored.LocalStorage;
var builder = WebApplication.CreateBuilder(args);
// Blazor WebAssembly의 경우: WebAssemblyHostBuilder.CreateDefault(args)
builder.Services.AddBlazoredLocalStorage();
// SessionStorage도 함께 사용한다면:
// builder.Services.AddBlazoredSessionStorage();public class UserSettings
{
public string Username { get; set; } = string.Empty;
public string Language { get; set; } = "ko";
public int FontSize { get; set; } = 14;
public bool DarkMode { get; set; } = false;
}@page "/user-settings"
@inject ILocalStorageService localStorage
<h2>사용자 설정</h2>
<div>
<label>사용자 이름:</label>
<input @bind='settings.Username' />
</div>
<div>
<label>글꼴 크기:</label>
<input type='number' @bind='settings.FontSize' min='10' max='24' />
</div>
<div>
<label>
<input type='checkbox' @bind='settings.DarkMode' /> 다크 모드
</label>
</div>
<button @onclick='SaveSettings'>설정 저장</button>
<button @onclick='LoadSettings'>설정 불러오기</button>
<button @onclick='DeleteSettings'>설정 초기화</button>
<p>@statusMessage</p>
@code {
private UserSettings settings = new();
private string statusMessage = "";
private async Task SaveSettings()
{
// 객체가 자동으로 JSON으로 직렬화되어 저장됩니다
await localStorage.SetItemAsync("userSettings", settings);
statusMessage = "✅ 설정이 저장되었습니다!";
}
private async Task LoadSettings()
{
var saved = await localStorage.GetItemAsync<UserSettings>("userSettings");
if (saved != null)
{
settings = saved;
statusMessage = $"✅ 설정 불러옴: {settings.Username}";
}
else
{
statusMessage = "저장된 설정이 없습니다.";
}
}
private async Task DeleteSettings()
{
await localStorage.RemoveItemAsync("userSettings");
settings = new UserSettings();
statusMessage = "설정이 초기화되었습니다.";
}
protected override async Task OnInitializedAsync()
{
await LoadSettings();
}
}Blazored.LocalStorage는 UserSettings 객체를 자동으로 JSON 문자열로 변환하여 저장합니다. 불러올 때도 JSON을 자동으로 역직렬화해 UserSettings 객체로 반환하므로, 직렬화 코드를 직접 작성할 필요가 없습니다.실습 3: SessionStorage로 임시 폼 데이터 유지하기
다단계 주문 폼에서 사용자가 중간에 이탈했다가 돌아와도 입력 내용이 유지되도록 SessionStorage에 임시 저장합니다.
@page "/order-step1"
@inject IJSRuntime JS
<h2>주문 1단계: 배송 정보</h2>
<input @bind='recipientName' placeholder='수령인 이름' />
<input @bind='address' placeholder='배송 주소' />
<button @onclick='SaveDraft'>임시 저장</button>
<button @onclick='ClearDraft'>초기화</button>
<p>@draftStatus</p>
@code {
private string recipientName = "";
private string address = "";
private string draftStatus = "";
protected override async Task OnInitializedAsync()
{
// 이전에 입력한 내용 자동 복원
var name = await JS.InvokeAsync<string?>("sessionStorage.getItem", "orderName");
var addr = await JS.InvokeAsync<string?>("sessionStorage.getItem", "orderAddr");
recipientName = name ?? "";
address = addr ?? "";
if (name != null) draftStatus = "이전 입력 내용을 복원했습니다.";
}
private async Task SaveDraft()
{
await JS.InvokeVoidAsync("sessionStorage.setItem", "orderName", recipientName);
await JS.InvokeVoidAsync("sessionStorage.setItem", "orderAddr", address);
draftStatus = "임시 저장 완료";
}
private async Task ClearDraft()
{
await JS.InvokeVoidAsync("sessionStorage.removeItem", "orderName");
await JS.InvokeVoidAsync("sessionStorage.removeItem", "orderAddr");
recipientName = "";
address = "";
draftStatus = "초기화되었습니다.";
}
}Blazored.LocalStorage가 설치되어 있다면 ILocalStorageService 대신 ISessionStorageService를 주입하면 SessionStorage를 동일한 API로 사용할 수 있습니다. 서비스 등록은 builder.Services.AddBlazoredSessionStorage()를 추가하세요.⚠️ 자주 틀리는 것 / 주의사항
1. Blazor Server에서 사전 렌더링(Pre-rendering) 문제
Blazor Server에서는 컴포넌트가 서버에서 먼저 렌더링됩니다. 이 단계에서 브라우저가 아직 준비되지 않아 OnInitializedAsync()에서 JS Interop을 호출하면 예외가 발생할 수 있습니다.
// ❌ Blazor Server에서 예외 발생 가능
protected override async Task OnInitializedAsync()
{
var data = await JS.InvokeAsync<string>("localStorage.getItem", "key");
}
// ✅ Blazor Server에서는 OnAfterRenderAsync 사용
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var data = await JS.InvokeAsync<string?>("localStorage.getItem", "key");
StateHasChanged(); // UI 갱신 명시적 호출 필요
}
}OnInitializedAsync()에서 JS Interop을 직접 사용해도 안전합니다.2. 민감한 정보 저장 절대 금지
LocalStorage는 JavaScript로 직접 접근 가능하여 XSS(Cross-Site Scripting) 공격에 취약합니다. 다음 정보는 절대 LocalStorage에 저장하면 안 됩니다:
- 비밀번호 (해시 값 포함)
- 신용카드 번호, 주민등록번호 등 개인 식별 정보
- 서버 인증 JWT 토큰 — HttpOnly 쿠키 사용이 더 안전합니다
3. null 반환값 처리 미흡
GetItemAsync는 키가 없거나 값이 null일 때 null을 반환합니다. null 체크 없이 바로 사용하면 NullReferenceException이 발생합니다.
// ❌ null 체크 없이 직접 사용 — 런타임 오류 위험
var settings = await localStorage.GetItemAsync<UserSettings>("settings");
Console.WriteLine(settings.Username); // NullReferenceException!
// ✅ null 병합 연산자로 안전하게 처리
var settings = await localStorage.GetItemAsync<UserSettings>("settings")
?? new UserSettings();
Console.WriteLine(settings.Username); // 안전4. 스토리지 용량 초과(QuotaExceededError)
브라우저마다 다르지만 일반적으로 5~10MB 제한이 있습니다. 대용량 데이터(이미지, 파일 등)는 IndexedDB를 사용하고, 스토리지 쓰기는 항상 예외 처리를 권장합니다.
try
{
await localStorage.SetItemAsync("largeData", bigObject);
}
catch (Exception ex)
{
// QuotaExceededError 등 용량 초과 처리
Console.WriteLine($"저장 실패: {ex.Message}");
// 사용자에게 알림 표시
}5. 직렬화 불가능한 타입 주의
Blazored.LocalStorage는 내부적으로 System.Text.Json을 사용합니다. 다음 상황에서 문제가 발생할 수 있습니다:
- 순환 참조가 있는 객체 → 직렬화 예외 발생
- private setter만 있는 속성 → 역직렬화 시 기본값 유지
DateTime타입 → UTC로 저장·복원됨 (로컬 시간과 차이 주의)
🎯 마무리
브라우저 스토리지는 Blazor 앱에서 서버 왕복 없이 클라이언트 상태를 유지하는 핵심 도구입니다. LocalStorage는 사용자 설정·캐시처럼 지속적으로 보관해야 할 데이터에, SessionStorage는 탭 단위의 임시 상태 관리에 적합합니다.
실제 프로젝트에서는 IJSRuntime 직접 호출보다 Blazored.LocalStorage 라이브러리를 활용하는 것이 타입 안전성과 자동 JSON 직렬화 측면에서 생산성이 크게 높아집니다. 다만 XSS 취약성을 항상 염두에 두고, 보안에 민감한 정보는 절대 브라우저 스토리지에 저장하지 않는 원칙을 지켜야 합니다.
- LocalStorage: 영구 보관, 동일 origin 모든 탭 공유 → 사용자 설정·캐싱에 적합
- SessionStorage: 탭 닫으면 삭제, 현재 탭에서만 유효 → 임시 데이터·폼 상태에 적합
- IJSRuntime: 별도 설치 없이
localStorage.setItem / getItemJS 함수 직접 호출 가능 - Blazored.LocalStorage: NuGet 설치 → 의존성 주입(DI) 등록(
AddBlazoredLocalStorage()) →ILocalStorageService로 타입 안전하게 사용 - Blazor Server 주의: JS Interop은
OnAfterRenderAsync(firstRender)에서 사용해야 사전 렌더링 예외 방지 가능 - 보안 원칙: 비밀번호·개인정보·JWT 토큰은 LocalStorage 저장 금지 (XSS 취약)
- GetItemAsync 반환값은 항상 null 체크, 대용량 쓰기는 try-catch로 예외 처리할 것