Next.js 13.4 환경이다.
문제상황
@headlessui/react 라이브러리를 활용해서 배너 닫기를 구현해놨다.
그래서 클라이언트에 유저가 방문하면 아래 화면처럼 배너가 열린다.
현재는 닫기를 눌러도, 새로고침시 다시 보이게 된다.
닫은 정보가 어딘가에 저장되지 않기 때문이다.
const [isShowing, setIsShowing] = useState(true);
그래서 이렇게 구현한 배너를 개선하기로 했다.
localStorage를 활용해서, 사용자가 배너 버튼 닫기를 누르면, 하루 동안 배너가 열리지 않기를 바랐다.
==> 즉, 새로고침을 해도 배너가 계속 닫혀있길 바란다.
그래서 useEffect를 활용하여 다음과 같이 코드를 구성했다.
"use client";
import { Transition } from "@headlessui/react";
import { useEffect, useState } from "react";
export default function Banner() {
const [isShowing, setIsShowing] = useState(true);
useEffect(() => {
let isBannerClosed = localStorage.getItem("isBannerClosed");
if (isBannerClosed) {
setIsShowing(false);
}
}, []);
// 배너를 닫을 때 로컬 스토리지에 상태를 저장
const handleCloseBanner = () => {
localStorage.setItem("isBannerClosed", "true");
setIsShowing(false);
};
return (
<Transition
show={isShowing}
enter="transition-opacity duration-75"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity duration-150"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
////{중간 생략}////
<strong className="font-semibold">소셜 로그인</strong>
<svg
viewBox="0 0 2 2"
className="mx-2 inline h-0.5 w-0.5 fill-current"
aria-hidden="true"
>
<circle cx={1} cy={1} r={1} />
</svg>
간편 로그인을 통해 방명록을 남기실 수 있습니다.
</p>
</div>
<div className="flex flex-1 justify-end">
<button type="button" className="-m-3 p-3 text-xs focus-visible:outline-offset-[-4px]" onClick={handleCloseBanner}>
<span className="sr-only">Dismiss</span>
하루 동안 닫기 X
</button>
</div>
</div>
</Transition>
);
}
배너를 닫으면 localStorage에 닫은 정보가 담긴다.
문제는 새로고침시 배너가 잠깐 보였다가, 닫히고 있어서 좋은 사용자 경험을 줄 수 가 없다.
즉 내가 계속 닫혀있길 기대했던 배너가 잠시라도 다시 보이는 게 거슬린다.
아래 gif처럼 말이다.
시도1
// 배너를 닫을 때 로컬 스토리지에 상태를 저장
const handleCloseBanner = () => {
localStorage.setItem("isBannerClosed", "true");
setIsShowing(false);
};
//새로 고침시 배너가 보이지 않도록 상태를 확인함
const shouldShowBanner = !localStorage.getItem("isBannerClosed");
return (
<Transition
show={isShowing && shouldShowBanner}
enter="transition-opacity duration-75"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity duration-150"
leaveFrom="opacity-100"
leaveTo="opacity-0"
시도1의 방법에선, localStorage에 값이 없어야만 값이 보이도록 구성했다.
기대하는 대로 새로고침을 해도 배너가 잠깐 보이는 등의 문제는 없이 작동하지만,
Unhandled Runtime Error
Error: localStorage is not defined
에러가 뜬다.
Next.js는 SSR로, 서버에서 렌더링을 하기에 window 객체에 접근할 수 없다고 한다.
깔끔하지 않은 해결방법이다.
시도2
생각을 전환했다.
useState() 의 기본 값을 false를 하는 것이다.
새로고침시 배너의 기본 상태는 뜨지 않는, 즉 false로 구현한다.
만약 localStorage에 "isBannerClosed" 값이 존재하면 계속 배너를 보여주지 않고,
이 값이 존재하지 않으면 (!isBannerClsed ), 배너를 띄워준다. setIsShowing(true)
"use client";
import { Transition } from "@headlessui/react";
import { useEffect, useState } from "react";
export default function Banner() {
const [isShowing, setIsShowing] = useState(false);
useEffect(() => {
let isBannerClosed = localStorage.getItem("isBannerClosed");
if (!isBannerClosed) {
setIsShowing(true);
}
}, []);
// 배너를 닫을 때 로컬 스토리지에 상태를 저장
const handleCloseBanner = () => {
localStorage.setItem("isBannerClosed", "true");
setIsShowing(true);
};
return (
<Transition
show={isShowing}
enter="transition-opacity duration-75"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity duration-150"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
해결되었다. 기대하는 대로 오류도 뜨지 않고, 새로고침하여도 배너도 지속적으로 닫혀있다.
이제 얼마나 닫을지 시간을 조정하는 코드도 추가한다. localStorage에 currentTime과 closedTime 값도 같이 저장하여 다룬다.
"use client";
import { Transition } from "@headlessui/react";
import { useEffect, useState } from "react";
export default function Banner() {
const [isShowing, setIsShowing] = useState(false);
useEffect(() => {
const isBannerClosed = localStorage.getItem("isBannerClosed");
const closedTime = localStorage.getItem("closedTime");
const currentTime = new Date().getTime();
if (!isBannerClosed || (closedTime && currentTime - closedTime > 24 * 60 * 60 * 1000)) { //24시간 동안 보지 않기
setIsShowing(true);
}
}, []);
// 배너 닫을 때 로컬 스토리지에 상태를 저장
const handleCloseBanner = () => {
const currentTime = new Date().getTime();
localStorage.setItem("isBannerClosed", "true");
localStorage.setItem("closedTime", currentTime.toString());
setIsShowing(false);
};
return (
<Transition
show={isShowing}
enter="transition-opacity duration-75"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity duration-150"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
알게 된 점.
- 배너 구현시 useState 기본 값은 false로 주는 것이 낫겠다.
- next.js의 SSR 특성으로 인해 localStorage를 활용시 주의하여야 한다.
댓글