프로젝트를 진행하면서 '만약 웹 스토리지를 사용할 수 없는 상황이라면 어떤 식으로 처리해야 할까'에 대한 고민을 했던 경험이 있었습니다. 브라우저의 시크릿 모드같이 Web Storage를 쓸 수 없는 상황이 발생한다면 localStorage나 sessionStorage 등에 접근하려고 할 때 에러가 발생하게 됩니다. 이 때, @toss/storage 패키지를 사용한다면 안전하게 스토리지의 값을 다룰 수 있게 됩니다. 이 라이브러리를 소개해주신 프론트엔드 개발자 승규님에게 감사드리며 토스에서는 이 문제를 어떤식으로 해결했는지 확인해보도록 하겠습니다.
브라우저의 시크릿 모드나 테스트 환경과 같이 Web Storage를 쓸 수 없는 환경에서 localStorage나 sessionStorage 등에 접근하려고 하면 에러가 발생합니다. @toss/storage 패키지는 그런 환경에서도 에러 없이 Web Storage를 쓸 수 있도록 돕는 라이브러리입니다.
먼저, @toss/storage의 전체 코드는 아래와 같습니다.
export interface Storage {
get(key: string): string | null;
set(key: string, value: string): void;
remove(key: string): void;
clear(): void;
}
class MemoStorage implements Storage {
private storage = new Map<string, string>();
public get(key: string) {
return this.storage.get(key) || null;
}
public set(key: string, value: string) {
this.storage.set(key, value);
}
public remove(key: string) {
this.storage.delete(key);
}
public clear() {
this.storage.clear();
}
}
class LocalStorage implements Storage {
public static canUse(): boolean {
const TEST_KEY = generateTestKey();
// 사용자가 쿠키 차단을 하는 경우 LocalStorage '접근' 시에 예외가 발생합니다.
try {
localStorage.setItem(TEST_KEY, 'test');
localStorage.removeItem(TEST_KEY);
return true;
} catch (err) {
return false;
}
}
public get(key: string) {
return localStorage.getItem(key);
}
public set(key: string, value: string) {
localStorage.setItem(key, value);
}
public remove(key: string) {
localStorage.removeItem(key);
}
public clear() {
localStorage.clear();
}
}
class SessionStorage implements Storage {
public static canUse(): boolean {
const TEST_KEY = generateTestKey();
// sessionStorage를 사용할 수 없는 경우에 대응합니다.
try {
sessionStorage.setItem(TEST_KEY, 'test');
sessionStorage.removeItem(TEST_KEY);
return true;
} catch (err) {
return false;
}
}
public get(key: string) {
return sessionStorage.getItem(key);
}
public set(key: string, value: string) {
sessionStorage.setItem(key, value);
}
public remove(key: string) {
sessionStorage.removeItem(key);
}
public clear() {
sessionStorage.clear();
}
}
function generateTestKey() {
return new Array(4)
.fill(null)
.map(() => Math.random().toString(36).slice(2))
.join('');
}
export function generateStorage(): Storage {
if (LocalStorage.canUse()) {
return new LocalStorage();
}
return new MemoStorage();
}
export function generateSessionStorage(): Storage {
if (SessionStorage.canUse()) {
return new SessionStorage();
}
return new MemoStorage();
}
export const safeLocalStorage = generateStorage();
export const safeSessionStorage = generateSessionStorage();
generateStorage()
), 세션 스토리지를 사용할 것인지(generateSessionStorage()
)를 결정해 아래 코드를 통해 라이브러리에 접근하여 사용할 수 있습니다.import { generateStorage } from '@toss/storage';
import { generateSessionStorage } from '@toss/storage';
const safeLocalStorage = generateStorage();
const safeSessionStorage = generateSessionStorage();
export function generateStorage(): Storage {
if (LocalStorage.canUse()) {
return new LocalStorage();
}
return new MemoStorage();
}
2-1. canUse 메서드에서는 랜덤한 값을 가진 길이 4짜리 배열을 생성해 로컬스토리지에 넣어 봅니다. 만약 오류 없이 값이 정상적으로 들어간다면 true를 반환하고, 오류가 발생한다면 false를 반환합니다.
class LocalStorage implements Storage {
public static canUse(): boolean {
const TEST_KEY = generateTestKey();
// 사용자가 쿠키 차단을 하는 경우 LocalStorage '접근' 시에 예외가 발생합니다.
try {
localStorage.setItem(TEST_KEY, 'test');
localStorage.removeItem(TEST_KEY);
return true;
} catch (err) {
return false;
}
}
function generateTestKey() {
return new Array(4)
.fill(null)
.map(() => Math.random().toString(36).slice(2))
.join('');
}
2-2. 다시 원래의 조건문으로 돌아와서, 문제가 없다면 LocalStorage 인스턴스를 반환하고, 문제가 있었다면 MemoStorage를 반환합니다.
export function generateStorage(): Storage {
if (LocalStorage.canUse()) {
return new LocalStorage();
}
return new MemoStorage();
}
여기서 MemoStorage는 Map 객체를 이용해 데이터를 메모리 상에 저장하도록 되어있다. 그렇기 때문에, 웹 세션을 새로고침하거나 브러우저를 종료할 경우, 저장된 데이터는 사라집니다.
class MemoStorage implements Storage { private storage = new Map<string, string>(); public get(key: string) { return this.storage.get(key) || null; }
import { generateSessionStorage } from '@toss/storage';
const safeSessionStorage = generateSessionStorage();
safeSessionStorage.set('key', 'value');
safeSessionStorage.get('key');
safeSessionStorage.remove('key');
safeSessionStorage.clear();
(보너스: 실제 코드로 사용)
저는 해당 스토리지가 필요한 파일마다 import 하는 것보다 하나의 파일에서 스토리지를 관리하는 것이 코드 중복을 줄일 수 있다고 생각하여 store 폴더에 별도의 파일을 만들어, 스토리지를 관리했습니다.
// typedStorage.ts
import { TypedStorage } from "@toss/storage/typed";
export const accessTokenStorage = new TypedStorage<string>("accessToken");
export const refreshTokenStorage = new TypedStorage<string>("refreshToken");
// 사용법
// accessTokenStorage.get('key');
// accessTokenStorage.set('key', 'value');
// accessTokenStorage.remove('key');
// accessTokenStorage.clear();
// 해당 스토리지의 값이 필요한 컴포넌트나 페이지 파일
import { accessTokenStorage } from "../../store/typedStorage";
const accessToken = accessTokenStorage.get();
코드를 확인해보니 웹 스토리지가 동작하지 않는 상황에서도 해당 기능이 동작할 수 있도록 안정성을 확보했다는 것을 확인할 수 있었습니다.