지금까지 배운 타입스크립트의 타입을 간단하게 정리해보겠습니다.
1. 문자
// 문자 타입 지정 예시
export const getString = (arg: string): string => {
return arg;
}
const result = getString('철수');
console.log(result)
2. 숫자
// 숫자 타입 지정 예시
export const getNumber = (arg: number): number => {
return arg;
}
const result = getString(123);
console.log(result)
3. any
// any 타입 지정 예시
export const getAny = (arg: any): number => {
return arg;
}
const result1 = getAny('철수');
const result2 = getAny(8);
const result3 = getAny(true);
인자에 string, number, boolean 등 어떤 타입이 들어가도 전부 any라고 반환됩니다.
이 때, 인자에 들어오는 값에 따라 반환되는 타입이 결정되도록 할 수는 없을까요?
타입스크립트의 Generic 타입을 사용하면, 인자에 들어오는 타입을 그대로 사용할 수 있습니다.
// Generic 사용 예제
export const getGeneric = <MyType>(arg: MyType): MyType => {
return arg;
}
const aaa: string = "철수";
const bbb: number = 8;
const ccc: boolean = true;
const result41 = getGeneric(aaa);
const result42 = getGeneric(bbb);
const result43 = getGeneric(ccc);
arg가 string 타입으로 들어갈 때에는 string 타입이, number 타입으로 들어갈 때에는 number 타입이, boolean 타입으로 들어갈 때에는 boolean 타입이 되는 것을 확인할 수 있습니다.
여러 개의 인자가 들어가는 함수에서 Generic을 적용하고 싶은 경우, 다음과 같은 응용도 가능합니다.
여기서 MyType 부분은 함수처럼 사용자가 원하는 이름을 지정해줄 수 있습니다.
MyType은 너무 길기 때문에 실무에서는 통상적으로 T
, U
, V
등 간단한 이름을 사용합니다.
Generic 타입을 실무에서는 어떤 경우에 사용할 수 있을까요?
useQuery, useMutation처럼 내가 만든 기능을 다른 사람에게 제공하는 경우
해당 기능에 들어오는 값의 타입을 예상할 수 없습니다. 이럴 때 Generic을 사용하면 들어오는 값의 타입에 따라 반환되는 값이나 컴포넌트의 타입이 결정
되도록 할 수 있습니다.
우리가 이전에 만들어 두었던 HOC에 적용해 볼까요?
HOC에 Generic 타입을 적용하는 방법을 이해하기 위해, 중간 단계부터 차근차근 학습해봅시다.
함수를 return하는 함수, HOF(Higher Order Function)에 type을 지정해봅시다.
인자에 항상 고정된 타입의 값만 들어오지 않을 수 있습니다.
그런 경우에는 어떻게 type을 지정해주면 될까요?
다음과 같이 any를 지정해줄 수도 있겠지만, 이런 경우는 코드의 안정성이 매우 떨어집니다.
그럴 때 앞에서 배운 Generic을 이용해서 타입을 지정해줄 수 있습니다.
Generic을 이용하면 인자에 어떠한 type이 들어가더라도 해당 type을 반환합니다.
함수 선언식으로 작성된 HOF를 화살표 함수로 바꾸어도 동일하게 Generic을 적용할 수 있습니다.
화살표 함수로 작성된 Generic 예시를 HOC(Higher Order Component)로 변경해봅시다.
이제 이전에 만들어두었던 withAuth라는 HOC에 Generic을 적용해볼까요?
Component의 타입은 React에서 제공하는 ComponentType을 이용합니다.
그리고 props에 반드시 객체가 들어가야 스프레드 연산자를 사용할 수 있기 때문에, extends를 사용해서 P라는 Generic 타입이 객체라는 사실을 명시해줍니다.
브라우저에는 변수와 같이 저장할 수 있는 공간
이 따로 존재합니다.
그렇다면, 변수에 저장해도 되는 것을 왜 굳이 브라우저에 저장하는 것일까요?
변수에 저장한 데이터는 새로고침을 하게되면 사라지게 됩니다.
하지만, 지금부터 우리가 사용하게 될 쿠키, 로컬스토리지, 세션스토리지 등은 새로고침을 하더라도 사라지지 않습니다.
💡 언제 사용하나요?
여러분은 혹시 비회원으로 쇼핑몰을 구경하신 경험이 있으신가요?
비회원으로 장바구니도 담아보신 적 있으신가요?
비회원으로 담는 장바구니는 로그인된 계정의 데이터베이스에 저장되는 것이 아닌, 브라우저의 저장소에 임시로 저장되는 것입니다.
물론 다 같은 저장소(스토리지) 이지만, 각각의 차별점을 가지고 있습니다.
쿠키
로컬스토리지
세션스토리지
- 데이터를 브라우저에 저장합니다.
- 브라우저를 종료할 때 삭제됩니다.
// 쿠키 저장
const onClickSaveCookie = () => {
document.cookie = "aaa=철수"
}
// 쿠키 조회
const onClickGetCookie = () => {
const mycookie = document.cookie
console.log(mycookie)
}
// 로컬 스토리지 저장
const onClickSaveLocal = () => {
localStorage.setItem("bbb", "영희");
};
// 로컬 스토리지 조회
const onClickGetLocal = () => {
const bbb = localStorage.getItem("bbb");
console.log(bbb);
};
// 세션 스토리지 저장
const onClickSaveSession = () => {
sessionStorage.setItem("ccc", "훈이");
};
// 세션 스토리지 조회
const onClickGetSession = () => {
const ccc = sessionStorage.getItem("ccc");
console.log(ccc);
};
로컬 스토리지와 세션 스토리지는 저장/조회하는 방법이 비슷하지만 쿠키는 다릅니다.
로컬/세션 스토리지는 localStorage.setItem("key","value")
혹은 sessionStorage.setItem("key","value")
로 저장하고, 쿠키는 document.cookie = "key=value"
로 저장할 수 있습니다.
const onClickSaveCookie = () => {
document.cookie = 'aaa=철수';
}
const onClickSaveLocal = () => {
localStorage.setItem('bbb', '영희');
}
const onClickSaveSession = () => {
const ccc = sessionStorage.setItem('ccc', '훈이);
}
로컬/세션 스토리지의 경우 꺼내오는 것도 간단합니다.
setItem()
을 getItem("key")
로 해당 key에 대한 value를 받아올 수 있습니다.
const onClickGetCookie = () => {
const aaa = document.cookie;
console.log(aaa);
}
const onClickGetLocal = () => {
const bbb = localStorage.getItem('bbb');
console.log(bbb);
}
const onClickGetSession = () => {
const ccc = sessionStorage.getItem('ccc');
console.log(ccc);
}
하지만 쿠키는 조금 다릅니다.
특정 키에 대한 호출이 아니고, 쿠키 전체를 가져와서 보여주고 있습니다.
그렇기에 쿠키 안에서 필요한 key에 대한 value를 뽑아오는 작업을 해주어야 합니다.
예시와 같은 쿠키에서 “aaa” 라는 key에 대한 value만 가져와 봅시다.
const onClickGetCookie = () => {
const aaa = document.cookie.split('; ').filter((el) => el.startsWith('aaa='))[0];
const result = aaa.replace('aaa=', '');
console.log(result);
}
세미콜론 ;
기준으로 split("; ")
해준 뒤 filter()
와 startWith()
를 사용해 해당 key에 대한 value만 골라내어 줄 수 있습니다.
브라우저 저장소를 설명하면서, 브라우저 저장소를 이용하면 비회원 전용 장바구니를 구현할 수 있다고 이야기했습니다.
그렇다면 쿠키, 로컬 스토리지, 세션 스토리지 중 어떤 것을 이용하는 것이 좋을까요?
답은 “다 괜찮다. 정답은 없다.” 입니다.
각 브라우저 저장소의 특성을 고려해 기획 의도에 적합한 저장소를 선택
하면 됩니다.
우리는 그 중 가장 일반적인 로컬 스토리지를 이용해서 실습해보겠습니다.
💡 주의!
데이터를 그대로 로컬 스토리지에 넣으면 내용이 제대로 입력되지 않고 [Object object] 와 같이 들어갑니다.
객체를 로컬 스토리지에 넣기 위해서는 반드시JSON.stringify등을 이용해 string으로 변경
해야 합니다.
💡 제외하고 싶은 데이터가 있는 경우
__typename처럼 장바구니에 넣을 필요가 없는 내용이 원본 데이터에 포함되어 있을 수 있습니다. 이런 경우에는 delete를 사용해서 빼고 싶은 내용을 직접 삭제하기보다는,rest 파라미터를 이용해서 나머지 데이터를 추출하는 방식으로 가공하는 편이 좋습니다.
const onClickBasket = (el: IBoard) => () => {
// 로컬스토리지에 baskets가 이미 있다면 해당 데이터를 불러온다.
const baskets = JSON.parse(localStorage.getItem("baskets") || "[]");
// 장바구니에 추가할 게시글 데이터(el)에서 필요 없는 내용을 제거한다.
const { __typename, ...newEl } = el;
// baskets에 새로운 데이터를 push한다.
baskets.push(newEl);
};
localStorage.setItem('baskets', JSON.stringify(baskets));
const temp = baskets.filter((basjetEl: IBoard) => basetEl._id === el._id);
if(temp.length === 1) {
alert('이미 담으신 물품입니다!!!');
return;
}
완성된 함수는 다음과 같은 형태가 됩니다.
// 비회원 장바구니에 클릭한 게시글을 넣어주는 함수
const onClickBasket = (el: IBoard) => () => {
const baskets = JSON.parse(localStorage.getItem("baskets") || "[]");
const temp = baskets.filter((basketEl: IBoard) => basketEl._id === el._id);
if (temp.length === 1) {
alert("이미 담으신 물품입니다!!!");
return;
}
const { __typename, ...newEl } = el;
baskets.push(newEl);
localStorage.setItem("baskets", JSON.stringify(baskets));
};
이제 로컬 스토리지에 넣은 재용을 불러와서 나만의 장바구니 페이지에서 볼 수 있게 해보겠습니다.
import { useState } from "react";
import { IBoard } from "../../src/commons/types/generated/types";
const BasketLoggedInPage = () => {
const [basketItems, setBasketItems] = useState([]);
const baskets = JSON.parse(localStorage.getItem("baskets") || "[]");
setBasketItems(baskets);
return (
<div>
<h1>나만의 장바구니(비회원전용!!)</h1>
{basketItems.map((el: IBoard) => (
<div key={el._id}>
<span>{el.writer}</span> |
<span>{el.title}</span>
</div>
))}
</div>
);
}
export default BasketLoggedInPage
하지만 위와 같이 코드를 짤 경우, **프론트엔드 서버에서 프리렌더링이 이루어질 때에는 localStorage가 존재하지 않기 때문에 오류가 발생**
합니다.
해당 문제를 해결하기 위하여 useEffect 안에 코드를 넣어줍니다.
useEffect를 사용하면 브라우저에서 페이지가 마운트 될 때에만 해당 코드가 실행
됩니다.
import { useEffect, useState } from "react";
import { IBoard } from "../../src/commons/types/generated/types";
const BasketLoggedInPage = () => {
const [basketItems, setBasketItems] = useState([]);
useEffect(() => {
const baskets = JSON.parse(localStorage.getItem("baskets") || "[]");
setBasketItems(baskets);
}, []);
return (
<div>
<h1>나만의 장바구니(비회원전용!!)</h1>
{basketItems.map((el: IBoard) => (
<div key={el._id}>
<span>{el.writer}</span> |
<span>{el.title}</span>
</div>
))}
</div>
);
}
export default BasketLoggedInPage