⚽️ 목표
강한 타입 결속력을 가지고 있으며 사용하기 쉬운 로컬, 세션 스토리지 관리 함수를 만들어보자.
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로 변경해주면 된다. 다른방식으로 직접 튜닝해서 사용해도 좋다.