[TIL] 2024-04-26 Popup 시간 처리 관련

H Kim·2024년 4월 26일
0

TIL

목록 보기
61/69
post-thumbnail

배경

회사 앱 홈 진입화면에 바텀시트 모달 형식의 광고 관련한 공간이 있다.
근데 우리는 그냥... 팝업이라고 부른다(팝업은 아니지만).
앱 새로 만들면서 원래부터 있었던 부분이 아니라 공지 관련해서 급하게 써야 하는 부분이 있었어서 만들어놨다가 그것이 광고로 확장된 케이스.
위에서 설명했다시피 급하게 만든 거라 백엔드 쪽에서 정보를 받아오는 게 아니라 프론트 단에서 하드코딩으로 팝업을 넣어서 배포를 해야 동작하는 형태였다.
몇 번이야 그냥 마케팅팀에서 요청 올 때마다 개발해서 배포했는데 이렇게 하면 팝업을 게시할 내릴 때릴 때도 무조건 실배포가 한 번을 이루어져야 하는 것이 불편하기도 하고 위험성도 너무 컸다(실제로 배포했다가 한 번 잘못되어서 급히 핫픽스 했을 때도 있었음).
그리고 이것을 개선해야겠다고 마음 먹었던 부분은 마케팅 팀이 요청을 너무 급박하게 줄 때가 많아서 허겁지겁 헐레벌떡 일을 하게 되는 경우가 빈번했다.

이를 작업하기 위해서는 총 3가지의 작업이 필요했는데,
1. 프로젝트 전용으로 만들어 놓은 커스텀훅인 usePopup을 수정하고
2. 팝업이 뜨는 index에서 동작하게 연결시키고
3. 범용적으로 쓸 수 있는 팝업 컴포넌트를 만들어야 했다.

회사에서는 이 곳을 계속해서 광고 관련해서 쓸 예정이기 때문에 후에 백엔드 리소스가 생기면(지금은 여기까지 작업 할 리소스가 없음) API만 연결하면 되게 만드는 게 목표였다.


usePopup

앱 자체가 웹앱으로 만들어져 있어서 앱과 정보 주고받는 부분과 스타일링 관련한 조정을 커스텀 훅으로 만들어서 관리하고 있다.
(코드 예시에서 관련 부분들은 삭제하고 기본동작만 남김)

const usePopup = () => {
  const [isPopupComplete, setPopup] = useState<boolean>(getPopupDelay());

  const handlePopup = async () => {
    setPopup(true);
  };

  const handlePopupComplete = () => {
    setPopup(!isPopupComplete);
  };

  const handlePopupAtHome = async (url?: string) => {
    if (!url) {
      await handlePopupComplete();
      return;
    }

    window.location.href = url;
    setPopup(!isPopupComplete);
  };

  const handlePopupDelayDay = (
    popupDescription: string | undefined,
    popupNotSeen: string | undefined,
    popupExpiredDate: Moment
  ) => {
    setPopup(!isPopupComplete);
    setPopupDelay();
    setPopupNew(popupDescription, popupExpiredDate);
  };

  return {
    isPopupComplete,
    onPopupAtHome: handlePopupAtHome,
    onPopup: handlePopup,
    onPopupComplete: handlePopupComplete,
    onPopupDelayDay: handlePopupDelayDay
  };
};

export default usePopup;

index

원래 홈페이지에 모든 로직이 다 있었는데 관련없는 코드를 삭제해서 간단해 보일 뿐 실제로는 매우 복잡하고 난리이기 때문에...
다 하고 나서 코드리뷰 받을 때 팝업 관련한 로직은 팝업 컴포넌트 안으로 넣어서 처리해 달라고 요청을 받았었다.
원래는 여기 위에 팝업 정보가 담긴 배열이 있었는데 그걸 다시 팝업 컴포넌트 안으로 넣으면서 동작 이상으로 작업이 많이 길어졌다.

const HomePage = () => {
  const { isPopupComplete, onPopupComplete, onPopupDelayDay, onPopupAtHome, onPopup } = usePopup();

  return (
    <PageLayout>
      <Popup
        isPopupComplete={isPopupComplete}
        onPopup={onPopup}
        onPopupComplete={onPopupComplete}
        onPopupDelayDay={onPopupDelayDay}
        onPopupAtHome={onPopupAtHome}
      />
    </PageLayout>
  );
};

export default HomePage;


interface PopupProps {
  isPopupComplete: boolean;
  onPopupComplete: () => void;
  onPopup: () => void;
  onPopupDelayDay: (description: string | undefined, periodNotSeen: string | undefined, popupExpiredDate: Moment) => void;
  onPopupAtHome: (urlLink?: string) => void;
}

interface IPopupInfo {
  startDate?: Moment;
  endDate?: Moment;
  imageInfo?: any;
  description?: string;
  periodNotSeen?: string;
  urlLink?: string;
}

const Popup = ({ isPopupComplete, onPopup, onPopupComplete, onPopupDelayDay, onPopupAtHome }: PopupProps) => {
  // 셋팅되는 팝업의 정보
  const [popupInfo, setPopupInfo] = useState<IPopupInfo>();
  // 기간에 따라 버튼에 노출되어야 하는 문구가 바뀌어야 해서 상태로 관리
  const [delayBtnText, setDelayBtnText] = useState<string>('');
  const [plusDate, setPlusDate] = useState<Moment>(moment());
  const [curPopup, setCurPopup] = useState<string>('');

  // popupInfo를 셋팅하는 useEffect hook
  useEffect(() => {
    // 팝업 인포의 배열
    // 실제로 이렇게 여러 개를 쭉 넣어놓는 경우는 없지만 
    // 여러가지 상황에 대응하기 위해서 팝업이 연속할 때, 팝업이 연속하지 않을 때를 상정하고 개발했다.
    const popupInfoArr: any[] = [
      {
        startDate: moment('2024-05-01 00:00:00'),
        endDate: moment('2024-05-10 23:59:59'),
        imageInfo: PopupOne,
        description: 'popup-one',
        periodNotSeen: 'years',
        urlLink: 'https://www.ditto.com/'
      },
      {
        startDate: moment('2024-05-15 00:00:00'),
        endDate: moment('2024-05-21 23:59:59'),
        imageInfo: PopupTwo,
        description: 'popup-two',
        periodNotSeen: 'weeks',
        urlLink: 'https://www.cookie.com/'
      },
      {
        startDate: moment('2024-05-22 00:00:00'),
        endDate: moment('2024-05-28 23:59:59'),
        imageInfo: PopupThree,
        description: 'popup-three',
        periodNotSeen: 'days',
        urlLink: 'https://www.hypeboy.com/'
      }
    ];
    // 팝업인포에서 기간이 지나지 않은 팝업정보만 걸러낸다.
    const filteredPopupInfoArr = popupInfoArr.filter(
      (el) => moment().isAfter(moment(el.startDate)) && moment().isBefore(moment(el.endDate))
    );
    // 가장 고생했던... 부분인데 직전팝업을 보지 않기 처리를 했고, 
    // 그 이후에 뜨는 새로운 팝업이 있으면 사용자가 보지않기 처리한 팝업이 아닌 새로운 팝업이기 때문에 보여줘야한다.
    // 그래서 로컬스토리지에 팝업이름과 팝업을 보지 않는 기간 두 개를 따로따로 관리했다.
    const popupNewLocalStorage = getPopupNew();
    // 팝업을 filter한 배열이 0이면 진행되는 팝업이 없으므로 
    // 두 가지 정보를 모두 지워주어야 다음 팝업이 문제없이 노출될 수 있다.
    if (filteredPopupInfoArr.length === 0) {
      removePopupDelay();
      removePopupNew();
    }
    // 보지 않기 처리 기간이 만료되었을 경우에도 두 가지 정보를 모두 지워준다.
    const delayExpired = popupNewLocalStorage?.popupExpiredDate;
    if (moment(delayExpired).isBefore(moment())) {
      removePopupDelay();
      removePopupNew();
      onPopupAtHome();
    }
    // 여기까지 와서 filter된 팝업 어레이에 정보가 있으면 이를 최종적으로 셋팅 될 popupInfo에 셋팅한다. 
    // 배열이 무한해도 현재 보여줘야 하는 팝업은 한 가지이기 때문에 첫번째 원소의 정보를 셋팅한다.
    if (filteredPopupInfoArr.length > 0) {
      setPopupInfo({
        startDate: filteredPopupInfoArr[0].startDate,
        endDate: filteredPopupInfoArr[0].endDate,
        imageInfo: filteredPopupInfoArr[0].imageInfo,
        description: filteredPopupInfoArr[0].description,
        periodNotSeen: filteredPopupInfoArr[0].periodNotSeen,
        urlLink: filteredPopupInfoArr[0].urlLink
      });
      setCurPopup(filteredPopupInfoArr[0].description);
    } else {
      onPopup();
    }
  }, [curPopup, onPopup, onPopupAtHome, onPopupComplete]);

  // 버튼문구를 바꿔주는 useEffect hook
  useEffect(() => {
    if (popupInfo?.periodNotSeen === 'days') {
      const popupExpiredDate = moment().endOf('days');
      setPlusDate(popupExpiredDate);
      setDelayBtnText('오늘 하루 안 보기');
    } else if (popupInfo?.periodNotSeen === 'weeks') {
      const popupExpiredDate = moment().add(1, 'weeks').endOf('days');
      const popupEndBeforeExpiredDate = moment(popupInfo?.endDate).isBefore(popupExpiredDate);
      if (popupEndBeforeExpiredDate) {
        setPlusDate(moment(popupInfo?.endDate));
      } else {
        setPlusDate(popupExpiredDate);
      }
      setDelayBtnText('일주일 동안 안 보기');
    } else if (popupInfo?.periodNotSeen === 'years') {
      const popupExpiredDate = moment().add(1, 'years').endOf('days');
      setPlusDate(popupExpiredDate);
      setDelayBtnText('다시 보지 않기');
    }
  }, [popupInfo?.description, popupInfo?.endDate, popupInfo?.periodNotSeen]);

  // 핍업게시가 완료되었거나 팝업정보가 없으면 null로 리턴하여 컴포넌트가 노출되지 않는다.
  if (isPopupComplete || !popupInfo) return null;

  return (
    <>
      <Popup
        className="bottom"
        src={popupInfo.imageInfo}
        alt="popup"
        onClick={() => onPopupAtHome(popupInfo.ezwelPopup, popupInfo.urlLink)}
      />
      <Button type="text" onClick={() => onPopupDelayDay(popupInfo?.description, popupInfo?.periodNotSeen, plusDate)}>
        {delayBtnText}
      </Button>
      <Button type="text" onClick={() => onPopupComplete()}>
        닫기
      </Button>
    </>
  );
};

export default Popup;

결과

개발에 배포된 모습!
기여워기여워...

이후로 들어오는 마케팅팀의 요청은 사전 배포로 자동으로 팝업이 조건에 따라 노출되었다가 미노출되거나 하는 중.
물론 아직도 사진을 바꿀 때는 실배포가 이루어지기는 해야해서 그냥 빨리 백엔드 작업이 되었으면... 하고 바라는 중이다.

0개의 댓글