home

강한 타입 결속성을 가진 Local/Session Storage

글 분류
main
키워드
browser
typescript
생성일
2023/06/04 03:35
최근 수정일
2023/06/13 10:38
작성중

⚽️ 목표

강한 타입 결속력을 가지고 있으며 사용하기 쉬운 로컬, 세션 스토리지 관리 함수를 만들어보자.

1. 들어가기 전에

클래스 기반으로 TypeScript를 활용하여 Local Storage를 활용하는 이글을(아마도)FE 개발자라면 한번씩 읽어 봤을 글이다.
너무나도 좋은 글이지만 아쉽게도 이 글은 2017년에 발행된 글이다. React Hook은 2019년 2월 16일에 release 됐으니 Class Component를 사용할 때 발행됐다는 뜻. 필자는 이 게시글에서 사용하는 방식에서 몇가지 개선 될 수 있는 사항을 확인했다.
클래스를 방식으로 Local Storage를 관리 - 함수 컴포넌트로 존재하는(React Hook) 코드 스페이스 상 에서 클래스를 이용하는것이 요즘 FE 개발자들에겐 코드 파악을 위한 추가적인 컨텍스트 스위칭이 필요해 코드 파악에 더 많은 리소스가 소요된다. → 함수 방식으로 접근 방식을 변경.
코드 양의 증가 - 사용할 때 마다 new 키워드를 이용해 인스턴스화를 해주고 여러 로컬 스토리지에 접근 시 여러 인스턴스를 생성해야하므로 결과적으로 작성되는 코드의 양이 증가한다. → 함수로 사용 시 더 간단하고 명확한 코드 사용 가능.
약한 타입 결속력 - 로컬 스토리지를 instance화 할때 제네릭으로 사용하려는 타입을 지정해준다. 결과적으로 우리가 로컬 스토리지에 접근하는 키 값은 정해져 있으므로 제네릭 보다는 우리가 실제로 사용하는 로컬 스토리지 키값에 매핑되는 결과값을 강한 타입 결속력으로 지정해주는게 더 효율적이다. → 입력하는 키 값에 따라 자동으로 추론되는 저장 데이터의 타입.
팀의 규칙이 정확히 정해져 있지 않은 팀의 경우(아마 대부분의 스타트업, 바쁘고 할일이 많다는 이유로 정확한 컨벤션, 규칙이 없는 경우가 많다.) “너도나도 만드는 Local / Session Storage” 현상이 발생한다.
필자는 다른 로컬/세션 스토리지 접근 방식을 제안하려 한다.
1.
함수 방식으로 변경
2.
강한 타입 결속력으로 쉬운 타입 추론

2. 함수 사용 방식으로 개선

클래스 기반이 아닌 함수 기반으로 스토리지로 접근하는 함수를 생성한다.

set, reset wrapping 함수

export const resetLocalStorage = (key: string) => { localStorage.removeItem(key); }; export const setLocalStorageItem = (key: string, value: unknown) => { const serializedData = JSON.stringify(value); localStorage.setItem(key, serializedData); };
TypeScript
복사
get(로컬 스토리지로 부터 데이터를 가져오는 함수)과 reset(로컬 스토리지에 존재하는 키값을 제거하는 함수) 함수를 생성한다.
lcoalStorage에는 문자열만 저장 가능하므로 JSON.stiringify 함수를 사용해 데이터를 문자열화 한다.

get wrapping 함수

export const getLocalStorageItem = (key: string) => { try { const deSerializedData = localStorage.getItem(key); if (!deSerializedData) return null; return JSON.parse(deSerializedData); } catch { return undefined; } };
TypeScript
복사
로컬 스토리지에서 해당되는 키의 값을 가져오는 함수를 작성한다.
저장되어 있는 데이터들은 문자열로 변형된 데이터들 이므로 JSON.parse() 함수를 이용해 deserialize한다.
이 때 잘못된 형태로 입력된 데이터들(쌍이 맞지 않는 ”{”, “[”,” ]”, “}”와 같은 케이스)을 deserialize 시도할 경우에 대비해 catch문에서 undefined를 출력한다.
JSON.parse에서 오류가 발생한 케이스와 실제 데이터가 없어서 출력되는 값을 다르게 설정하여 서로 다른 로직으로 대응할 수 있도록 적용했다.

2. 끝나지 않았다.

NO

get 함수의 문제점

export const getLocalStorageItem = (key: string) => { // ... 생략 }
TypeScript
복사
“가져오려는 localStorage의 키가 string이여야 한다.” 라는 타이핑만 됐다.
입력 가능한 키의 종류가 강한 타입이 아닌 string이므로 오타로 인해 잘못된 값 또는 없는 값을 참조할 수 있다. → 추가로 팀의 컨벤션에서 벗어난 스토리지 생성까지 가능하다.
출력 값을 추론할 수 없다. 입력한 키의(사전에 지정된 키일 경우)의 출력 값이 추론되지 않고(당연하게도) any 타입이 된다.
any 타입의 결과물

set 함수의 문제점

export const setLocalStorageItem = (key: string, value: unknown) => { // ... 생략 }
TypeScript
복사
파라미터에 “설정하려는 localStorage의 키가 string이여야 하며 그 값은 unkown이다.” 라는 타이핑만 됐다.
입력 가능한 키의 종류가 강한 타입이 아닌 string이므로 오타로 인해 잘못된 키 또는 없는 키를 가진 값을 참조할 수 있다.
키에 해당하는 값의 타입이 추론되지 않고 unkown 이므로 값이 정해지지 않은 타입으로 설정될 수 있다.
WrongKey, wrongValue → 단어 그대로 잘못된 키/값을 입력했음에도 어떠한 타입 에러도 출력되지 않는다.
잘못된 키,값 쌍 지정시 에러 추론되지 않음

3. 강한 타입 결속력 + 타입 추론

요구사항

get, set 함수 모두 사전에 정의된 키 값만 설정 가능해야한다.
generic을 사용하지 않고 키 값에 따라 출력값의 타입 또는 set 일 경우 set 하는 value의 타입이 추론되어야 한다.
get 함수의 키값은 사전에 정의된 키 값만 입력 가능해야하며, 사전에 정의된 키 값에 따라 반환 값의 타입이 추론되어야 한다.
set 함수의 키값은 사전에 정의된 키 값만 입력 가능해야하며, set하는 value값의 타입은 입력되는 키값에 따라 자동으로 추론되어야 한다.

로컬스토리지의 타입

type CommandType = { command: string; result: string }; type CountType = number; type AccessTokenType = { token: string; userName: string }; export interface LocalStorageDef { COMMAND: CommandType[]; COUNT: CountType; ACCESS_TOKEY: AccessTokenType; }
TypeScript
복사
성공적인 타입 추론 테스트를 위해 로컬 스토리지에 저장되는 타입을 3 가지로 설정했다.
command와 result property가 존재하는 CommandType
numbe 타입을 가진 CountType
token과 userName이 property로 존재하는 AccessTokenType

4.1. 결과 - Get, Reset 함수

export interface LocalStorageGetItemDef { <T extends keyof LocalStorageDef>(key: T): | LocalStorageDef[T] | null | undefined; }; export const getLocalStorageItem: LocalStorageGetItemDef = (key) => { try { const serializedData = localStorage.getItem(key); if (serializedData === null) { resetLocalStorage(key); return null; } return JSON.parse(serializedData); } catch { return undefined; } };
TypeScript
복사
제네릭(T)를 이용해 입력되는 파라미터의 타입을 제한할 수 있으며 입력되는 파라미터에 따라 가져오는 반환 값을 추론할 수 있다.
즉, 우리는 “사전에 정의된 키값에만 접근이 가능하며 반환 값 또한 추론할 수 있다.”
COMMAND키 값을 인자로 전달할 경우 사전에 정의한 CommandType 또는 null 또는 undefined가 자동으로 추론된다.
사전에 정의되지 않은 키를 입력할 경우 타입 에러가 출력된다.
export interface LocalStorageResetItemDef { <T extends keyof LocalStorageDef>(key: T): void; }; export const resetLocalStorage: LocalStorageResetItemDef = (key) => { localStorage.removeItem(key); };
TypeScript
복사
Get 함수와 비슷한 방식이며 차이점이 있다면 반환 값이 없다는 것 이다.

4.2 결과 - Set 함수

export interface LocalStorageSetItemDef { <T extends keyof LocalStorageDef>( key: T, value: LocalStorageDef[T] ): void; }; export const setLocalStorageItem: LocalStorageSetItemDef = ( key, value ) => { const serializedData = JSON.stringify(value); localStorage.setItem(key, serializedData); };
TypeScript
복사
제네릭(T)를 이용해 입력되는 키의 타입을 제한할 수 있으며 value의 경우 키에 해당되는 value만 입력 가능하다.
즉, 우리는 “사전에 정의된 키값에 해당되는 키값 만 set 가능하며 사전에 설정한 키와 매칭되는 값의 타입만 입력 가능하다.”
사전에 정의된 키와 값의 쌍을 인자로 전달할 경우 아무런 타입에러가 존재하지 않는다.
키값이 잘못됐을 경우(사전에 정의되지 않은 키값) 타입에러가 출력된다.

5. 결론

// 관리 하려는 값들의 타입 지정 type KeyThatYouWant = {valueThatYouWant1: string; valueThatYouWant2} type CommandType = { command: string; result: string }; type CountType = number; type AccessTokenType = { token: string; userName: string }; // 접근하려는 키값을 프로퍼티의 키값으로 지정 export interface LocalStorageDef { COMMAND: CommandType[]; COUNT: CountType; ACCESS_TOKEY: AccessTokenType; }; export interface LocalStorageGetItemDef { <T extends keyof LocalStorageDef>(key: T): | LocalStorageDef[T] | null | undefined; }; export const getLocalStorageItem: LocalStorageGetItemDef = (key) => { try { const serializedData = localStorage.getItem(key); if (serializedData === null) { resetLocalStorage(key); return null; } return JSON.parse(serializedData); } catch { return undefined; } }; export interface LocalStorageSetItemDef { <T extends keyof LocalStorageDef>( key: T, value: LocalStorageDef[T] ): void; }; export const setLocalStorageItem: LocalStorageSetItemDef = ( key, value ) => { const serializedData = JSON.stringify(value); localStorage.setItem(key, serializedData); };
TypeScript
복사
사전에 약속되지 않은 키또는 값을 조작하려 시도할 경우 바로 타입에러가 출력되는, “아 이거 키값이 뭐였더라?”라고 할 필요 없이 타입 추론되는걸 찾아가면 되는 로컬 스토리지 조작 함수가 탄생했다.
물론 세션 스토리지 접근함수는 wrapping 함수의 내부 키워드만 sessionStorage로 변경해주면 된다. 다른방식으로 직접 튜닝해서 사용해도 좋다.

다른 시도

참조