-
LocalStorage + Typescript :: 마이구미Typescript 2021. 10. 30. 21:17반응형
이 글은 로컬 스토리지(LocalStorage) 를 타입스크립트 함께 사용하는 방식을 다룬다.
로컬스토리지는 HTML5 에서 제공된 기능으로 브라우저에서 지원하고 있는 저장소이다.
타입스크립트를 활용하여 로컬스토리지를 사용하면서 런타임에 발생할 수 있는 이슈들을 개선해본다.
로컬 스토리지 - https://developer.mozilla.org/ko/docs/Web/API/Window/localStorage로컬 스토리지는 브라우저에서 제공해주는 key-value 저장소로써, 많은 경우에서 유용하게 사용중이다.
자체적으로 제공해주는 메소드로 '저장', '삭제', '조회', '초기화' 를 쉽게 작성할 수 있다.
// Key - "accessToken" | Value - "eyJraWQiOiJVQ2JINXNS" localStorage.setItem('accessToken', 'eyJraWQiOiJVQ2JINXNS'); // Key - "accessToken" 인 값 가져오기 const data = localStorage.getItem('accessToken'); // Key - "accessToken" 제거하기 localStorage.removeItem('accessToken'); // 로컬 스토리지 초기화 localStorage.clear();
누구나 쉽게 파악할 수 있고, 자체적으로 지원해주는 기능이기 때문에 개선할 점은 딱히 없어보인다.
하지만 만약 개선할 점이 있다면, 그 이유는 key, value 모두 타입이 string 이라는 것이다.
string 타입이 문제가 되는 이유는 크게 2가지로 분류할 수 있다.
만약 Key 를 통해 조회해야하는데 Key 가 없으면?
const data = localStorage.getItem('accessToekn');
accessToken 을 실수로 오타가 존재하는 코드이다.
만약 상수로 빼서 관리한다하더라도, 그 상수가 실제로 로컬 스토리지 Key 로 사용 중이라는 것을 추정할 수 없다.
타입스크립트를 사용해봤다면, IDE 에서 실제로 사용할 수 있는 것들만 추론해서 알려주는 게 얼마나 편하고 안정적인지 알 것이다.
만약 Value 에 배열이나 객체를 저장하고 싶다면?
Value 는 string 타입으로 저장해야하기 때문에 string 이 아니라면 string 형태로 변환해줘야한다.
localStorage.setItem('userData', JSON.stringify({ name: 'mygumi' }));
반대로 위처럼 저장된 값을 조회해야 한다면, 객체로 변환해서 사용해야한다.
const data = JSON.parse(localStorage.getItem('userData'));
위와 같은 문제점들을 타입스크립트를 통해 개선한다면, 런타임 환경에서 많은 부분이 유용하다.
이미 존재하는 기능을 개선해야한다면, 대부분 그 기능을 Wrapper 로 감싸서 타입을 보다 명확하게 부여해주는 방식이 있다.
즉, LocalStorage 를 Wrapper 로 감싸는 것이다.
type LocalStorage = typeof window.localStorage; abstract class Storage<T extends string> { private readonly storage: LocalStorage; protected constructor(getStorage = (): LocalStorage => window.localStorage) { this.storage = getStorage(); } }
추상 클래스로 선언된 Storage 는 window.localStorage 를 단순히 감싼 Wrapper 형태이다. (추상 클래스?)
추후 추상 클래스 Wrapper 는 실제 인스턴스화가 가능한 클래스에서 상속하게 된다.
추상 클래스를 상속할 때 선언되는 제네릭 타입 T 는 로컬 스토리지를 접근하는 key 를 의미한다.
이것이 의미하는 것은 Wrapper 에 localStorage 의 getItem, removeItem, clear 등을 위한 메소드를 구현해보면 알 수 있다.
protected get(key: T): string | null { return this.storage.getItem(key); } protected set(key: T, value: string): void { this.storage.setItem(key, value); } protected clearItem(key: T): void { this.storage.removeItem(key); }
즉, 추상 클래스를 위해 선언되는 제네릭 타입 T 는 key 파라미터의 타입으로 선언된 모습을 볼 수 있다.
결과적으로 올바르지 않은 key 를 전달한다면, 타입 에러를 발생하여 타입의 안정성을 향상시켜준다. (아래 이미지 첨부)
set 의 경우에는 두번째 파라미터로 타입이 string 인 value 를 볼 수 있다.
로컬 스토리지에 set 할 경우에는 무조건 string 타입으로 변환하는 것을 타입 추론으로 요구하는 것이다. (아래 이미지 첨부)
전체 코드는 다음과 같다.
type LocalStorage = typeof window.localStorage; const PREFIX_LOCALSTORAGE = 'Mygumi_'; export default abstract class Storage<T extends string> { private readonly storage: LocalStorage; protected constructor(getStorage = (): LocalStorage => window.localStorage) { this.storage = getStorage(); } private getOriginKey(key: T) { return `${PREFIX_LOCALSTORAGE}${key}`; } protected get(key: T): string | null { return this.storage.getItem(this.getOriginKey(key)); } protected set(key: T, value: string): void { this.storage.setItem(this.getOriginKey(key), value); } protected clearItem(key: T): void { this.storage.removeItem(this.getOriginKey(key)); } protected clearItems(keys: T[]): void { keys.forEach(key => this.clearItem(this.getOriginKey(key) as T)); } }
여기서 하나 더 언급하자면, 로컬 스토리지의 Key 를 위한 Prefix 가 존재하는 것을 볼 수 있다.
언듯 보면, 다른 도메인과 Key 겹치는 일은 존재하는 것을 방지하는 거라고 생각할 수 있다.
하지만 로컬 스토리지의 유지 범위는 origin 기준이다. (protocol://host:port)
그렇기에 다른 서비스와 Key 가 겹치는 일은 존재하지 않는다.
이제 실제로 추상 클래스를 상속받아 사용하는 클래스를 작성해보자.
enum UserStorageKey { ACCESS_TOKEN = 'accessToken', REFRESH_TOKEN = 'refreshToken', USER_DATA = 'userData' } class UserStorage extends Storage<UserStorageKey> { constructor() { super(); } }
위에서 언급했듯이, 제네릭 타입으로 선언된 enum 을 넘김으로써 선언된 Key 가 아니라면 타입 에러를 발생하게 된다.
set 의 경우에도 마찬가지이다.
로컬 스토리지의 value 는 string 타입이기 때문에 Wrapper 에서 value 의 타입을 string 으로 정의하였다.
그렇기에 set 하는 쪽에서 string 으로 변환해서 넘기지 않으면 타입 에러를 발생하게 된다.
전체 코드는 다음과 같다.
import Storage from './index'; enum UserStorageKey { ACCESS_TOKEN = 'accessToken', REFRESH_TOKEN = 'refreshToken', USER_DATA = 'userData' } type UserAttribute = { name: 'nickname' | 'email' | 'profile'; value: string; }; interface UserData { UserAttributes: UserAttribute[]; UserName: string; } class UserStorage extends Storage<UserStorageKey> { constructor() { super(); } getAccessToken() { return this.get(UserStorageKey.ACCESS_TOKEN); } setAccessToken(accessToken: string) { this.set(UserStorageKey.ACCESS_TOKEN, accessToken); } getRefreshToken() { return this.get(UserStorageKey.REFRESH_TOKEN); } setRefreshToken(refreshToken: string) { this.set(UserStorageKey.REFRESH_TOKEN, refreshToken); } getUserData(): UserData | null { const data = this.get(UserStorageKey.USER_DATA); if (data) { return JSON.parse(data); } return null; } setUserData(userData: UserData) { this.set(UserStorageKey.USER_DATA, JSON.stringify(userData)); } clear() { this.clearItems([ UserStorageKey.ACCESS_TOKEN, UserStorageKey.REFRESH_TOKEN, UserStorageKey.USER_DATA ]); } } export default new UserStorage();
Wrapper 의 역할을 크게 2가지로 설명했지만, 그 이외에도 유용한 용도가 많다.
아래 링크들을 참고하길 바란다.
참고한 글
https://betterprogramming.pub/creating-localstorage-wrapper-with-typescript-7ff6b71b35cb
더 보면 좋은 글
https://medium.com/banksalad/safe-localstorage-using-typescdript-eac147f59ae
반응형'Typescript' 카테고리의 다른 글
덕 타이핑(Duck typing) 이란? :: 마이구미 (2) 2020.02.23