개발 학습일지(TIL)

TIL : Next.js localStorage를 활용한 배너, 하루 동안 닫기 구현

Veams 2023. 6. 8.

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를 활용시 주의하여야 한다.

댓글