From 240bf6e637a486b223afe5cb1083a0377f17b366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EC=98=81?= Date: Wed, 13 May 2026 12:10:45 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=EC=A0=9C=ED=95=9C=EB=90=9C=20?= =?UTF-8?q?=EB=B8=8C=EB=9D=BC=EC=9A=B0=EC=A0=80=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EC=9D=98=20Storage=20=EC=A0=91=EA=B7=BC=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=20=EB=B0=A9=EC=96=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/hooks/auth/useAuth.ts | 12 ++- src/utils/hooks/auth/useAutoLogin.ts | 4 +- src/utils/ts/apiClient.ts | 10 ++- src/utils/ts/env.ts | 116 ++++++++++++++++++++------- src/utils/zustand/myLectures.ts | 17 ++-- 5 files changed, 115 insertions(+), 44 deletions(-) diff --git a/src/utils/hooks/auth/useAuth.ts b/src/utils/hooks/auth/useAuth.ts index adfce0ce1..b2004da5c 100644 --- a/src/utils/hooks/auth/useAuth.ts +++ b/src/utils/hooks/auth/useAuth.ts @@ -1,3 +1,4 @@ +import { useCallback } from 'react'; import { useMutation } from '@tanstack/react-query'; import { refresh } from 'api/auth'; import { COOKIE_KEY } from 'static/url'; @@ -8,10 +9,13 @@ import { useTokenStore } from 'utils/zustand/auth'; const useAuth = () => { const { setToken, setRefreshToken } = useTokenStore.getState(); - const getRefreshToken = () => { - const refreshTokenStorage = isomorphicLocalStorage.getItem('refresh-token-storage'); - return refreshTokenStorage && JSON.parse(refreshTokenStorage).state.refreshToken; - }; + const getRefreshToken = useCallback(() => { + const refreshTokenStorage = isomorphicLocalStorage.getJSONItem<{ state?: { refreshToken?: string } } | null>( + 'refresh-token-storage', + null, + ); + return refreshTokenStorage?.state?.refreshToken ?? null; + }, []); const { mutateAsync: refreshAccessToken } = useMutation({ mutationFn: async (refresh_token: string) => { diff --git a/src/utils/hooks/auth/useAutoLogin.ts b/src/utils/hooks/auth/useAutoLogin.ts index cecd74f7d..73139da95 100644 --- a/src/utils/hooks/auth/useAutoLogin.ts +++ b/src/utils/hooks/auth/useAutoLogin.ts @@ -3,10 +3,10 @@ import useAuth from './useAuth'; const useAutoLogin = () => { const { refreshAccessToken, getRefreshToken } = useAuth(); - const refreshToken = getRefreshToken(); useEffect(() => { const autoLogin = async () => { + const refreshToken = getRefreshToken(); if (!refreshToken) return; try { await refreshAccessToken(refreshToken); @@ -16,7 +16,7 @@ const useAutoLogin = () => { }; void autoLogin(); - }, [refreshAccessToken, refreshToken]); + }, [refreshAccessToken, getRefreshToken]); }; export default useAutoLogin; diff --git a/src/utils/ts/apiClient.ts b/src/utils/ts/apiClient.ts index 3be685c3b..827997736 100644 --- a/src/utils/ts/apiClient.ts +++ b/src/utils/ts/apiClient.ts @@ -10,6 +10,7 @@ import { useTokenStore } from 'utils/zustand/auth'; import { useServerStateStore } from 'utils/zustand/serverState'; import { redirectToClub, redirectToLogin } from './auth'; import { deleteCookie, getCookieDomain, setCookie } from './cookie'; +import { isomorphicLocalStorage } from './env'; import { saveTokensToNative } from './iosBridge'; const API_URL = process.env.NEXT_PUBLIC_API_PATH; @@ -166,10 +167,11 @@ export default class APIClient { deleteCookie(COOKIE_KEY.AUTH_TOKEN, domain ? { domain } : undefined); try { - const storage = window.localStorage.getItem('refresh-token-storage'); - const refreshToken = storage - ? (JSON.parse(storage) as { state: { refreshToken: string } }).state.refreshToken - : null; + const storage = isomorphicLocalStorage.getJSONItem<{ state?: { refreshToken?: string } } | null>( + 'refresh-token-storage', + null, + ); + const refreshToken = storage?.state?.refreshToken ?? null; if (refreshToken) { await this.refreshAccessToken(refreshToken); diff --git a/src/utils/ts/env.ts b/src/utils/ts/env.ts index 0c56e80eb..180ebfc3f 100644 --- a/src/utils/ts/env.ts +++ b/src/utils/ts/env.ts @@ -1,38 +1,98 @@ export const isBrowser = typeof window !== 'undefined'; export const isServer = !isBrowser; -/** isomorphic-localStorage */ -export const isomorphicLocalStorage = { - getItem(key: string): string | null { - return isBrowser ? window.localStorage.getItem(key) : null; - }, - setItem(key: string, value: string): void { - if (isBrowser) window.localStorage.setItem(key, value); - }, - removeItem(key: string): void { - if (isBrowser) window.localStorage.removeItem(key); - }, - clear(): void { - if (isBrowser) window.localStorage.clear(); - }, +type StorageVariant = 'local' | 'session'; + +const stringifyStorageValue = (value: unknown): string | null => { + try { + const serializedValue = JSON.stringify(value); + return typeof serializedValue === 'string' ? serializedValue : null; + } catch { + return null; + } }; -/** isomorphic-sessionStorage */ -export const isomorphicSessionStorage = { - getItem(key: string): string | null { - return isBrowser ? window.sessionStorage.getItem(key) : null; - }, - setItem(key: string, value: string): void { - if (isBrowser) window.sessionStorage.setItem(key, value); - }, - removeItem(key: string): void { - if (isBrowser) window.sessionStorage.removeItem(key); - }, - clear(): void { - if (isBrowser) window.sessionStorage.clear(); - }, +export const getBrowserStorage = (variant: StorageVariant): Storage | null => { + if (!isBrowser) return null; + + try { + return variant === 'local' ? window.localStorage : window.sessionStorage; + } catch { + return null; + } +}; + +const createIsomorphicStorage = (variant: StorageVariant) => { + const storageAdapter = { + getItem(key: string): string | null { + const storage = getBrowserStorage(variant); + if (!storage) return null; + + try { + return storage.getItem(key); + } catch { + return null; + } + }, + setItem(key: string, value: string): void { + const storage = getBrowserStorage(variant); + if (!storage) return; + + try { + storage.setItem(key, value); + } catch { + // 제한된 브라우저 환경에서는 Storage 접근이 불가능하거나 용량이 가득 찰 수 있습니다. + } + }, + getJSONItem(key: string, defaultValue: T): T { + const item = storageAdapter.getItem(key); + if (!item) return defaultValue; + + try { + return JSON.parse(item) as T; + } catch { + return defaultValue; + } + }, + setJSONItem(key: string, value: unknown): void { + const serializedValue = stringifyStorageValue(value); + if (serializedValue === null) return; + + storageAdapter.setItem(key, serializedValue); + }, + removeItem(key: string): void { + const storage = getBrowserStorage(variant); + if (!storage) return; + + try { + storage.removeItem(key); + } catch { + // 제한된 브라우저 환경에서는 Storage 접근이 불가능할 수 있습니다. + } + }, + clear(): void { + const storage = getBrowserStorage(variant); + if (!storage) return; + + try { + storage.clear(); + } catch { + // 제한된 브라우저 환경에서는 Storage 접근이 불가능할 수 있습니다. + } + }, + }; + + return storageAdapter; }; +export const getStorageJSONValue = (value: unknown): string | null => stringifyStorageValue(value); + +/** isomorphic-localStorage */ +export const isomorphicLocalStorage = createIsomorphicStorage('local'); + +/** isomorphic-sessionStorage */ +export const isomorphicSessionStorage = createIsomorphicStorage('session'); + /** isomorphic-document */ export const isomorphicDocument = { getElementById(id: string): HTMLElement | null { diff --git a/src/utils/zustand/myLectures.ts b/src/utils/zustand/myLectures.ts index 42f661cb4..9fa1fcf71 100644 --- a/src/utils/zustand/myLectures.ts +++ b/src/utils/zustand/myLectures.ts @@ -1,4 +1,5 @@ import { Lecture } from 'api/timetable/entity'; +import { isomorphicLocalStorage } from 'utils/ts/env'; import { create } from 'zustand'; import { useShallow } from 'zustand/react/shallow'; @@ -8,6 +9,10 @@ interface TimetableInfoFromLocalStorage { const MY_LECTURES_KEY = 'my-lectures'; +const getInitialLectures = (): TimetableInfoFromLocalStorage => { + return isomorphicLocalStorage.getJSONItem(MY_LECTURES_KEY, {}); +}; + type State = { lectures: TimetableInfoFromLocalStorage; }; @@ -25,14 +30,14 @@ interface TimeStringState { } export const useLecturesStore = create((set, get) => ({ - lectures: typeof window !== 'undefined' ? JSON.parse(localStorage.getItem(MY_LECTURES_KEY) ?? '{}') : {}, + lectures: getInitialLectures(), action: { addLecture: (lecture, semester) => { const timetableInfoList = get().lectures; const newValue = [...(timetableInfoList[semester] || [])]; newValue.push(lecture); - localStorage.setItem(MY_LECTURES_KEY, JSON.stringify({ ...timetableInfoList, [semester]: newValue })); + isomorphicLocalStorage.setJSONItem(MY_LECTURES_KEY, { ...timetableInfoList, [semester]: newValue }); set(() => ({ lectures: { ...timetableInfoList, [semester]: newValue } })); }, removeLecture: (lecture, semester) => { @@ -40,10 +45,10 @@ export const useLecturesStore = create((set, get) => ({ const timetableInfoWithNewValue = timetableInfoList[semester].filter( (newValue) => lecture.code !== newValue.code || lecture.lecture_class !== newValue.lecture_class, ); - localStorage.setItem( - MY_LECTURES_KEY, - JSON.stringify({ ...timetableInfoList, [semester]: timetableInfoWithNewValue }), - ); + isomorphicLocalStorage.setJSONItem(MY_LECTURES_KEY, { + ...timetableInfoList, + [semester]: timetableInfoWithNewValue, + }); set(() => ({ lectures: { ...timetableInfoList, [semester]: timetableInfoWithNewValue } })); }, }, From a325849818bcaab41f0dc191bb483f73fd4f5850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EC=98=81?= Date: Wed, 13 May 2026 12:11:07 +0900 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20Storage=20=EC=A7=81=EC=A0=91=20?= =?UTF-8?q?=EC=A0=91=EA=B7=BC=EC=9D=84=20=EC=95=88=EC=A0=84=20=EB=9E=98?= =?UTF-8?q?=ED=8D=BC=20=EC=82=AC=EC=9A=A9=EC=9C=BC=EB=A1=9C=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Course/hooks/useSelectedCourses.ts | 14 ++----- .../IndexComponents/IndexCafeteria/index.tsx | 4 +- .../Review/components/ReviewButton/index.tsx | 3 +- .../components/Review/index.tsx | 3 +- .../StoreReviewPage/ReviewForm/ReviewForm.tsx | 4 +- src/components/Store/utils/durationTime.ts | 6 ++- .../MainTimetablePage/DefaultPage/index.tsx | 11 +++--- .../components/MainTimetable/index.tsx | 3 +- .../hooks/useTimetableMutation.ts | 7 +++- .../Header/MobileHeader/Panel/index.tsx | 3 +- .../layout/Header/MobileHeader/index.tsx | 7 ++-- .../layout/Header/PCHeader/index.tsx | 7 ++-- src/components/ui/IntroToolTip/index.tsx | 3 +- src/pages/_app.tsx | 3 +- src/pages/auth/modifyinfo/index.tsx | 3 +- src/pages/graduation/index.tsx | 7 ++-- src/pages/store/[id].tsx | 29 +++++++------- src/pages/store/index.tsx | 5 ++- src/pages/timetable/index.tsx | 3 +- src/utils/hooks/abTest/useABTestView.ts | 7 ++-- src/utils/hooks/auth/useLogout.ts | 3 +- src/utils/hooks/state/useLocalStorage.ts | 16 ++++---- src/utils/hooks/state/useWebStorage.ts | 39 ++++++++++++------- src/utils/ts/auth.ts | 7 ++-- 24 files changed, 110 insertions(+), 87 deletions(-) diff --git a/src/components/Course/hooks/useSelectedCourses.ts b/src/components/Course/hooks/useSelectedCourses.ts index ce10a06a5..a8944df57 100644 --- a/src/components/Course/hooks/useSelectedCourses.ts +++ b/src/components/Course/hooks/useSelectedCourses.ts @@ -1,23 +1,15 @@ import { useMemo, useState } from 'react'; import { PreCourse } from 'api/course/entity'; +import { isomorphicSessionStorage } from 'utils/ts/env'; const SELECTED_COURSES_KEY = 'selected-courses'; const getStoredCourses = (): PreCourse[] => { - try { - const stored = sessionStorage.getItem(SELECTED_COURSES_KEY); - return stored ? JSON.parse(stored) : []; - } catch { - return []; - } + return isomorphicSessionStorage.getJSONItem(SELECTED_COURSES_KEY, []); }; const storeCourses = (courses: PreCourse[]) => { - try { - sessionStorage.setItem(SELECTED_COURSES_KEY, JSON.stringify(courses)); - } catch { - // sessionStorage 용량 초과 등 무시 - } + isomorphicSessionStorage.setJSONItem(SELECTED_COURSES_KEY, courses); }; export const getCourseKey = (course: PreCourse) => { diff --git a/src/components/IndexComponents/IndexCafeteria/index.tsx b/src/components/IndexComponents/IndexCafeteria/index.tsx index cc5c1de14..59a04794d 100644 --- a/src/components/IndexComponents/IndexCafeteria/index.tsx +++ b/src/components/IndexComponents/IndexCafeteria/index.tsx @@ -45,12 +45,12 @@ function IndexCafeteria() { }; const handleTooltipContentButtonClick = () => { - localStorage.setItem('cafeteria-tooltip', 'used'); + isomorphicLocalStorage.setItem('cafeteria-tooltip', 'used'); handleMoreClick(); }; const handleTooltipCloseButtonClick = () => { - localStorage.setItem('cafeteria-tooltip', 'used'); + isomorphicLocalStorage.setItem('cafeteria-tooltip', 'used'); closeTooltip(); }; diff --git a/src/components/Store/StoreDetailPage/components/Review/components/ReviewButton/index.tsx b/src/components/Store/StoreDetailPage/components/Review/components/ReviewButton/index.tsx index a3f9a14be..a8614a115 100644 --- a/src/components/Store/StoreDetailPage/components/Review/components/ReviewButton/index.tsx +++ b/src/components/Store/StoreDetailPage/components/Review/components/ReviewButton/index.tsx @@ -4,6 +4,7 @@ import useStoreDetail from 'components/Store/StoreDetailPage/hooks/useStoreDetai import useLogger from 'utils/hooks/analytics/useLogger'; import useModalPortal from 'utils/hooks/layout/useModalPortal'; import { useUser } from 'utils/hooks/state/useUser'; +import { isomorphicSessionStorage } from 'utils/ts/env'; import styles from './index.module.scss'; export const REVEIW_LOGIN = ['리뷰를 작성하기 ', '리뷰 작성은 회원만 사용 가능합니다.']; @@ -46,7 +47,7 @@ export default function ReviewButton({ id, goReviewPage }: { id: string; goRevie event_label: 'shop_detail_view_review_write', value: storeDetail.name, }); - sessionStorage.setItem('enterReview', new Date().getTime().toString()); + isomorphicSessionStorage.setItem('enterReview', new Date().getTime().toString()); }; return ( diff --git a/src/components/Store/StoreDetailPage/components/Review/index.tsx b/src/components/Store/StoreDetailPage/components/Review/index.tsx index 12e7ccb93..cc4cdb54f 100644 --- a/src/components/Store/StoreDetailPage/components/Review/index.tsx +++ b/src/components/Store/StoreDetailPage/components/Review/index.tsx @@ -2,6 +2,7 @@ import { Suspense, useEffect } from 'react'; import { useRouter } from 'next/router'; import { DropdownProvider } from 'components/Store/StoreDetailPage/hooks/useDropdown'; import ROUTES from 'static/routes'; +import { isomorphicSessionStorage } from 'utils/ts/env'; import AverageRating from './components/AverageRating/AverageRating'; import ReviewButton from './components/ReviewButton'; import ReviewList from './components/ReviewList/ReviewList'; @@ -12,7 +13,7 @@ export default function ReviewPage({ id }: { id: string }) { useEffect(() => { // 리뷰 페이지 진입 시간 기록 - sessionStorage.setItem('enterReviewPage', new Date().getTime().toString()); + isomorphicSessionStorage.setItem('enterReviewPage', new Date().getTime().toString()); }, []); return ( diff --git a/src/components/Store/StoreReviewPage/ReviewForm/ReviewForm.tsx b/src/components/Store/StoreReviewPage/ReviewForm/ReviewForm.tsx index 86820814b..f77d21aa7 100644 --- a/src/components/Store/StoreReviewPage/ReviewForm/ReviewForm.tsx +++ b/src/components/Store/StoreReviewPage/ReviewForm/ReviewForm.tsx @@ -11,6 +11,7 @@ import ROUTES from 'static/routes'; import useLogger from 'utils/hooks/analytics/useLogger'; import useImageUpload from 'utils/hooks/ui/useImageUpload'; import useScrollToTop from 'utils/hooks/ui/useScrollToTop'; +import { isomorphicSessionStorage } from 'utils/ts/env'; import uuidv4 from 'utils/ts/uuidGenerater'; import styles from './ReviewForm.module.scss'; @@ -65,7 +66,8 @@ function ReviewForm({ storeDetail, mutate, initialData = {} }: Props) { }; const reviewSuccessLogging = () => { - const getReviewDurationTime = (new Date().getTime() - Number(sessionStorage.getItem('enterReview'))) / 1000; + const getReviewDurationTime = + (new Date().getTime() - Number(isomorphicSessionStorage.getItem('enterReview'))) / 1000; logger.actionEventClick({ team: 'BUSINESS', event_label: 'shop_detail_view_review_write_done', diff --git a/src/components/Store/utils/durationTime.ts b/src/components/Store/utils/durationTime.ts index cd2436f19..9502e0ee1 100644 --- a/src/components/Store/utils/durationTime.ts +++ b/src/components/Store/utils/durationTime.ts @@ -1,9 +1,11 @@ +import { isomorphicSessionStorage } from 'utils/ts/env'; + const MAIN_ENTRY_TIME = 'main-entry-time'; const CATEGORY_ENTRY_TIME = 'category-entry-time'; const initializeTime = (key: string) => { const currentTime = new Date().getTime(); - sessionStorage.setItem(key, currentTime.toString()); + isomorphicSessionStorage.setItem(key, currentTime.toString()); }; export const initializeMainEntryTime = () => initializeTime(MAIN_ENTRY_TIME); @@ -11,7 +13,7 @@ export const initializeCategoryEntryTime = () => initializeTime(CATEGORY_ENTRY_T const getTime = (key: string) => { const currentTime = new Date().getTime(); - const mainEntryTime = Number(sessionStorage.getItem(key)) || currentTime; + const mainEntryTime = Number(isomorphicSessionStorage.getItem(key)) || currentTime; return (currentTime - mainEntryTime) / 1000; }; diff --git a/src/components/TimetablePage/MainTimetablePage/DefaultPage/index.tsx b/src/components/TimetablePage/MainTimetablePage/DefaultPage/index.tsx index 49e15e8a8..72e448c11 100644 --- a/src/components/TimetablePage/MainTimetablePage/DefaultPage/index.tsx +++ b/src/components/TimetablePage/MainTimetablePage/DefaultPage/index.tsx @@ -7,6 +7,7 @@ import MainTimetable from 'components/TimetablePage/components/MainTimetable'; import TimetableList from 'components/TimetablePage/components/TimetableList'; import ROUTES from 'static/routes'; import useLogger from 'utils/hooks/analytics/useLogger'; +import { isomorphicSessionStorage } from 'utils/ts/env'; import styles from './DefaultPage.module.scss'; interface DefaultPageProps { @@ -19,14 +20,14 @@ export default function DefaultPage({ timetableFrameId, setCurrentFrameId }: Def const logger = useLogger(); const handlePopState = React.useCallback(() => { // swipe로 뒤로가기 시 - if (sessionStorage.getItem('swipeToBack') === 'true') { + if (isomorphicSessionStorage.getItem('swipeToBack') === 'true') { logger.actionEventSwipe({ team: 'USER', event_label: 'timetable_back', value: 'OS스와이프', previous_page: '시간표', current_page: '메인', - duration_time: (new Date().getTime() - Number(sessionStorage.getItem('enterTimetablePage'))) / 1000, + duration_time: (new Date().getTime() - Number(isomorphicSessionStorage.getItem('enterTimetablePage'))) / 1000, }); history.back(); return; @@ -38,7 +39,7 @@ export default function DefaultPage({ timetableFrameId, setCurrentFrameId }: Def value: '뒤로가기버튼', previous_page: '시간표', current_page: '메인', - duration_time: (new Date().getTime() - Number(sessionStorage.getItem('enterTimetablePage'))) / 1000, + duration_time: (new Date().getTime() - Number(isomorphicSessionStorage.getItem('enterTimetablePage'))) / 1000, }); }, [logger]); @@ -51,11 +52,11 @@ export default function DefaultPage({ timetableFrameId, setCurrentFrameId }: Def }, []); React.useEffect(() => { - sessionStorage.setItem('swipeToBack', 'false'); + isomorphicSessionStorage.setItem('swipeToBack', 'false'); const handleWheel = (e: WheelEvent) => { // 한 번에 최소 100px만큼 스크롤(드래그) 시 swipe했다고 간주 if (e.deltaX < -100) { - sessionStorage.setItem('swipeToBack', 'true'); + isomorphicSessionStorage.setItem('swipeToBack', 'true'); } }; window.addEventListener('wheel', handleWheel); diff --git a/src/components/TimetablePage/components/MainTimetable/index.tsx b/src/components/TimetablePage/components/MainTimetable/index.tsx index bd78f8b4a..b488a89ea 100644 --- a/src/components/TimetablePage/components/MainTimetable/index.tsx +++ b/src/components/TimetablePage/components/MainTimetable/index.tsx @@ -19,6 +19,7 @@ import ROUTES from 'static/routes'; import useLogger from 'utils/hooks/analytics/useLogger'; import useBooleanState from 'utils/hooks/state/useBooleanState'; import useTokenState from 'utils/hooks/state/useTokenState'; +import { isomorphicSessionStorage } from 'utils/ts/env'; import { useSemester } from 'utils/zustand/semester'; import DownloadTimetableModal from './DownloadTimetableModal'; import styles from './MyLectureTimetable.module.scss'; @@ -143,7 +144,7 @@ function ValidMainTimetable({ timetableFrameId }: { readonly timetableFrameId: n team: 'USER', event_label: 'timetable', value: '이미지저장', - duration_time: (new Date().getTime() - Number(sessionStorage.getItem('enterTimetablePage'))) / 1000, + duration_time: (new Date().getTime() - Number(isomorphicSessionStorage.getItem('enterTimetablePage'))) / 1000, }); openModal(); } diff --git a/src/components/TimetablePage/hooks/useTimetableMutation.ts b/src/components/TimetablePage/hooks/useTimetableMutation.ts index c9a132d92..8ca0dbde7 100644 --- a/src/components/TimetablePage/hooks/useTimetableMutation.ts +++ b/src/components/TimetablePage/hooks/useTimetableMutation.ts @@ -9,6 +9,7 @@ import { } from 'api/timetable/entity'; import useToast from 'components/feedback/Toast/useToast'; import useTokenState from 'utils/hooks/state/useTokenState'; +import { isomorphicSessionStorage } from 'utils/ts/env'; import showToast from 'utils/ts/showToast'; import { useLecturesAction } from 'utils/zustand/myLectures'; import { useSemester } from 'utils/zustand/semester'; @@ -67,7 +68,9 @@ export default function useTimetableMutation(timetableFrameId: number) { // 강의 복원 const restoreLecture = (id: number[]) => { - const restoredLecture = JSON.parse(sessionStorage.getItem('restoreLecture')!); + const restoredLecture = isomorphicSessionStorage.getJSONItem('restoreLecture', null); + if (!restoredLecture || typeof restoredLecture !== 'object') return; + if ('name' in restoredLecture) { addLectureFromLocalStorage(restoredLecture, `${semester?.year}${semester?.term}`); } else { @@ -107,7 +110,7 @@ export default function useTimetableMutation(timetableFrameId: number) { const removeMyLecture = useMutation({ mutationFn: async ({ clickedLecture, id }: RemoveMyLectureProps & { disableRecoverButton?: boolean }) => { - sessionStorage.setItem('restoreLecture', JSON.stringify(clickedLecture)); + isomorphicSessionStorage.setJSONItem('restoreLecture', clickedLecture); if (clickedLecture && 'name' in clickedLecture) { return Promise.resolve(removeLectureFromLocalStorage(clickedLecture, `${semester?.year}${semester?.term}`)); } diff --git a/src/components/layout/Header/MobileHeader/Panel/index.tsx b/src/components/layout/Header/MobileHeader/Panel/index.tsx index 653ee03f7..7fe3be52d 100644 --- a/src/components/layout/Header/MobileHeader/Panel/index.tsx +++ b/src/components/layout/Header/MobileHeader/Panel/index.tsx @@ -13,6 +13,7 @@ import useTokenState from 'utils/hooks/state/useTokenState'; import { useUser } from 'utils/hooks/state/useUser'; import { useBodyScrollLock } from 'utils/hooks/ui/useBodyScrollLock'; import { useEscapeKeyDown } from 'utils/hooks/ui/useEscapeKeyDown'; +import { isomorphicSessionStorage } from 'utils/ts/env'; import { useMobileSidebar } from 'utils/zustand/mobileSidebar'; import type { Portal } from 'components/modal/Modal/PortalProvider'; import styles from './Panel.module.scss'; @@ -62,7 +63,7 @@ export default function Panel({ openModal }: PanelProps) { value: '햄버거', previous_page: '시간표', current_page: title, - duration_time: (new Date().getTime() - Number(sessionStorage.getItem('enterTimetablePage'))) / 1000, + duration_time: (new Date().getTime() - Number(isomorphicSessionStorage.getItem('enterTimetablePage'))) / 1000, }); } }; diff --git a/src/components/layout/Header/MobileHeader/index.tsx b/src/components/layout/Header/MobileHeader/index.tsx index f92a75f2a..a3271da37 100644 --- a/src/components/layout/Header/MobileHeader/index.tsx +++ b/src/components/layout/Header/MobileHeader/index.tsx @@ -11,6 +11,7 @@ import ROUTES from 'static/routes'; import useLogger from 'utils/hooks/analytics/useLogger'; import { useResetHeaderButton } from 'utils/hooks/layout/useResetHeaderButton'; import useParamsHandler from 'utils/hooks/routing/useParamsHandler'; +import { isomorphicSessionStorage } from 'utils/ts/env'; import { backButtonTapped } from 'utils/ts/iosBridge'; import { useHeaderTitle } from 'utils/zustand/customTitle'; import { useHeaderButtonStore } from 'utils/zustand/headerButtonStore'; @@ -46,8 +47,8 @@ export default function MobileHeader({ openModal }: MobileHeaderProps) { event_label: 'shop_detail_view_back', value: response.name, event_category: 'click', - current_page: sessionStorage.getItem('cameFrom') || '', - duration_time: (new Date().getTime() - Number(sessionStorage.getItem('enter_storeDetail'))) / 1000, + current_page: isomorphicSessionStorage.getItem('cameFrom') || '', + duration_time: (new Date().getTime() - Number(isomorphicSessionStorage.getItem('enter_storeDetail'))) / 1000, }); // 상점 내 뒤로가기 버튼 로깅 router.back(); return; @@ -59,7 +60,7 @@ export default function MobileHeader({ openModal }: MobileHeaderProps) { value: '뒤로가기버튼', previous_page: '시간표', current_page: '메인', - duration_time: (new Date().getTime() - Number(sessionStorage.getItem('enterTimetablePage'))) / 1000, + duration_time: (new Date().getTime() - Number(isomorphicSessionStorage.getItem('enterTimetablePage'))) / 1000, }); } diff --git a/src/components/layout/Header/PCHeader/index.tsx b/src/components/layout/Header/PCHeader/index.tsx index a7ef7b143..43b85a671 100644 --- a/src/components/layout/Header/PCHeader/index.tsx +++ b/src/components/layout/Header/PCHeader/index.tsx @@ -11,6 +11,7 @@ import { useSessionLogger } from 'utils/hooks/analytics/useSessionLogger'; import { useLogout } from 'utils/hooks/auth/useLogout'; import useModalPortal from 'utils/hooks/layout/useModalPortal'; import useTokenState from 'utils/hooks/state/useTokenState'; +import { isomorphicSessionStorage } from 'utils/ts/env'; import type { Portal } from 'components/modal/Modal/PortalProvider'; import styles from './PCHeader.module.scss'; @@ -133,7 +134,7 @@ export default function PCHeader({ openModal }: PCHeaderProps) { value: '로고', previous_page: '시간표', current_page: '메인', - duration_time: (new Date().getTime() - Number(sessionStorage.getItem('enterTimetablePage'))) / 1000, + duration_time: (new Date().getTime() - Number(isomorphicSessionStorage.getItem('enterTimetablePage'))) / 1000, }); } if (pathname.includes(ROUTES.Store()) && search.includes('state')) { @@ -144,8 +145,8 @@ export default function PCHeader({ openModal }: PCHeaderProps) { event_label: 'shop_detail_view_back', value: shopName.name, event_category: 'logo', - current_page: sessionStorage.getItem('cameFrom') || '전체보기', - duration_time: (new Date().getTime() - Number(sessionStorage.getItem('enter_storeDetail'))) / 1000, + current_page: isomorphicSessionStorage.getItem('cameFrom') || '전체보기', + duration_time: (new Date().getTime() - Number(isomorphicSessionStorage.getItem('enter_storeDetail'))) / 1000, }); } if (pathname.includes(ROUTES.GraduationCalculator())) { diff --git a/src/components/ui/IntroToolTip/index.tsx b/src/components/ui/IntroToolTip/index.tsx index 63563aae8..553c80abd 100644 --- a/src/components/ui/IntroToolTip/index.tsx +++ b/src/components/ui/IntroToolTip/index.tsx @@ -1,4 +1,5 @@ import CloseIcon from 'assets/svg/tooltip-close-icon.svg'; +import { isomorphicLocalStorage } from 'utils/ts/env'; import styles from './IntroToolTip.module.scss'; interface IntroToolTipProps { @@ -8,7 +9,7 @@ interface IntroToolTipProps { export default function IntroToolTip({ content, closeTooltip }: IntroToolTipProps) { const handleTooltipCloseButtonClick = () => { - localStorage.setItem('store-review-tooltip', 'used'); + isomorphicLocalStorage.setItem('store-review-tooltip', 'used'); closeTooltip(); }; return ( diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index db7b2a536..4994605d5 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -16,6 +16,7 @@ import { COOKIE_KEY } from 'static/url'; import useAutoLogin from 'utils/hooks/auth/useAutoLogin'; import useMount from 'utils/hooks/state/useMount'; import { getCookie } from 'utils/ts/cookie'; +import { isomorphicLocalStorage } from 'utils/ts/env'; import { requestTokensFromNative, setTokensFromNative } from 'utils/ts/iosBridge'; import { useServerStateStore } from 'utils/zustand/serverState'; @@ -103,7 +104,7 @@ export default function App({ Component, pageProps }: AppPropsWithAuth) { } // 로깅을 위한 userId 전달 및 gtag 함수 정의 if (typeof window !== 'undefined') { - const userId = localStorage.getItem('uuid') || ''; + const userId = isomorphicLocalStorage.getItem('uuid') || ''; window.dataLayer = window.dataLayer || []; diff --git a/src/pages/auth/modifyinfo/index.tsx b/src/pages/auth/modifyinfo/index.tsx index 9797c244c..3037a716d 100644 --- a/src/pages/auth/modifyinfo/index.tsx +++ b/src/pages/auth/modifyinfo/index.tsx @@ -36,6 +36,7 @@ import useModalPortal from 'utils/hooks/layout/useModalPortal'; import useBooleanState from 'utils/hooks/state/useBooleanState'; import useTokenState from 'utils/hooks/state/useTokenState'; import { useUser } from 'utils/hooks/state/useUser'; +import { isomorphicLocalStorage } from 'utils/ts/env'; import showToast from 'utils/ts/showToast'; import { isStudentUser } from 'utils/ts/userTypeGuards'; import { useTokenStore } from 'utils/zustand/auth'; @@ -1132,7 +1133,7 @@ const useModifyInfoForm = () => { const router = useRouter(); const isMobile = useMediaQuery(); const onSuccess = () => { - localStorage.setItem(STORAGE_KEY.USER_INFO_COMPLETION, COMPLETION_STATUS.COMPLETED); + isomorphicLocalStorage.setItem(STORAGE_KEY.USER_INFO_COMPLETION, COMPLETION_STATUS.COMPLETED); router.push(ROUTES.Main()); showToast('success', '성공적으로 정보를 수정하였습니다.'); queryClient.invalidateQueries({ queryKey: authQueryKeys.all }); diff --git a/src/pages/graduation/index.tsx b/src/pages/graduation/index.tsx index 09e234430..08d307f56 100644 --- a/src/pages/graduation/index.tsx +++ b/src/pages/graduation/index.tsx @@ -21,6 +21,7 @@ import useModalPortal from 'utils/hooks/layout/useModalPortal'; import useBooleanState from 'utils/hooks/state/useBooleanState'; import useTokenState from 'utils/hooks/state/useTokenState'; import { useScrollLock } from 'utils/hooks/ui/useScrollLock'; +import { isomorphicSessionStorage } from 'utils/ts/env'; import { useSemester } from 'utils/zustand/semester'; import styles from './GraduationCalculatorPage.module.scss'; @@ -59,10 +60,10 @@ function GraduationCalculatorComponent() { useEffect(() => { if (!token) return; - const isFirstVisit = sessionStorage.getItem('visitedGraduationPage'); + const isFirstVisit = isomorphicSessionStorage.getItem('visitedGraduationPage'); if (!isFirstVisit) { - sessionStorage.setItem('visitedGraduationPage', 'true'); + isomorphicSessionStorage.setItem('visitedGraduationPage', 'true'); portalManager.open(() => ); } @@ -70,7 +71,7 @@ function GraduationCalculatorComponent() { const logger = useLogger(); const handlePopState = React.useCallback(() => { - if (sessionStorage.getItem('swipeToBack') === 'true') { + if (isomorphicSessionStorage.getItem('swipeToBack') === 'true') { logger.actionEventSwipe({ team: 'USER', event_label: 'graduation_calculator_back', diff --git a/src/pages/store/[id].tsx b/src/pages/store/[id].tsx index f93e83a43..ab0986288 100644 --- a/src/pages/store/[id].tsx +++ b/src/pages/store/[id].tsx @@ -32,6 +32,7 @@ import useModalPortal from 'utils/hooks/layout/useModalPortal'; import useParamsHandler from 'utils/hooks/routing/useParamsHandler'; import useTokenState from 'utils/hooks/state/useTokenState'; import useScrollToTop from 'utils/hooks/ui/useScrollToTop'; +import { isomorphicSessionStorage } from 'utils/ts/env'; import getDayOfWeek from 'utils/ts/getDayOfWeek'; import { isNotFoundKoinError, @@ -140,7 +141,7 @@ function StoreDetailPage({ id }: Props) { useEffect(() => { if (enterCategoryTimeRef.current === null) { const currentTime = new Date().getTime(); - sessionStorage.setItem('enter_storeDetail', currentTime.toString()); + isomorphicSessionStorage.setItem('enter_storeDetail', currentTime.toString()); enterCategoryTimeRef.current = currentTime; } }, [logger, testValue]); @@ -154,21 +155,21 @@ function StoreDetailPage({ id }: Props) { const storeType = searchParams.get('type') ?? 'shop'; const portalManager = useModalPortal(); const onClickCallNumber = () => { - if (searchParams.get('state') === '리뷰' && sessionStorage.getItem('enterReviewPage')) { + if (searchParams.get('state') === '리뷰' && isomorphicSessionStorage.getItem('enterReviewPage')) { logger.actionEventClick({ team: 'BUSINESS', event_label: 'shop_detail_view_review_back', value: '', previous_page: '리뷰', current_page: '전화', - duration_time: (new Date().getTime() - Number(sessionStorage.getItem('enterReviewPage'))) / 1000, + duration_time: (new Date().getTime() - Number(isomorphicSessionStorage.getItem('enterReviewPage'))) / 1000, }); } logger.actionEventClick({ team: 'BUSINESS', event_label: `${storeType}_call`, value: storeDetail!.name, - duration_time: (new Date().getTime() - Number(sessionStorage.getItem('enter_storeDetail'))) / 1000, + duration_time: (new Date().getTime() - Number(isomorphicSessionStorage.getItem('enter_storeDetail'))) / 1000, }); }; @@ -184,14 +185,14 @@ function StoreDetailPage({ id }: Props) { )); }; const onClickList = () => { - if (searchParams.get('state') === '리뷰' && sessionStorage.getItem('enterReviewPage')) { + if (searchParams.get('state') === '리뷰' && isomorphicSessionStorage.getItem('enterReviewPage')) { logger.actionEventClick({ team: 'BUSINESS', event_label: 'shop_detail_view_review_back', value: storeDetail.name, previous_page: '리뷰', current_page: '전체보기', - duration_time: (new Date().getTime() - Number(sessionStorage.getItem('enterReviewPage'))) / 1000, + duration_time: (new Date().getTime() - Number(isomorphicSessionStorage.getItem('enterReviewPage'))) / 1000, }); } logger.actionEventClick({ @@ -199,8 +200,8 @@ function StoreDetailPage({ id }: Props) { event_label: 'shop_detail_view_back', value: storeDetail!.name, event_category: 'ShopList', - current_page: sessionStorage.getItem('cameFrom') || '전체보기', - duration_time: (new Date().getTime() - Number(sessionStorage.getItem('enter_storeDetail'))) / 1000, + current_page: isomorphicSessionStorage.getItem('cameFrom') || '전체보기', + duration_time: (new Date().getTime() - Number(isomorphicSessionStorage.getItem('enter_storeDetail'))) / 1000, }); }; const onClickEventList = () => { @@ -258,16 +259,16 @@ function StoreDetailPage({ id }: Props) { React.useEffect(() => { if (searchParams.get('state') !== '리뷰') { - if (sessionStorage.getItem('enterReviewPage')) { + if (isomorphicSessionStorage.getItem('enterReviewPage')) { logger.actionEventClick({ team: 'BUSINESS', event_label: 'shop_detail_view_review_back', value: storeDetail.name, previous_page: '리뷰', current_page: searchParams.get('state') || '메뉴', - duration_time: (new Date().getTime() - Number(sessionStorage.getItem('enterReviewPage'))) / 1000, + duration_time: (new Date().getTime() - Number(isomorphicSessionStorage.getItem('enterReviewPage'))) / 1000, }); - sessionStorage.removeItem('enterReviewPage'); + isomorphicSessionStorage.removeItem('enterReviewPage'); } } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -281,13 +282,13 @@ function StoreDetailPage({ id }: Props) { event_label: 'shop_detail_view_back', value: storeDetail.name, event_category: 'swipe', - current_page: sessionStorage.getItem('cameFrom') || '전체보기', - duration_time: (new Date().getTime() - Number(sessionStorage.getItem('enter_storeDetail'))) / 1000, + current_page: isomorphicSessionStorage.getItem('cameFrom') || '전체보기', + duration_time: (new Date().getTime() - Number(isomorphicSessionStorage.getItem('enter_storeDetail'))) / 1000, }); }; window.addEventListener('popstate', handlePopState); return () => { - sessionStorage.removeItem('enterReviewPage'); + isomorphicSessionStorage.removeItem('enterReviewPage'); window.removeEventListener('popstate', handlePopState); }; }, diff --git a/src/pages/store/index.tsx b/src/pages/store/index.tsx index 0cb9bce8c..987809e6e 100644 --- a/src/pages/store/index.tsx +++ b/src/pages/store/index.tsx @@ -29,6 +29,7 @@ import useBooleanState from 'utils/hooks/state/useBooleanState'; import { useLocalStorage } from 'utils/hooks/state/useLocalStorage'; import useMount from 'utils/hooks/state/useMount'; import useScrollToTop from 'utils/hooks/ui/useScrollToTop'; +import { isomorphicLocalStorage, isomorphicSessionStorage } from 'utils/ts/env'; import { STORE_PUBLIC_SSR_CACHE_CONTROL, withCacheControl } from 'utils/ts/withCacheControl'; import type { StoreSorterType, StoreFilterType, StoreCategory } from 'api/store/entity'; import styles from './StorePage.module.scss'; @@ -155,7 +156,7 @@ function Store() { const selectedCategory = router.query.category ? Number(router.query.category) : -1; const handleTooltipCloseButtonClick = () => { - localStorage.setItem('store-review-tooltip', 'used'); + isomorphicLocalStorage.setItem('store-review-tooltip', 'used'); closeTooltip(); }; @@ -219,7 +220,7 @@ function Store() { initializeCategoryEntryTime(); enterCategoryTimeRef.current = currentTime; } - sessionStorage.setItem('cameFrom', categories.shop_categories[selectedCategory]?.name || '전체보기'); + isomorphicSessionStorage.setItem('cameFrom', categories.shop_categories[selectedCategory]?.name || '전체보기'); }, [categories, selectedCategory]); const handleBenefitClick = () => { diff --git a/src/pages/timetable/index.tsx b/src/pages/timetable/index.tsx index e7eac02e3..e990afc2a 100644 --- a/src/pages/timetable/index.tsx +++ b/src/pages/timetable/index.tsx @@ -18,6 +18,7 @@ import useMediaQuery from 'utils/hooks/layout/useMediaQuery'; import useTokenState from 'utils/hooks/state/useTokenState'; import useScrollToTop from 'utils/hooks/ui/useScrollToTop'; import { getRecentSemester } from 'utils/timetable/semester'; +import { isomorphicSessionStorage } from 'utils/ts/env'; import { parseServerSideParams } from 'utils/ts/parseServerSideParams'; import { clearServerAuthCookies, isServerAuthError } from 'utils/ts/ssrAuth'; import { withCacheControl } from 'utils/ts/withCacheControl'; @@ -107,7 +108,7 @@ function TimetablePage() { : initialFrameId; useEffect(() => { - sessionStorage.setItem('enterTimetablePage', new Date().getTime().toString()); + isomorphicSessionStorage.setItem('enterTimetablePage', new Date().getTime().toString()); }, []); return ( diff --git a/src/utils/hooks/abTest/useABTestView.ts b/src/utils/hooks/abTest/useABTestView.ts index 777426056..bb90aa86c 100644 --- a/src/utils/hooks/abTest/useABTestView.ts +++ b/src/utils/hooks/abTest/useABTestView.ts @@ -1,16 +1,15 @@ import { useSuspenseQuery } from '@tanstack/react-query'; import { abTestQueries } from 'api/abTest/queries'; +import { isomorphicLocalStorage } from 'utils/ts/env'; export const useABTestView = (title: string, authorization?: string) => { - const accessHistoryId = typeof window !== 'undefined' ? localStorage.getItem('access_history_id') : null; + const accessHistoryId = isomorphicLocalStorage.getItem('access_history_id'); const { data: abTestView } = useSuspenseQuery(abTestQueries.assign(title, authorization, accessHistoryId)); // 최초 편입 시 if (abTestView.access_history_id) { - if (typeof window !== 'undefined') { - localStorage.setItem('access_history_id', abTestView.access_history_id.toString()); - } + isomorphicLocalStorage.setItem('access_history_id', abTestView.access_history_id.toString()); } return abTestView.variable_name || 'default'; diff --git a/src/utils/hooks/auth/useLogout.ts b/src/utils/hooks/auth/useLogout.ts index 1fb9a3dd6..1fa807a3e 100644 --- a/src/utils/hooks/auth/useLogout.ts +++ b/src/utils/hooks/auth/useLogout.ts @@ -2,6 +2,7 @@ import { STORAGE_KEY } from 'static/auth'; import ROUTES from 'static/routes'; import { COOKIE_KEY } from 'static/url'; import { deleteCookie, getCookieDomain } from 'utils/ts/cookie'; +import { isomorphicSessionStorage } from 'utils/ts/env'; import { useTokenStore } from 'utils/zustand/auth'; export const useLogout = () => { @@ -12,7 +13,7 @@ export const useLogout = () => { setRefreshToken(''); deleteCookie(COOKIE_KEY.AUTH_TOKEN); // 배포 후 기존 도메인 없는 쿠키들의 하위 호환성을 위해 임시 유지 deleteCookie(COOKIE_KEY.AUTH_TOKEN, domain ? { domain: domain } : undefined); - sessionStorage.removeItem(STORAGE_KEY.MODAL_SESSION_SHOWN); + isomorphicSessionStorage.removeItem(STORAGE_KEY.MODAL_SESSION_SHOWN); setToken(''); window.location.href = ROUTES.Main(); }; diff --git a/src/utils/hooks/state/useLocalStorage.ts b/src/utils/hooks/state/useLocalStorage.ts index b91fccec7..571c2b6f9 100644 --- a/src/utils/hooks/state/useLocalStorage.ts +++ b/src/utils/hooks/state/useLocalStorage.ts @@ -1,4 +1,5 @@ import { useSyncExternalStore } from 'react'; +import { getStorageJSONValue, isomorphicLocalStorage } from 'utils/ts/env'; // storage 이벤트 구독 function subscribe(callback: () => void) { @@ -9,12 +10,7 @@ function subscribe(callback: () => void) { function getSnapshot(key: string, defaultValue: T): T { if (typeof window === 'undefined') return defaultValue; - try { - const item = localStorage.getItem(key); - return item ? (JSON.parse(item) as T) : defaultValue; - } catch { - return defaultValue; - } + return isomorphicLocalStorage.getJSONItem(key, defaultValue); } export function useLocalStorage(key: string, defaultValue: T) { @@ -27,13 +23,15 @@ export function useLocalStorage(key: string, defaultValue: T) { // setter const setValue = (newValue: T | null) => { if (typeof window === 'undefined') return; + const nextStorageValue = newValue === null ? null : getStorageJSONValue(newValue); + if (newValue === null) { - localStorage.removeItem(key); + isomorphicLocalStorage.removeItem(key); } else { - localStorage.setItem(key, JSON.stringify(newValue)); + isomorphicLocalStorage.setJSONItem(key, newValue); } // 같은 탭에서도 리렌더링 되도록 강제로 이벤트 발생 - window.dispatchEvent(new StorageEvent('storage', { key, newValue: newValue ? JSON.stringify(newValue) : null })); + window.dispatchEvent(new StorageEvent('storage', { key, newValue: nextStorageValue })); }; return [value, setValue] as const; diff --git a/src/utils/hooks/state/useWebStorage.ts b/src/utils/hooks/state/useWebStorage.ts index a23c5c7c5..d428f4a69 100644 --- a/src/utils/hooks/state/useWebStorage.ts +++ b/src/utils/hooks/state/useWebStorage.ts @@ -1,15 +1,26 @@ import { useSyncExternalStore } from 'react'; +import { + getBrowserStorage, + getStorageJSONValue, + isomorphicLocalStorage, + isomorphicSessionStorage, +} from 'utils/ts/env'; type StorageVariant = 'local' | 'session'; -const getStorage = (variant: StorageVariant): Storage => variant === 'local' ? localStorage : sessionStorage; +const getStorage = (variant: StorageVariant) => getBrowserStorage(variant); + +const getIsomorphicStorage = (variant: StorageVariant) => ( + variant === 'local' ? isomorphicLocalStorage : isomorphicSessionStorage +); const subscribe = (variant: StorageVariant, key: string) => { return (callback: () => void) => { if (typeof window === 'undefined') return () => {}; const onStorage = (event: StorageEvent) => { - if (event.key === key && event.storageArea === getStorage(variant)) { + const storage = getStorage(variant); + if (event.key === key && (!storage || event.storageArea === storage)) { callback(); } }; @@ -22,12 +33,7 @@ const subscribe = (variant: StorageVariant, key: string) => { const getSnapshot = (variant: StorageVariant, key: string, defaultValue: T) => { return () => { if (typeof window === 'undefined') return defaultValue; - try { - const item = getStorage(variant).getItem(key); - return item ? (JSON.parse(item) as T) : defaultValue; - } catch { - return defaultValue; - } + return getIsomorphicStorage(variant).getJSONItem(key, defaultValue); }; }; @@ -42,17 +48,22 @@ export function useWebStorage(key: string, defaultValue: T, options?: { varia const setValue = (newValue: T | null) => { if (typeof window === 'undefined') return; + const nextStorageValue = newValue === null ? null : getStorageJSONValue(newValue); - const storage = getStorage(variant); if (newValue === null) { - storage.removeItem(key); + getIsomorphicStorage(variant).removeItem(key); } else { - storage.setItem(key, JSON.stringify(newValue)); + getIsomorphicStorage(variant).setJSONItem(key, newValue); + } + + const storage = getStorage(variant); + const storageEventInit: StorageEventInit = { key, newValue: nextStorageValue }; + + if (storage) { + storageEventInit.storageArea = storage; } - window.dispatchEvent( - new StorageEvent('storage', { key, newValue: newValue ? JSON.stringify(newValue) : null, storageArea: storage }), - ); + window.dispatchEvent(new StorageEvent('storage', storageEventInit)); }; return [value, setValue] as const; diff --git a/src/utils/ts/auth.ts b/src/utils/ts/auth.ts index fa7d63b3a..8c990aabf 100644 --- a/src/utils/ts/auth.ts +++ b/src/utils/ts/auth.ts @@ -1,17 +1,18 @@ import ROUTES from 'static/routes'; +import { isomorphicSessionStorage } from './env'; const REDIRECT_KEY = 'REDIRECT_AFTER_LOGIN'; export function setRedirectPath(path: string) { - sessionStorage.setItem(REDIRECT_KEY, path); + isomorphicSessionStorage.setItem(REDIRECT_KEY, path); } export function getRedirectPath(): string { - return sessionStorage.getItem(REDIRECT_KEY) || ROUTES.Main(); + return isomorphicSessionStorage.getItem(REDIRECT_KEY) || ROUTES.Main(); } export function clearRedirectPath() { - sessionStorage.removeItem(REDIRECT_KEY); + isomorphicSessionStorage.removeItem(REDIRECT_KEY); } export function redirectToLogin(currentPath?: string) {